More UI Tweaks to the visualiser

This commit is contained in:
Colin Dawson 2026-03-13 09:12:42 +00:00
parent 9e4b687432
commit 47b0528275

View File

@ -409,6 +409,112 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
return sharedLaneByTarget;
}, [edges, levelByGuid, edgeLaneOffsetByKey, nodePositions]);
const mergedRightIncomingByTarget = React.useMemo(() => {
const forwardIncomingCount = new Map<string, number>();
edges.forEach((edge) => {
const sourceLevel = levelByGuid.get(edge.sourceGuid) ?? 0;
const targetLevel = levelByGuid.get(edge.targetGuid) ?? 0;
if (targetLevel > sourceLevel) {
forwardIncomingCount.set(
edge.targetGuid,
(forwardIncomingCount.get(edge.targetGuid) ?? 0) + 1,
);
}
});
const sharedLaneByTarget = new Map<string, number>();
edges.forEach((edge) => {
const source = nodePositions.get(edge.sourceGuid);
const target = nodePositions.get(edge.targetGuid);
if (!source || !target) {
return;
}
const sourceLevel = levelByGuid.get(edge.sourceGuid) ?? 0;
const targetLevel = levelByGuid.get(edge.targetGuid) ?? 0;
const levelDelta = targetLevel - sourceLevel;
if (levelDelta <= 0) {
return;
}
const targetForwardCount = forwardIncomingCount.get(edge.targetGuid) ?? 0;
const shouldMergeAdjacent = levelDelta === 1 && targetForwardCount >= 3;
if (!(levelDelta > 1 || shouldMergeAdjacent)) {
return;
}
const sourceShift =
(edge.sourceIndex - (edge.sourceCount - 1) / 2) * 2.25;
const laneSpread =
edgeLaneOffsetByKey.get(`${edge.sourceGuid}->${edge.targetGuid}`) ?? 0;
const isLongDownward = levelDelta > 1;
const startX = isLongDownward ? source.x + source.width / 2 : source.x;
const startY = isLongDownward
? source.y + sourceShift * 0.35 + laneSpread * 0.25
: source.y + source.height / 2;
const endX = target.x + target.width / 2;
const endY = isLongDownward
? target.y
: target.y - target.height / 2 + target.height * 0.5;
const verticalSpan = Math.abs(endY - startY);
const longRouteExtra =
Math.max(0, verticalSpan - 10) * 0.2 + (levelDelta - 1) * 1.2;
const laneBaseOffset = isLongDownward ? 6 : 2.5;
const candidateLaneX =
Math.max(startX, endX) +
laneBaseOffset +
Math.abs(sourceShift) +
(isLongDownward ? longRouteExtra : 0) +
laneSpread * 1.2;
const current = sharedLaneByTarget.get(edge.targetGuid);
sharedLaneByTarget.set(
edge.targetGuid,
current === undefined
? candidateLaneX
: Math.max(current, candidateLaneX),
);
});
return sharedLaneByTarget;
}, [edges, levelByGuid, edgeLaneOffsetByKey, nodePositions]);
const rightMergeEligibleByTarget = React.useMemo(() => {
const forwardIncomingCount = new Map<string, number>();
const longIncomingCount = new Map<string, number>();
edges.forEach((edge) => {
const sourceLevel = levelByGuid.get(edge.sourceGuid) ?? 0;
const targetLevel = levelByGuid.get(edge.targetGuid) ?? 0;
if (targetLevel > sourceLevel) {
forwardIncomingCount.set(
edge.targetGuid,
(forwardIncomingCount.get(edge.targetGuid) ?? 0) + 1,
);
if (targetLevel - sourceLevel > 1) {
longIncomingCount.set(
edge.targetGuid,
(longIncomingCount.get(edge.targetGuid) ?? 0) + 1,
);
}
}
});
const eligibility = new Map<
string,
{ mergeLong: boolean; mergeAdjacent: boolean }
>();
forwardIncomingCount.forEach((count, targetGuid) => {
eligibility.set(targetGuid, {
mergeLong: (longIncomingCount.get(targetGuid) ?? 0) >= 2,
mergeAdjacent: count >= 3,
});
});
return eligibility;
}, [edges, levelByGuid]);
const connectedByGuid = React.useMemo(() => {
const map = new Map<string, Set<string>>();
@ -597,29 +703,49 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
);
const isMergedLeft =
levelDelta < 0 && mergedLeftLaneX !== undefined;
const mergedRightLaneX = mergedRightIncomingByTarget.get(
edge.targetGuid,
);
const rightMergeEligibility = rightMergeEligibleByTarget.get(
edge.targetGuid,
);
const isMergedRight =
mergedRightLaneX !== undefined &&
((levelDelta > 1 && rightMergeEligibility?.mergeLong) ||
(levelDelta === 1 && rightMergeEligibility?.mergeAdjacent));
let path = "";
let labelX = 0;
let labelY = 0;
let labelAnchor: "start" | "middle" | "end" = "middle";
if (levelDelta > 1) {
if (levelDelta > 1 || (levelDelta === 1 && isMergedRight)) {
// Multi-level downward jumps: route on the right side.
const startX = source.x + source.width / 2;
const startY = source.y + sourceShift * 0.35 + laneSpread * 0.25;
const isLongDownward = levelDelta > 1;
const startX = isLongDownward
? source.x + source.width / 2
: source.x;
const startY = isLongDownward
? source.y + sourceShift * 0.35 + laneSpread * 0.25
: source.y + source.height / 2;
const endX = target.x + target.width / 2;
const endY = target.y + targetShift * 0.35 + laneSpread * 0.25;
const endY = isLongDownward
? target.y + targetShift * 0.35 + laneSpread * 0.25
: target.y;
const verticalSpan = Math.abs(endY - startY);
const longRouteExtra =
Math.max(0, verticalSpan - 10) * 0.2 + (levelDelta - 1) * 1.2;
const laneX =
mergedRightLaneX ??
Math.max(startX, endX) +
6 +
Math.abs(sourceShift) +
longRouteExtra +
laneSpread * 1.2;
path = `M ${startX} ${startY} L ${laneX} ${startY} L ${laneX} ${endY} L ${endX} ${endY}`;
path = isMergedRight
? `M ${startX} ${startY} L ${laneX} ${startY} L ${laneX} ${endY}`
: `M ${startX} ${startY} L ${laneX} ${startY} L ${laneX} ${endY} L ${endX} ${endY}`;
labelX = startX + 0.9;
labelY = startY - 1.5 - Math.abs(laneSpread) * 0.12;
labelAnchor = "start";
@ -716,7 +842,9 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
className={`visualiser-outcome-connector-line${edgeIntensityClass}${edgeIsDim ? " is-dim" : ""}`}
d={path}
markerEnd={
isMergedLeft ? undefined : "url(#visualiser-arrow)"
isMergedLeft || isMergedRight
? undefined
: "url(#visualiser-arrow)"
}
/>
{label && (
@ -770,6 +898,43 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
);
},
)}
{Array.from(mergedRightIncomingByTarget.entries()).map(
([targetGuid, laneX]) => {
const target = nodePositions.get(targetGuid);
if (!target) {
return null;
}
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 &&
(levelByGuid.get(edge.targetGuid) ?? 0) >
(levelByGuid.get(edge.sourceGuid) ?? 0),
));
const mergedIsDim = hoveredGuid !== null && !mergedIsActive;
const mergedIntensityClass =
hoveredGuid === targetGuid
? " is-active-incoming"
: mergedIsActive
? " is-active-outgoing"
: "";
return (
<path
key={`merged-right-${targetGuid}`}
className={`visualiser-outcome-connector-line${mergedIntensityClass}${mergedIsDim ? " is-dim" : ""}`}
d={`M ${laneX} ${endY} L ${endX} ${endY}`}
markerEnd="url(#visualiser-arrow)"
/>
);
},
)}
</svg>
</div>
</div>