Start over #1

Closed
opened 2026-10-12 06:20:31 +00:00 by rpickler · 5 comments
Owner

This tool was a re-implementation of something I had written before, and it just didn't come out the same. The purpose was to visualize our backlog in jira and how projects flow and overlap. The visual concept was largely based off the london tube map.

The way it works:

  • All tasks in jira are given labels for the projects they're relevant to
  • Epics are kinda irrelevant, as a task in jira can be part of multiple projects
  • Tasks are prioritized relative to each other in the context of each label. Simply manually sorting them on the backlog with only that label displayed
  • Each label translates into a 'tube' line in the visual language of the london tube map
  • Each task is a station on that line
  • the order of stations (tasks) on the line (label) is defined by the priority
  • tasks with more than one label are interchange stations, like Oxford Circus

That gives the primary visualization, but there were some extras I also put in

  • Tasks in sprints were grouped together in a box
    • Those tasks were isolated before isolated before the prioritization ordering described above
  • The sprints would be displayed in order of execution
  • completed sprints were greyed out

I originally implemented this in python and graphviz, but graphviz is simply too limited to really achieve the look I want.

@hermes-bot Search around for existing tools that could create this visual. Come up with a plan for developing. Test some visuals and post them here. Do not implement the project, but make a development plan. Post the plan here when you're done

This tool was a re-implementation of something I had written before, and it just didn't come out the same. The purpose was to visualize our backlog in jira and how projects flow and overlap. The visual concept was largely based off the london tube map. The way it works: * All tasks in jira are given labels for the projects they're relevant to * Epics are kinda irrelevant, as a task in jira can be part of multiple projects * Tasks are prioritized relative to each other in the context of each label. Simply manually sorting them on the backlog with only that label displayed * Each label translates into a 'tube' line in the visual language of the london tube map * Each task is a station on that line * the order of stations (tasks) on the line (label) is defined by the priority * tasks with more than one label are interchange stations, like Oxford Circus That gives the primary visualization, but there were some extras I also put in * Tasks in sprints were grouped together in a box * Those tasks were isolated before isolated before the prioritization ordering described above * The sprints would be displayed in order of execution * completed sprints were greyed out I originally implemented this in python and graphviz, but graphviz is simply too limited to really achieve the look I want. @hermes-bot Search around for existing tools that could create this visual. Come up with a plan for developing. Test some visuals and post them here. Do not implement the project, but make a development plan. Post the plan here when you're done
Member

Research, tested visuals, and development plan

Per the ask: I searched the tool landscape, tested two visual prototypes, and drafted a plan. No project implementation — prototypes only (code + renders are in the repo under prototypes/, branch main).

1. Existing tools — survey

Tool Verdict
Graphviz Your prior art confirms it: hierarchical layout can't do controlled metro geometry (45° bends, per-line routing, station rings). Ruled out.
nicola/tubemaps (JS lib) Models London-tube data (lines/stations/connections), but only for pathfinding over a real geographic dataset — it doesn't lay out a synthetic map. Abandoned (2014).
bartromgens/metroflow Metro-map-style flowcharts on canvas — right visual language, explicitly pre-alpha, wrong domain (flowcharts, not prioritized lanes).
@dvhb/metro Requires a hand-authored SVG map + Angular. Not generative.
ELK / elkjs The strongest building block: layered layout with orthogonal edge routing, ports, compound nodes (sprint boxes!). Doesn't produce tube geometry (45° joins, interchange rings) out of the box, but could drive automatic lane ordering.
Mermaid / PlantUML / Gantt libs None express "same task on multiple lines with interchange semantics".

Conclusion: no off-the-shelf tool does this. The layout is a small custom metro-map drawing problem (there's real literature on it: abstract metro network layout, e.g. the Birmingham metro-map work — worth citing as design inspiration). The right architecture is a thin custom SVG renderer with a small layout pass; everything below is ~600 lines of code, not a framework job.

2. Tested visuals

I built a data-driven prototype renderer (sample backlog: 5 labels, 16 issues, 5 interchanges, 4 sprints with 2 done) and iterated twice:

v2 — dark theme, sprint boxes, greyed done sprints, interchange rings with connectors:

v2 dark

v3 — TfL light style, white interchange rings on each joined lane:

v3 light

Source (runnable, stdlib + cairosvg only): prototypes/proto.py (dark) and prototypes/proto_light.py (light). The geometry model from the issue maps 1:1 onto the rendering model:

  • lane (horizontal tube line) = label
  • station = issue; order along the lane = backlog rank when filtered to that label
  • interchange (ring on each joined lane + connector) = multi-label issue ("Oxford Circus")
  • rounded box per column = sprint, in execution order, dashed column = unprioritised backlog
  • done sprints rendered grey (line segments, stations, labels all desaturate)

Key thing the tests proved: the hard visual problems are station-label collisions (solved with alternating above/below placement) and interchange connector geometry (vertical link vs. 45° dog-leg — both viable, 45° looks more "tube"). Neither is possible cleanly in Graphviz; both are trivial in direct SVG.

3. Development plan

Stack: TypeScript + Vite, plain SVG rendering (d3 only for zoom/pan and scales if needed). Output: static map JSON + SPA, or server-rendered PNG/SVG snapshot. Keep the renderer pure-function (mapSpec → SVG) — the prototype already validates this shape.

Phase 1 — Jira data layer (1–2 days).

  • JQL per label to fetch issues; labels become lines.
  • Rank extraction is the crux: rank is only meaningful per filtered backlog view. Use the GreenHopper rank field (rank custom field or Rank composite) and sort client-side per label — the API's rank ordering matches "only that label displayed" if you sort by rank and treat epics as irrelevant (per the brief, epics are ignored entirely; a task's projects come from labels only).
  • Sprints: execution order from the board (sprint startDate/sequence); "done" from sprint state.
  • Cache to a JSON snapshot so the renderer never talks to Jira directly.

Phase 2 — Layout engine (2–4 days, the interesting part).

  • Lanes: one row per label. Column = sprint (execution order), then unprioritised-backlog column.
  • X within column = rank order within that label.
  • Multi-label task: primary lane (e.g. highest-priority label) fixes its Y; each additional label adds a ring + connector at the same X. Where two lines cross, offset X slightly (greedy push) to keep labels readable.
  • Lane ordering: minimise crossings — start with label co-occurrence clustering (labels that share many interchange tasks want to be adjacent); if it ever needs real optimisation, this is a textbook metro-map/seriation problem, and ELK's layered algorithm is a fallback for automatic lane assignment.
  • Collision pass for text (the alternating-placement trick from the prototype; a real label-rect overlap check upgrades it).

Phase 3 — Renderer polish (1–2 days).

  • TfL palette per label, 45° bends, interchange rings, sprint boxes with greyed-done treatment, legend.
  • Tooltips + click-through to the Jira issue; filter by label/sprint/assignee.

Phase 4 — Delivery (1 day).

  • Cron: pull Jira snapshot → regenerate map → publish. Static artifact keeps Jira credentials server-side; the map itself is just an SVG.

Total: roughly a week of focused work for v1; the layout engine is the only part with depth, and the prototype has already de-risked its geometry.

Open questions before implementation:

  1. Jira Cloud or Server? (determines rank field access and API auth)
  2. Rank-per-label vs one global rank: confirm sorting the full backlog and filtering-by-label reproduces your manual per-label ordering.
  3. Primary lane choice for a multi-label task when its labels disagree on priority — first label alphabetically, or highest-priority label?
## Research, tested visuals, and development plan Per the ask: I searched the tool landscape, tested two visual prototypes, and drafted a plan. No project implementation — prototypes only (code + renders are in the repo under `prototypes/`, branch `main`). ### 1. Existing tools — survey | Tool | Verdict | |---|---| | **Graphviz** | Your prior art confirms it: hierarchical layout can't do controlled metro geometry (45° bends, per-line routing, station rings). Ruled out. | | **nicola/tubemaps** (JS lib) | Models London-tube *data* (lines/stations/connections), but only for pathfinding over a real geographic dataset — it doesn't *lay out* a synthetic map. Abandoned (2014). | | **bartromgens/metroflow** | Metro-map-style flowcharts on canvas — right visual language, explicitly pre-alpha, wrong domain (flowcharts, not prioritized lanes). | | **@dvhb/metro** | Requires a hand-authored SVG map + Angular. Not generative. | | **ELK / elkjs** | The strongest *building block*: layered layout with orthogonal edge routing, ports, compound nodes (sprint boxes!). Doesn't produce tube geometry (45° joins, interchange rings) out of the box, but could drive automatic lane ordering. | | **Mermaid / PlantUML / Gantt libs** | None express "same task on multiple lines with interchange semantics". | **Conclusion:** no off-the-shelf tool does this. The layout is a small custom **metro-map drawing** problem (there's real literature on it: abstract metro network layout, e.g. the Birmingham metro-map work — worth citing as design inspiration). The right architecture is a thin custom SVG renderer with a small layout pass; everything below is ~600 lines of code, not a framework job. ### 2. Tested visuals I built a data-driven prototype renderer (sample backlog: 5 labels, 16 issues, 5 interchanges, 4 sprints with 2 done) and iterated twice: **v2 — dark theme, sprint boxes, greyed done sprints, interchange rings with connectors:** ![v2 dark](https://git.richardpickler.com/sees.ai/tube-map-planner/raw/branch/main/prototypes/v2_tube_map.png) **v3 — TfL light style, white interchange rings on each joined lane:** ![v3 light](https://git.richardpickler.com/sees.ai/tube-map-planner/raw/branch/main/prototypes/v3_tube_map.png) Source (runnable, stdlib + cairosvg only): [`prototypes/proto.py`](https://git.richardpickler.com/sees.ai/tube-map-planner/src/branch/main/prototypes/proto.py) (dark) and [`prototypes/proto_light.py`](https://git.richardpickler.com/sees.ai/tube-map-planner/src/branch/main/prototypes/proto_light.py) (light). The geometry model from the issue maps 1:1 onto the rendering model: - lane (horizontal tube line) = **label** - station = **issue**; order along the lane = backlog rank when filtered to that label - interchange (ring on each joined lane + connector) = **multi-label issue** ("Oxford Circus") - rounded box per column = **sprint, in execution order**, dashed column = unprioritised backlog - done sprints rendered grey (line segments, stations, labels all desaturate) Key thing the tests proved: the hard visual problems are **station-label collisions** (solved with alternating above/below placement) and **interchange connector geometry** (vertical link vs. 45° dog-leg — both viable, 45° looks more "tube"). Neither is possible cleanly in Graphviz; both are trivial in direct SVG. ### 3. Development plan **Stack:** TypeScript + Vite, plain SVG rendering (d3 only for zoom/pan and scales if needed). Output: static map JSON + SPA, or server-rendered PNG/SVG snapshot. Keep the renderer pure-function (`mapSpec → SVG`) — the prototype already validates this shape. **Phase 1 — Jira data layer (1–2 days).** - JQL per label to fetch issues; labels become lines. - **Rank extraction** is the crux: rank is only meaningful *per filtered backlog view*. Use the GreenHopper rank field (`rank` custom field or `Rank` composite) and sort client-side per label — the API's rank ordering matches "only that label displayed" if you sort by rank and treat epics as irrelevant (per the brief, epics are ignored entirely; a task's projects come from labels only). - Sprints: execution order from the board (sprint `startDate`/`sequence`); "done" from sprint state. - Cache to a JSON snapshot so the renderer never talks to Jira directly. **Phase 2 — Layout engine (2–4 days, the interesting part).** - Lanes: one row per label. Column = sprint (execution order), then unprioritised-backlog column. - X within column = rank order within that label. - Multi-label task: primary lane (e.g. highest-priority label) fixes its Y; each additional label adds a ring + connector at the same X. Where two lines cross, offset X slightly (greedy push) to keep labels readable. - Lane ordering: minimise crossings — start with label co-occurrence clustering (labels that share many interchange tasks want to be adjacent); if it ever needs real optimisation, this is a textbook metro-map/seriation problem, and ELK's layered algorithm is a fallback for automatic lane assignment. - Collision pass for text (the alternating-placement trick from the prototype; a real label-rect overlap check upgrades it). **Phase 3 — Renderer polish (1–2 days).** - TfL palette per label, 45° bends, interchange rings, sprint boxes with greyed-done treatment, legend. - Tooltips + click-through to the Jira issue; filter by label/sprint/assignee. **Phase 4 — Delivery (1 day).** - Cron: pull Jira snapshot → regenerate map → publish. Static artifact keeps Jira credentials server-side; the map itself is just an SVG. Total: roughly a week of focused work for v1; the layout engine is the only part with depth, and the prototype has already de-risked its geometry. **Open questions before implementation:** 1. Jira Cloud or Server? (determines rank field access and API auth) 2. Rank-per-label vs one global rank: confirm sorting the full backlog and filtering-by-label reproduces your manual per-label ordering. 3. Primary lane choice for a multi-label task when its labels disagree on priority — first label alphabetically, or highest-priority label?
Author
Owner

The visuals you made really stand out, but they don't really convey the meaning of when lines converge. The importance there is it shows when a particular task really needs attention. A lot of initiatives depend on it. Therefore, the version I made before drew the lines generally vertically, but they were allowed to curve and cross each other. See if you can draw something along those lines, but using similar data to what you had before. Also, do it with a hundred tasks, not just a handful.

@hermes-bot Just some drawings. This was nice. I'm just looking for another response here.

The visuals you made really stand out, but they don't really convey the meaning of when lines converge. The importance there is it shows when a particular task really needs attention. A lot of initiatives depend on it. Therefore, the version I made before drew the lines generally vertically, but they were allowed to curve and cross each other. See if you can draw something along those lines, but using similar data to what you had before. Also, do it with a hundred tasks, not just a handful. @hermes-bot Just some drawings. This was nice. I'm just looking for another response here.
Member

v4 — back to the vertical look: lanes run top→bottom through the sprints, and they're free to curve, cross and converge. Convergence is now the point: every time initiatives share a task, their lines pull off their columns and braid into one interchange; the more that pile on, the more lines converge at that spot. Stations where 3+ initiatives meet get an amber halo — the "this needs attention" signal.

v4 vertical curving lanes

Same data model as v2/v3 (labels = lanes, tasks = stations, rank order within each sprint band, grey = done sprints), but at 100 tasks: 56 single-label, 32 two-label, 9 three-label, 3 four-label → 44 interchanges, 10 hot stations, and ~186 line crossings measured in the geometry. The underpass casing (dark outline per line) keeps crossings readable where lines overlap.

Routing rules, for the record:

  • Each lane has a home column (canonical order = greedy co-occurrence clustering, so labels that share many tasks start adjacent and braids stay short).
  • At a multi-label task, its lanes converge on the cluster midpoint and stack in reversed order — the crossing is the convergence.
  • Lines are free curves (Catmull-Rom through waypoints); only time-monotonic (never flow backwards).

Source: prototypes/proto_v4.py (rendered v4_tube_map.svg/.png next to it; same stdlib + cairosvg recipe).

Trade-off I can see from this experiment: at 100 tasks the crossing density is the honest picture of the dependency pile-on, but it does make individual lanes hard to trace across a hot band — which is arguably exactly when you shouldn't be able to trace a single initiative through it. Two knobs if you want the map to stay traceable: thinner lines + wider columns, or dim non-interchange tasks in done sprints even further so the braids dominate.

No implementation — drawings only, as asked.

v4 — back to the vertical look: lanes run top→bottom through the sprints, and they're free to curve, cross and converge. Convergence is now the point: every time initiatives share a task, their lines pull off their columns and braid into one interchange; the more that pile on, the more lines converge at that spot. Stations where 3+ initiatives meet get an amber halo — the "this needs attention" signal. ![v4 vertical curving lanes](https://git.richardpickler.com/sees.ai/tube-map-planner/raw/branch/main/prototypes/v4_tube_map.png) Same data model as v2/v3 (labels = lanes, tasks = stations, rank order within each sprint band, grey = done sprints), but at **100 tasks**: 56 single-label, 32 two-label, 9 three-label, 3 four-label → 44 interchanges, 10 hot stations, and ~186 line crossings measured in the geometry. The underpass casing (dark outline per line) keeps crossings readable where lines overlap. Routing rules, for the record: - Each lane has a home column (canonical order = greedy co-occurrence clustering, so labels that share many tasks start adjacent and braids stay short). - At a multi-label task, its lanes converge on the cluster midpoint and stack in reversed order — the crossing *is* the convergence. - Lines are free curves (Catmull-Rom through waypoints); only time-monotonic (never flow backwards). Source: [`prototypes/proto_v4.py`](https://git.richardpickler.com/sees.ai/tube-map-planner/src/branch/main/prototypes/proto_v4.py) (rendered `v4_tube_map.svg/.png` next to it; same stdlib + cairosvg recipe). Trade-off I can see from this experiment: at 100 tasks the crossing density is the honest picture of the dependency pile-on, but it does make individual lanes hard to trace across a hot band — which is arguably exactly when you shouldn't be able to trace a single initiative through it. Two knobs if you want the map to stay traceable: thinner lines + wider columns, or dim non-interchange tasks in done sprints even further so the braids dominate. No implementation — drawings only, as asked.
Author
Owner

Much closer this time around. Let's do one more iteration. The interchange stations should be drawn as a single circle. Don't make a separate circle for each line. And see if you can straighten out those lines a bit. Catmull-rom is possibly not the best choice. Points close together can cause strange tangents. bezier spline would probably do better.

Much closer this time around. Let's do one more iteration. The interchange stations should be drawn as a single circle. Don't make a separate circle for each line. And see if you can straighten out those lines a bit. Catmull-rom is possibly not the best choice. Points close together can cause strange tangents. bezier spline would probably do better.
Member

v5 — both fixes in:

1. Single interchange circle. All lines of a multi-label task now converge on one shared centre point and one white circle is drawn over the join (TfL style) — no more per-line rings. The braid visibly disappears into the station. 44 interchanges, 44 circles (checked programmatically against the SVG).

2. Bezier instead of Catmull-Rom. You were right about the tangents. Every Catmull-Rom tangent derives from the neighbouring waypoints, and with stations only 22px apart vertically those tangents got wild. The new curve is a cubic bezier chain whose control arms are always vertical (the time axis) with length clamped to half the segment — so tangent at a station no longer depends on point spacing, and consecutive same-column stations render as exactly straight verticals.

v5 single-circle interchanges, bezier lanes

Straightness, measured over all 167 curve segments (same seed, same 100 tasks — prototypes/compare_v4_v5.py samples both chains and compares):

metric v4 Catmull-Rom v5 bezier
max deviation off the straight chord 353 px 38 px
mean deviation 11.3 px 5.3 px
segments with horizontal direction reversal (loops/S-wiggles) 78 0

So: lines between stations are now near-straight sweeps with one controlled lane-change curve each, and the geometry can't loop no matter how close the points get.

Source: prototypes/proto_v5.py (renders v5_tube_map.svg/.png alongside).

One honest trade-off of the single circle: at an interchange you can no longer see which line is which inside the station — the lines visually terminate at the ring. That's how the real Tube map does it, so arguably correct, but if you want traceability through hot stations, an option is to colour the circle's stroke with a pie of the converging line colours. Still drawings-only, as instructed.

v5 — both fixes in: **1. Single interchange circle.** All lines of a multi-label task now converge on one shared centre point and one white circle is drawn over the join (TfL style) — no more per-line rings. The braid visibly disappears *into* the station. 44 interchanges, 44 circles (checked programmatically against the SVG). **2. Bezier instead of Catmull-Rom.** You were right about the tangents. Every Catmull-Rom tangent derives from the *neighbouring* waypoints, and with stations only 22px apart vertically those tangents got wild. The new curve is a cubic bezier chain whose control arms are always **vertical** (the time axis) with length clamped to half the segment — so tangent at a station no longer depends on point spacing, and consecutive same-column stations render as exactly straight verticals. ![v5 single-circle interchanges, bezier lanes](https://git.richardpickler.com/sees.ai/tube-map-planner/raw/branch/main/prototypes/v5_tube_map.png) Straightness, measured over all 167 curve segments (same seed, same 100 tasks — [`prototypes/compare_v4_v5.py`](https://git.richardpickler.com/sees.ai/tube-map-planner/src/branch/main/prototypes/compare_v4_v5.py) samples both chains and compares): | metric | v4 Catmull-Rom | v5 bezier | |---|---|---| | max deviation off the straight chord | 353 px | 38 px | | mean deviation | 11.3 px | 5.3 px | | segments with horizontal direction reversal (loops/S-wiggles) | 78 | 0 | So: lines between stations are now near-straight sweeps with one controlled lane-change curve each, and the geometry can't loop no matter how close the points get. Source: [`prototypes/proto_v5.py`](https://git.richardpickler.com/sees.ai/tube-map-planner/src/branch/main/prototypes/proto_v5.py) (renders `v5_tube_map.svg/.png` alongside). One honest trade-off of the single circle: at an interchange you can no longer see *which* line is which inside the station — the lines visually terminate at the ring. That's how the real Tube map does it, so arguably correct, but if you want traceability through hot stations, an option is to colour the circle's stroke with a pie of the converging line colours. Still drawings-only, as instructed.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sees.ai/tube-map-planner#1
No description provided.