Pie and Donut
Polar share series. angleField sets the sector value, labelField the name (legend, sector labels, tooltip).
A sector label is one label made of two parts — the name and the value — each with its own font, colour and format. label.placement decides where the whole of it goes: 'outside' on a callout line, 'inside' the sector. Inside labels are haloed in the sector colour and take an automatic contrast colour.
The name comes out of labelField, and that field has a format like any other. labelName says how its value becomes text, once for the whole series — the legend, the tooltip heading and the name half of the label all read the same. It takes format (a serializable string) and formatter (a function, for what a string cannot express):
labelName: { format: '%d.%m.%Y' },Where a label wants something shorter than the legend, label.category has a format of its own and overrides labelName; it answers for the name exactly as value.format/value.formatter answer for the number:
label: { category: { format: '%d.%m' }, value: { enabled: true } },import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Browser share' },
series: [
{
type: 'pie',
angleField: 'share',
angleName: 'Share, %',
labelField: 'browser',
// name and share read as one label, the share in its own smaller line
label: { value: { enabled: true } },
},
],
};
}export function getData() {
return [
{ browser: 'Chrome', share: 64 },
{ browser: 'Safari', share: 19 },
{ browser: 'Edge', share: 6 },
{ browser: 'Firefox', share: 5 },
{ browser: 'Other', share: 6 },
];
}Spacing and corner rounding for pie
The same sectorSpacing and cornerRadius also work without a ring:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Pie with spacing and rounded corners' },
series: [
{
type: 'pie',
angleField: 'share',
angleName: 'Share, %',
labelField: 'device',
sectorSpacing: 3,
cornerRadius: 7,
// one-line label: the name, the separator and the value in a row
label: { layout: 'inline', value: { enabled: true, type: 'value', format: '.0f' } },
},
],
};
}export function getData() {
return [
{ device: 'Smartphones', share: 48 },
{ device: 'Laptops', share: 24 },
{ device: 'Tablets', share: 14 },
{ device: 'Desktops', share: 9 },
{ device: 'Other', share: 5 },
];
}Donut
innerRadiusRatio creates a ring; innerLabels adds text in the center.
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Order statuses' },
series: [
{
type: 'donut',
angleField: 'count',
angleName: 'Orders',
labelField: 'status',
innerRadiusRatio: 0.62,
// callout lines: length and style of each segment are configured separately
calloutLine: {
radial: { length: 10, strokeWidth: 1 },
horizontal: { length: 14, strokeWidth: 1 },
},
innerLabels: [
{ text: '2193', fontSize: 22, fontWeight: 'bold' },
{ text: 'orders', fontSize: 12 },
],
},
],
legend: { position: 'right' },
};
}export function getData() {
return [
{ status: 'Completed', count: 1840 },
{ status: 'Cancelled', count: 215 },
{ status: 'Refunded', count: 96 },
{ status: 'Failed', count: 42 },
];
}Rotation, colors, labels inside the sectors
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Rotation and sector labels' },
subtitle: { text: 'rotation: -90, labels inside the sectors' },
series: [
{
type: 'pie',
angleField: 'amount',
angleName: 'Share, %',
labelField: 'source',
rotation: -90,
fills: ['#1d4fd7', '#27c08d', '#f4a236', '#9a7bff'],
// the whole label sits in the sector: the name above, the share under it
label: {
placement: 'inside',
category: { fontWeight: 'bold' },
value: { enabled: true },
},
},
],
legend: { position: 'right' },
};
}export function getData() {
return [
{ source: 'Organization', amount: 46 },
{ source: 'Grants', amount: 28 },
{ source: 'Partners', amount: 16 },
{ source: 'Other', amount: 10 },
];
}Custom tooltip
The series tooltip.renderer receives the whole datum, so the tooltip can display any fields:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Custom sector tooltip' },
series: [
{
type: 'donut',
angleField: 'visits',
labelField: 'channel',
innerRadiusRatio: 0.55,
tooltip: {
renderer: ({ datum, color }) => ({
heading: String(datum.channel),
rows: [
{ label: 'Visits', value: String(datum.visits), color },
{ label: 'Conversion', value: `${datum.conversion}%` },
],
}),
},
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ channel: 'Organic', visits: 18200, conversion: 4.1 },
{ channel: 'Ads', visits: 9400, conversion: 2.6 },
{ channel: 'Social', visits: 6100, conversion: 1.9 },
{ channel: 'Email', visits: 2800, conversion: 6.4 },
];
}Spacing and corner rounding
sectorSpacing is the gap between sectors (px), cornerRadius rounds the corners:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
const money = new Intl.NumberFormat('en-US');
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Sector spacing and corner radius' },
series: [
{
type: 'donut',
angleField: 'amount',
labelField: 'fund',
innerRadiusRatio: 0.55,
sectorSpacing: 6,
cornerRadius: 8,
label: { placement: 'outside' },
calloutLine: {
radial: { length: 12, strokeWidth: 1 },
horizontal: { length: 16, stroke: '#9aa1ad', strokeWidth: 1 },
},
tooltip: {
renderer: ({ label, value, color }) => ({
heading: label,
rows: [{ label: 'Amount', value: money.format(Number(value)), color }],
}),
},
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ fund: 'Stocks', amount: 32129675 },
{ fund: 'Bonds', amount: 9196461 },
{ fund: 'Funds', amount: 5248310 },
{ fund: 'Deposits', amount: 3933726 },
{ fund: 'Currency', amount: 3354246 },
];
}A long tail of small sectors
A narrow sector is drawn however narrow it gets: the gap sectorSpacing asks for gives way rather than eating the sector it was meant to separate.
Every sector gets a label too, whatever its size — by default the crowded ones simply overlap. Two options thin them out, and they answer different questions.
label.minShare decides which sectors are worth a label at all: below that share of the total a sector is drawn but left unlabelled. This is what makes a long tail readable — the numbers that carry the chart get their callout, the slivers stay in the ring and in the tooltip.
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Labels for what carries the chart' },
subtitle: { text: 'minShare leaves the tail of slivers unlabelled' },
legend: { enabled: false },
series: [
{
type: 'donut',
angleField: 'revenue',
angleName: 'Revenue',
labelField: 'brand',
sectorSpacing: 4,
cornerRadius: 6,
// under four percent of the total a sector is drawn but not labelled
label: { minShare: 0.04, value: { enabled: true, type: 'value', format: ',.0f' } },
},
],
};
}export function getData() {
return [
{ brand: 'JeansMar', revenue: 569566 },
{ brand: 'Cashmerehous', revenue: 274719 },
{ brand: 'Inside Out', revenue: 250483 },
{ brand: 'Cashmere & Silk', revenue: 216170 },
{ brand: 'Fifoshka', revenue: 214121 },
{ brand: 'Larne dress', revenue: 183819 },
{ brand: 'Fashion House', revenue: 159575 },
{ brand: 'Be Very', revenue: 149460 },
{ brand: 'Blues & Jeans', revenue: 121152 },
{ brand: 'Bat Norton', revenue: 113117 },
{ brand: 'Kurtkin Mir', revenue: 111099 },
{ brand: 'Thing!', revenue: 109086 },
{ brand: 'Stella', revenue: 105043 },
{ brand: 'Be Trandy', revenue: 94960 },
{ brand: 'Jeans club', revenue: 90908 },
{ brand: 'Velvet Season', revenue: 90904 },
{ brand: 'Egoista', revenue: 14139 },
{ brand: 'Mon Ami', revenue: 12820 },
{ brand: 'Pretty Wool', revenue: 11304 },
{ brand: 'Lace Age', revenue: 9877 },
{ brand: 'Silk Way', revenue: 8461 },
{ brand: 'Nordwind', revenue: 7215 },
{ brand: 'Bella Riva', revenue: 6088 },
{ brand: 'Trikotage', revenue: 4930 },
{ brand: 'Zima', revenue: 3742 },
{ brand: 'Mimi', revenue: 2611 },
];
}label.avoidOverlap decides whether there is room for a label. The labels stack in rows down each side of the pie, and once a side runs out of rows the narrowest sectors on it are the ones that lose theirs — no threshold to pick, but which labels survive depends on the size of the chart.
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'A long tail of small sectors' },
subtitle: { text: 'avoidOverlap hands out labels until the sides run out of rows' },
legend: { enabled: false },
series: [
{
type: 'donut',
angleField: 'revenue',
angleName: 'Revenue',
labelField: 'brand',
sectorSpacing: 4,
cornerRadius: 6,
// the sectors of the tail are slivers, and slivers still get drawn
label: { avoidOverlap: true, value: { enabled: true, type: 'value', format: ',.0f' } },
},
],
};
}export function getData() {
return [
{ brand: 'JeansMar', revenue: 569566 },
{ brand: 'Cashmerehous', revenue: 274719 },
{ brand: 'Inside Out', revenue: 250483 },
{ brand: 'Cashmere & Silk', revenue: 216170 },
{ brand: 'Fifoshka', revenue: 214121 },
{ brand: 'Larne dress', revenue: 183819 },
{ brand: 'Fashion House', revenue: 159575 },
{ brand: 'Be Very', revenue: 149460 },
{ brand: 'Blues & Jeans', revenue: 121152 },
{ brand: 'Bat Norton', revenue: 113117 },
{ brand: 'Kurtkin Mir', revenue: 111099 },
{ brand: 'Thing!', revenue: 109086 },
{ brand: 'Stella', revenue: 105043 },
{ brand: 'Be Trandy', revenue: 94960 },
{ brand: 'Jeans club', revenue: 90908 },
{ brand: 'Velvet Season', revenue: 90904 },
{ brand: 'Egoista', revenue: 14139 },
{ brand: 'Mon Ami', revenue: 12820 },
{ brand: 'Pretty Wool', revenue: 11304 },
{ brand: 'Lace Age', revenue: 9877 },
{ brand: 'Silk Way', revenue: 8461 },
{ brand: 'Nordwind', revenue: 7215 },
{ brand: 'Bella Riva', revenue: 6088 },
{ brand: 'Trikotage', revenue: 4930 },
{ brand: 'Zima', revenue: 3742 },
{ brand: 'Mimi', revenue: 2611 },
];
}The two combine: minShare picks the sectors worth labelling, avoidOverlap guarantees that what is left never collides.
Values in the legend
legendValue shows the sector value to the right of the label; the tooltip here is anchored to the cursor (tooltip.position.anchorTo: 'pointer'):
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Values in the legend' },
series: [
{
type: 'donut',
angleField: 'amount',
labelField: 'source',
innerRadiusRatio: 0.62,
sectorSpacing: 3,
cornerRadius: 4,
label: { enabled: false },
innerLabels: [
{ text: 'Total', fontSize: 13 },
{ text: '$86K', fontSize: 24, fontWeight: 'bold' },
],
legendValue: {
enabled: true,
formatter: ({ value }) => `$${value}K`,
},
},
],
legend: { position: 'right' },
tooltip: { position: { anchorTo: 'pointer', yOffset: -8 } },
};
}export function getData() {
return [
{ source: 'Equipment sales', amount: 35 },
{ source: 'Online training', amount: 12 },
{ source: 'Athlete contracts', amount: 5 },
{ source: 'Merchandise', amount: 21 },
{ source: 'Sponsored events', amount: 13 },
];
}Donut progress
A thin ring + innerLabels makes a compact indicator:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Year progress' },
series: [
{
type: 'donut',
angleField: 'value',
labelField: 'state',
innerRadiusRatio: 0.78,
rotation: 0,
fills: ['#21a06c', '#e8eaee'],
label: { enabled: false },
innerLabels: [
{ text: '68%', fontSize: 26, fontWeight: 'bold' },
{ text: 'of plan complete', fontSize: 12 },
],
},
],
legend: { enabled: false },
};
}export function getData() {
return [
{ state: 'Done', value: 68 },
{ state: 'Remaining', value: 32 },
];
}Options
Options common to all series (name, showInLegend, tooltip.renderer, …) are covered in Common series options.
| Option | Type | Default | Description |
|---|---|---|---|
angleField | string | — | sector value (required) |
labelField | string | — | sector name |
fills | ColorValue[] | theme palette | sector colors around the circle |
strokes | ColorValue[] | theme palette | sector colors around the circle |
rotation | Degrees | 0 | start angle |
outerRadiusRatio | Fraction | 0.85 | fraction of the available radius used by the chart (labels go in the remaining space) |
innerRadiusRatio (donut) | Fraction | 0.6 | inner radius |
angleName | string | angleField name | value label in the tooltip |
labelName.format | string | — | how the labelField value becomes text: legend, tooltip heading, label |
labelName.formatter | ({ datum, value }) => string | — | the same, when a format string cannot express it |
sectorSpacing | Pixels | 0 | constant-width gap between sectors |
cornerRadius | Pixels | 0 | sector corner rounding |
label.enabled | boolean | on with labelField | sector labels |
label.placement | 'outside' | 'inside' | 'outside' | beside the pie on a callout line, or in the sector |
label.layout | 'stacked' | 'inline' | 'stacked' | the value on its own line under the name, or in a row with it |
label.separator | string | ' · ' | what separates the parts of an inline label |
label.positionRatio | Fraction | 0.7 | position along the radius (inside placement) |
label.minShare | Fraction | 0 | share of the total a sector needs before it is worth a label |
label.avoidOverlap | boolean | false | drop the labels there is no room for instead of letting them overlap |
label.category.enabled | boolean | on with labelField | the sector name as part of the label |
label.category.format | string | — | format string for the name field ('%d.%m.%Y', ',.0f') |
label.category.formatter | ({ datum, label, value, share }) => … | — | text of the name half |
label.category.fontSize | Pixels | 11 | name font |
label.category.fontFamily | string | theme font | font family |
label.category.fontWeight | string | number | normal | font weight |
label.category.color | ColorValue | foreground / auto contrast | text color |
label.value.enabled | boolean | false | the sector value as part of the label |
label.value.type | 'percent' | 'value' | 'percent' | share of the total, or the angleField value |
label.value.format | string | — | format string (',.2f', '.1%') |
label.value.formatter | ({ datum, label, value, share }) => string | — | full control over the text |
label.value.fontSize | Pixels | 11 | value font |
label.value.color | ColorValue | foreground / auto contrast | text color |
calloutLine.radial.length | Pixels | 20 | radial segment length |
calloutLine.radial.stroke | ColorValue | sector color | radial segment color |
calloutLine.radial.strokeWidth | Pixels | 1 | width |
calloutLine.horizontal.length | Pixels | 20 | length of the tail toward the label |
calloutLine.horizontal.stroke | ColorValue | same as radial | tail color |
calloutLine.horizontal.strokeWidth | Pixels | same as radial | tail width |
legendValue.enabled | boolean | false | sector value in the legend |
legendValue.formatter | ({ datum, label, value, color }) => string | value | value format |
tooltip.renderer | ({ datum, label, value, color }) => … | — | custom tooltip |
innerLabels[] | { text, fontSize?, fontWeight?, color? } | — | lines in the donut center |
innerCircle.fill | ColorValue | — | donut center fill |
innerRadiusRatio | Fraction | 0.6 | donut inner radius |
Clicking a legend item hides the sector (shares are recalculated); legend items that do not fit are paginated with arrows.