Skip to content

Pie / Donut

Composition

"What share does each part take of the whole?" The oldest question in charting, and a pie still answers it best when there are only a handful of slices. Each wedge is sized by value and labelled with its percentage; slices sort by value so the biggest reads first. Want a donut instead? It is the same chart - set innerRadiusRatio above 0 to carve out the hole (the context then reports mode: "donut").

Example
canvas · responsive
Go deeper: Insights guide·DevTools guide

The donut variant is one prop away - here the same shares with innerRadiusRatio: 0.6, a small padAngle, and rounded corners:

Example
canvas · responsive
Go deeper: Insights guide·DevTools guide

Keep slice counts low (≈ 6 or fewer). For many categories, a bar or treemap reads more precisely than a pie.

When to reach for it

  • Board-deck shares. A handful of slices, each labelled with its percentage: still the fastest "who takes what share of the whole" a slide can carry.
  • Dashboard donuts. Set innerRadiusRatio and the carved-out centre becomes prime real estate for the headline number the pie is proving.

Heavy data on WebGPU Experimental

PieChart has an opt-in renderer="webgpu" that paints the slices as GPU-drawn arcs while labels, legend and tooltips stay on the SVG layer. It is capability-gated: on a browser without WebGPU it downgrades to canvas automatically, and getContext().renderer reports whichever actually painted.

⚗️ Experimental - not yet stable. WebGPU rendering is an opt-in preview. It needs a WebGPU-capable browser (Chrome / Edge, or Safari 26+); everywhere else it falls back to canvas automatically. Axes, labels and tooltips stay on the SVG layer - only the data marks are painted on the GPU.
Heavy-data demo · 40 slices… detecting

Play through the years

Give every slice a date and flip on timeline: the pie chart becomes a year-by-year story with its own play button and scrubber, snapshotting one period's shares at a time. Off by default - nothing changes until a chart opts in.

Press the play button under the chart: it steps through the years, one snapshot at a time. Drag the scrubber to jump to any year.

tsx
const ref = useRef<PieChartHandle>(null);

<PieChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()
vue
<PieChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:pieChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applyPieChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-pie-chart id="c"></michi-vz-pie-chart>
<script>
  const el = document.getElementById("c");
  el.timeline = { speedMs: 1000, loop: true };
  // el.getTimeline() -> play() / pause() / seek(year)
</script>
  • speedMs sets the pace, loop wraps around, autoplay: true starts on mount, showControl: false hides the built-in bar.
  • Values glide between periods by default (interpolate); tune the motion with tweenMs and easing, or set interpolate: false for hard cuts. Reduced motion always gets the hard cut.
  • The headless controller is always available: chart.timeline() exposes play() / pause() / toggle() / seek(period) / stepForward() / stepBack(), plus onStep and formatPeriod in the config for custom UI.
  • A filter (top-N, sorting) still applies inside each period, so only the top 5 slices per year make the cut.
  • Slices without a date stay visible in every period.

Usage

tsx
import { PieChart } from "@michi-vz/react";

export default () => <PieChart {...props} />; // props = the chart options
vue
<script setup>
import { PieChart } from "@michi-vz/vue";
</script>

<template>
  <PieChart :options="props" />
</template>
svelte
<script>
  import { pieChart } from "@michi-vz/svelte";
</script>

<div use:pieChart={props}></div>
ts
// main.ts - register the elements once
import "@michi-vz/angular";
import { applyPieChartProps } from "@michi-vz/angular";

// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-pie-chart #c></michi-vz-pie-chart>
applyPieChartProps(this.c.nativeElement, props);
html
<script type="module" src="https://cdn.jsdelivr.net/npm/@michi-vz/wc/dist/michi-vz-wc.bundle.js"></script>

<michi-vz-pie-chart id="c"></michi-vz-pie-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet, innerRadiusRatio, …
</script>
ts
import { mountPieChart } from "@michi-vz/core";

const chart = mountPieChart(el, props);
chart.update(next);
chart.getContext(); // renderer-agnostic, LLM-ready
chart.destroy();

Data shape

Each dataSet item is one slice: a label, a value, and an optional color.

ts
const props = {
  innerRadiusRatio: 0, // 0 = pie; e.g. 0.6 = donut
  showLabels: true,    // % labels inside large-enough slices
  showLegend: true,
  dataSet: [
    { label: "Industry", value: 281, color: "#005aba" },
    { label: "Agri-food", value: 381, color: "#f0a500" },
    { label: "Materials", value: 132, color: "#2aa39a" },
  ],
};

Pie vs donut

One engine renders both. innerRadiusRatio is the hole as a fraction of the outer radius: 0 is a solid pie, 0.6 a donut. padAngle (radians) adds a gap between slices and cornerRadius rounds the arc corners. The slices, tooltip, getContext() and SVG/canvas parity are identical either way.

API

Props are typed as PieChartProps in @michi-vz/core. Shared across all charts: width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", or experimental "webgpu"), highlightItems, disabledItems, and the on* callbacks. onChartDataProcessed / getContext() return the renderer-agnostic ChartContext. Full reference: Pie / Donut API.