import type { SpecDataProcessing, DagNode } from "@/lib/types"; /** * Renders a data-processing pipeline as a DAG: stages are columns (left→right), nodes are boxes stacked * within their stage, and dependency edges are SVG connectors. Node order carries no meaning — the edges do. * Edge kinds are color-coded (sequential/branch/fan_in); per-cell vs bulk scope and viz-only terminals are * styled distinctly. */ const COLW = 190; const ROWH = 64; const NODEW = 168; const NODEH = 48; const PADX = 4; const HEADER = 22; const GUTTER = COLW - NODEW; // empty lane between two node columns const EDGE_COLOR: Record = { sequential: "var(--color-border)", branch: "#f59e0b", fan_in: "#0ea5e9", }; /** * Orthogonal, gutter-routed connector from the right border of `a` to the left border of `b`, with * lightly rounded corners. Anchors sit on the box borders (never inside a box); vertical travel runs * in a column gutter rather than diagonally across the boxes between the two nodes. Any horizontal * segment that still grazes an intervening box is occluded by the opaque node layer drawn on top. */ function edgePath(a: { x: number; y: number }, b: { x: number; y: number }): string { const sx = a.x + NODEW; // exit at the source's right border const sy = a.y + NODEH / 2; const tx = b.x; // enter at the target's left border const ty = b.y + NODEH / 2; if (Math.abs(ty - sy) < 1) return `M ${sx} ${sy} L ${tx} ${ty}`; // same row → straight // vertical lane: in the gutter just before the target column for forward edges, just past the // source for same-column / backward edges — always a clear gutter, never over a box. const midx = tx - sx > GUTTER ? tx - GUTTER / 2 : tx > sx ? (sx + tx) / 2 : sx + GUTTER / 2; const v = ty > sy ? 1 : -1; const h1 = midx > sx ? 1 : -1; const h2 = tx > midx ? 1 : -1; const r = Math.min( 6, Math.abs(ty - sy) / 2, Math.abs(midx - sx) || 6, Math.abs(tx - midx) || 6, ); return [ `M ${sx} ${sy}`, `L ${midx - h1 * r} ${sy}`, `Q ${midx} ${sy} ${midx} ${sy + v * r}`, `L ${midx} ${ty - v * r}`, `Q ${midx} ${ty} ${midx + h2 * r} ${ty}`, `L ${tx} ${ty}`, ].join(" "); } export function DataProcessingDag({ dp }: { dp: SpecDataProcessing }) { const nodes = dp.nodes ?? []; const edges = dp.edges ?? []; if (nodes.length === 0) return null; // stage ids in declared order, else discovered order const stageDefs = dp.stages ?? []; const declared = stageDefs.map((s) => s.id); const discovered = Array.from(new Set(nodes.map((n) => n.stage ?? "_"))); const stageIds = declared.length ? declared : discovered; // any node whose stage isn't declared falls into a trailing "_" column for (const s of discovered) if (!stageIds.includes(s)) stageIds.push(s); const stageLabel = new Map(stageDefs.map((s) => [s.id, s.label])); const perStage = new Map(); for (const id of stageIds) perStage.set(id, []); for (const n of nodes) perStage.get(n.stage && perStage.has(n.stage) ? n.stage : stageIds[stageIds.length - 1])!.push(n); const pos = new Map(); stageIds.forEach((sid, si) => { (perStage.get(sid) ?? []).forEach((n, ni) => pos.set(n.id, { x: si * COLW, y: HEADER + ni * ROWH })); }); const maxRows = Math.max(1, ...stageIds.map((s) => perStage.get(s)?.length ?? 0)); const width = Math.max(NODEW, (stageIds.length - 1) * COLW + NODEW) + PADX; const height = HEADER + maxRows * ROWH + 8; return (
{/* edges */} {edges.map((e, i) => { const a = pos.get(e.from); const b = pos.get(e.to); if (!a || !b) return null; const c = EDGE_COLOR[e.kind ?? "sequential"] ?? EDGE_COLOR.sequential; return ( ); })} {/* stage headers */} {stageIds.map((sid, si) => stageLabel.get(sid) ? (
{stageLabel.get(sid)}
) : null, )} {/* nodes */} {nodes.map((n) => { const p = pos.get(n.id); if (!p) return null; return (
{n.label} {(n.tool || n.scope) && ( {n.tool} {n.tool && n.scope ? " · " : ""} {n.scope === "per_cell" ? "per-cell" : n.scope === "bulk" ? "bulk" : ""} )}
); })}
); }