Anatomy of a chart builder
A builder is a plain Go function (func fooSVG(data …) string) using a
strings.Builder and fmt.Fprintf. Structure it in back-to-front layers, each
wrapped in a <g> that sets --fg once:
viewBox 0 0 W H (fixed logical units; the SVG scales to its container)
└─ root <svg> style="inline-size: 100%; block-size: auto; --fg: -0.5;" role="img" aria-label="…"
├─ <g> gridlines --fg: -0.13 stroke=currentColor
├─ <g> axes --fg: -0.4 stroke=currentColor
├─ foLabel(...) labels (foreignObject, --type: -2 / --fg: -0.8 built in)
└─ data marks --fg: 1 via hueStyle(idx, n), fill-opacity for overlap
Conventions that keep charts consistent and legible:
- Fixed
viewBox, fluid render. Pick logicalW, H(shipped sizes: 720×380 for the Metrics stacks, 460×240 shelf bars, 240×60 sparkline) and reserve margins (ml, mr, mt, mb) for labels. The root styleinline-size: 100%; block-size: autolets it scale to any container while the math stays in fixed units. - Scale functions. Write small closures
xp(v),yp(v)that map a data value into pixel space. Remember SVG y grows downward, soypsubtracts from the bottom. Add ~8% headroom to max values so marks don't touch the frame. - Area-proportional sizing. For bubbles, radius ∝
sqrt(value/max)so area (not radius) encodes magnitude — radius scaling lies about the data. Keep a floor (r ≥ 4) so tiny/zero values stay visible. - Translucent marks.
fill-opacity: 0.4–0.5on bubbles/areas so overlaps read; a full-opacitycurrentColorstroke gives each mark a crisp edge. - Native tooltips. Put a
<title>…</title>inside each mark — free hover text, no JS. - Text:
foLabel(x, y, w, h, align, inner)— never<text>, never afont:declaration. Escape any user-supplied label withscEsc. - Compact number formatters. Small helpers for
$1.2k, trimmed decimals, etc., so axis ticks and tooltips stay readable.