system.cssthe engine GitHub

Anatomy of a chart builder

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 logical W, 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 style inline-size: 100%; block-size: auto lets 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, so yp subtracts 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.5 on bubbles/areas so overlaps read; a full-opacity currentColor stroke 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 a font: declaration. Escape any user-supplied label with scEsc.
  • Compact number formatters. Small helpers for $1.2k, trimmed decimals, etc., so axis ticks and tooltips stay readable.