motion-pie

Pie and donut charts for parts of a whole. The pie sweeps in when it scrolls into view, the slices spring to new sizes when the data changes, and the hovered slice springs outward. The legend shows each share.

Preview

Hover a slice, or press New data.

<motion-pie
  values="42, 28, 18, 12"
  labels="Direct, Search, Social, Email"
  label="Traffic sources"
></motion-pie>

<motion-pie
  donut
  values="1450, 620, 380, 240"
  labels="Rent, Food, Transport, Fun"
  label="Monthly budget"
  format="currency:EUR"
></motion-pie>

<button id="new-data">New data</button>

Set values from JavaScript to change the data. The slices spring to their new sizes:

const pie = document.querySelector('motion-pie')
const datasets = ['42, 28, 18, 12', '25, 40, 20, 15', '30, 15, 35, 20']
let step = 0

document.querySelector('#new-data').addEventListener('click', () => {
  step = (step + 1) % datasets.length
  pie.values = datasets[step]
})

Quick start

Save this as index.html and open it in a browser. No install, no build step.

<!doctype html>
<html>
  <head>
    <script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/dist/motion-pie.js/+esm"></script>
  </head>
  <body>
    <motion-pie
      values="42, 28, 18, 12"
      labels="Direct, Search, Social, Email"
      label="Traffic sources"
    ></motion-pie>
  </body>
</html>

In a project with a bundler, install the package and import the component once:

npm install motion-components
import 'motion-components/motion-pie'

When to use a pie

A pie shows how a whole splits into a few parts. Use it for six slices or fewer. When there are more, the extra slices are combined into one slice called "Other". To compare values that are close together, use a bar chart: lengths are easier to compare than angles.

Donut

Add donut to cut out the center. The center shows the total, with total-label as its caption. When you hover a slice, the center shows that slice instead of a tooltip.

<motion-pie
  donut
  values="1450, 620, 380, 240"
  labels="Rent, Food, Transport, Fun"
  total-label="Per month"
  format="currency:EUR"
></motion-pie>

Give the chart its data

1. Numbers in attributes

One number per slice in values, and one name per slice in labels.

<motion-pie values="42, 28, 18, 12" labels="Direct, Search, Social, Email"></motion-pie>

2. A table

Wrap a <table> with the labels in the first column and the values in the second. Screen readers still read the table, and the pie updates when a cell changes. Each value is the first number in its cell, and a cell's data-value overrides its text; see how cells are read.

<motion-pie label="Traffic sources">
  <table>
    <tr><th>Source</th><th>Visits</th></tr>
    <tr><td>Direct</td><td>4200</td></tr>
    <tr><td>Search</td><td>2800</td></tr>
    <tr><td>Social</td><td>1800</td></tr>
    <tr><td>Email</td><td>1200</td></tr>
  </table>
</motion-pie>

3. From JavaScript

document.querySelector('motion-pie').data = {
  labels: ['Direct', 'Search', 'Social', 'Email'],
  values: [4200, 2800, 1800, 1200],
}

More patterns, such as a period switcher, a chart from a form and data from a JSON file, are on the Chart Recipes page.

Frameworks

Write the data into attributes, or wrap a table.

<script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/dist/motion-pie.js/+esm"></script>

<motion-pie values="42, 28, 18, 12" labels="Direct, Search, Social, Email"></motion-pie>

React 19 sets data as a property, so pass the object from state. The slices spring to every new value.

import 'motion-components/motion-pie'

export function BudgetChart({ categories, amounts }) {
  return (
    <motion-pie
      donut
      format="currency:EUR"
      data={{ labels: categories, values: amounts }}
    ></motion-pie>
  )
}

Tell Vue that motion-* tags are custom elements (see Getting started), then bind :data.

<script setup>
import 'motion-components/motion-pie'

const props = defineProps(['categories', 'amounts'])
</script>

<template>
  <motion-pie
    donut
    format="currency:EUR"
    :data="{ labels: props.categories, values: props.amounts }"
  ></motion-pie>
</template>

Colors and size

Slices use the same color-blind-safe palette as motion-chart, in light and dark mode. Override it with --mc-chart-1 to --mc-chart-6.

<motion-pie
  values="60, 40"
  labels="Done, Open"
  style="--mc-chart-1: #16a34a; --mc-chart-2: #d4d4d8; --mc-chart-height: 12rem"
></motion-pie>

Properties

Attribute Property Type Default Description
values values string '' Comma-separated numbers, one per slice
labels labels string '' Comma-separated slice labels
data data object null Property only: { labels, values }. Takes precedence over a table and values
donut donut boolean false Cuts out the center and shows the total there
total-label totalLabel string 'Total' Caption under the total in a donut
format format string '' 'compact', 'percent', 'currency:EUR' or 'unit:kilometer'
label label string '' Accessible name of the chart
trigger trigger string 'view' When the sweep-in runs: 'view' or 'mount'
duration duration number 0.8 Spring duration in seconds
bounce bounce number 0.25 Spring bounciness when the values change
tooltip tooltip boolean true Set tooltip="false" to hide the tooltip. A donut uses its center instead
legend legend boolean true Set legend="false" to hide the legend

CSS custom properties

Property Type Default Description
--mc-chart-height length 16rem Height of the chart, including the legend
--mc-chart-1 … --mc-chart-6 color blue, orange, aqua, yellow, magenta, green Slice colors, in order
--mc-chart-surface color Canvas Background behind the chart. Used for the gap between slices and the tooltip

JavaScript API

replay() sweeps the pie in again. It also has play(), pause(), finish(), cancel(), playState and the motion-start and motion-finish events. See the JavaScript API.

Accessibility

Screen readers get the data as a table with each value and share. Give the chart a label. Press Tab to focus it, then the arrow keys to move between slices, Home and End for the first and last, and Esc to clear. Each slice is announced with its value and share. With prefers-reduced-motion, the pie shows at once and changes without animation.