# Pie Chart

Composable pie and donut charts with animated updates, accessible values, tooltips, and legends.

- Package: `@mielui/svelte`
- Component version: `1.0.1`
- 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 pie-chart
~~~~

## Chart guides

- [Pie chart](/docs/components/pie-chart/pie.md): Show a few categories as shares of a total.
- [Donut chart](/docs/components/pie-chart/donut.md): Place a total or active value inside a category breakdown.

## 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.


### Arc

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| innerRadius | number \| undefined | 0.68 | No | No | Hole size as a share of the radius. Zero draws a full pie. |
| cornerRadius | number \| undefined | 4 | No | No | Corner radius of each segment in pixels. |
| padAngle | number \| undefined | 0.035 | No | No | Gap between segments in radians. |
| class | string \| undefined | - | No | No | Classes added to the element. |



### Label

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[{ total: number; active: PieChartDatum \| undefined; }]> \| undefined | - | No | No | Content rendered inside. |

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

### Legend

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[{ item: PieChartDatum; label: string; value: string; percentage: number; }]> \| undefined | - | No | No | Content rendered inside. |

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

### Plot

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

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 | PieChartLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| data | readonly PieChartDatum[] | - | Yes | No | Segments to plot, each with a key and a value. |
| 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. |
| 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 |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[{ item: PieChartDatum; label: string; value: string; percentage: number; }]> \| undefined | - | No | No | Content rendered inside. |

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

### Public types

~~~~ts
import type { Snippet } from 'svelte';
import type { HTMLAttributes, SVGAttributes } from 'svelte/elements';
import Root from './pie-chart.svelte';
import Arc from './pie-chart-arc.svelte';
import Label from './pie-chart-label.svelte';
import Legend from './pie-chart-legend.svelte';
import Plot from './pie-chart-plot.svelte';
import Tooltip from './pie-chart-tooltip.svelte';

export type PieChartDatum = { key: string; value: number };
export type PieChartConfig = Record<
    string,
    { label: string; color?: string; format?: (value: number) => string }
>;
export type PieChartLabels = {
    loading?: string;
    empty?: string;
    category?: string;
    value?: string;
};

export type PieChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: PieChartLabels;
    /** Segments to plot, each with a key and a value. */
    data: readonly PieChartDatum[];
    /** Label, color, and value format for each series, keyed by series. */
    config: PieChartConfig;
    'aria-label': string;
    /** 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';
    /** Content rendered inside. */
    children?: Snippet;
};
export type PieChartPlotProps = HTMLAttributes<HTMLDivElement>;
export type PieChartArcProps = {
    /** Hole size as a share of the radius. Zero draws a full pie. */
    innerRadius?: number;
    /** Corner radius of each segment in pixels. */
    cornerRadius?: number;
    /** Gap between segments in radians. */
    padAngle?: number;
    /** Classes added to the element. */
    class?: string;
};
export type PieChartLabelProps = Omit<SVGAttributes<SVGTextElement>, 'children'> & {
    /** Content rendered inside. */
    children?: Snippet<[{ total: number; active: PieChartDatum | undefined }]>;
};
export type PieChartTooltipProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
    /** Content rendered inside. */
    children?: Snippet<[{ item: PieChartDatum; label: string; value: string; percentage: number }]>;
};
export type PieChartLegendProps = Omit<HTMLAttributes<HTMLUListElement>, 'children'> & {
    /** Content rendered inside. */
    children?: Snippet<[{ item: PieChartDatum; label: string; value: string; percentage: number }]>;
};
export { Arc, Label, Legend, Plot, Root, Tooltip };
~~~~

## Examples

### Donut States

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

    const records = [
        { key: 'completed', value: 72 },
        { key: 'remaining', value: 28 }
    ];
    const config = {
        completed: { label: 'Completed', color: 'var(--chart-1)' },
        remaining: { label: 'Remaining', color: 'var(--chart-2)' }
    };

    let dataState = $state('ready');
    let animation = $state<'reveal' | 'live' | 'none'>('reveal');
    let replay = $state(0);

    const data = $derived.by(() => {
        if (dataState === 'empty') {
            return [];
        }

        if (dataState === 'zero') {
            return records.map((item) => ({ ...item, value: 0 }));
        }

        return 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.Trigger value="zero">Zero total</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}`}
        <PieChart.Root
            {data}
            {config}
            {animation}
            loading={dataState === 'loading'}
            aria-label="Quarterly orders"
        >
            <PieChart.Plot>
                <PieChart.Arc />
                <PieChart.Label />
            </PieChart.Plot>
            <PieChart.Legend />
            <PieChart.Tooltip />
        </PieChart.Root>
    {/key}
</div>
~~~~

### Pie States

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

    const records = [
        { key: 'completed', value: 72 },
        { key: 'remaining', value: 28 }
    ];
    const config = {
        completed: { label: 'Completed', color: 'var(--chart-1)' },
        remaining: { label: 'Remaining', color: 'var(--chart-2)' }
    };

    let dataState = $state('ready');
    let animation = $state<'reveal' | 'live' | 'none'>('reveal');
    let replay = $state(0);

    const data = $derived.by(() => {
        if (dataState === 'empty') {
            return [];
        }

        if (dataState === 'zero') {
            return records.map((item) => ({ ...item, value: 0 }));
        }

        return 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.Trigger value="zero">Zero total</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}`}
        <PieChart.Root
            {data}
            {config}
            {animation}
            loading={dataState === 'loading'}
            aria-label="Quarterly orders"
        >
            <PieChart.Plot>
                <PieChart.Arc innerRadius={0} />
            </PieChart.Plot>
            <PieChart.Legend />
            <PieChart.Tooltip />
        </PieChart.Root>
    {/key}
</div>
~~~~

### States

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

    const records = [
        { key: 'completed', value: 72 },
        { key: 'remaining', value: 28 }
    ];
    const config = {
        completed: { label: 'Completed', color: 'var(--chart-2)' },
        remaining: { label: 'Remaining', color: 'var(--chart-1)' }
    };

    let view = $state<'loading' | 'empty' | 'ready'>('loading');

    const data = $derived(view === 'empty' ? [] : records);

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

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

    function showEmpty() {
        view = 'empty';
    }
</script>

<div class="w-full max-w-lg">
    <div class="flex flex-wrap justify-center gap-2">
        <Button variant={view === 'loading' ? 'primary' : 'secondary'} onclick={showLoading}>
            Loading
        </Button>
        <Button variant={view === 'empty' ? 'primary' : 'secondary'} onclick={showEmpty}>
            Empty
        </Button>
        <Button variant={view === 'ready' ? 'primary' : 'secondary'} onclick={showReady}>
            Ready
        </Button>
    </div>
    <PieChart.Root {data} {config} loading={view === 'loading'} aria-label="Checklist completion">
        <PieChart.Plot>
            <PieChart.Arc />
            <PieChart.Label />
        </PieChart.Plot>
        {#if view === 'ready'}
            <PieChart.Legend />
        {/if}
        <PieChart.Tooltip />
    </PieChart.Root>
</div>
~~~~

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