From 1a3aaac5b3d1f1af7ac2d72df89ec529ca22a133 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Fri, 13 Mar 2026 00:55:21 +0000 Subject: [PATCH] The visualiser now has on hover highlighting --- src/Sass/visualiser.scss | 42 +++++++ .../components/VisualisetTab.tsx | 104 ++++++++++++++---- 2 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/Sass/visualiser.scss b/src/Sass/visualiser.scss index 2528f67..4bcdd0e 100644 --- a/src/Sass/visualiser.scss +++ b/src/Sass/visualiser.scss @@ -46,6 +46,23 @@ text-align: center; letter-spacing: 0.2px; 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 { @@ -160,6 +177,10 @@ stroke-width: 2px; stroke-dasharray: 4, 4; vector-effect: non-scaling-stroke; + transition: + opacity 0.16s ease, + stroke-width 0.16s ease, + stroke 0.16s ease; } .visualiser-outcome-connector-arrow { @@ -176,6 +197,27 @@ stroke-linecap: round; stroke-linejoin: round; 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) { diff --git a/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx b/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx index 4e55789..ad09638 100644 --- a/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx +++ b/src/modules/manager/workflowTemplates/components/VisualisetTab.tsx @@ -295,6 +295,7 @@ const VisualiserTab: React.FC = ({ }, []); const levels = React.useMemo(() => buildLevels(tasks), [tasks]); const edges = React.useMemo(() => buildEdges(tasks), [tasks]); + const [hoveredGuid, setHoveredGuid] = React.useState(null); const levelByGuid = React.useMemo(() => { const levelMap = new Map(); levels.forEach((level, levelIndex) => { @@ -408,6 +409,31 @@ const VisualiserTab: React.FC = ({ return sharedLaneByTarget; }, [edges, levelByGuid, edgeLaneOffsetByKey, nodePositions]); + const connectedByGuid = React.useMemo(() => { + const map = new Map>(); + + tasks.forEach((task) => { + map.set(task.config.guid as string, new Set()); + }); + + edges.forEach((edge) => { + const source = map.get(edge.sourceGuid) ?? new Set(); + source.add(edge.targetGuid); + map.set(edge.sourceGuid, source); + + const target = map.get(edge.targetGuid) ?? new Set(); + target.add(edge.sourceGuid); + map.set(edge.targetGuid, target); + }); + + return map; + }, [tasks, edges]); + const relatedGuids = React.useMemo(() => { + if (!hoveredGuid) { + return new Set(); + } + return connectedByGuid.get(hoveredGuid) ?? new Set(); + }, [hoveredGuid, connectedByGuid]); React.useEffect(() => { const measurePositions = () => { @@ -470,6 +496,22 @@ const VisualiserTab: React.FC = ({ }; }, [levels]); + const labelCollisionState = new Map(); + 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 (
@@ -483,22 +525,31 @@ const VisualiserTab: React.FC = ({ gap: "40px", }} > - {level.map((task) => ( -
-
- - {(task.config.name as string) || task.type} + {level.map((task) => { + const guid = task.config.guid as string; + const isHovered = hoveredGuid === guid; + const isRelated = + hoveredGuid !== null && relatedGuids.has(guid); + const isDim = + hoveredGuid !== null && !isHovered && !isRelated; + + return ( +
setHoveredGuid(guid)} + onMouseLeave={() => setHoveredGuid(null)} + > +
+ + {(task.config.name as string) || task.type} +
-
- ))} + ); + })}
))} @@ -643,11 +694,17 @@ const VisualiserTab: React.FC = ({ return tEnum(matchedOutcome ?? outcome); }) .join(", "); + const edgeIsActive = + hoveredGuid !== null && + (edge.sourceGuid === hoveredGuid || + edge.targetGuid === hoveredGuid); + const edgeIsDim = hoveredGuid !== null && !edgeIsActive; + const adjustedLabelY = getAdjustedLabelY(labelX, labelY - 1.25); return ( = ({ {label && ( @@ -676,11 +733,20 @@ const VisualiserTab: React.FC = ({ const endX = target.x - target.width / 2; 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 (