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:
coord
CoordKind
required
The coordinate system this contract paints under — 'cartesian', 'flip', or 'polar'. A geom binds one contract per coord.
render
(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.
renderHover
(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.
renderHoverCompanions
(input) => ReactNode
required
Cross-layer hints — e.g. dots dropped on a line at the hovered x. Return null to opt out.
guideMode
'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
SwatchShape
The swatch shape shown for this geom in the legend and tooltip.
renderHighlight
(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.
hitTest
(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