# Live updates

> Give a chart new data through its handle, and let it keep its SVG with liveDomRenderer(), so elements stay, plugins keep what they added and updates can animate.

`lineChart()`, `barChart()`, `pieChart()` and `chart()` draw the chart right away and return a handle. Keep it: it's how you give the chart new data, and how you tell it you're done with it.

Here's a temperature sensor that reports every second. The chart drops the oldest reading, adds the new one and keeps its SVG while doing it. The line under the chart counts what each update did to the SVG: almost every element is kept, many of them with new positions, and only the oldest reading's elements are removed and the new one's added. The axis follows the readings, so when their range changes, its grid lines and labels change too. Press **Edit** to change the code:

**A live sensor** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import { lineChart, type SceneNode } from 'chartist';
import { liveDomRenderer } from 'chartist/live';
import { firstReadings, onReading } from './readings';

const readout = document.querySelector('#changes')!;
const count = (node: SceneNode): number =>
  1 + node.children.reduce((sum, child) => sum + count(child), 0);

const chart = lineChart('#chart', firstReadings, {
  // Keep the SVG, and know each reading by its label.
  renderer: liveDomRenderer({ key: 'label' }),
  showArea: true,
  on: {
    // What each render did to the SVG's elements.
    mount: ({ scene, changes, cause }) => {
      const total = count(scene);
      if (cause === 'create') {
        readout.textContent = `Drew ${total} elements.`;
        return;
      }
      const tally = { add: 0, update: 0, remove: 0 };
      for (const change of changes) {
        tally[change.op] += 1;
      }
      readout.textContent =
        `Kept ${total - tally.add} of ${total} elements (${tally.update} changed), ` +
        `added ${tally.add}, removed ${tally.remove}.`;
    }
  }
});

// A new reading every second: the oldest one goes, the new one comes.
onReading(data => chart.update({ data }));
```

`src/readings.ts`:

```ts
// A temperature sensor that reports every second. The readings come from
// a seeded generator, so every run shows the same ones.

let seed = 42;
const random = () => {
  seed = (seed * 16807) % 2147483647;
  return (seed - 1) / 2147483646;
};

let data = {
  labels: ['1s', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', '10s'],
  series: [[21.2, 21.4, 21.3, 21.7, 21.9, 21.8, 22.1, 22, 22.3, 22.2]]
};

/** The last ten readings. */
export const firstReadings = data;

/** Calls `listener` every second with the last ten readings. */
export function onReading(listener: (readings: typeof data) => void) {
  let second = 10;
  setInterval(() => {
    second += 1;
    const [values] = data.series;
    const next = values[values.length - 1] + random() - 0.5;
    const reading = Math.round(Math.min(23.5, Math.max(20.5, next)) * 10) / 10;
    // Drop the oldest reading and add the new one.
    data = {
      labels: [...data.labels.slice(1), `${second}s`],
      series: [[...values.slice(1), reading]]
    };
    listener(data);
  }, 1000);
}
```

`src/style.css`:

```css
#changes {
  margin: 0.5rem 0 0;
  font:
    0.8rem/1.4 ui-monospace,
    monospace;
  color: #6b6b6b;
}
```

`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="changes"></p>
  </body>
</html>
```

[`liveDomRenderer()`](#keep-the-svg), the `key` option, [animations](#animate-updates) and [what changed](#what-changed) are what the rest of this guide is about. More live charts are in the [examples](https://chartist.dev/examples/#live).

The handle's methods:

| Method            | What it does                                                                       |
| ----------------- | ---------------------------------------------------------------------------------- |
| `update(changes)` | Merges the changes into the config and draws the chart again                       |
| `detach()`        | Stops following the container's size; the chart stays as it is                     |
| `destroy()`       | Detaches, removes the SVG and removes every listener                               |
| `on()`, `off()`   | Adds and removes listeners, see [Events and plugins](https://chartist.dev/docs/events-and-plugins/)       |
| `setOptions()`    | Presets only: changes the options and draws again, see [new options](#new-options) |

## New data

Pass `update()` the parts of the config that changed. The merge is shallow: each key you pass replaces that key, the rest stays. `update({ data })` replaces all of the data, labels included, so pass the whole data object, not only the series that changed. The same goes for `axes` and `layers` in a `chart()` config.

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

const labels = ['Mon', 'Tue', 'Wed'];
const handle = chart('#chart', {
  data: { labels, series: [[1, 3, 2]] },
  axes: cartesianAxes([
    { id: 'x', side: 'bottom', type: stepAxis() },
    { id: 'y', side: 'left', type: autoScaleAxis() }
  ]),
  layers: [line()]
});

// New data; the axes and layers stay.
handle.update({ data: { labels, series: [[2, 4, 3]] } });

// Another look; the data stays.
handle.update({ layers: [line({ showArea: true, showPoint: false })] });
```

Every update runs the whole pipeline again, and your plugins and listeners stay. They aren't part of what `update()` takes: add and remove listeners with `on()` and `off()`.

## New options

`update()` takes the config of `chart()`, so on a preset chart it replaces the `data`, but options like `showArea` or `stackBars` aren't part of it. The presets' handles have `setOptions()` for those. The options you pass extend the current ones; with `{ replace: true }` they start from the defaults instead, and the options you gave before are gone:

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

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

// Areas too; showPoint: false stays.
chart.setOptions({ showArea: true });

// From the defaults: points again, and areas.
chart.setOptions({ showArea: true }, { replace: true });
```

[Responsive options](https://chartist.dev/docs/responsive/) still apply on top, and new data from `update({ data })` stays when they change. `plugins`, `on` and `renderer` are set at creation.

## Keep the SVG

> **Experimental.** `chartist/live` is new with 2.0. Its API may still change in
> a 2.x minor before it becomes stable; the rest of Chartist is stable.

What a render does to the page depends on the chart's renderer:

- **`domRenderer()`**, the default, draws a new SVG on every render and replaces the old one: on an update, a resize, a responsive change, or when web fonts finish loading. It's small, and right for charts that rarely change. But the elements are new each time: a hovered bar loses its `:hover`, a focused point its focus, what a plugin added to an element is gone, and [CSS animations](https://chartist.dev/docs/animations/) play again.
- **`liveDomRenderer()`**, from `chartist/live`, draws the SVG once and then changes it in place. An element whose value is still in the data stays the same element, and only the attributes, classes and text that changed are set. New values get new elements, gone ones are removed. The result is the same SVG a fresh chart would draw.

Opt in per chart with the `renderer` option, on the presets and on `chart()`:

```ts
import { barChart } from 'chartist';
import { liveDomRenderer } from 'chartist/live';

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

Reach for it when a chart updates while people look at it: dashboards, streams, charts with tooltips or other interactive plugins. A chart that's drawn once doesn't need it.

> **1.x code always gets a new SVG.** The 1.x classes (`new LineChart()`) and
> charts with the `compat` plugin or 1.x plugins converted with
> `fromLegacyPlugin` draw a new SVG on every render, even with
> `liveDomRenderer()`: their `draw` listeners expect new elements. To update
> such a chart live, move its plugins to [v2
> plugins](https://chartist.dev/docs/events-and-plugins/#plugins-for-live-updates).

### Importing chartist/live

With a bundler, import it next to `chartist`:

```ts
import { lineChart } from 'chartist';
import { liveDomRenderer } from 'chartist/live';

lineChart('#chart', { series: [[1, 3, 2]] }, { renderer: liveDomRenderer() });
```

Without one, load `live.min.js` from the same CDN folder as `index.min.js`. It imports `./index.min.js`, so both share one copy of Chartist:

```html
<script type="module">
  import { lineChart } from 'https://cdn.jsdelivr.net/npm/chartist@2/dist/index.min.js';
  import { liveDomRenderer } from 'https://cdn.jsdelivr.net/npm/chartist@2/dist/live.min.js';

  lineChart('#chart', { series: [[1, 3, 2]] }, { renderer: liveDomRenderer() });
</script>
```

## Which value is which

To keep an element, the renderer has to know which value of the new data it stands for. The `key` option says what identifies a value:

- **`'index'`**, the default: its position in its series. The third value stays the third value. Right when the data has the same values in the same order, with new numbers.
- **`'label'`**: the label at its position. Right for a sliding window like the sensor above, where every reading moves one place to the left. With index keys, each point would stay where it is and take its neighbor's value; with label keys, the point of `5s` keeps its element and moves left.
- **A function** that gets `{ seriesIndex, valueIndex, value, label, meta }` and returns a string or a number. Right when values carry an id, or when they change their order:

```ts
import { barChart } from 'chartist';
import { liveDomRenderer } from 'chartist/live';

// Each value carries its team's id; the bars are sorted by score.
barChart(
  '#chart',
  {
    labels: ['Otters', 'Owls', 'Ants'],
    series: [
      [
        { value: 12, meta: 'otters' },
        { value: 9, meta: 'owls' },
        { value: 4, meta: 'ants' }
      ]
    ]
  },
  { renderer: liveDomRenderer({ key: ({ meta }) => String(meta) }) }
);
```

Series are matched by their index, and grid lines and axis labels by their tick value: when the scale changes from 0 to 10 to 0 to 15, the lines of 0, 5 and 10 move and the one of 15 is added. Values with the same key in a series are matched in their order, so repeated labels work like index keys. The key only decides which elements are kept, never what's drawn.

## What changed

The `render` and `mount` events say why the chart rendered and what the render did, for every renderer:

- `cause`: `create` for the first render, then `update` (from `update()` and `setOptions()`), `resize`, `responsive` or `fonts`;
- `previous`: the previous render's scene, if there was one;
- `changes`: one entry per node, in document order. `add` for a new node, `update` for a node that changed (with its `previous` node), `remove` for a node that's gone. In `mount`, `elementOf()` gives each entry's element, for a `remove` the one just taken out of the page.

With `domRenderer()`, every render is a replacement: every previous node is removed and every new one added. With `liveDomRenderer()`, the changes are what it changed.

That makes `add` entries the place to set up an element once. Listeners added in `mount` to every element would pile up on elements that stay, so add them to new elements only. This works the same with both renderers:

```ts
import { barChart } from 'chartist';
import { liveDomRenderer } from 'chartist/live';

barChart(
  '#chart',
  { labels: ['Q1', 'Q2', 'Q3', 'Q4'], series: [[5, 4, 3, 7]] },
  {
    renderer: liveDomRenderer(),
    on: {
      mount: ({ cause, changes, elementOf }) => {
        for (const change of changes) {
          if (change.op === 'add' && change.node.role === 'bar') {
            elementOf(change.node)?.addEventListener('click', () =>
              console.log('clicked a bar')
            );
          }
        }
        console.log(`${cause}: ${changes.length} changes`);
      }
    }
  }
);
```

## Cleaning up

Some things can't be set up once: a `title` that shows a value changes with the value. A `mount` listener can return a function that undoes what it did. It runs right before the next render touches the SVG, and on `destroy()`:

```ts
import { findAll, lineChart } from 'chartist';
import { liveDomRenderer } from 'chartist/live';

const SVG = 'http://www.w3.org/2000/svg';

lineChart(
  '#chart',
  { series: [[1, 3, 2, 4]] },
  {
    renderer: liveDomRenderer(),
    on: {
      mount: ({ scene, elementOf }) => {
        // A tooltip with its value on every point.
        const titles = findAll(scene, { role: 'point' }).map(point => {
          const title = document.createElementNS(SVG, 'title');
          title.textContent = String(point.attrs['ct:value']);
          elementOf(point)?.append(title);
          return title;
        });
        // Gone before the next render adds new ones.
        return () => titles.forEach(title => title.remove());
      }
    }
  }
);
```

Plugins can do the same, and their `install` can return a cleanup function too, for what they set up outside the chart. See [Events and plugins](https://chartist.dev/docs/events-and-plugins/#cleaning-up).

## Animate updates

With `smilAnimations()`, updates move: bars grow and shrink, points and lines slide to their new place, pie slices turn. Updates take 300 ms with `ease-out`. The timing is part of the chart's look, so it's in CSS: set `--ct-update-duration` and `--ct-update-easing` on the chart or its container to change it, or `--ct-update-duration: 0s` to turn the animations off.

**Animated updates** (runs in the browser):

`src/style.css`:

```css
#chart {
  /* Updates move the bars from their old values to the new ones. */
  --ct-update-duration: 700ms;
  /* A curve that overshoots a little, then settles. */
  --ct-update-easing: cubic-bezier(0.34, 1.56, 0.64, 1);
}
```

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import { barChart } from 'chartist';
import { liveDomRenderer, smilAnimations } from 'chartist/live';
import { firstNumbers, onNumbers } from './numbers';

const chart = barChart(
  '#chart',
  firstNumbers,
  // How long the animations take is up to style.css.
  { renderer: liveDomRenderer({ animations: smilAnimations() }) }
);

onNumbers(data => chart.update({ data }));
```

`src/numbers.ts`:

```ts
// Numbers by region from a seeded generator, so every run shows the same.

let seed = 7;
const next = () => {
  seed = (seed * 16807) % 2147483647;
  return (seed - 1) / 2147483646;
};

const labels = ['North', 'East', 'South', 'West'];
const random = () => labels.map(() => Math.round(next() * 9) + 1);

/** Two series by region. */
export const firstNumbers = {
  labels,
  series: [
    [5, 4, 3, 7],
    [3, 2, 9, 5]
  ]
};

/** Calls `listener` with new numbers every two seconds. */
export function onNumbers(listener: (numbers: typeof firstNumbers) => void) {
  setInterval(() => listener({ labels, series: [random(), random()] }), 2000);
}
```

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

`--ct-update-easing` takes any CSS easing: the keywords (`ease-out` is the default), `cubic-bezier()` curves, including ones that overshoot like the one above, `linear()` and `steps()`. The animations run with SMIL, which reaches what CSS can't: bars and points are `<line>` elements, and the ends of a line are attributes, not CSS properties. SMIL moves them, and paths, in every browser.

A few things to know:

- **The attributes hold the new values at once.** The animation is drawn on top of them, so whatever you read from the DOM is the final state.
- **An update during an animation continues from where the elements are drawn**, without a jump. Pie slices start from their previous shape.
- **Only `update` and `responsive` renders animate.** A resize, the first render and loaded fonts draw the new state at once.
- **Nothing moves for people who ask for less motion.** Under `prefers-reduced-motion: reduce`, updates aren't animated, whatever the CSS says.
- **Firefox doesn't animate HTML labels.** It runs no SMIL inside a `foreignObject`, so there the axis labels move to their new place at once. SVG text labels (`labels: 'svg'`) move in every browser.

Pies animate too. Each slice turns to its new size around the pie, and its label moves around with it. With `key: 'label'`, a slice keeps its element even when the order of the parties changes:

**Votes by district** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import { pieChart } from 'chartist';
import { liveDomRenderer, smilAnimations } from 'chartist/live';
import { firstCount, onCount, parties } from './count';

let votes = firstCount;
const total = () => votes.reduce((sum, value) => sum + value, 0);

const chart = pieChart(
  '#chart',
  { labels: parties, series: votes },
  {
    // Slices are known by their party, and turn to their new share.
    renderer: liveDomRenderer({ key: 'label', animations: smilAnimations() }),
    // Each slice shows its share.
    labelTransform: (label, index) =>
      `${label}\n${Math.round((votes[index] / total()) * 100)}%`
  }
);

// The next district's results.
onCount(count => {
  votes = count;
  chart.update({ data: { labels: parties, series: votes } });
});
```

`src/count.ts`:

```ts
// The results of the districts as they report, from a seeded generator, so
// every run counts the same votes.

let seed = 3;
const random = () => {
  seed = (seed * 16807) % 2147483647;
  return (seed - 1) / 2147483646;
};

/** The parties, in the order of the slices. */
export const parties = ['Greens', 'Blues', 'Reds', 'Yellows'];

/** A district's votes by party: each district has a mood of its own. */
const district = () => parties.map(() => Math.round(50 + random() * 450));

/** The first district's votes. */
export const firstCount = district();

/** Calls `listener` with the next district's votes every 1.5 seconds. */
export function onCount(listener: (votes: number[]) => void) {
  setInterval(() => listener(district()), 1500);
}
```

`src/style.css`:

```css
/* White labels on the slices, in light and dark mode. */
#chart .ct-label {
  fill: #fff;
  color: #fff;
}
```

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

A gauge is a donut on half a ring. Its second slice is the rest of the scale, drawn as a faint track in CSS, so the load moves along the ring:

**Live gauge** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import { pieChart } from 'chartist';
import { liveDomRenderer, smilAnimations } from 'chartist/live';
import { firstLoad, onLoad } from './load';

// The load, and the rest of the scale as a track, on a half ring: a total
// of 200 leaves the lower half empty.
const series = (load: number) => [load, 100 - load];

const chart = pieChart(
  '#chart',
  { series: series(firstLoad) },
  {
    renderer: liveDomRenderer({ animations: smilAnimations() }),
    donut: true,
    donutWidth: 40,
    startAngle: 270,
    total: 200,
    showLabel: false
  }
);

onLoad(load => chart.update({ data: { series: series(load) } }));
```

`src/load.ts`:

```ts
// A CPU's load in percent from a seeded generator, so every run shows the
// same load.

let seed = 75386;
const random = () => {
  seed = (seed * 16807) % 2147483647;
  return (seed - 1) / 2147483646;
};

/** The load when the chart starts. */
export const firstLoad = 35;

/** Calls `listener` with the load every 1.2 seconds. */
export function onLoad(listener: (load: number) => void) {
  // Busy, then idle, then busy again: anywhere from 10% to 95%.
  setInterval(() => listener(Math.round(10 + random() * 85)), 1200);
}
```

`src/style.css`:

```css
/* The rest of the scale: a faint track in the text color, light or dark. */
#chart .ct-series-b .ct-slice-donut {
  stroke: currentColor;
  stroke-opacity: 0.12;
}
```

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

How to animate your own layers and plugins: [keyframes](https://chartist.dev/docs/events-and-plugins/#keyframes) in the plugin guide, and [Animations](https://chartist.dev/docs/animations/#update-animations) for the rest of the animation story.

### Streaming

For data that arrives every second or faster, keep the duration at or below the update interval. Every update starts new animations on top of the running ones, and when they come faster than they end, the chart is always on its way somewhere and never quite there. With a `linear` easing and the duration of the interval, a stream scrolls without stopping.

Here, four network speed samples a second come in as touching bars, with their running average as a line on top. The samples are known by their number, so they all slide one bar to the left:

**Network speed** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import {
  autoScaleAxis,
  axisGrid,
  axisLabels,
  bar,
  cartesianAxes,
  chart,
  line,
  stepAxis
} from 'chartist';
import { liveDomRenderer, smilAnimations } from 'chartist/live';
import { SAMPLES, firstSamples, onSample } from './samples';

// The bars are as wide as one sample, so they touch.
let step = 0;
const container = document.querySelector<HTMLElement>('#chart')!;

const live = chart(container, {
  data: firstSamples,
  axes: cartesianAxes([
    {
      id: 'time',
      side: 'bottom',
      type: stepAxis(),
      showGrid: false,
      showLabel: false
    },
    { id: 'mbps', side: 'left', type: autoScaleAxis(), low: 0, high: 140 }
  ]),
  layers: [
    axisGrid({ axes: ['mbps'] }),
    bar({ series: ['speed'], x: 'time', y: 'mbps' }),
    line({
      series: ['average'],
      x: 'time',
      y: 'mbps',
      align: 'center',
      showPoint: false
    }),
    axisLabels()
  ],
  // Samples are known by their number, so they slide to the left.
  renderer: liveDomRenderer({ key: 'label', animations: smilAnimations() }),
  on: {
    layout: ({ area }) => void (step = area.width / SAMPLES),
    mount: () => container.style.setProperty('--ct-bar-width', `${step}px`)
  }
});

onSample(data => live.update({ data }));
```

`src/samples.ts`:

```ts
// Network speed samples, four per second, from a seeded generator, so every
// run measures the same speeds.

let seed = 11;
const random = () => {
  seed = (seed * 16807) % 2147483647;
  return (seed - 1) / 2147483646;
};

/** How many samples the chart shows. */
export const SAMPLES = 60;

let sample = 0;
let speed = 80;
const measure = () => {
  // Wanders around 80 Mbit/s, with a dip now and then.
  speed += (80 - speed) * 0.2 + (random() - 0.5) * 30;
  if (random() < 0.04) {
    speed *= 0.3;
  }
  sample += 1;
  return { label: sample, mbps: Math.max(1, Math.round(speed)) };
};
let samples = Array.from({ length: SAMPLES }, measure);

/** The samples by number, as a speed series and its running average. */
const dataOf = () => {
  const speeds = samples.map(s => s.mbps);
  const average = speeds.map((_, i) => {
    const recent = speeds.slice(Math.max(0, i - 7), i + 1);
    return recent.reduce((sum, value) => sum + value, 0) / recent.length;
  });
  return {
    labels: samples.map(s => s.label),
    series: [
      { name: 'speed', data: speeds },
      { name: 'average', data: average }
    ]
  };
};

/** The last 60 samples. */
export const firstSamples = dataOf();

/** Calls `listener` with the last 60 samples, four times a second. */
export function onSample(listener: (data: typeof firstSamples) => void) {
  setInterval(() => {
    samples = [...samples.slice(1), measure()];
    listener(dataOf());
  }, 250);
}
```

`src/style.css`:

```css
#chart {
  /* Faster than the samples come in, and steady. */
  --ct-update-duration: 200ms;
  --ct-update-easing: linear;
  --ct-line-width: 2px;
}

/* Salmon bars, and the average as a red line on top. */
#chart .ct-bar {
  stroke: var(--ct-series-b, #f05b4f);
}

#chart .ct-line {
  stroke: var(--ct-series-a, #d70206);
}
```

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

The [seismograph](https://chartist.dev/examples/live/seismograph/) in the examples updates every 80 ms.

## Data and options at once

`update()` takes any part of the config, so new data and new layers can arrive in one update. It's one render, so it's one animation: every bar moves from where it was to where it goes. These bars switch between grouped and stacked every two and a half seconds, with new numbers each time:

**Grouped and stacked** (runs in the browser):

`src/main.ts`:

```ts
import 'chartist/styles.css';
import './style.css';
import {
  autoScaleAxis,
  axisGrid,
  axisLabels,
  bar,
  cartesianAxes,
  chart,
  stepAxis
} from 'chartist';
import { liveDomRenderer, smilAnimations } from 'chartist/live';
import { salesData } from './sales';

// The same bars, grouped or stacked: only the layer's option differs.
const layers = (stackBars: boolean) => [
  axisGrid(),
  bar({ stackBars }),
  axisLabels()
];

let stacked = false;
const live = chart('#chart', {
  data: salesData(),
  axes: cartesianAxes([
    { id: 'months', side: 'bottom', type: stepAxis() },
    { id: 'sales', side: 'left', type: autoScaleAxis(), low: 0 }
  ]),
  layers: layers(stacked),
  renderer: liveDomRenderer({ animations: smilAnimations() })
});

// New numbers and the other layout, in one update: every bar moves from
// where it was to where it goes.
setInterval(() => {
  stacked = !stacked;
  live.update({ data: salesData(), layers: layers(stacked) });
}, 2500);
```

`src/sales.ts`:

```ts
// Sales by month in three series, from a seeded generator, so every run
// shows the same numbers.

let seed = 21;
const random = () => {
  seed = (seed * 16807) % 2147483647;
  return (seed - 1) / 2147483646;
};

const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
const series = () => months.map(() => Math.round(2 + random() * 8));

/** New sales numbers for every month, in three series. */
export const salesData = () => ({
  labels: months,
  series: [series(), series(), series()]
});
```

`src/style.css`:

```css
#chart {
  --ct-update-duration: 600ms;
  --ct-update-easing: ease-in-out;
}
```

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

The axis moves with them: its range grows for the stacks, and the grid lines and labels of the ticks that stay slide to their new place. On a preset, options change with `setOptions()` and data with `update()`, which are two renders: the second one picks up where the first one's animation is.

## What it costs

Live updates are opt-in, in their own entry point, so charts that don't use them don't carry them. Minified and gzipped, `liveDomRenderer()` adds about 1.4 kB to a chart, and `smilAnimations()` about 4 kB more. What the core needs for them (the previous render for renderers, `cause` and `changes`, cleanup functions) is about 0.6 kB, in every chart.

## Redrawing on resize

You don't need to listen to `resize` on the window. A chart watches its container with a `ResizeObserver` and lays itself out again when the container's size changes, whether the window, a sidebar or a CSS grid changed it. Only the layout and later phases run again; the config and data are reused.

Two things make this work: the container needs a size, and the chart needs room to follow it. [Sizing charts](https://chartist.dev/docs/sizing-charts/) covers both. For different options at different sizes, see [Responsive sugar topping](https://chartist.dev/docs/responsive/).

Charts with [SVG labels](https://chartist.dev/docs/server-rendering/#svg-or-html-labels) also lay out again when web fonts finish loading, since Chartist may have measured the labels before the font arrived.

## Detach and destroy

`detach()` stops a chart from following its container (and, for presets, the media queries of their responsive options). The SVG and your listeners stay, and `update()` still draws. Use it to freeze a chart, for example before printing or while you animate its container.

`destroy()` is for when the chart goes away: it detaches, runs the cleanup functions, removes the SVG from the container and removes every listener. Calling it twice is safe. In a component framework, destroy the chart where the component cleans up, so no observer keeps a removed element alive:

```ts
import { lineChart, type ChartHandle } from 'chartist';

let handle: ChartHandle | undefined;

export function mount(element: HTMLElement) {
  handle = lineChart(element, { series: [[1, 3, 2]] });
}

export function unmount() {
  handle?.destroy();
  handle = undefined;
}
```

In the API reference: [the chart handle](https://chartist.dev/api/chart/#ChartHandle) and [chartist/live](https://chartist.dev/api/live/).

Next: [animate charts with CSS](https://chartist.dev/docs/animations/), or [write a plugin](https://chartist.dev/docs/events-and-plugins/).
