Donut Chart

Ring-based variant of the pie chart with an empty centre for annotations or totals.

npm install @plocks/charts

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`} /> ); }
Scan to open this page on your phoneplocks.dev/charts/DonutChart