Donut Chart
Ring-based variant of the pie chart with an empty centre for annotations or totals.
Source
LLM docs
Package
npm install @plocks/chartsExamples
6
Properties
60
Playground
Basics
Circular proportional chart with a hollow center (variant of pie chart).
Loading demo…
import { DonutChart } from '@plocks/charts';
import { SEGMENTS } from './data';
export function Demo() {
return (
<DonutChart
title="Team allocation"
size={260}
data={SEGMENTS}
/>
);
}Annual Expense Allocation
Loading demo…
import { DonutChart } from '@plocks/charts';
import { DEPARTMENT_ALLOCATIONS } from './data';
const formatBudget = (value: number) => `$${Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value)}M`;
export function Demo() {
return (
<DonutChart
title="Annual Expense Allocation"
subtitle="FY26 operating plan"
size={300}
data={DEPARTMENT_ALLOCATIONS}
padAngle={1.8}
legend={{ position: 'bottom' }}
centerLabel={() => 'Budget'}
centerSubLabel={() => 'Allocation by function'}
centerValueFormatter={(value) => formatBudget(value)}
/>
);
}Customer Segment Growth
Loading demo…
import { DonutChart } from '@plocks/charts';
import type { DonutChartDataPoint } from '@plocks/charts';
import { ARR_SEGMENTS, GROWTH_CONTRIBUTION } from './data';
const formatMillions = (value: number) => `${Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value)}M`;
const getRingId = (slice?: DonutChartDataPoint | null) => (slice as any)?.ringId as string | undefined;
export function Demo() {
return (
<DonutChart
title="ARR Mix by Segment"
subtitle="FY26 to date"
size={320}
ringGap={16}
rings={[
{
id: 'arr',
label: 'Annual Recurring Revenue',
data: ARR_SEGMENTS,
padAngle: 1.8,
showInLegend: true,
},
{
id: 'growth',
label: 'YoY Growth Contribution',
data: GROWTH_CONTRIBUTION,
thicknessRatio: 0.16,
padAngle: 1.2,
showInLegend: false,
},
]}
primaryRingIndex={0}
legendRingIndex={0}
centerLabel={(total, _data, focused) => {
if (focused) {
return focused.label;
}
return 'Total ARR';
}}
centerSubLabel={(total, _data, focused) => {
if (!focused) return 'YoY growth vs. FY25';
return getRingId(focused) === 'growth' ? 'Growth contribution' : 'Segment share';
}}
centerValueFormatter={(value, _total, focused) => {
if (focused && getRingId(focused) === 'growth') {
return `${value.toFixed(0)}%`;
}
return `$${formatMillions(value)}`;
}}
legend={{ position: 'bottom' }}
/>
);
}Data Center Power
Loading demo…
import { DonutChart } from '@plocks/charts';
import { POWER_BY_SUBSYSTEM } from './data';
const formatMegawatts = (value: number) => `${value.toFixed(1)} MW`;
export function Demo() {
return (
<DonutChart
title="Data Center Power Draw"
subtitle="May 2025 peak load"
size={320}
data={POWER_BY_SUBSYSTEM}
padAngle={2}
legend={{ position: 'right', align: 'start' }}
padding={{ top: 140, right: 168, bottom: 72, left: 72 }}
centerLabel={() => 'Power load'}
centerSubLabel={() => 'Across campus subsystems'}
centerValueFormatter={(value) => formatMegawatts(value)}
labels={{
show: true,
position: 'outside',
showPercentage: true,
showValue: true,
valueFormatter: ({ value }) => formatMegawatts(value),
leaderLine: { width: 1.5 },
}}
/>
);
}Employee Geography Workstyle
Loading demo…
import { View, Text } from 'react-native';
import { DonutChart } from '@plocks/charts';
import { REGION_HEADCOUNT, WORK_STYLE, remoteRatio } from './data';
const formatHeadcount = (value: number) => Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value);
export function Demo() {
return (
<DonutChart
title="Employee Distribution"
subtitle="Geography and work style"
size={320}
ringGap={18}
rings={[
{
id: 'region',
label: 'Regional distribution',
data: REGION_HEADCOUNT,
padAngle: 2.2,
thicknessRatio: 0.3,
showInLegend: false,
},
{
id: 'work-style',
label: 'Work style mix',
data: WORK_STYLE,
thicknessRatio: 0.18,
padAngle: 1.5,
showInLegend: true,
},
]}
primaryRingIndex={0}
legendRingIndex={1}
renderCenterContent={({ focusedSlice, primaryRing, total }) => {
const isWorkStyle = focusedSlice?.ringId === 'work-style';
const headline = focusedSlice ? focusedSlice.label : 'Headcount';
const valueText = focusedSlice
? isWorkStyle
? `${Math.round((focusedSlice.percentage || 0) * 100)}%`
: formatHeadcount(focusedSlice.value)
: formatHeadcount(primaryRing?.total ?? total);
const helperText = focusedSlice
? isWorkStyle
? 'of workforce'
: 'global headcount'
: `Remote ${Math.round(remoteRatio * 100)}%`;
return (
<View style={{ alignItems: 'center', justifyContent: 'center' }}>
<Text
style={{
fontSize: 12,
fontWeight: '600',
textTransform: 'uppercase',
color: '#868E96',
marginBottom: 2,
}}
>
{headline}
</Text>
<Text
style={{
fontSize: 26,
fontWeight: '700',
color: '#212529',
}}
>
{valueText}
</Text>
<Text
style={{
fontSize: 12,
color: '#495057',
marginTop: 4,
}}
>
{helperText}
</Text>
</View>
);
}}
labels={{
show: true,
rings: ['work-style'],
position: 'outside',
showPercentage: true,
leaderLine: { width: 1.4 },
}}
legend={{ position: 'bottom' }}
/>
);
}Marketplace Fulfillment Mix
Loading demo…
import { DonutChart } from '@plocks/charts';
import { FULFILLMENT_PARTNERS } from './data';
const formatOrders = (value: number) => `${value.toFixed(2)}M`;
export function Demo() {
return (
<DonutChart
title="Marketplace Fulfillment Mix"
subtitle="Orders fulfilled in Q3"
size={300}
data={FULFILLMENT_PARTNERS}
padAngle={1.6}
isolateOnClick
legend={{ position: 'bottom' }}
centerLabel={() => 'Orders'}
centerSubLabel={() => 'Fulfilled volume by partner'}
centerValueFormatter={(value) => `${formatOrders(value)} total`}
/>
);
}Related charts
Further reading
React Native design for iOS, Android, and Web.
Quick Links
Resources