Skip to main content
A custom geom is a new kind of shape a chart can draw. It has two halves: a compile half — a Geom definition that declares the geom’s aesthetics and places its observations in data space — and a render half that paints them. This page covers the compile half; the geom renderers page covers paint. The guiding rule: the compiler owns every position, the renderer invents none. Your geom declares which position roles it needs, writes their values in data units, and the compiler scales them onto the axes. An observation then stays anchored through any domain change — zoom, a log scale, a flipped coordinate — because its coordinates were resolved by the same scales as every other geom.

Anatomy of a geom

Extend the Geom class, parameterised by its params type. Here’s a lollipop — a dot on a stem dropped to the baseline:

What the definition declares

type
string
required
The geom’s name, written as const. It becomes the typed builder method (kit.geom.lollipop) and the renderer registry key.
defaultParams
object
Default values for the geom’s styling params. The type parameter on Geom<Params> types both these and the params the builder accepts.
positionRoles
readonly PositionRole[]
required
The positions the geom occupies, each { axis, role, valueKind }. A 'point' role is a single coordinate on its axis; 'min' / 'max' form an interval (like a bar or area); 'scalar' is a magnitude. A role may name a custom positional aes — the lollipop’s max role is exposed as the y aesthetic. Write the array as const so the typed builder can constrain aes to exactly these aesthetics.
aesthetics
readonly GeomAesthetic[]
The visual and data aesthetics the geom reads beyond position — here, color. Each is { kind, name }, with kind one of 'visual' (trained through a visual scale, like color or size) or 'data' (a raw input a layout geom hands to its algorithm).
supportedCoordTypes
readonly CoordType[]
Which coordinate systems the geom compiles under. The lollipop is cartesian-only.
spatialKind
'buckets' | 'rects' | 'points' | 'noop' | 'render-hit-test'
How the compiler builds the hover hit-test index from the geom’s scaled positions. 'buckets' groups by x (lines, areas), 'rects' uses rectangular bounds (bars), 'points' uses point proximity, 'noop' opts out. Use 'render-hit-test' for a layout geom whose geometry the compiler can’t see.

The compile step

compile(input) receives the layer’s data (a Dataset) and returns a CompiledGeom{ data, mapping }. This is where the geom writes any positions it derives rather than reads from the mapping. The lollipop’s baseline isn’t in the data, so compile adds it as a constant column in data units; the compiler then scales yMin and yMax through the shared y-scale like any other position. Do the minimum here: derive positions, and leave scaling, stacking, and axis placement to the pipeline. A geom that computes screen pixels in compile has crossed the compile/render seam.

Registering and using it

The compile half pairs with a render half through defineGeomRenderer, and the paired result goes into the plugins array. From there the typed builder method appears, constrained to exactly the aesthetics and params the definition declared:
A native spec doesn’t auto-infer position scales the way the chart-type recipes do — declare scale.x() and scale.y() explicitly, or the positions resolve to NaN. See Scales.