# Custom layers

> Write your own layers for chart() and the presets, from drawing to binding axes and series, widening axes and refining what other layers drew.

Every chart in Chartist is drawn by layers: `line()`, `bar()`, `pie()`, the grid and the labels. They're plain objects with a few functions, and yours work exactly like the built-in ones. A layer is a good way to add a new kind of mark to a chart, and to share it: it's a function that returns an object, with no setup and nothing to install.

Layers and [plugins](https://chartist.dev/docs/events-and-plugins/) do different jobs. A **layer draws** something of its own: lollipops, a heat strip, error bars. A **plugin changes** what's drawn and reacts to the chart's events: tooltips, legends, animations. If your idea is "a new kind of chart", it's a layer.

## A first layer

A lollipop chart: a stem from zero and a dot for each value. The layer is in `src/lollipop.ts`; the chart uses it next to the grid and the labels.

**A lollipop layer** (runs in the browser):

`src/lollipop.ts`:

```ts
import { alphaNumerate, node, type Layer } from 'chartist';

// The roles this layer gives its nodes, so listeners and plugins can find
// them with a typed context.
declare module 'chartist' {
  interface NodeRoles {
    lollipop: {
      ctx: {
        readonly seriesIndex: number;
        readonly valueIndex: number;
        readonly value: number;
      };
      attrs: { readonly cx: number; readonly cy: number; readonly r: number };
    };
  }
}

export interface LollipopOptions {
  /** Series to draw, by name or index; default: all. */
  readonly series?: readonly (number | string)[];
  /** Axis ids; default: the first X and the first Y axis. */
  readonly x?: string;
  readonly y?: string;
}

/** A stem from zero and a dot for each value. */
export function lollipop(options: LollipopOptions = {}): Layer {
  return {
    type: 'lollipop',
    // Drawn where bars are, so the chart gets the bar chart's class, which
    // centers the X labels.
    chartType: 'bar',
    coordinates: ['cartesian'],
    axes: { x: options.x, y: options.y },
    series: options.series,
    // The stems start at zero, so the Y axis must include it.
    extent: ({ axes }) => (axes.y ? { [axes.y]: { low: 0, high: 0 } } : {}),
    compose({ area, axes, normalized, seriesIndexes }) {
      const { x, y } = axes;
      if (!x || !y || !normalized) {
        throw new Error('Layer "lollipop" needs an x and a y axis');
      }
      // Centered in each step, where bars sit.
      const shift = x.kind === 'step' ? (x.step ?? 0) / 2 : 0;
      const base = area.y1 - y.project(0);

      return normalized.series.map((values, local) => {
        // The series' index in the whole chart, for its class and color.
        const seriesIndex = seriesIndexes?.[local] ?? local;
        let group = node(
          'g',
          {},
          `ct-series ct-series-${alphaNumerate(seriesIndex)}`
        );
        values.forEach((value, valueIndex) => {
          // Values are normalized to { x, y }, or undefined for a hole.
          const v =
            value && typeof value === 'object' && 'y' in value
              ? value.y
              : undefined;
          if (v === undefined) {
            return;
          }
          const cx = area.x1 + shift + x.project(value, valueIndex, values);
          const cy = area.y1 - y.project(value, valueIndex, values);
          group = group
            .append(
              node('line', { x1: cx, y1: base, x2: cx, y2: cy }, 'ct-stem')
            )
            .append(
              node('circle', { cx, cy, r: 6 }, 'ct-lollipop', {
                role: 'lollipop',
                ctx: { seriesIndex, valueIndex, value: v }
              })
            );
        });
        return group;
      });
    }
  };
}
```

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import {
  autoScaleAxis,
  axisGrid,
  axisLabels,
  cartesianAxes,
  chart,
  stepAxis
} from 'chartist';
import { lollipop } from './lollipop';

chart('#chart', {
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    series: [{ name: 'visits', data: [12, 9, 14, 7, 11] }]
  },
  axes: cartesianAxes({
    x: { type: stepAxis() },
    y: { type: autoScaleAxis() }
  }),
  layers: [axisGrid(), lollipop(), axisLabels()]
});
```

`src/style.css`:

```css
/* The series group sets --ct-color; stems and dots take it. */
.ct-stem {
  stroke: var(--ct-color);
  stroke-width: 2px;
}

.ct-lollipop {
  fill: var(--ct-color);
}
```

`index.html`:

```html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script type="module" src="/src/main.ts"></script>
  </head>
  <body>
    <div id="chart" class="ct-golden-section"></div>
  </body>
</html>
```

Use it like any layer: in `layers` of [`chart()`](https://chartist.dev/docs/composed-charts/), or on a preset with `layers: { below: [...], above: [...] }`.

## What a layer is made of

| Field         | What it does                                                                                        |
| ------------- | --------------------------------------------------------------------------------------------------- |
| `type`        | A name for error messages, such as `lollipop`                                                       |
| `coordinates` | Where it can draw: `['cartesian']`, `['polar']` or both. Using it elsewhere throws                  |
| `compose`     | Draws the layer: gets the data, axes and chart area, returns scene nodes                            |
| `axes`        | The ids of the X and Y axis to draw on; by default the first of each                                |
| `series`      | The series to draw, by name or index; by default all of them                                        |
| `extent`      | Widens the axes beyond the layer's values, such as to include zero                                  |
| `refine`      | Changes nodes that any layer drew, after all of them have drawn                                     |
| `chartType`   | The chart class it gives `chart()` without a `className`: `'bar'` centers the X labels, for example |

Only `type`, `coordinates` and `compose` are required. A layer is data: create it with a function that takes options, and keep no state in it. The same layer object may draw several charts, and every resize draws again.

## compose: drawing

`compose(context)` runs on every render, after the axes are created. It returns the layer's top-level nodes, usually one group per series. Its context has:

| Field           | What it is                                                                                     |
| --------------- | ---------------------------------------------------------------------------------------------- |
| `area`          | The drawing area in px: `x1`, `x2` (left, right), `y1` (bottom), `y2` (top), `width`, `height` |
| `axes`          | The bound `x` and `y` axes, and `all` axes of the chart                                        |
| `normalized`    | The layer's series as the axes see them: `{ x, y }` values, `undefined` for holes              |
| `data`          | The layer's series as given, with names, classes and meta                                      |
| `seriesIndexes` | Each series' index in the whole chart, when the layer selects some                             |
| `layerIndex`    | The layer's position in `layers`                                                               |
| `measureText`   | Measures text with the page's fonts, for layers that lay out labels                            |

**From values to px.** `axis.project(value, index, series)` gives the distance in px from the start of the axis. Y grows upward and SVG downward, so a Y position is `area.y1 - y.project(value)`, and an X position `area.x1 + x.project(value)`. A step axis places by index instead of value; its `step` is the width of one step.

**Nodes.** Create them with `node(element, attributes, classes, { role, ctx })` and nest them with `append()`. A node with a `role` and a `ctx` (its data context) is what plugins and listeners see: they find it by role and read its values. Declare your roles in `NodeRoles`, as the example does, and `node()` and the listeners check their context.

**Colors.** Put the series' nodes in a group with `ct-series ct-series-<letter>` (`alphaNumerate(seriesIndex)` gives the letter): the default stylesheet sets `--ct-color` on it, and your CSS reads it. Use the chart-wide index from `seriesIndexes`, so a series keeps its color whichever layer draws it.

Keys come for free: nodes with a series and value index get one from the layer index, so the scene stays stable between renders.

## Axes and series

A layer draws on one X and one Y axis. Give it `x` and `y` options and pass them on as `axes`, as the built-in layers do, so users can bind it to any axis by id. The `series` option works the same way: pass it on as `series`, and `compose` gets only those series, with their chart-wide indexes in `seriesIndexes`.

The selection also decides what the axes fit: **each axis covers the series of the layers bound to it.** A layer that draws no series (a reference line, an annotation) sets `series: []`, so it doesn't shape any axis.

**extent** adds to that. It runs before the axes are created and returns, by axis id, the range the layer needs:

```ts
import type { Layer } from 'chartist';

export const withZero = (layer: Layer): Layer => ({
  ...layer,
  // Keep zero on the layer's Y axis, wherever its values are.
  extent: ({ axes }) => (axes.y ? { [axes.y]: { low: 0, high: 0 } } : {})
});
```

The axis covers every layer's extent and the data together. An axis's own `low` and `high` still win.

## refine: changing what others drew

Some layers need to change other layers' nodes: the [band](https://chartist.dev/docs/target-lines-and-bands/#in-or-out-of-the-band) marks the points inside it. That's `refine(node, context)`. Once every layer has composed, it's called for each node with a role, from all layers, before the `compose` events reach plugins:

```ts
import type { Layer } from 'chartist';

/** Marks the highest point of every line with ct-max. */
export const highlightMax = (): Layer => ({
  type: 'highlightMax',
  coordinates: ['cartesian'],
  // Draws nothing of its own.
  compose: () => [],
  refine(current, { normalized }) {
    if (current.role !== 'point' || !normalized) {
      return undefined;
    }
    const ys = normalized.series
      .flat()
      .flatMap(value =>
        value && typeof value === 'object' && 'y' in value ? [value.y] : []
      );
    const { value } = current.ctx;
    const y =
      value && typeof value === 'object' && 'y' in value ? value.y : undefined;
    return y === Math.max(...ys) ? current.addClass('ct-max') : undefined;
  }
});
```

Refining has rules, so layers can't break each other:

- **One node at a time.** `refine` gets the node it's called for, not the scene. To compare with other values, work from the data in the context, as above.
- **No structural changes.** Return the node with changed attributes, classes or context, or `undefined` to leave it. Whatever you return keeps the node's key and children, and refining can't drop a node. Removing and rearranging nodes is a plugin's job.
- **Your own context.** The context is the one your `compose` got: your layer's data and axes. A layer with `series: []` sees no data here either; leave `series` out, as above, to see all of it.
- **Order.** Every layer composes first, so a layer can refine the nodes of layers listed after it. Refining layers run in the order of `layers`, and plugins see the result in their `compose` events, where they can still change it.
- **No state.** Don't count or remember between calls: the same layer object may refine several charts.

Leave `refine` out when you don't need it: charts without a refining layer skip the step.

## Sharing a layer

A layer is a function, so a package of layers is a module of functions. Depend on `chartist` as a peer dependency, export a function per layer with typed options, and ship a small stylesheet with your classes, using `--ct-color` and the other `--ct-*` properties so the user's theme applies.

What you get for free:

- **Presets:** users add your layer to `lineChart()` and `barChart()` with `layers`, no `chart()` needed.
- **Server rendering:** layers are pure, so they work in [`toScene()` and `renderToString()`](https://chartist.dev/docs/server-rendering/).
- **The terminal:** `@chartist-js/terminal` draws nodes of unknown roles by their element (lines, paths, circles, text).
- **Plugins:** your nodes go through the `compose` events like the built-in ones.

In the API reference: [`Layer`](https://chartist.dev/api/layers/#Layer), [`LayerContext`](https://chartist.dev/api/layers/#LayerContext) and [`node`](https://chartist.dev/api/scene/#node).

Next: [events and plugins](https://chartist.dev/docs/events-and-plugins/), to change charts instead of drawing on them, or the [target line and band layers](https://chartist.dev/docs/target-lines-and-bands/).
