Pie, donut and sunburst
Twelve browsers' share of traffic as the part-of-a-whole family: a pie that becomes a donut through innerRadius, callout labels on leader lines or shares on the slices, small slices folded into Other with topN, a legend that hides and isolates, a slice click that filters the grid beside it, and a sunburst of the same numbers nested under their engine that drills in on click.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.
About this example
Pie, donut and sunburst charts: innerRadius turns the pie into a donut, dataLabels placement outside draws callout labels on leader lines that never overlap, topN on rowsToChartSpec folds small slices into Other, a slice click selects it and the grid follows, a legend click hides a slice and the shares recompute, and specToTreemap on a split spec builds a sunburst that drills down on click. Free, in @svgrid/grid.
Twelve browsers' share of traffic, drawn as the part-of-a-whole family:
innerRadiusturns the pie into a donut; the slider runs the whole way.dataLabels.placement: 'outside'draws callout labels on leader lines, pushed apart so they never overlap;'inside'writes the share on the slice and hides it where the slice is too thin.- Small slices fold into "Other" through
topNonrowsToChartSpec, so the chart says eight things, not twelve. - Click a slice to select it (the grid on the right follows). With the labels on the slices a legend appears; click an item to hide it and the shares recompute, double-click to isolate one.
- The sunburst is the same shares nested under their engine, from
specToTreemapon a split spec; it drills down on click.
Free, in @svgrid/grid.
Imports, features and API used
Imports: @svgrid/grid, ../shared/ChartDataGrid.svelte, ../shared/ViewSwitch.svelte
Table features registered: rowSortingFeature
Columns: browser (Browser), engine (Engine), sessions (Sessions)
Frequently asked questions
How do I turn a pie into a donut?
Set innerRadius on the spec, a fraction of the radius from 0 to about 0.85. Anything above zero is a donut.
How do I put the labels outside the slices?
Set dataLabels: { placement: outside }. The labels sit on leader lines and are pushed apart so they do not overlap; placement inside writes the share on the slice instead.
How do I group small slices into Other?
Pass topN to rowsToChartSpec. Everything past the top N is summed into one Other category, named by otherLabel.
Related documentation
Source code (441-chart-pie-donut.svelte)
<!-- Documented in: docs/help/charts/gallery.md -->
<script lang="ts">
/**
* 441. Pie, donut and sunburst
* ----------------------------
* Twelve browsers' share of traffic, drawn as the part-of-a-whole family:
*
* - `innerRadius` turns the pie into a donut; the slider runs the whole way.
* - `dataLabels.placement: 'outside'` draws callout labels on leader lines,
* pushed apart so they never overlap; `'inside'` writes the share on the
* slice and hides it where the slice is too thin.
* - Small slices fold into "Other" through `topN` on `rowsToChartSpec`, so
* the chart says eight things, not twelve.
* - Click a slice to select it (the grid on the right follows). With the
* labels on the slices a legend appears; click an item to hide it and
* the shares recompute, double-click to isolate one.
* - The sunburst is the same shares nested under their engine, from
* `specToTreemap` on a split spec; it drills down on click.
*
* Free, in @svgrid/grid.
*/
import { SvChart, SvGrid, rowsToChartSpec, specToTreemap, tableFeatures, rowSortingFeature, type ChartPointRef, type ChartSpec, type GridColumns } from '@svgrid/grid'
import ChartDataGrid from '../shared/ChartDataGrid.svelte'
import ViewSwitch from '../shared/ViewSwitch.svelte'
const features = tableFeatures({ rowSortingFeature })
type Row = { browser: string; engine: string; sessions: number }
const rows: Row[] = [
{ browser: 'Chrome', engine: 'Blink', sessions: 61200 },
{ browser: 'Safari', engine: 'WebKit', sessions: 19800 },
{ browser: 'Edge', engine: 'Blink', sessions: 6900 },
{ browser: 'Firefox', engine: 'Gecko', sessions: 3400 },
{ browser: 'Samsung Internet', engine: 'Blink', sessions: 2600 },
{ browser: 'Opera', engine: 'Blink', sessions: 1900 },
{ browser: 'Brave', engine: 'Blink', sessions: 1100 },
{ browser: 'Vivaldi', engine: 'Blink', sessions: 420 },
{ browser: 'DuckDuckGo', engine: 'WebKit', sessions: 380 },
{ browser: 'Arc', engine: 'Blink', sessions: 310 },
{ browser: 'Tor', engine: 'Gecko', sessions: 140 },
{ browser: 'Other', engine: 'Other', sessions: 850 },
]
const columns: GridColumns<Row> = [
{ field: 'browser', header: 'Browser', width: 150 },
{ field: 'engine', header: 'Engine', width: 90 },
{ field: 'sessions', header: 'Sessions', width: 100, align: 'right', cellDataType: 'number', format: { type: 'number' } },
]
let inner = $state(0.55)
let placement = $state<'outside' | 'inside' | 'none'>('outside')
let topN = $state(8)
let picked = $state<ChartPointRef[]>([])
let drillPath = $state<string[]>([])
const pie = $derived<ChartSpec>({
...rowsToChartSpec(rows, { type: 'pie', category: 'browser', value: 'sessions', reduce: 'sum', sort: 'value-desc', topN }),
innerRadius: inner,
title: 'Browser share',
subtitle: `Sessions this month, top ${topN}`,
dataLabels: placement === 'none' ? { show: false } : { show: true, placement },
height: 360,
})
const byEngine = rowsToChartSpec(rows, { type: 'bar', category: 'engine', value: 'sessions', series: 'browser', reduce: 'sum' })
const sunburst: ChartSpec = {
type: 'sunburst',
categories: [],
series: [],
tree: specToTreemap(byEngine),
title: 'Browsers by engine',
subtitle: 'Click a ring to drill in',
height: 360,
}
const pickedName = $derived(picked[0]?.category ?? null)
// "Other" is the folded tail: every browser the top-N cut off, plus the row
// that is literally called Other.
const shown = $derived.by(() => {
if (!pickedName) return rows
if (pickedName !== 'Other') return rows.filter((r) => r.browser === pickedName)
const kept = new Set(pie.categories.filter((c) => c !== 'Other'))
return rows.filter((r) => !kept.has(r.browser))
})
// Chart | Grid: the same spec as the chart or as the rows behind it.
let view = $state<'chart' | 'grid'>('chart')
</script>
<section class="wrap">
<header class="chrome">
<ViewSwitch bind:value={view} options={[['chart', 'Chart'], ['grid', 'Grid']]} />
<label class="ctl">
Hole
<input type="range" min="0" max="0.85" step="0.05" bind:value={inner} />
<span class="muted">{Math.round(inner * 100)}%</span>
</label>
<label class="ctl">
Labels
<select bind:value={placement}>
<option value="outside">Callouts</option>
<option value="inside">On the slice</option>
<option value="none">None</option>
</select>
</label>
<label class="ctl">
Slices
<select bind:value={topN}>
<option value={5}>Top 5 + Other</option>
<option value={8}>Top 8 + Other</option>
<option value={12}>All</option>
</select>
</label>
<span class="note">
A pie that becomes a donut, callout labels on leader lines, small slices folded into Other, a
legend that hides and isolates, and a sunburst of the same numbers nested under their engine.
</span>
</header>
<div class="row">
<div class="pane pane-chart">
{#if view === 'chart'}
<SvChart spec={pie} legend={placement === 'outside' ? false : 'bottom'} selectable bind:selected={picked} autosize />
{:else}
<ChartDataGrid spec={pie} />
{/if}
</div>
<div class="pane pane-chart">
{#if view === 'chart'}
<SvChart spec={sunburst} drillable bind:drillPath legend={false} toolbar={false} autosize />
{:else}
<ChartDataGrid spec={sunburst} />
{/if}
</div>
<div class="grid-host">
<div class="muted grid-title">{pickedName ? `${pickedName} selected` : 'Click a slice to filter'}</div>
<SvGrid data={shown} {columns} {features} sortable rowHeight={26} containerHeight="100%" fitColumns responsive />
</div>
</div>
</section>
<style>
.wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 10px;
overflow: auto;
}
.chrome {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
flex: none;
}
.note,
.muted {
font-size: 12px;
color: var(--sg-muted, #64748b);
}
.ctl {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--sg-fg, #0f172a);
white-space: nowrap;
}
.ctl select {
font: inherit;
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 6px;
background: var(--sg-bg, #fff);
color: inherit;
padding: 2px 6px;
}
.row {
display: flex;
flex: none;
gap: 12px;
flex-wrap: wrap;
min-height: 0;
}
.pane {
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 10px;
background: var(--sg-bg, #fff);
padding: 8px 10px;
min-width: 0;
}
.pane-chart {
flex: 1 1 340px;
}
.grid-host {
flex: 1 1 260px;
min-width: 0;
min-height: 300px;
display: flex;
flex-direction: column;
gap: 4px;
}
.grid-title {
flex: none;
}
</style>More Charts examples
- Chart a selection (context menu) - With integrated charting enabled, the right-click menu gains a Chart selected range item: select a block of cells, right-click, and the chart panel opens scoped to that range - the Excel chart-this gesture, built in. The item appears only when charting is on and is appended to the default context menu automatically.
- Chart view of the grid - The `chart` prop turns the same <SvGrid> into a chart, driven by the grid’s filtered + sorted rows (search + sort flow through). A view of the grid like board and scheduler, but the renderer is free: the grid lazy-loads a built-in view wrapping the standalone SvChart via rowsToChartSpec. Flip Table <-> Chart (bar / line / area) over one source of truth.
- Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
- Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
- Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.