---
title: "JavaScript Chart Events, Drilldown and Zoom"
description: "Chart events for click and legend, declarative drilldown with breadcrumbs, drag-zoom and a navigator strip, plus live data driven by reactive states."
source: https://lemonadejs.com/charts/docs/interactivity/
---

# Chart events, drilldown and zoom

## Events

All event names are lowercase (the platform convention):

```js
html`<${Charts} type="bar" series="${series}"
    onpointclick="${(point, { seriesIndex, pointIndex }) => select(point)}"
    onlegendclick="${(key, visible) => console.log(key, visible)}"
    ondrilldown="${(key, depth) => console.log('into', key)}"
    ondrillup="${(depth) => console.log('back to', depth)}" />`
```

Clicking a legend entry toggles that series' visibility, and the chart
rescales to the remaining data automatically.

## Drilldown

Declarative: map a category (or slice) name to the chart it opens.
Descending renders a breadcrumb for the way back, with no handlers needed:

<!--example-->

```js
import Charts from '@lemonadejs/charts';

const App = () => html`<${Charts} type="bar" title="Sales by region (click a bar)"
    categories="${['Americas', 'EMEA', 'APAC']}"
    series="${[{ name: 'Sales', data: [82, 61, 47] }]}" labels
    drilldown="${{
        Americas: { categories: ['US', 'CA', 'BR', 'MX'], series: [{ name: 'Sales', data: [40, 18, 15, 9] }] },
        EMEA: { categories: ['DE', 'UK', 'FR', 'ES'], series: [{ name: 'Sales', data: [22, 17, 12, 10] }] },
        APAC: { categories: ['CN', 'JP', 'IN', 'AU'], series: [{ name: 'Sales', data: [19, 12, 10, 6] }] },
    }}" />`;
```

Levels nest (a drilldown target can carry its own `drilldown`), and each
level may change `type` and `title` too.

## Zoom and navigator

For dense series, `zoom` enables drag-select along the x-axis (with a
reset button), and `navigator` renders an overview strip below the plot
with a draggable, resizable window:

<!--example-->

```js
import Charts from '@lemonadejs/charts';

const data = Array.from({ length: 60 }, (_, i) =>
    Math.round(70 + Math.sin(i / 5) * 20 + Math.random() * 8));

const App = () => html`<${Charts} type="line"
    categories="${data.map((_, i) => 'D' + (i + 1))}"
    series="${[{ name: 'Price', data }]}"
    markers="${false}" zoom navigator legend="${false}" />`;
```

## Live data

Props are states, so live charts are just assignment. A polling dashboard,
a websocket feed, or a simulation all look the same:

<!--example-->

```js
import Charts from '@lemonadejs/charts';

const App = (props, { state, onMount }) => {
    const series = state([{ name: 'Load', data: [50] }]);
    onMount(() => {
        const timer = setInterval(() => {
            const prev = series.value[0].data;
            const next = [...prev.slice(-29),
                Math.max(5, Math.min(95, prev[prev.length - 1] + (Math.random() - 0.5) * 14))];
            series.value = [{ name: 'Load', data: next }];
        }, 800);
        return () => clearInterval(timer);   // cleanup on unmount
    });
    return html`<${Charts} type="line" series="${series}" smooth
        markers="${false}" legend="${false}" height="${220}"
        ymin="${0}" ymax="${100}" />`;
};
```

Remember the v6 contract: build a new array and **assign**. In-place
`push` alone does not trigger (or call `.touch()` after mutating).

`animate` (on by default) gives entrance/update transitions and respects
`prefers-reduced-motion`.

## Export

`toolbar` shows a small toolbar with CSV download; SVG export is
available for the SVG-native types. Both work entirely client-side: the
chart *is* the document.