Core compute returns semantic regions, not raw drawing primitives
Every `compute*` function returns a model with named semantic regions — `headerStats`, `scaleBar`, `plot`, `legend`, `emptyState` — not a flat list of shapes. Layout policy lives in compute; renderers paint regions by name. New renderers (canvas, Figma, static export) plug in without re-deriving layout.
Context
Two candidate architectures for a chart library:
-
Draw-list core — compute returns a flat list of SVG primitives (
<path>,<circle>,<text>). Renderers paint the list. Simplest possible API, but every consumer re-derives layout; a Figma plugin can’t reposition the legend without parsing shapes; accessibility roles (header, plot, legend) are lost. -
Semantic regions — compute returns a structured model:
headerStats,scaleBar,plot,legend,emptyState. Renderers map regions to outputs. Layout policy is centralised; accessibility and cross-renderer parity are free; new renderers drop in.
Decision
Adopt semantic regions as the ComponentModel<TPlot, TLegend> contract.
Every chart returns the same top-level shape, even if regions are null.
React renderers paint by region name. Future renderers (static export,
Figma plugin, canvas) consume the same shape. Layout is a compute concern,
not a renderer concern.
Consequences
- Static export composition (e.g. a scouting report PDF with multiple charts) can position regions from the model without rendering first.
- Consolidated legends across multiple charts become a composition-layer concern, not a per-chart hack — all legend regions share the same contract.
- Renderers are thin. Adding a canvas renderer doesn’t require re-writing compute; it needs one region → canvas mapping.
- New regions (e.g.
tooltip,brushControls) extend the model type; renderers unaware of them drop them silently, without crashing. - Empty state is a first-class region, not a render-layer branch — the
empty shell decision (
empty-state-shell-preserved) composes cleanly with this.