Skip to content

Viewer: enforce minimum font-size readability threshold on zoom #6

@hugolytics

Description

@hugolytics

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)

  1. 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.
  2. Label visibility culling — hide labels whose rendered bounding box would be smaller than a legibility threshold; optionally show them on hover.
  3. Element spacing awareness — when zoomed out far enough that elements overlap, either cluster them or indicate density rather than rendering illegible overlapping boxes.
  4. 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

  • At fit-zoom on a dense diagram, primary node labels are legible (not culled, not overlapping).
  • Secondary labels / annotations are suppressed at low zoom and revealed as zoom increases.
  • No label ever renders below ~11 px on screen.
  • "Smart labels" mode is togglable (UI button or keyboard shortcut).
  • No regression on zoom, pan, fit, or reset behaviour.
  • Evaluated and decided on labelgun vs rbush-custom before implementation begins.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions