Skip to content
ShineUI
Install

Pie chart

Pie, donut and nightingale.

<sui-chart-pie>ChartsStable

About

Pie, donut and nightingale chart for composition: expense mix, volume by weekday. Slices walk the brand ramp and one item may take the accent. The legend is hover-linked: a row lifts its slice, a slice dims the other rows, and clicking a row hides its item and re-shares the rest. The donut center reads the visible total.

Usage

<sui-chart-pie heading="Operating expense mix" subtitle="Donut · hover a slice or a legend row · YTD 2026" kind="donut" center-label="Total opex" caption="Vehicle costs flagged orange, up 18% against plan"></sui-chart-pie> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-chart-pie"); el.items = [ { label: "Payroll & wages", value: 289400 }, { label: "Rent", value: 42000 }, { label: "Marketing", value: 21350 }, { label: "Insurance", value: 18600 }, { label: "Vehicle", value: 14220, accent: true }, { label: "Other", value: 51060 }, ]; /* SuiPieItem[] */ el.addEventListener("suiHover", (e) => console.log(e.detail)); </script>
import { SuiChartPie } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { label: "Payroll & wages", value: 289400 }, { label: "Rent", value: 42000 }, { label: "Marketing", value: 21350 }, { label: "Insurance", value: 18600 }, { label: "Vehicle", value: 14220, accent: true }, { label: "Other", value: 51060 }, ]; /* SuiPieItem[] */ return <SuiChartPie heading="Operating expense mix" subtitle="Donut · hover a slice or a legend row · YTD 2026" kind="donut" centerLabel="Total opex" caption="Vehicle costs flagged orange, up 18% against plan" items={items} onSuiHover={(e) => console.log(e.detail)} />; }
<script setup> import { SuiChartPie } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { label: "Payroll & wages", value: 289400 }, { label: "Rent", value: 42000 }, { label: "Marketing", value: 21350 }, { label: "Insurance", value: 18600 }, { label: "Vehicle", value: 14220, accent: true }, { label: "Other", value: 51060 }, ]; // SuiPieItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiChartPie heading="Operating expense mix" subtitle="Donut · hover a slice or a legend row · YTD 2026" kind="donut" center-label="Total opex" caption="Vehicle costs flagged orange, up 18% against plan" :items="items" @sui-hover="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiChartPie } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChartPie], template: `<sui-chart-pie heading="Operating expense mix" subtitle="Donut · hover a slice or a legend row · YTD 2026" kind="donut" center-label="Total opex" caption="Vehicle costs flagged orange, up 18% against plan" [items]="items" (suiHover)="onChange($event)"></sui-chart-pie>`, }) export class Example { items = [ { label: "Payroll & wages", value: 289400 }, { label: "Rent", value: 42000 }, { label: "Marketing", value: 21350 }, { label: "Insurance", value: 18600 }, { label: "Vehicle", value: 14220, accent: true }, { label: "Other", value: 51060 }, ]; // SuiPieItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Donut

The plot is one tab stop named by an aria-label on the host, else by every visible slice with its figure, and the arrow keys walk the slices. Slices take the ramp in turn and the accent item --accent. Legend rows are buttons with aria-pressed: hover or focus one to lift its slice, click it to hide the item.

<sui-chart-pie heading="Operating expense mix" subtitle="Donut · hover a slice or a legend row · YTD 2026" kind="donut" center-label="Total opex" caption="Vehicle costs flagged orange, up 18% against plan" money></sui-chart-pie>
// setup(root): properties and events setup(root) { root.querySelector('sui-chart-pie').items = MIX; }

Pie

kind="pie" fills the center, so there is no center total; the legend still carries every figure and share. The legend sits beside the plot and drops under it when the card is too narrow to show every figure and share with the start of each name beside the plot.

<sui-chart-pie heading="Revenue by stream" subtitle="Pie" kind="pie" money></sui-chart-pie>
// setup(root): properties and events setup(root) { root.querySelector('sui-chart-pie').items = [ { label: 'Product sales', value: 892450 }, { label: 'Service revenue', value: 214830 }, { label: 'Freight income', value: 12240 }, ]; }

Nightingale

kind="nightingale" encodes the value in radius as well as angle, which is the polar area chart. Without money the figures print as plain counts, which is the default. With money, currency names the ISO code, USD by default, and the page's locale writes the figures and the shares.

<sui-chart-pie heading="Invoice volume by weekday" subtitle="Nightingale · radius encodes the count" kind="nightingale" caption="Wednesday is billing day, flagged orange"></sui-chart-pie>
// setup(root): properties and events setup(root) { root.querySelector('sui-chart-pie').items = [ { label: 'Mon', value: 34 }, { label: 'Tue', value: 41 }, { label: 'Wed', value: 52, accent: true }, { label: 'Thu', value: 38 }, { label: 'Fri', value: 22 }, ]; }

Events

suiHover fires when the hovered item changes, reporting { index }, -1 on leave. A slice hover, an arrow key on the plot and a legend hover or focus all fire it, so a keyboard user gets the same readout. The buttons write hoverIndex directly, which lifts the slice but emits nothing, as sui-chart-synced does.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-pie id="pie-events" heading="Operating expense mix" subtitle="Hover a slice, or Tab through the legend" money></sui-chart-pie> <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center"> <sui-button id="pie-rent" size="sm" variant="outline" label="Show rent"></sui-button> <sui-button id="pie-clear" size="sm" variant="ghost" label="Clear"></sui-button> <span id="pie-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#pie-events'); const log = root.querySelector('#pie-log'); el.items = MIX; el.addEventListener('suiHover', (e) => { const i = e.detail.index; log.textContent = i < 0 ? 'Idle.' : `${MIX[i].label}: $${MIX[i].value.toLocaleString('en-US')}.`; }); root.querySelector('#pie-rent').addEventListener('click', () => { el.hoverIndex = 1; }); root.querySelector('#pie-clear').addEventListener('click', () => { el.hoverIndex = -1; }); }

Empty and loading

loading stands a shimmer in for the plot and marks the host aria-busy while the figures are on their way; the frame and its heading stay. A chart with nothing to plot keeps its frame and says so, and emptyMessage is the sentence. Both are the same on every chart.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-chart-pie heading="Operating expense mix" subtitle="Fetching the quarter" diameter="120" loading></sui-chart-pie> <sui-chart-pie heading="Operating expense mix" subtitle="This quarter" diameter="120" empty-message="No expenses were posted this quarter."></sui-chart-pie> </div>

Keyboard

KeyAction
TabMoves to the plot, then through the legend rows. A focused row lifts its slice and the donut center reads its value.
ArrowRight ArrowDownOn the plot, moves to the next slice clockwise and lifts it; a reader hears the item, its figure and its share.
ArrowLeft ArrowUpOn the plot, moves to the previous slice.
Home EndOn the plot, moves to the first or the last slice.
EscapeOn the plot, leaves the slice; the plot keeps focus.
Enter SpaceOn a legend row, hides its item and re-shares the rest; press again to bring it back.

API

<sui-chart-pie>

Properties

PropertyAttributeTypeDefaultDescription
captioncaptionstringstring | undefinedFooter note under the legend, separated by a hairline.
centerLabelcenter-labelstringstring | undefinedLabel under the donut center total. Defaults to "Total". While an item is hovered, the center shows that item's value and label instead. Both fit the hole: the figure shrinks to 12px, then prints compact ($289k), and the label takes two lines, then an ellipsis; the legend keeps it whole.
currencycurrencystring'USD'The ISO 4217 code money prints in: USD by default, EUR, JPY. The page's locale (setLocale(), else <html lang>) places the symbol and sets the grouping and the decimal mark, so EUR on a German page reads 289.400 €, and the shares 47 %. An unknown code prints the bare figure. Ignored without money.
diameterdiameternumber168Diameter of the plot in px: the pie or donut is drawn in a square this wide.
emptyMessageempty-messagestringstring | undefinedSentence drawn in place of the plot when there are no items or every value is zero. Defaults to "Nothing to plot yet."; the frame and its heading stay. The chart filters nothing, so this is its only empty string.
headingheadingstringstring | undefinedChart heading (the reference title prop, renamed because title is a native attribute). With what the chart shows, it names the plot for a reader; an aria-label or aria-labelledby on the host names the plot instead, and this summary then describes it.
hoverIndexhover-indexnumber-1Hovered item position in items; -1 when idle. The pointer, the arrow keys on the plot and legend focus set it, and the chart lifts that slice, dims the other rows and reads the item in the donut center. A write from outside does the same and emits nothing, which is how sui-chart-synced mirrors a sibling panel; an index whose item is hidden draws nothing, since its slice is not on screen.
itemsproperty onlySuiPieItem[][]Slices to draw. Each item gets a legend row: a button with aria-pressed that hides the item on click and re-shares the rest. Property, not attribute.
kindkind"donut" | "nightingale" | "pie"'donut'donut (default) shows the center total; pie fills the center; nightingale (a polar area chart) encodes value in radius as well as angle. Reflected.
loadingloadingbooleanfalseThe figures are still on their way: a shimmer stands in for the plot, the frame and its heading stay, and the host is aria-busy. Reflected, so a page can style the wait.
moneymoneybooleanfalseFormat values as exact money in currency ($289,400); a center total too wide for the hole turns compact ($289k). Off, the figures print plainly, which is what a count wants; set it for a balance. Reflected.
subtitlesubtitlestringstring | undefinedSecondary line under the heading.

Events

EventDetailReactVueDescription
suiHoverSuiChartHoverDetailonSuiHover@sui-hoverFires when the hovered item changes; index is the item position, or -1 when nothing is hovered. Hovering a slice or a legend row, reaching a slice with the arrow keys and focusing a legend row all count as hover.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the plot, the chart's one tab stop, from which the arrow keys reach the slices. Does nothing while there is nothing to plot.

Slots

SlotDescription
header-endControls at the end of the head row, after the heading (a period select, an export button).

Types

SuiChartHoverDetail interfaceDetail of suiHover: the hovered index, or -1 when the pointer leaves the plot. What the index counts is each chart's own: an x position here, a point on a scatter, an axis on a radar.
indexnumber
SuiPieItem interfaceOne slice. accent paints it in the accent: reserve it for the single flagged category; color overrides the ramp.
labelstring
valuenumber
accent?boolean
color?stringThe slice's own color, which wins over accent; its legend swatch follows. Defaults to the ramp step for its place in items. Any CSS color or var(--token) works; use a token so the color follows the theme. A slice past the fourth keeps its hatch.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also