More work on the visualiser

This commit is contained in:
Colin Dawson 2026-02-25 23:47:19 +00:00
parent 64c601d8c8
commit 5852d1f67a

View File

@ -89,6 +89,24 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
return 0;
}
// If multiple predecessors, position task at the average of their columns
if (predecessors.length > 1) {
const predColumns = predecessors
.map((predGuid) => {
const predTask = byGuid.get(predGuid);
return predTask ? getColumn(predTask) : 0;
})
.filter((col) => col !== undefined);
if (predColumns.length > 0) {
const avgColumn =
predColumns.reduce((sum, col) => sum + col, 0) / predColumns.length;
const column = Math.round(avgColumn);
columnByGuid.set(guid, column);
return column;
}
}
// Get predecessor column and find which child index this is
const predTask = byGuid.get(predecessors[0]);
if (!predTask) {
@ -231,19 +249,130 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
if (numColumns <= 1) return [50];
return levelTasks.map((task) => {
const predecessors = (task.config.predecessors as string[]) ?? [];
const taskColumn =
taskColumnMap.current.get(task.config.guid as string) ?? 0;
if (predecessors.length > 1) {
const predColumns = predecessors
.map((predGuid) => taskColumnMap.current.get(predGuid))
.filter((col) => col !== undefined) as number[];
if (predColumns.length > 0) {
const avgPredColumn =
predColumns.reduce((sum, col) => sum + col, 0) / predColumns.length;
const gridCol = avgPredColumn - columnRange.min;
// Map averaged column to percentage for centered merges
return ((gridCol + 0.5) / numColumns) * 100;
}
}
const gridCol = taskColumn - columnRange.min;
// Map grid column to percentage (e.g., 2 columns: col 0 = 25%, col 1 = 75%)
return ((gridCol + 0.5) / numColumns) * 100;
});
};
const getPredecessorPositionsByColumn = (
levelTasks: CreateWorkflowTemplateVersion["tasks"][],
): number[] => {
const numColumns = columnRange.max - columnRange.min + 1;
if (numColumns <= 1) return [50];
const predColumns = new Set<number>();
levelTasks.forEach((task) => {
const preds = (task.config.predecessors as string[]) ?? [];
preds.forEach((predGuid) => {
const predColumn = taskColumnMap.current.get(predGuid);
if (predColumn !== undefined) {
predColumns.add(predColumn);
}
});
});
return Array.from(predColumns).map((predColumn) => {
const gridCol = predColumn - columnRange.min;
return ((gridCol + 0.5) / numColumns) * 100;
});
};
const renderConnector = (
count: number,
positions?: number[],
predecessorPositions?: number[],
currentLevelTasks?: CreateWorkflowTemplateVersion["tasks"][],
) => {
// Check if any task in this level has multiple predecessors from different columns
const hasMultiplePredecessorColumns =
currentLevelTasks &&
currentLevelTasks.some((task) => {
const preds = (task.config.predecessors as string[]) ?? [];
if (preds.length <= 1) return false;
const predColumns = preds
.map((predGuid) => taskColumnMap.current.get(predGuid))
.filter((col) => col !== undefined) as number[];
return new Set(predColumns).size > 1; // Multiple different columns
});
// If we have a merging scenario (multiple predecessors converging)
if (
hasMultiplePredecessorColumns &&
predecessorPositions &&
predecessorPositions.length > 1 &&
positions
) {
const minPredX = Math.min(...predecessorPositions);
const maxPredX = Math.max(...predecessorPositions);
return (
<svg
aria-hidden="true"
className="visualiser-connector-branch"
viewBox="0 0 100 40"
preserveAspectRatio="none"
>
{/* Vertical lines from each predecessor */}
{predecessorPositions.map((x, idx) => (
<line
key={`pred-${idx}`}
className="visualiser-connector-branch-line"
x1={x}
y1="0"
x2={x}
y2="14"
/>
))}
{/* Horizontal line connecting all predecessors */}
<line
className="visualiser-connector-branch-line"
x1={minPredX}
y1="14"
x2={maxPredX}
y2="14"
/>
{/* Vertical lines down to children */}
{positions.map((x, idx) => (
<g key={`child-${idx}`}>
<line
className="visualiser-connector-branch-line"
x1={x}
y1="14"
x2={x}
y2="28"
/>
<polygon
className="visualiser-connector-branch-arrow"
points={`${x - 1.5},28 ${x + 1.5},28 ${x},34`}
/>
</g>
))}
</svg>
);
}
if (count <= 1) {
// For a single child, use predecessor position if available
let startX = 50;
@ -431,7 +560,8 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
renderConnector(
levels[0].length,
getConnectorPositionsByColumn(levels[0]),
undefined,
getPredecessorPositionsByColumn(levels[0]),
levels[0],
)}
{levels.map((level, index) => (
<React.Fragment key={`level-${index}`}>
@ -461,6 +591,10 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
)?.length;
const numColumns = columnRange.max - columnRange.min + 1;
const predecessors = task.config.predecessors as
| string[]
| undefined;
let styleObj: React.CSSProperties = {};
if (isRootTask && numColumns > 1) {
@ -470,6 +604,28 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
display: "flex",
justifyContent: "center",
};
} else if (predecessors && predecessors.length > 1) {
// Tasks with multiple predecessors - span across predecessor columns
const predColumns = predecessors
.map((predGuid) => taskColumnMap.current.get(predGuid))
.filter((col) => col !== undefined) as number[];
if (predColumns.length > 1) {
const minPredCol = Math.min(...predColumns);
const maxPredCol = Math.max(...predColumns);
const startGridCol = minPredCol - columnRange.min + 1;
const endGridCol = maxPredCol - columnRange.min + 2;
styleObj = {
gridColumn: `${startGridCol} / ${endGridCol}`,
display: "flex",
justifyContent: "center",
};
} else {
// Fallback to normal positioning
const gridColumn = taskColumn - columnRange.min + 1;
styleObj = { gridColumn };
}
} else {
// Child tasks positioned in their specific column
const gridColumn = taskColumn - columnRange.min + 1;
@ -503,12 +659,14 @@ const VisualiserTab: React.FC<VisualiserTabProps> = ({
? renderConnector(
levels[index + 1].length,
getConnectorPositionsByColumn(levels[index + 1]),
getConnectorPositionsByColumn(levels[index]),
getPredecessorPositionsByColumn(levels[index + 1]),
levels[index + 1],
)
: renderConnector(1, undefined, undefined)}
: renderConnector(1, undefined, undefined, undefined)}
</React.Fragment>
))}
{levels.length === 0 && renderConnector(1)}
{levels.length === 0 &&
renderConnector(1, undefined, undefined, undefined)}
<div className="visualiser-node">
<div className="visualiser-node-content">
<span>End</span>