1 GitHub star = 1 new component.

Star the repo
Charts & Statschartsvgzero-dependencysunbursthierarchydonutbreakdown

Sunburst Chart

Two-level hierarchical donut with parent categories inside and their children on the outer ring.

Live Preview

Cloud spend breakdown

Service → resource

  • Compute
  • Storage
  • Network
  • Data
$16kMonthly spend
View data
GroupItemValueShare
ComputeVMs$4.2k26.3%
ComputeFunctions$1.8k11.3%
ComputeContainers$2.6k16.3%
StorageObject$1.9k11.9%
StorageBlock$1.1k6.9%
NetworkCDN$1.4k8.8%
NetworkEgress$9005.6%
DataPostgres$1.6k10.0%
DataRedis$5003.1%

Usage

Copy and paste this code into your React component:

tsx
import { SunburstChart } from 'react-component-kit-ui/charts';

<SunburstChart data={[
    { label: 'Compute', children: [{ label: 'VMs', value: 4200 }, { label: 'Functions', value: 1800 }] },
    { label: 'Storage', children: [{ label: 'Object', value: 1900 }] },
  ]} prefix="$" />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/SunburstChart.tsx + SunburstChart.css:

SunburstChart.tsx
'use client';

import ChartFrame from './ChartFrame';
import { arcPath, colorAt, formatValue, sum } from './chart-utils';
import './SunburstChart.css';

/**
 * SunburstChart — Two-level hierarchical donut: inner ring for parents, outer
 * ring for children (shaded variants of the parent hue).
 *
 * @param {Array<{label: string, children: Array<{label: string, value: number}>}>} data - Hierarchy.
 * @param {string} centerLabel - Caption in the middle.
 * @param {string} prefix - Value prefix.
 * @param {string} suffix - Value suffix.
 * @param {string[]} colors - Optional per-parent colors.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
export interface SunburstChartProps {
  title?: string;
  subtitle?: string;
  data?: Array<{ label: string; children: Array<{ label: string; value: number }> }>;
  centerLabel?: string;
  prefix?: string;
  suffix?: string;
  colors?: string[];
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function SunburstChart({
  title = 'Cloud spend breakdown',
  subtitle = 'Service → resource',
  data = [
    { label: 'Compute', children: [{ label: 'VMs', value: 4200 }, { label: 'Functions', value: 1800 }, { label: 'Containers', value: 2600 }] },
    { label: 'Storage', children: [{ label: 'Object', value: 1900 }, { label: 'Block', value: 1100 }] },
    { label: 'Network', children: [{ label: 'CDN', value: 1400 }, { label: 'Egress', value: 900 }] },
    { label: 'Data', children: [{ label: 'Postgres', value: 1600 }, { label: 'Redis', value: 500 }] },
  ],
  centerLabel = 'Monthly spend',
  prefix = '$',
  suffix = '',
  colors,
  variant = 'glass',
  size = 'md',
  className = '',
}: SunburstChartProps) {
  const S = 300;
  const c = S / 2;
  const total = sum(data.flatMap((d) => d.children.map((ch) => ch.value))) || 1;
  const GAP = 0.01;
  let a = 0;
  const parents = data.slice(0, 8).map((d, pi) => {
    const pv = sum(d.children.map((ch) => ch.value));
    const a0 = a;
    const sweep = (pv / total) * Math.PI * 2;
    a += sweep;
    const color = colorAt(pi, colors);
    let ca = a0;
    const kids = d.children.map((ch, ci) => {
      const s = (ch.value / total) * Math.PI * 2;
      const k0 = ca;
      ca += s;
      const mix = 100 - ci * (40 / Math.max(1, d.children.length - 1));
      return { ...ch, d: arcPath(c, c, 146, 104, k0 + GAP / 2, k0 + s - GAP / 2), color: `color-mix(in srgb, ${color} ${mix}%, #0d0d0f)` };
    });
    return { label: d.label, value: pv, color, d: arcPath(c, c, 100, 62, a0 + GAP / 2, a0 + sweep - GAP / 2), kids };
  });
  const fmt = (v: number) => formatValue(v, prefix, suffix);
  const pct = (v: number) => `${((v / total) * 100).toFixed(1)}%`;
  return (
    <>
      <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} legend={parents.map((p) => ({ label: p.label, color: p.color }))} table={{ headers: ['Group', 'Item', 'Value', 'Share'], rows: parents.flatMap((p) => p.kids.map((k) => [p.label, k.label, fmt(k.value), pct(k.value)])) }}>
        <div className="sbt" data-rk-355acb="">
          <svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: sunburst chart, total ${fmt(total)}`} data-rk-355acb="">
            {parents.map((p, __i: number) => (
              <g key={__i} data-rk-355acb="">
                <path className="cf-mark cf-dim" d={p.d} fill={p.color} tabIndex={0} role="img" aria-label={`${p.label}: ${fmt(p.value)} (${pct(p.value)})`} data-tip-title={p.label} data-tip={`${p.color}::${fmt(p.value)} · ${pct(p.value)}`} data-rk-355acb="" />
                {p.kids.map((k, __i: number) => (
                  <path key={__i} className="cf-mark cf-dim" d={k.d} fill={k.color} tabIndex={0} role="img" aria-label={`${p.label} / ${k.label}: ${fmt(k.value)}`} data-tip-title={`${p.label} › ${k.label}`} data-tip={`${k.color}::${fmt(k.value)} · ${pct(k.value)}`} data-rk-355acb="" />
                ))}
              </g>
            ))}
            <text x={c} y={c + 2} textAnchor="middle" className="sbt__total" data-rk-355acb="">{fmt(total)}</text>
            <text x={c} y={c + 20} textAnchor="middle" className="cf-label" data-rk-355acb="">{centerLabel}</text>
          </svg>
        </div>
      </ChartFrame>
    </>
  );
}

export default SunburstChart;
SunburstChart.css
/* SunburstChart — ported from astro-component-kit (charts/SunburstChart.astro) */

.sbt:where([data-rk-355acb]) { max-width: 340px; margin: 0 auto; }
.sbt__total:where([data-rk-355acb]) { fill: var(--c-text-1, #fff); font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }

Quick Info

Category
Charts & Stats
Filename
SunburstChart.tsx
Dependencies
None — pure React + CSS
Tags
chartsvgzero-dependencysunbursthierarchydonutbreakdown