Skip to content
Draft. This essay is a stub or a work in progress — read it as a sketch, not settled documentation.

Architecture Overview

This is the map. It sketches how a GoFish chart goes from a declarative description to an SVG, and points at the essays that cover each part in depth.

A chart is a tree

GoFish is declarative: you never tell it to draw — you describe what the chart is, and the engine works out the rest. That description is an abstract syntax tree of GoFishNodes. Two kinds of node make up the tree:

  • Marks — the things you can see: rect, ellipse, line, ribbon, text.
  • Graphical operators — composition: stackX, spread, layer, connect, coord, and so on. An operator arranges its children; it has no appearance of its own.

The fluent frontendchart(data).flow(...).mark(...) — is sugar. It desugars into exactly this tree, built from the mark and operator factories.

Three passes

Rendering the tree is not one traversal but three, each answering a different question. A node implements only the passes it participates in.

1 · Domain inference. Before anything can be sized, the engine works out the data ranges in play — the domains. GoFish distinguishes a node's underlying space (is this dimension continuous, ordinal, or undefined? — and if continuous, is it a position, a size, or a difference, which since #586 are the three origin states of one continuous kind) and infers position and size domains separately. This pass leans on the monotonic algebra to track, symbolically, how each subtree depends on the data — and to prune subtrees that don't depend on it at all.

2 · Layout. With domains known, each node computes its size. Layout dispatches on the continuous space's origin state: a SIZE (free) dimension resolves through the monotonic machinery, a POSITION (anchored) dimension through position scales. Bounding boxes (the bbox model) are the common currency.

3 · Placement & render. Final absolute positions are assigned, then the tree is turned into pixels in two sub-passes: lower — walk the baked scenegraph and emit a flat display list of positioned primitives in absolute pixels — and paint — a single backend turns that IR into output (SVG today). No shape emits SVG itself; each owns a lower() that describes it as a backend-agnostic display-list item. The live SVG backend is reactive — it runs through SolidJS — so a chart can update without a full rebuild. The coord operator is the notable special case: it flattens its subtree into a flat, absolutely-positioned list before applying its coordinate transform. A coordinate transform is parameterized (polar()/clock() take innerRadius/centralAngle/startAngle/…) and may declare axis-name aliases (theta/r); a small top-down pass before layout resolves those aliases into the canonical x/y/w/h facets (see Pass 5.5). A second top-down pass, resolveEmbedding (a GoFishNode method wired into the pipeline before layout), authors each dim's embedded flag — whether a coord warps that axis's extent into an arc/wedge (point/line/area) — gating a mark's own size on whether its measure matches the axis it sits in (see Pass 8.5).

The placement pass treats every real node as a Placeable: it can expose its dimensions, accept final placement pins, report whether an axis is already data-positioned, and answer where a local anchor (min, center, max, or baseline) sits inside its own box. That localAnchor method is what lets the constraint solver express alignment and distribution as equations over sibling boxes rather than as order-sensitive imperative writes.

Chrome is just more tree. Axes, legends, and axis titles are not privileged render-time fixtures. Before layout, elaboration passes rewrite each of them into ordinary marks and constraints — Layer-wrapped Rect/Text nodes seated by align/distribute/position — so chrome flows through the same three passes as the data marks. Axes are recursive: every node can render its own resolved underlying-space type as an axis, so a nested grouping (a grouped or faceted chart) renders one ordinal axis per level — an outer lake axis and a per-facet species axis — each living at its own tree node, the way faceting already nests. Each axis names itself off its own space's measure (a continuous axis by its unit, an ordinal axis by its grouping field), so an axis title is derived from the data model, not a syntactic hint. The orchestrator (gofish.tsx) then sizes the SVG off the laid-out tree's measured extent on all four sides — the legend's overhang past the content, the axis/title gutters past the origin, and any content a constraint seated beyond the canvas (a marginal histogram's bands above and to the right of a scatter); there are no fixed chrome margins. See Axes (which also covers axis titles) and Legends.

The full, code-level walkthrough of all three passes is Layout & Render Passes.

Cross-cutting machinery

A few systems thread through every pass rather than belonging to one:

  • Contexts. scopeContext, scaleContext, keyContext carry variable scoping, color/axis scales, and named-element tracking down the tree.
  • Coordinate transforms. linear, polar, bipolar, wavy, clock — pluggable mappings from one plane to another, applied during render.
  • Names & scoping. Marks can be named and referenced across charts via ref(name) (one node) or selectAll(name) (many); scoping is deliberately hygienic.

Where to go next