More UI Tweaks to the visualiser
This commit is contained in:
parent
9e4b687432
commit
47b0528275
@ -409,6 +409,112 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
|
|
||||||
return sharedLaneByTarget;
|
return sharedLaneByTarget;
|
||||||
}, [edges, levelByGuid, edgeLaneOffsetByKey, nodePositions]);
|
}, [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 connectedByGuid = React.useMemo(() => {
|
||||||
const map = new Map<string, Set<string>>();
|
const map = new Map<string, Set<string>>();
|
||||||
|
|
||||||
@ -597,29 +703,49 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
);
|
);
|
||||||
const isMergedLeft =
|
const isMergedLeft =
|
||||||
levelDelta < 0 && mergedLeftLaneX !== undefined;
|
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 path = "";
|
||||||
let labelX = 0;
|
let labelX = 0;
|
||||||
let labelY = 0;
|
let labelY = 0;
|
||||||
let labelAnchor: "start" | "middle" | "end" = "middle";
|
let labelAnchor: "start" | "middle" | "end" = "middle";
|
||||||
|
|
||||||
if (levelDelta > 1) {
|
if (levelDelta > 1 || (levelDelta === 1 && isMergedRight)) {
|
||||||
// Multi-level downward jumps: route on the right side.
|
// Multi-level downward jumps: route on the right side.
|
||||||
const startX = source.x + source.width / 2;
|
const isLongDownward = levelDelta > 1;
|
||||||
const startY = source.y + sourceShift * 0.35 + laneSpread * 0.25;
|
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 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 verticalSpan = Math.abs(endY - startY);
|
||||||
const longRouteExtra =
|
const longRouteExtra =
|
||||||
Math.max(0, verticalSpan - 10) * 0.2 + (levelDelta - 1) * 1.2;
|
Math.max(0, verticalSpan - 10) * 0.2 + (levelDelta - 1) * 1.2;
|
||||||
const laneX =
|
const laneX =
|
||||||
|
mergedRightLaneX ??
|
||||||
Math.max(startX, endX) +
|
Math.max(startX, endX) +
|
||||||
6 +
|
6 +
|
||||||
Math.abs(sourceShift) +
|
Math.abs(sourceShift) +
|
||||||
longRouteExtra +
|
longRouteExtra +
|
||||||
laneSpread * 1.2;
|
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;
|
labelX = startX + 0.9;
|
||||||
labelY = startY - 1.5 - Math.abs(laneSpread) * 0.12;
|
labelY = startY - 1.5 - Math.abs(laneSpread) * 0.12;
|
||||||
labelAnchor = "start";
|
labelAnchor = "start";
|
||||||
@ -716,7 +842,9 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
|
|||||||
className={`visualiser-outcome-connector-line${edgeIntensityClass}${edgeIsDim ? " is-dim" : ""}`}
|
className={`visualiser-outcome-connector-line${edgeIntensityClass}${edgeIsDim ? " is-dim" : ""}`}
|
||||||
d={path}
|
d={path}
|
||||||
markerEnd={
|
markerEnd={
|
||||||
isMergedLeft ? undefined : "url(#visualiser-arrow)"
|
isMergedLeft || isMergedRight
|
||||||
|
? undefined
|
||||||
|
: "url(#visualiser-arrow)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{label && (
|
{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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user