Problem
At default zoom / fit-to-screen, dense D2 diagrams are unreadable — labels are tiny, elements overlap visually, and there is no way to understand the structure without zooming in manually. Screenshot evidence: diagram with ~20+ nodes renders at fit-zoom where nothing is legible.
This is fundamentally not just a font-size problem. It is a level-of-detail problem: the viewer needs to be smart about which elements to show, at what size, and with how much spacing — depending on the current zoom level and the available viewport area.
Expanded scope (beyond font clamping)
- Semantic zoom / Level of Detail (LOD) — at low zoom, suppress or collapse low-priority elements (edge labels, nested annotations, secondary nodes); at high zoom, reveal full detail.
- Label visibility culling — hide labels whose rendered bounding box would be smaller than a legibility threshold; optionally show them on hover.
- Element spacing awareness — when zoomed out far enough that elements overlap, either cluster them or indicate density rather than rendering illegible overlapping boxes.
- Minimum font-size floor — even when elements are shown, clamp rendered font size to a readable minimum (original scope).
Constraint: D2 produces a static SVG
DiaScope renders a pre-computed SVG from D2. We cannot re-run the D2 layout at runtime. Any LOD logic must operate as a post-processing pass on the SVG DOM at render/zoom time.
Candidate libraries to evaluate (rather than reinvent)
| Library |
What it brings |
Notes |
labelgun |
Label collision detection & priority-based hiding — widely used in mapping |
Designed for exactly this: hide overlapping labels, keep high-priority ones |
rbush |
Fast R-tree spatial index for 2D bounding-box collision queries |
Lightweight; powers labelgun and mapbox; useful for building custom culling |
d3-zoom |
Zoom transform + event hooks where LOD logic can be wired |
Already likely in play; the zoom event is the right hook for LOD updates |
@interactjs/interact |
Alternative if more gesture control is needed |
Probably overkill |
| Cartography / MapLibre label placement |
Art-of-the-state label placement from the mapping world |
Too heavy to adopt wholesale, but algorithms are worth studying |
Recommended starting point: rbush for spatial indexing + a custom SVG post-processor that culls/shows <text> and <g> elements based on their rendered bounding box at the current zoom transform. labelgun is worth prototyping if the rbush approach feels like too much custom logic.
Proposed behaviour (revised)
- At zoom-to-fit / low zoom: apply LOD pass — hide labels below legibility threshold, optionally replace dense clusters with a summary indicator.
- At medium zoom: show primary labels; suppress secondary/nested annotations.
- At high zoom (zoomed in): show everything; clamp font size to minimum floor so nothing is ever tinier than ~11 px rendered.
- All of this behind a "Smart labels" toggle — off means pure geometric SVG scaling (current behaviour).
Open questions
- Which SVG elements map to "primary" vs "secondary" in D2 output — needs analysis of D2 SVG structure.
- Should culled labels be replaced with a dot/indicator, hidden entirely, or shown on hover?
- Does
labelgun work cleanly against SVG getBoundingClientRect or does it need a coordinate transform layer?
- Should the LOD thresholds be fixed, derived from container size, or user-configurable?
Acceptance criteria
Problem
At default zoom / fit-to-screen, dense D2 diagrams are unreadable — labels are tiny, elements overlap visually, and there is no way to understand the structure without zooming in manually. Screenshot evidence: diagram with ~20+ nodes renders at fit-zoom where nothing is legible.
This is fundamentally not just a font-size problem. It is a level-of-detail problem: the viewer needs to be smart about which elements to show, at what size, and with how much spacing — depending on the current zoom level and the available viewport area.
Expanded scope (beyond font clamping)
Constraint: D2 produces a static SVG
DiaScope renders a pre-computed SVG from D2. We cannot re-run the D2 layout at runtime. Any LOD logic must operate as a post-processing pass on the SVG DOM at render/zoom time.
Candidate libraries to evaluate (rather than reinvent)
labelgunrbushd3-zoomzoomevent is the right hook for LOD updates@interactjs/interactRecommended starting point:
rbushfor spatial indexing + a custom SVG post-processor that culls/shows<text>and<g>elements based on their rendered bounding box at the current zoom transform.labelgunis worth prototyping if the rbush approach feels like too much custom logic.Proposed behaviour (revised)
Open questions
labelgunwork cleanly against SVGgetBoundingClientRector does it need a coordinate transform layer?Acceptance criteria
labelgunvsrbush-custom before implementation begins.