# Chart

Composable animated bar, line, and area charts built on LayerChart.

- Package: `@mielui/svelte`
- Component version: `1.1.0`
- Depends on Mielui components: _internal/utils, card, skeleton
- Shared utilities: components/_internal/theme-changes, components/_internal/surface, components/_internal/chart-tooltip-surface, transition, utils.cn, utils.createContext

## Install

~~~~sh
pnpm dlx @mielui/svelte add chart
~~~~

## Chart guides

- [Bar chart](/docs/components/chart/bar.md): Compare categories with grouped or stacked bars.
- [Line chart](/docs/components/chart/line.md): Follow changes over time without a filled area.
- [Area chart](/docs/components/chart/area.md): Show the amount over time with a filled series.
- [Mixed chart](/docs/components/chart/mixed.md): Compare totals and benchmarks in one plot.

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Area

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| key | string | - | Yes | No | Series to draw. Matches a data field and a `config` entry. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| strokeWidth | number \| undefined | - | No | No | Thickness of the line in pixels. |



### Bar

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| key | string | - | Yes | No | Series to draw. Matches a data field and a `config` entry. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| radius | number \| undefined | 4 | No | No | Corner radius of each bar in pixels. |



### Grid

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| ticks | number \| undefined | 5 | No | No | Number of grid lines to aim for. |



### Legend

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[{ key: string; label: string; color: string; }]> \| undefined | - | No | No | Renders one legend entry. Omit it for a dot and the series label. |



### Line

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| key | string | - | Yes | No | Series to draw. Matches a data field and a `config` entry. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| strokeWidth | number \| undefined | - | No | No | Thickness of the line in pixels. |



### Plot

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Marks, axes, and grid drawn inside the plot. |
| empty | Snippet<[]> \| undefined | - | No | No | Replaces the built-in empty state. |
| loading | Snippet<[]> \| undefined | - | No | No | Replaces the built-in loading skeleton. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| aria-label | string | - | Yes | No | Defines a string value that labels the current element. |
| labels | ChartLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| data | readonly T[] | - | Yes | No | Rows to plot. Each row holds a category and one numeric field per series. |
| config | { [x: string]: { label: string; color?: string \| undefined; format?: ((value: number) => string) \| undefined; }; } | - | Yes | No | Label, color, and value format for each series, keyed by series. |
| x | string | - | Yes | No | Field of each row that holds its category. |
| orientation | "vertical" \| "horizontal" \| undefined | 'vertical' | No | No | `horizontal` lays categories down the side and values across. |
| stacked | boolean \| undefined | false | No | No | Stacks bars of the same category instead of placing them side by side. |
| loading | boolean \| undefined | false | No | No | Shows the loading state in place of the content. |
| animation | "reveal" \| "live" \| "none" \| undefined | 'reveal' | No | No | `reveal` animates in once, `live` keeps a subtle motion running, and `none` is static. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Tooltip

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[{ label: string; values: { key: string; label: string; value: number; formatted: string; color: string; }[]; }]> \| undefined | - | No | No | Renders the tooltip body for the inspected category. |



### XAxis

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| ticks | number \| undefined | - | No | No | Number of ticks to aim for on a value axis. |
| format | ((value: string \| number) => string) \| undefined | - | No | No | Formats each tick label. |



### YAxis

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| ticks | number \| undefined | - | No | No | Number of ticks to aim for on a value axis. |
| format | ((value: string \| number) => string) \| undefined | - | No | No | Formats each tick label. |



### Public types

~~~~ts
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { ChartRow, Config } from './context';

export type ChartLabels = {
    categories?: string;
    inspect?: (category: string) => string;
    loading?: string;
    empty?: string;
    emptyDescription?: string;
    /** Cell text in the accessible data table when a point has no value. */
    noData?: string;
};

export type ChartProps<T extends ChartRow = ChartRow> = Omit<
    HTMLAttributes<HTMLDivElement>,
    'children'
> & {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: ChartLabels;
    /** Rows to plot. Each row holds a category and one numeric field per series. */
    data: readonly T[];
    /** Label, color, and value format for each series, keyed by series. */
    config: Config;
    /** Field of each row that holds its category. */
    x: keyof T & string;
    /** `horizontal` lays categories down the side and values across. */
    orientation?: 'vertical' | 'horizontal';
    /** Stacks bars of the same category instead of placing them side by side. */
    stacked?: boolean;
    /** Shows the loading state in place of the content. */
    loading?: boolean;
    /** `reveal` animates in once, `live` keeps a subtle motion running, and `none` is static. */
    animation?: 'reveal' | 'live' | 'none';
    'aria-label': string;
    /** Content rendered inside. */
    children?: Snippet;
};

export type ChartPlotProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
    /** Marks, axes, and grid drawn inside the plot. */
    children?: Snippet;
    /** Replaces the built-in empty state. */
    empty?: Snippet;
    /** Replaces the built-in loading skeleton. */
    loading?: Snippet;
};

export type ChartBarProps = {
    /** Series to draw. Matches a data field and a `config` entry. */
    key: string;
    /** Classes added to the element. */
    class?: string;
    /** Corner radius of each bar in pixels. */
    radius?: number;
};

export type ChartLineProps = {
    /** Series to draw. Matches a data field and a `config` entry. */
    key: string;
    /** Classes added to the element. */
    class?: string;
    /** Thickness of the line in pixels. */
    strokeWidth?: number;
};

export type ChartAreaProps = ChartLineProps;

export type ChartXAxisProps = {
    /** Classes added to the element. */
    class?: string;
    /** Number of ticks to aim for on a value axis. */
    ticks?: number;
    /** Formats each tick label. */
    format?: (value: string | number) => string;
};

export type ChartYAxisProps = ChartXAxisProps;

export type ChartGridProps = {
    /** Classes added to the element. */
    class?: string;
    /** Number of grid lines to aim for. */
    ticks?: number;
};

export type ChartLegendProps = {
    /** Classes added to the element. */
    class?: string;
    /** Renders one legend entry. Omit it for a dot and the series label. */
    children?: Snippet<[{ key: string; label: string; color: string }]>;
};

export type ChartTooltipProps = {
    /** Classes added to the element. */
    class?: string;
    /** Renders the tooltip body for the inspected category. */
    children?: Snippet<
        [
            {
                label: string;
                values: {
                    key: string;
                    label: string;
                    value: number;
                    formatted: string;
                    color: string;
                }[];
            }
        ]
    >;
};

export { default as Area } from './area.svelte';
export { default as Bar } from './bar.svelte';
export { default as Grid } from './grid.svelte';
export { default as Legend } from './legend.svelte';
export { default as Line } from './line.svelte';
export { default as Plot } from './plot.svelte';
export { default as Root } from './root.svelte';
export { default as Tooltip } from './tooltip.svelte';
export { default as XAxis } from './x-axis.svelte';
export { default as YAxis } from './y-axis.svelte';
~~~~

## Examples

### Area States

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Chart from '@mielui/svelte/components/chart';
    import * as Tabs from '@mielui/svelte/components/tabs';

    let dataState = $state('ready');
    let animation = $state<'reveal' | 'live' | 'none'>('reveal');
    let replay = $state(0);
    const records = [
        { month: 'April', orders: 48 },
        { month: 'May', orders: 72 },
        { month: 'June', orders: 61 }
    ];
    const config = { orders: { label: 'Orders', color: 'var(--chart-1)' } };
    const data = $derived(dataState === 'empty' ? [] : records);

    function changeAnimation(value: string) {
        if (value === 'reveal' || value === 'live' || value === 'none') {
            animation = value;
        }
    }
</script>
<div class="w-full space-y-4">
    <div class="flex flex-wrap items-center gap-3">
        <Tabs.Root bind:value={dataState} variant="ghost">
            <div role="group" aria-label="Chart data state">
                <Tabs.List>
                    <Tabs.Trigger value="ready">Ready</Tabs.Trigger>
                    <Tabs.Trigger value="loading">Loading</Tabs.Trigger>
                    <Tabs.Trigger value="empty">No data</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Tabs.Root value={animation} onValueChange={changeAnimation} variant="ghost">
            <div role="group" aria-label="Chart animation">
                <Tabs.List>
                    <Tabs.Trigger value="reveal">Reveal</Tabs.Trigger>
                    <Tabs.Trigger value="live">Live</Tabs.Trigger>
                    <Tabs.Trigger value="none">None</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Button
            variant="secondary"
            onclick={() => {
                replay += 1;
            }}
        >
            Replay
        </Button>
    </div>
    {#key `${animation}-${replay}`}
        <Chart.Root
            {data}
            {config}
            {animation}
            x="month"
            loading={dataState === 'loading'}
            aria-label="Quarterly orders"
        >
            <Chart.Plot>
                <Chart.Grid />
                <Chart.XAxis />
                <Chart.YAxis />
                <Chart.Area key="orders" />
            </Chart.Plot>
            <Chart.Legend />
            <Chart.Tooltip />
        </Chart.Root>
    {/key}
</div>
~~~~

### Bar States

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Chart from '@mielui/svelte/components/chart';
    import * as Tabs from '@mielui/svelte/components/tabs';

    let dataState = $state('ready');
    let animation = $state<'reveal' | 'live' | 'none'>('reveal');
    let replay = $state(0);
    const records = [
        { month: 'April', orders: 48 },
        { month: 'May', orders: 72 },
        { month: 'June', orders: 61 }
    ];
    const config = { orders: { label: 'Orders', color: 'var(--chart-1)' } };
    const data = $derived(dataState === 'empty' ? [] : records);

    function changeAnimation(value: string) {
        if (value === 'reveal' || value === 'live' || value === 'none') {
            animation = value;
        }
    }
</script>
<div class="w-full space-y-4">
    <div class="flex flex-wrap items-center gap-3">
        <Tabs.Root bind:value={dataState} variant="ghost">
            <div role="group" aria-label="Chart data state">
                <Tabs.List>
                    <Tabs.Trigger value="ready">Ready</Tabs.Trigger>
                    <Tabs.Trigger value="loading">Loading</Tabs.Trigger>
                    <Tabs.Trigger value="empty">No data</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Tabs.Root value={animation} onValueChange={changeAnimation} variant="ghost">
            <div role="group" aria-label="Chart animation">
                <Tabs.List>
                    <Tabs.Trigger value="reveal">Reveal</Tabs.Trigger>
                    <Tabs.Trigger value="live">Live</Tabs.Trigger>
                    <Tabs.Trigger value="none">None</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Button
            variant="secondary"
            onclick={() => {
                replay += 1;
            }}
        >
            Replay
        </Button>
    </div>
    {#key `${animation}-${replay}`}
        <Chart.Root
            {data}
            {config}
            {animation}
            x="month"
            loading={dataState === 'loading'}
            aria-label="Quarterly orders"
        >
            <Chart.Plot>
                <Chart.Grid />
                <Chart.XAxis />
                <Chart.YAxis />
                <Chart.Bar key="orders" />
            </Chart.Plot>
            <Chart.Legend />
            <Chart.Tooltip />
        </Chart.Root>
    {/key}
</div>
~~~~

### Line States

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Chart from '@mielui/svelte/components/chart';
    import * as Tabs from '@mielui/svelte/components/tabs';

    let dataState = $state('ready');
    let animation = $state<'reveal' | 'live' | 'none'>('reveal');
    let replay = $state(0);
    const records = [
        { month: 'April', orders: 48 },
        { month: 'May', orders: 72 },
        { month: 'June', orders: 61 }
    ];
    const config = { orders: { label: 'Orders', color: 'var(--chart-1)' } };
    const data = $derived(dataState === 'empty' ? [] : records);

    function changeAnimation(value: string) {
        if (value === 'reveal' || value === 'live' || value === 'none') {
            animation = value;
        }
    }
</script>
<div class="w-full space-y-4">
    <div class="flex flex-wrap items-center gap-3">
        <Tabs.Root bind:value={dataState} variant="ghost">
            <div role="group" aria-label="Chart data state">
                <Tabs.List>
                    <Tabs.Trigger value="ready">Ready</Tabs.Trigger>
                    <Tabs.Trigger value="loading">Loading</Tabs.Trigger>
                    <Tabs.Trigger value="empty">No data</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Tabs.Root value={animation} onValueChange={changeAnimation} variant="ghost">
            <div role="group" aria-label="Chart animation">
                <Tabs.List>
                    <Tabs.Trigger value="reveal">Reveal</Tabs.Trigger>
                    <Tabs.Trigger value="live">Live</Tabs.Trigger>
                    <Tabs.Trigger value="none">None</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Button
            variant="secondary"
            onclick={() => {
                replay += 1;
            }}
        >
            Replay
        </Button>
    </div>
    {#key `${animation}-${replay}`}
        <Chart.Root
            {data}
            {config}
            {animation}
            x="month"
            loading={dataState === 'loading'}
            aria-label="Quarterly orders"
        >
            <Chart.Plot>
                <Chart.Grid />
                <Chart.XAxis />
                <Chart.YAxis />
                <Chart.Line key="orders" />
            </Chart.Plot>
            <Chart.Legend />
            <Chart.Tooltip />
        </Chart.Root>
    {/key}
</div>
~~~~

### Mixed States

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Chart from '@mielui/svelte/components/chart';
    import * as Tabs from '@mielui/svelte/components/tabs';

    let dataState = $state('ready');
    let animation = $state<'reveal' | 'live' | 'none'>('reveal');
    let replay = $state(0);
    const records = [
        { month: 'April', orders: 48, target: 60 },
        { month: 'May', orders: 72, target: 65 },
        { month: 'June', orders: 61, target: 70 }
    ];
    const config = {
        orders: { label: 'Orders', color: 'var(--chart-1)' },
        target: { label: 'Target', color: 'var(--chart-2)' }
    };
    const data = $derived(dataState === 'empty' ? [] : records);

    function changeAnimation(value: string) {
        if (value === 'reveal' || value === 'live' || value === 'none') {
            animation = value;
        }
    }
</script>
<div class="w-full space-y-4">
    <div class="flex flex-wrap items-center gap-3">
        <Tabs.Root bind:value={dataState} variant="ghost">
            <div role="group" aria-label="Chart data state">
                <Tabs.List>
                    <Tabs.Trigger value="ready">Ready</Tabs.Trigger>
                    <Tabs.Trigger value="loading">Loading</Tabs.Trigger>
                    <Tabs.Trigger value="empty">No data</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Tabs.Root value={animation} onValueChange={changeAnimation} variant="ghost">
            <div role="group" aria-label="Chart animation">
                <Tabs.List>
                    <Tabs.Trigger value="reveal">Reveal</Tabs.Trigger>
                    <Tabs.Trigger value="live">Live</Tabs.Trigger>
                    <Tabs.Trigger value="none">None</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Button
            variant="secondary"
            onclick={() => {
                replay += 1;
            }}
        >
            Replay
        </Button>
    </div>
    {#key `${animation}-${replay}`}
        <Chart.Root
            {data}
            {config}
            {animation}
            x="month"
            loading={dataState === 'loading'}
            aria-label="Quarterly orders"
        >
            <Chart.Plot>
                <Chart.Grid />
                <Chart.XAxis />
                <Chart.YAxis />
                <Chart.Bar key="orders" />
                <Chart.Line key="target" />
            </Chart.Plot>
            <Chart.Legend />
            <Chart.Tooltip />
        </Chart.Root>
    {/key}
</div>
~~~~

### States

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Chart from '@mielui/svelte/components/chart';
    import * as EmptyState from '@mielui/svelte/components/empty-state';

    let state = $state<'ready' | 'loading' | 'empty'>('ready');
    const records = [
        { month: 'April', orders: 48 },
        { month: 'May', orders: 72 },
        { month: 'June', orders: 61 }
    ];
    const data = $derived(state === 'empty' ? [] : records);
    const config = { orders: { label: 'Orders', color: 'var(--chart-1)' } };

    function showReady() {
        state = 'ready';
    }

    function showLoading() {
        state = 'loading';
    }

    function showEmpty() {
        state = 'empty';
    }
</script>
<div class="w-full space-y-4">
    <div class="flex flex-wrap gap-2">
        <Button variant="secondary" onclick={showReady}>Show data</Button>
        <Button variant="secondary" onclick={showLoading}>Show loading</Button>
        <Button variant="secondary" onclick={showEmpty}>Show empty</Button>
    </div>
    <Chart.Root
        {data}
        {config}
        x="month"
        loading={state === 'loading'}
        aria-label="Orders this quarter"
    >
        <Chart.Plot>
            {#snippet empty()}
                <EmptyState.Root class="border-0 p-4">
                    <EmptyState.Header>
                        <EmptyState.Title>No orders in this period</EmptyState.Title>
                        <EmptyState.Description>
                            Try another period to see your order history.
                        </EmptyState.Description>
                    </EmptyState.Header>
                    <EmptyState.Actions>
                        <Button variant="secondary" onclick={showReady}>Show last quarter</Button>
                    </EmptyState.Actions>
                </EmptyState.Root>
            {/snippet}
            <Chart.Grid />
            <Chart.XAxis />
            <Chart.YAxis />
            <Chart.Bar key="orders" />
        </Chart.Plot>
        <Chart.Tooltip />
    </Chart.Root>
</div>
~~~~

For the rendered reference, visit [/docs/components/chart](/docs/components/chart).
