The visualiser now has on hover highlighting
This commit is contained in:
parent
f506f047b8
commit
1a3aaac5b3
@ -46,6 +46,23 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
box-shadow: 0 6px 14px rgba($blue, 0.2);
|
box-shadow: 0 6px 14px rgba($blue, 0.2);
|
||||||
|
transition:
|
||||||
|
opacity 0.16s ease,
|
||||||
|
transform 0.16s ease,
|
||||||
|
box-shadow 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualiser-node.is-hovered {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 10px 22px rgba($blue, 0.33);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualiser-node.is-related {
|
||||||
|
box-shadow: 0 8px 18px rgba($blue, 0.27);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualiser-node.is-dim {
|
||||||
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visualiser-node-content {
|
.visualiser-node-content {
|
||||||
@ -160,6 +177,10 @@
|
|||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
stroke-dasharray: 4, 4;
|
stroke-dasharray: 4, 4;
|
||||||
vector-effect: non-scaling-stroke;
|
vector-effect: non-scaling-stroke;
|
||||||
|
transition:
|
||||||
|
opacity 0.16s ease,
|
||||||
|
stroke-width 0.16s ease,
|
||||||
|
stroke 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visualiser-outcome-connector-arrow {
|
.visualiser-outcome-connector-arrow {
|
||||||
@ -176,6 +197,27 @@
|
|||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
transition:
|
||||||
|
opacity 0.16s ease,
|
||||||
|
fill 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualiser-outcome-connector-line.is-active {
|
||||||
|
stroke-width: 2.6px;
|
||||||
|
stroke: rgba($blue, 0.75);
|
||||||
|
stroke-dasharray: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualiser-outcome-connector-line.is-dim {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualiser-outcome-connector-label.is-active {
|
||||||
|
fill: darken($blue, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualiser-outcome-connector-label.is-dim {
|
||||||
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include color-mode(dark) {
|
@include color-mode(dark) {
|
||||||
|
|||||||
@ -295,6 +295,7 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
const levels = React.useMemo(() => buildLevels(tasks), [tasks]);
|
const levels = React.useMemo(() => buildLevels(tasks), [tasks]);
|
||||||
const edges = React.useMemo(() => buildEdges(tasks), [tasks]);
|
const edges = React.useMemo(() => buildEdges(tasks), [tasks]);
|
||||||
|
const [hoveredGuid, setHoveredGuid] = React.useState<string | null>(null);
|
||||||
const levelByGuid = React.useMemo(() => {
|
const levelByGuid = React.useMemo(() => {
|
||||||
const levelMap = new Map<string, number>();
|
const levelMap = new Map<string, number>();
|
||||||
levels.forEach((level, levelIndex) => {
|
levels.forEach((level, levelIndex) => {
|
||||||
@ -408,6 +409,31 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
|
|
||||||
return sharedLaneByTarget;
|
return sharedLaneByTarget;
|
||||||
}, [edges, levelByGuid, edgeLaneOffsetByKey, nodePositions]);
|
}, [edges, levelByGuid, edgeLaneOffsetByKey, nodePositions]);
|
||||||
|
const connectedByGuid = React.useMemo(() => {
|
||||||
|
const map = new Map<string, Set<string>>();
|
||||||
|
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
map.set(task.config.guid as string, new Set<string>());
|
||||||
|
});
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
const source = map.get(edge.sourceGuid) ?? new Set<string>();
|
||||||
|
source.add(edge.targetGuid);
|
||||||
|
map.set(edge.sourceGuid, source);
|
||||||
|
|
||||||
|
const target = map.get(edge.targetGuid) ?? new Set<string>();
|
||||||
|
target.add(edge.sourceGuid);
|
||||||
|
map.set(edge.targetGuid, target);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, [tasks, edges]);
|
||||||
|
const relatedGuids = React.useMemo(() => {
|
||||||
|
if (!hoveredGuid) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
return connectedByGuid.get(hoveredGuid) ?? new Set<string>();
|
||||||
|
}, [hoveredGuid, connectedByGuid]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const measurePositions = () => {
|
const measurePositions = () => {
|
||||||
@ -470,6 +496,22 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
};
|
};
|
||||||
}, [levels]);
|
}, [levels]);
|
||||||
|
|
||||||
|
const labelCollisionState = new Map<string, number[]>();
|
||||||
|
const getAdjustedLabelY = (x: number, preferredY: number): number => {
|
||||||
|
const bucket = Math.round(x / 8).toString();
|
||||||
|
const used = labelCollisionState.get(bucket) ?? [];
|
||||||
|
|
||||||
|
let y = preferredY;
|
||||||
|
const minGap = 2.1;
|
||||||
|
while (used.some((existingY) => Math.abs(existingY - y) < minGap)) {
|
||||||
|
y += minGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
used.push(y);
|
||||||
|
labelCollisionState.set(bucket, used);
|
||||||
|
return y;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="visualiser-root">
|
<div className="visualiser-root">
|
||||||
<div className="visualiser-container" ref={visualiserContainerRef}>
|
<div className="visualiser-container" ref={visualiserContainerRef}>
|
||||||
@ -483,22 +525,31 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
gap: "40px",
|
gap: "40px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{level.map((task) => (
|
{level.map((task) => {
|
||||||
<div
|
const guid = task.config.guid as string;
|
||||||
key={task.config.guid as string}
|
const isHovered = hoveredGuid === guid;
|
||||||
className="visualiser-node"
|
const isRelated =
|
||||||
data-guid={task.config.guid as string}
|
hoveredGuid !== null && relatedGuids.has(guid);
|
||||||
>
|
const isDim =
|
||||||
<div className="visualiser-node-content">
|
hoveredGuid !== null && !isHovered && !isRelated;
|
||||||
<ValidationErrorIcon
|
|
||||||
visible={
|
return (
|
||||||
taskValidation[task.config.guid as string] === false
|
<div
|
||||||
}
|
key={guid}
|
||||||
/>
|
className={`visualiser-node${isHovered ? " is-hovered" : ""}${isRelated ? " is-related" : ""}${isDim ? " is-dim" : ""}`}
|
||||||
<span>{(task.config.name as string) || task.type}</span>
|
data-guid={guid}
|
||||||
|
onMouseEnter={() => setHoveredGuid(guid)}
|
||||||
|
onMouseLeave={() => setHoveredGuid(null)}
|
||||||
|
>
|
||||||
|
<div className="visualiser-node-content">
|
||||||
|
<ValidationErrorIcon
|
||||||
|
visible={taskValidation[guid] === false}
|
||||||
|
/>
|
||||||
|
<span>{(task.config.name as string) || task.type}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -643,11 +694,17 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
return tEnum(matchedOutcome ?? outcome);
|
return tEnum(matchedOutcome ?? outcome);
|
||||||
})
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
const edgeIsActive =
|
||||||
|
hoveredGuid !== null &&
|
||||||
|
(edge.sourceGuid === hoveredGuid ||
|
||||||
|
edge.targetGuid === hoveredGuid);
|
||||||
|
const edgeIsDim = hoveredGuid !== null && !edgeIsActive;
|
||||||
|
const adjustedLabelY = getAdjustedLabelY(labelX, labelY - 1.25);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={`${edge.sourceGuid}-${edge.targetGuid}`}>
|
<g key={`${edge.sourceGuid}-${edge.targetGuid}`}>
|
||||||
<path
|
<path
|
||||||
className="visualiser-outcome-connector-line"
|
className={`visualiser-outcome-connector-line${edgeIsActive ? " is-active" : ""}${edgeIsDim ? " is-dim" : ""}`}
|
||||||
d={path}
|
d={path}
|
||||||
markerEnd={
|
markerEnd={
|
||||||
isMergedLeft ? undefined : "url(#visualiser-arrow)"
|
isMergedLeft ? undefined : "url(#visualiser-arrow)"
|
||||||
@ -656,8 +713,8 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
{label && (
|
{label && (
|
||||||
<text
|
<text
|
||||||
x={labelX}
|
x={labelX}
|
||||||
y={labelY - 1.25}
|
y={adjustedLabelY}
|
||||||
className="visualiser-outcome-connector-label"
|
className={`visualiser-outcome-connector-label${edgeIsActive ? " is-active" : ""}${edgeIsDim ? " is-dim" : ""}`}
|
||||||
textAnchor={labelAnchor}
|
textAnchor={labelAnchor}
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
>
|
>
|
||||||
@ -676,11 +733,20 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
|
|
||||||
const endX = target.x - target.width / 2;
|
const endX = target.x - target.width / 2;
|
||||||
const endY = target.y;
|
const endY = target.y;
|
||||||
|
const mergedIsActive =
|
||||||
|
hoveredGuid !== null &&
|
||||||
|
(hoveredGuid === targetGuid ||
|
||||||
|
edges.some(
|
||||||
|
(edge) =>
|
||||||
|
edge.targetGuid === targetGuid &&
|
||||||
|
edge.sourceGuid === hoveredGuid,
|
||||||
|
));
|
||||||
|
const mergedIsDim = hoveredGuid !== null && !mergedIsActive;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
key={`merged-left-${targetGuid}`}
|
key={`merged-left-${targetGuid}`}
|
||||||
className="visualiser-outcome-connector-line"
|
className={`visualiser-outcome-connector-line${mergedIsActive ? " is-active" : ""}${mergedIsDim ? " is-dim" : ""}`}
|
||||||
d={`M ${laneX} ${endY} L ${endX} ${endY}`}
|
d={`M ${laneX} ${endY} L ${endX} ${endY}`}
|
||||||
markerEnd="url(#visualiser-arrow)"
|
markerEnd="url(#visualiser-arrow)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user