Skip to content

feat(explain): dagre pipeline-graph layout (injected seam)#38

Merged
BorisTyshkevich merged 3 commits into
mainfrom
feat/explain-dagre-layout
Jun 25, 2026
Merged

feat(explain): dagre pipeline-graph layout (injected seam)#38
BorisTyshkevich merged 3 commits into
mainfrom
feat/explain-dagre-layout

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What

Replaces the hand-rolled layered layout for the Pipeline view with @dagrejs/dagre (maintained v3, MIT) — full Sugiyama: network-simplex ranking, crossing-minimization, Brandes–Köpf coordinates, and routed edge bend points. dagre is wired as an injected seam, exactly like app.Chart:

  • env.Dagre || win.dagre in createApp; main.js does import Dagre from '@dagrejs/dagre' and passes it in.
  • Our DOT parser (core/dot.js) and SVG drawer (ui/explain-graph.js) are unchanged.
  • New pure wrapper core/dot-layout.jsdagreLayout(dagre, graph) — takes the injected lib and returns the same {nodes,edges,width,height} shape the drawer already consumes (dagre node centres → top-left; dagre edge polylines).

Why it's better

On the antalya/ontime fact-dim-join pipeline:

Before (hand-rolled) After (dagre)
viewBox 4654 × 534 ~1300 × 800
shape 19 nodes in one row ~12 ranks, ≤5 wide
edges long diagonals through boxes routed with bend points

The graph reads as a proper query DAG: parallel read branches converge at JoiningTransform, then the sort/limit chain. With the fullscreen pan/zoom from #37, complex plans are now legible.

Cost

Bundle +39 KB (dagre's ESM build inlined; 367 → 407 KB) — a deliberate second runtime dependency (CLAUDE.md hard-rule 4), behind the seam pattern the rule prescribes.

Tests

  • core/dot-layout.js 100% (drives real dagre — pure, no DOM); core/dot.js 100% (parser only now); layoutGraph removed.
  • e2e harness loads dagre's self-contained dagre.esm.js from node_modules; the pipeline spec still asserts the vertical/parallel structure.
  • 807 unit tests pass; coverage gate holds. Verified live on antalya/ontime.

Deployed to otel + antalya for review. Builds on merged #37.

🤖 Generated with Claude Code

BorisTyshkevich and others added 3 commits June 25, 2026 15:02
Replace the hand-rolled layered layout with @dagrejs/dagre — full Sugiyama
(network-simplex ranking, crossing-minimization, Brandes–Köpf coordinates) with
routed edge bend points. dagre is injected like app.Chart (env.Dagre || win.dagre);
our DOT parser (core/dot.js) and SVG drawer (ui/explain-graph.js) are unchanged.
The pure wrapper core/dot-layout.js takes the injected dagre and returns the same
{nodes,edges,width,height} shape the drawer consumes.

On the antalya/ontime fact-dim-join pipeline this turns a 4654×534 strip (19 nodes
in one row, long diagonal edges) into a balanced ~1300×800 DAG (~12 ranks, ≤5 wide)
with cleanly routed edges. Bundle: +39 KB (dagre inlined).

- core/dot.js: drop layoutGraph (now dagre); keep parseDot.
- core/dot-layout.js (new): dagreLayout(dagre, graph) + nodeWidth, 100% covered.
- ui/explain-graph.js: buildPipelineSvg(rawText, dagre); renderExplainGraph(app, r).
- app.js: Dagre seam; main.js: import + inject; fake-app: inject real dagre.
- e2e: pipeline.html loads dagre's ESM build from node_modules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBS74oUsXarGkCRQKEFLu
…n layout test

Code review follow-ups for the dagre layout seam:
- README, CLAUDE.md (hard-rule 4), and build.mjs header now state both bundled
  runtime deps (Chart.js + @dagrejs/dagre), and the Pipeline section reflects that
  layout is delegated to dagre via core/dot-layout.js (DOT parse stays pure in dot.js).
- dot-layout.test.js now asserts the centre→top-left coordinate conversion and that
  edge points are finite {x,y} pairs, so a broken transform can't pass green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBS74oUsXarGkCRQKEFLu
… ⌘/Ctrl-wheel-zoom)

The inline pipeline pane used native scrollbars while fullscreen used drag/zoom —
inconsistent. Extract a shared attachPanZoom() used by both: drag to pan (grab
cursor), plain wheel to pan, ⌘/Ctrl+wheel to zoom at the cursor, double-click to
fit, and fit-on-render. The inline pane drops scrollbars (overflow:hidden) and
becomes the same pan/zoom surface as the overlay.

- explain-graph.js: attachPanZoom() shared by renderExplainGraph (inline) and
  openPipelineFullscreen (overlay, which adds the −/+/Fit buttons).
- styles.css: .explain-graph-view → overflow:hidden, cursor grab/grabbing, svg 100%.
- tests: assert ⌘/Ctrl+wheel zoom vs plain-wheel pan + double-click fit; e2e holds
  Control for the fullscreen zoom; inline now fitted (width 100%, fitted viewBox).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBS74oUsXarGkCRQKEFLu
@BorisTyshkevich BorisTyshkevich merged commit 6b0166e into main Jun 25, 2026
4 checks passed
@BorisTyshkevich BorisTyshkevich deleted the feat/explain-dagre-layout branch June 25, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant