# Events and plugins

> Every chart is built in phases, and each phase is an event you can listen to and change. Plugins package those listeners so you can share them.

Chartist keeps its core small and leaves the extras to you. Tooltips, value labels, legends, a bar that turns red past a threshold: none of them are options. Instead, every step of drawing a chart is an event, and a listener can change what that step produces. A plugin is a set of such listeners with a name, ready to share.

## The pipeline

A chart goes through seven phases, in this order. Each one emits an event with what it produced:

| Phase     | Payload                            | What it is                                                                               |
| --------- | ---------------------------------- | ---------------------------------------------------------------------------------------- |
| `resolve` | `{ config }`                       | The config with defaults applied: padding, coordinates, layers, axes                     |
| `prepare` | `{ data }`                         | The data, each series as `{ values, name, className, meta }`                             |
| `layout`  | `{ width, height, padding, area }` | The chart's size and the drawing area inside the padding and axes                        |
| `compose` | a scene node                       | Each element the layers draw: series, lines, points, bars, slices, grid lines and labels |
| `scene`   | `{ scene }`                        | The whole tree, its root the `svg`                                                       |
| `render`  | `{ scene, output }`                | The renderer has written the SVG                                                         |
| `mount`   | `{ scene, output, elementOf }`     | The SVG is in the page; `elementOf(node)` gives you a node's element                     |

A listener that returns a new payload changes the phase, and later phases build on the change. Return nothing to leave it as it is. The return values of `render` and `mount` listeners are ignored: by then, the SVG exists.

When the container resizes, the chart runs again from `layout`. The config and data haven't changed, so `resolve` and `prepare` don't run, and their listeners aren't called.

## Listening

Pass listeners in the config of `chart()`, by phase name:

```ts
import { autoScaleAxis, cartesianAxes, chart, line, stepAxis } from 'chartist';

chart('#chart', {
  data: { labels: ['Mon', 'Tue', 'Wed'], series: [[1, 3, 2]] },
  axes: cartesianAxes([
    { id: 'x', side: 'bottom', type: stepAxis() },
    { id: 'y', side: 'left', type: autoScaleAxis() }
  ]),
  layers: [line()],
  on: {
    layout: ({ width, height }) =>
      console.log(`laid out at ${width}×${height}`),
    mount: ({ scene }) => console.log('mounted', scene)
  }
});
```

The presets take the same listeners:

```ts
import { lineChart } from 'chartist';

lineChart(
  '#chart',
  { series: [[1, 3, 2]] },
  { on: { mount: () => console.log('drawn') } }
);
```

Or call `on()` and `off()` on the chart handle. Both return the handle, so they chain. `off(name, listener)` removes that listener. `off(name)` without one removes the listeners you added to that event, with `on()` or in the `on` config, and leaves the ones of plugins alone. The name `'*'` listens to all events, with the event name as the first argument:

```ts
import { lineChart } from 'chartist';

const chart = lineChart('#chart', { series: [[1, 3, 2]] });

const log = (name: string, payload: unknown) => console.log(name, payload);
chart.on('*', log);
chart.update({}); // logs every phase of this render
chart.off('*', log);
```

> **The first render happens right away.** `chart()` and the presets draw the
> chart before they return, so a listener added with `on()` afterwards sees the
> next render, not the first. For the first one, pass it in `on`, or use a
> plugin.

## Scene nodes

The `compose` event is where most plugins work. Its payload is a scene node: a plain description of an SVG element with its `name`, `attrs`, `classes` and `children`, plus what Chartist knows about it:

- `role`: `series`, `line`, `point`, `area`, `bar`, `slice`, `grid`, `gridBackground` or `label`
- `ctx`: its data, such as `seriesIndex`, `valueIndex`, `value` and `meta`
- `key`: a stable id like `0/1/3` (layer, series, value)

In TypeScript, checking the role narrows the node: after `node.role === 'point'`, `node.ctx.x` is a number and `node.ctx.path` is an error, since only lines, areas and slices have a path. `findAll(scene, { role: 'bar' })` returns bar nodes, and the builder methods keep the type. The live examples' editor shows it too: type `node.ctx.` inside a role check, and it lists that role's fields. If your plugin adds nodes with a role of its own, declare it in the `NodeRoles` interface, the same way as [custom events](#custom-events).

Every node with a role passes through the listeners, children before their parents. So a series listener sees its points and lines as the other listeners left them.

Nodes are immutable. Their methods (`attr()`, `addClass()`, `removeClass()`, `append()`, `remove()`, `replace()`, `text()`, `empty()`) return a new node, and you return that node from the listener.

### Dynamic coloring

Color isn't only for stylesheets. Here a plugin reads each bar's value and gives it a hue from red to green:

**Dynamic coloring** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import { barChart, type ChartPlugin } from 'chartist';

const values = [3, 6, 9, 12, 9, 6, 3, 6, 9, 12, 9, 6];
const min = Math.min(...values);
const max = Math.max(...values);

// Colors each bar by its value: red for low, green for high.
const heat: ChartPlugin = {
  name: 'heat',
  install(chart) {
    chart.on('compose', node => {
      if (node.role !== 'bar') {
        return;
      }
      // Bar values are { x, y } (or a hole).
      const { value } = node.ctx;
      const y =
        value && typeof value === 'object' && 'y' in value ? value.y : min;
      const share = (y - min) / (max - min);
      const hue = Math.round(share * 120);
      return node.attr({ style: `stroke: hsl(${hue} 70% 45%)` });
    });
  }
};

barChart(
  '#chart',
  { series: [values] },
  { axisX: { showLabel: false }, plugins: [heat] }
);
```

`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>
```

### Change, replace or drop

To change a node, return it with new attributes or classes, as above. To replace it, return a different node; keep its `role`, `ctx` and `key` so later listeners still know what it is. To drop it, return `null`:

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

/** Circles instead of points, and no points for zero values. */
export const circles: ChartPlugin = {
  name: 'circles',
  install(chart) {
    chart.on('compose', current => {
      if (current.role !== 'point') {
        return;
      }
      const { x, y, value } = current.ctx;
      // Line values are { x, y } (or a hole).
      if (value && typeof value === 'object' && 'y' in value && value.y === 0) {
        return null;
      }
      return node('circle', { cx: x, cy: y, r: 5 }, 'ct-circle', {
        role: current.role,
        ctx: current.ctx,
        key: current.key
      });
    });
  }
};
```

The circle has none of the point's classes, so style it yourself, for example with `.ct-circle { fill: var(--ct-color); }`.

### Decorate

To add elements, append them to a parent. A value above each bar goes into the bar's series, which comes through the listener after its bars:

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

export const valueLabels: ChartPlugin = {
  name: 'value-labels',
  install(chart) {
    chart.on('compose', series => {
      if (series.role !== 'series') {
        return;
      }
      return series.children
        .filter(child => child.role === 'bar')
        .reduce((result, bar) => {
          const { x2, y2 } = bar.attrs;
          const { value: v } = bar.ctx;
          const value =
            v && typeof v === 'object' && 'y' in v ? v.y : undefined;
          return result.append(
            node(
              'text',
              { x: x2, y: y2 - 8, 'text-anchor': 'middle' },
              'ct-value'
            ).text(String(value))
          );
        }, series);
    });
  }
};
```

## Writing a plugin

A plugin is an object with a `name` and an `install` function. `install` runs once, before the first render, with the chart handle, and registers listeners on it. They stay for every later render, after updates and resizes too. For options, write a function that returns the plugin:

```ts
import { barChart, type ChartPlugin } from 'chartist';

/** Bars at or above the limit get the class ct-over. */
const threshold = (limit: number): ChartPlugin => ({
  name: 'threshold',
  install(chart) {
    chart.on('compose', current => {
      if (current.role !== 'bar') {
        return;
      }
      // A number, { x, y } (with an X scale axis) or a hole.
      const { value } = current.ctx;
      const y =
        typeof value === 'number'
          ? value
          : value && 'y' in value
            ? value.y
            : undefined;
      return y !== undefined && y >= limit
        ? current.addClass('ct-over')
        : undefined;
    });
  }
});

barChart(
  '#chart',
  { labels: ['Q1', 'Q2', 'Q3', 'Q4'], series: [[5, 4, 3, 7]] },
  { plugins: [threshold(5)] }
);
```

Plugins go into `plugins`, in the config of `chart()` or the options of the presets. Plugins install in order, so with two listeners on the same event, the earlier plugin's runs first. `chart()` also takes a `[plugin, options]` pair as an entry, and `install` receives the options as its second argument.

## Custom events

Plugins can emit events of their own with `chart.emit(name, payload)`. Declare each event and its payload in the `ChartEvents` interface (TypeScript merges your declaration into Chartist's), list it in `provides`, and listen to it with `on()` like to any other event. A misspelled name or a wrong payload is then a type error, not a silent no-op. This plugin reports clicks on bars; the page listens and shows the clicked value. Press **Edit** and click a bar:

**A custom event** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import { barChart, findAll, type ChartPlugin } from 'chartist';

// The event and its payload, declared so `emit` and `on` are typed.
declare module 'chartist' {
  interface ChartEvents {
    'bar:select': { series: number; index: number; value?: number };
  }
}

// Emits "bar:select" when someone clicks a bar.
const selectBars: ChartPlugin = {
  name: 'select-bars',
  provides: ['bar:select'],
  install(chart) {
    chart.on('mount', ({ scene, elementOf }) => {
      for (const bar of findAll(scene, { role: 'bar' })) {
        elementOf(bar)?.addEventListener('click', () =>
          chart.emit('bar:select', {
            series: bar.ctx.seriesIndex,
            index: bar.ctx.valueIndex,
            // Bar values are { x, y } (or a hole).
            value:
              bar.ctx.value &&
              typeof bar.ctx.value === 'object' &&
              'y' in bar.ctx.value
                ? bar.ctx.value.y
                : undefined
          })
        );
      }
    });
  }
};

const labels = ['Q1', 'Q2', 'Q3', 'Q4'];
const chart = barChart(
  '#chart',
  {
    labels,
    series: [
      [5, 4, 3, 7],
      [3, 2, 9, 5]
    ]
  },
  { plugins: [selectBars] }
);

const output = document.querySelector('#selected');

chart.on('bar:select', ({ series, index, value }) => {
  if (output) {
    output.textContent = `Series ${series + 1}, ${labels[index]}: ${value}`;
  }
});
```

`src/style.css`:

```css
#chart .ct-bar {
  cursor: pointer;
  stroke-width: 24px;
}

#chart .ct-bar:hover {
  opacity: 0.7;
}
```

`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>
    <p id="selected" aria-live="polite">Click a bar.</p>
  </body>
</html>
```

The click handlers go on in `mount`, the first phase in which the elements exist. There's no cleanup to do: every render draws a new SVG, and the old elements go away with their handlers.

Phase names are reserved: emitting `compose` or `mount` throws. A prefix such as `bar:` keeps your names apart from other plugins'.

## Sharing a plugin

A plugin is plain data and functions, so sharing one is publishing a module. Export a function that takes the options and returns the plugin, and type it with `ChartPlugin`:

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

export interface HighlightOptions {
  className?: string;
}

export function highlightLast(options: HighlightOptions = {}): ChartPlugin {
  const className = options.className ?? 'ct-last';
  return {
    name: 'highlight-last',
    install(chart) {
      chart.on('compose', current => {
        if (current.role !== 'series') {
          return;
        }
        const last = current.children.at(-1);
        return last && current.replace(last, last.addClass(className));
      });
    }
  };
}
```

A few things keep a plugin working everywhere:

- Change the scene in `compose` and `scene`, not the DOM. Then it also works when the chart is [rendered to a string](https://chartist.dev/docs/server-rendering/), where `render` and `mount` never happen.
- Touch elements only in `mount`, and only through `elementOf`.
- List `chartist` as a peer dependency, so your plugin uses the reader's copy.

## Publishing a plugin

Publish your plugin the way Chartist v2 is published: as an ES module with types.

```json
{
  "name": "chartist-plugin-highlight-last",
  "type": "module",
  "exports": {
    ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
  },
  "files": ["dist"],
  "keywords": ["chartist", "chartist-plugin"],
  "peerDependencies": { "chartist": "^2.0.0" },
  "sideEffects": false
}
```

- **Import, don't read globals.** Import what you need from `chartist`. Don't read the global `Chartist`: it only exists on pages that load Chartist's UMD build, or that import `chartist/compat` for old plugins.
- **No UMD build.** Pages without a bundler load ES modules directly from a CDN, and an import map tells the browser where `chartist` is, for your plugin's imports too:

  ```html
  <script type="importmap">
    {
      "imports": {
        "chartist": "https://cdn.jsdelivr.net/npm/chartist@2.0.0-rc.2/dist/index.min.js"
      }
    }
  </script>
  <script type="module">
    import { lineChart } from 'chartist';
    import { highlightLast } from 'https://cdn.jsdelivr.net/npm/chartist-plugin-highlight-last/dist/index.js';

    lineChart('#chart', data, { plugins: [highlightLast()] });
  </script>
  ```

  Chartist still publishes a UMD build and a global `Chartist`, but only so that old pages and the plugins of 0.x keep working. They are for backwards compatibility, not something new plugins should build on.

- **Name it `chartist-plugin-…`** and add the `chartist-plugin` keyword, so people find it on npm.
- **Work in `compose` and `scene`** where you can. Then your plugin also draws when a chart is [rendered on a server](https://chartist.dev/docs/server-rendering/), and the plugins page can tag it as SSR-capable.

**Don't forget to tell us about your plugin.** Once it's on npm, open a pull request that adds it to the [plugins page](https://chartist.dev/plugins/), where readers find the community's plugins (the list is [`apps/docs/src/data/plugins.ts`](https://github.com/chartist-js/chartist/blob/next/apps/docs/src/data/plugins.ts)), or show it in [GitHub Discussions](https://github.com/chartist-js/chartist/discussions).

> **Coming from 1.x?** v2 charts don't emit `draw`, `created` or the other 1.x
> events. Listening to one logs a hint. Your 1.x plugins still run: wrap them
> with `fromLegacyPlugin` from `chartist/compat`. See [Coming from
> 1.x](https://chartist.dev/docs/coming-from-1x/), and the [plugins page](https://chartist.dev/plugins/) for the
> community's plugins running on v2.

In the API reference: [events and plugins](https://chartist.dev/api/events-and-plugins/) and [the scene](https://chartist.dev/api/scene/).

Next: [render charts on a server](https://chartist.dev/docs/server-rendering/), or [animate them](https://chartist.dev/docs/animations/).
