> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Extending

The engine ships with a fixed set of built-in geoms, stats, and transforms — but the grammar is open. You can register your own and they become first-class: a typed builder method, compiled through the same pipeline, painted through the same renderer registry. This is the advanced surface; most charts never need it.

There are two ways in. **Plugins** extend the grammar itself — new geoms, stats, and transforms that compile and render like the built-ins. **Slots** are lighter: they replace how one *region* of the chart renders (a header, legend, or tooltip) without touching the grammar. Most of this section is about plugins; slots are a self-contained escape hatch on the renderer.

<Note>
  Before writing a plugin, check whether a [render-only
  override](/sdk-next/extending/geom-renderers#render-only-overrides) or an
  existing geom composed differently gets you there. A new geom is the right
  tool for a genuinely new shape, not a restyle of an existing one.
</Note>

## One array, both halves

Everything you add is registered through a single `plugins` array. Passing it once seeds two things at once — what you can *write* (the typed builder methods) and what can *compile and render* — so the authoring surface and the runtime can never drift apart.

```tsx theme={null}
import { createGraphyKit, defineGeomRenderer } from '@graphysdk/react-renderer';

const lollipop = defineGeomRenderer(new LollipopGeom(), {
  coord: 'cartesian',
  render: ({ layer }) => <LollipopMarks layer={layer} />,
  renderHover: ({ primary }) => (
    <LollipopMark observation={primary.observation} isHovered />
  ),
  renderHoverCompanions: () => null,
});

const kit = createGraphyKit({ plugins: [lollipop] });
// kit.geom.lollipop(...) now exists and is typed from the definition
```

`LollipopGeom` is the [compile half](/sdk-next/extending/custom-geoms); the object literal is the [render contract](/sdk-next/extending/geom-renderers). A stat or a transform has no render half, so it goes into the same array on its own.

## Two entry points

<ParamField path="createGraphyKit" type="react-renderer">
  The ergonomic one. Returns the typed builder (`geom` / `stat` / `transform` /
  `scale` / `coord` / `createSpec` / `pipe`) **and** a `GraphProvider` already
  bound to the same plugins. Reach for this in a React app.
</ParamField>

<ParamField path="createGraphyBuilder + <GraphProvider plugins>" type="viz-engine">
  The primitives `createGraphyKit` wraps. Use `createGraphyBuilder({ plugins })` for the headless authoring surface and
  pass the *same array* to `<GraphProvider plugins={...}>`. Choose this for framework-agnostic or advanced wiring.
</ParamField>

Both take the same `plugins` array. The kit is pure sugar — it calls `createGraphyBuilder` and pre-binds a provider, nothing more.

## What you can add

| Extension                | Adds                                      | Halves                                                     |
| ------------------------ | ----------------------------------------- | ---------------------------------------------------------- |
| **Custom geom**          | A new kind of shape                       | Compile half (`Geom`) + render half (`defineGeomRenderer`) |
| **Render-only override** | A restyle of a built-in geom's paint      | Render half only — geometry unchanged                      |
| **Custom stat**          | A per-layer data reshaping before mapping | Compile half only                                          |
| **Custom transform**     | A dataset reshaping                       | Compile half only                                          |

A geom has two halves because it both *places* observations (compile) and *paints* them (render). Stats and transforms only touch data, so they're compile-only — no renderer. A stat is a `Stat` subclass, a transform a `TransformStrategy` object; each goes into `plugins` on its own and earns a typed builder method (`kit.stat.<type>()`, `kit.transform.<transformType>()`) beside the built-in [statistics](/sdk-next/advanced/statistics) and [transforms](/sdk-next/advanced/transforms).

<CardGroup cols={2}>
  <Card title="Slots" icon="puzzle-piece" href="/sdk-next/extending/slots">
    Replace a region's render — no plugin, no geom.
  </Card>

  <Card title="Custom geoms" icon="shapes" href="/sdk-next/extending/custom-geoms">
    Define a new shape with `Geom` — position roles, aesthetics, and the compile
    step.
  </Card>

  <Card title="Geom renderers" icon="paintbrush" href="/sdk-next/extending/geom-renderers">
    `defineGeomRenderer`, render-only overrides, hit-testing, and hover.
  </Card>

  <Card title="Statistics" icon="calculator" href="/sdk-next/advanced/statistics">
    The layer-scoped summaries a `stat` computes — the vocabulary a custom
    `Stat` joins.
  </Card>

  <Card title="Transforms" icon="table" href="/sdk-next/advanced/transforms">
    The dataset reshapings a `transform` applies — the vocabulary a custom
    `TransformStrategy` joins.
  </Card>
</CardGroup>

## Diagnostics

A plugin mistake surfaces as a `VizDiagnostic` rather than a throw, so the chart keeps rendering around it. All but one are warnings, and several are the only signal that an otherwise-silent plugin is broken — read them before reaching for a debugger.

| Code                               | Severity | Fires when                                                                                                                            |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `MISSING_GEOM_RENDERER`            | error    | A compile definition is in `plugins` with no render half registered for its `type`                                                    |
| `MISSING_GEOM_RENDERER`            | warning  | A compiled layer's `(geom, coord)` pair resolves to no renderer under the chart's coord system — the layer paints nothing             |
| `DUPLICATE_REGISTERED_TYPE`        | warning  | Two definitions claim one `(kind, key)`, or two render-only overrides claim one `(geom, coord)`; the last in the array wins           |
| `MISSING_RENDER_HIT_TEST`          | warning  | A `'render-hit-test'` layer whose renderer supplies neither a `hitTest` factory nor an overlay render — the layer has no hover        |
| `RENDER_HIT_TEST_IDENTITY`         | warning  | A `'render-hit-test'` geom left on the default `identityKey: 'x-group'`, or keyed on a `{ variable }` its `compile()` never emits     |
| `CONFLICTING_RENDER_HIT_TEST`      | warning  | A renderer declares both a `hitTest` factory and an overlay render; they're alternatives, and only the overlay is used                |
| `OVERLAY_REQUIRES_RENDER_HIT_TEST` | warning  | A renderer is overlay-hosted but its layer's `spatialKind` is not `'render-hit-test'`, so every `pushHover` resolves against no index |
| `MISSING_ANCHOR_CAPABILITY`        | warning  | A `'render-hit-test'` geom implements no `resolveAnchorPosition`, so annotations anchored to its observations are dropped             |

Some are computed once, when the provider reads the `plugins` array; the rest are recomputed per compile, since they depend on what a layer actually compiled to.

## Related

* [How a chart is built](/sdk-next/concepts/how-a-chart-is-built) — the pipeline your plugin joins
* [Rendering](/sdk-next/rendering/index) — the renderer that hosts slots and geom renderers
