Skip to main content
A geom renderer owns the React paint for one (geom, coord) composition — the render half of a geom, opposite its compile half. Renderers paint observations and respond to hover; they don’t own layout, guides, or data-label placement. The registry is keyed on the (geom, coord) pair, so bar × cartesian (a column) and bar × polar (a pie wedge) are independent renderers that share no code. Adding one never touches the other.

defineGeomRenderer is dual-target

The same function binds a renderer two ways, depending on what you pass first:

A whole custom geom

Pass a Geom instance to pair both halves. The result carries the compile definition on .definition, so dropping it into plugins registers the geom and derives its typed builder method:

Render-only overrides

Pass a built-in geom name to rebind only the paint half. The built-in compile half keeps running — positions, stacking, scales, and axes are unchanged — so only the look differs. This is how you restyle bars without reinventing bar geometry:
The name is constrained to the built-in geom union, so overriding an unknown name is a compile-time error. To restyle a custom geom, rebind the definition you already hold rather than its name.

The contract

Every renderer implements a GeomRenderContract:
CoordKind
required
The coordinate system this contract paints under — 'cartesian', 'flip', or 'polar'. A geom binds one contract per coord.
(input) => ReactNode
required
Paints the observations. Receives { layer, coordSystem, isAnimated, formattingLocale }. The { fn, options: { overlay: true } } form paints into a screen-aligned portal instead — see Live geoms.
(input) => ReactNode
required
Paints the hover state for this layer’s observations. Receives the hovered primary hit plus its group and related hits, the coordSystem, and the panelRect.
(input) => ReactNode
required
Cross-layer hints — e.g. dots dropped on a line at the hovered x. Return null to opt out.
'crosshair' | 'band' | null
The positional guide drawn when this is the hovered layer: 'band' shades the hovered category (bars), 'crosshair' draws a rule at the hovered value (lines, areas). Omit or pass null to draw none (scatter points).
SwatchShape
The swatch shape shown for this geom in the legend and tooltip.
(input) => ReactNode
Optional repaint of the matched subset for the highlight overlay. Falls back to render when omitted. Override it when the plain grouped render would misrepresent a matched subset.
(input) => RenderHitTester
A per-cursor spatial query for a layout geom. Only consulted when the compile half declares spatialKind: 'render-hit-test'.

Reading scaled positions

The renderer reads coordinates off each Observation with the value readers — it never re-derives a position. The compiler already scaled them; the reader hands you the resolved value, and helpers convert it to the paint frame:
A reader returns null for a missing or unscaled value — guard for it and skip the observation rather than painting NaN. For a render-only override of a built-in geom, higher-level readers like getBarRectBounds(mainAxis, observation) hand you the geom’s normalized [0,1] bounds directly.

Hover

Hover has two render entry points, both in the contract:
  • renderHover repaints this layer’s hovered observation — usually the same one drawn bolder or haloed. Because positions come from the compiled observation, the highlight lands exactly over the base shape.
  • renderHoverCompanions paints hints on other layers at the hovered x. Return null when the observation is its own highlight.

Hit-testing layout geoms

Most geoms are hit-tested from their scaled positions — the compiler builds the index from spatialKind. But a layout geom computes its geometry with an algorithm render-side (Sankey ribbons, Treemap tiles, Voronoi cells), so the compiler can’t see the shapes to index them. Such a geom declares spatialKind: 'render-hit-test' on its compile half and supplies a hitTest factory on the contract. The factory runs once per data change and returns a per-cursor tester; the cursor arrives in panel-local [0,1] (top-left origin), and the tester returns the identity key of the observation under it, or null:
The renderer registers the tester on your behalf, so central hover, tooltips, and guides work the same as for a built-in geom. (useGeomHitTest is the underlying hook, but the contract’s hitTest is the path you write against.)

Live and draggable geoms

A geom that runs its own simulation or drag interaction — a force-directed graph — must own its pointer events. For that, render takes the overlay-hosted form: { fn, options: { overlay: true } }. Its function receives input.overlay with a pushHover to feed the central hover store and the panelRect for placement:
useGeomHover(layerId) is the lower-level push hook the overlay form wraps — reach for it only when the overlay form doesn’t cover your case.
  • Custom geoms — the compile half a renderer pairs with
  • Interactivity — the hover model renderers plug into
  • Slots — replace a region’s render without writing a geom