Nested Donut Chart — Highcharts

A nested donut chart displays hierarchical data as multiple concentric rings, where each ring represents a level of the hierarchy. Inner rings show parent categories while outer rings show their subdivisions. This visualization effectively reveals part-to-whole relationships across multiple levels while maintaining the familiar donut format.

Nested Donut Chart rendered with Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// donut-nested: Nested Donut Chart
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-08-18

//# anyplot-orientation: square
const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// Annual revenue ($M) by business unit (inner ring) and product line (outer ring).
const businessUnits = [
  {
    name: "Cloud Services",
    color: t.palette[0],
    products: [
      { name: "Compute", value: 42 },
      { name: "Storage", value: 27 },
      { name: "Networking", value: 15 },
    ],
  },
  {
    name: "Hardware",
    color: t.palette[1],
    products: [
      { name: "Servers", value: 24 },
      { name: "Laptops", value: 19 },
      { name: "Peripherals", value: 8 },
    ],
  },
  {
    name: "Software Licensing",
    color: t.palette[2],
    products: [
      { name: "Enterprise", value: 33 },
      { name: "SMB", value: 12 },
    ],
  },
  {
    name: "Consulting",
    color: t.palette[3],
    products: [
      { name: "Implementation", value: 14 },
      { name: "Training", value: 9 },
      { name: "Support", value: 7 },
    ],
  },
];

// Inner ring: one slice per business unit, sized by the sum of its products.
const innerData = businessUnits.map((unit) => ({
  name: unit.name,
  y: unit.products.reduce((sum, product) => sum + product.value, 0),
  color: unit.color,
}));

// Outer ring: one slice per product, tinted around its parent's hue so the
// color family (same hue, varying lightness) reads as belonging to that unit.
// Because both rings share the same order and total, slice boundaries line up.
const outerData = businessUnits.flatMap((unit) =>
  unit.products.map((product, i) => {
    const offset = (i - (unit.products.length - 1) / 2) * 0.18;
    return {
      name: product.name,
      y: product.value,
      color: Highcharts.color(unit.color).brighten(offset).get(),
    };
  }),
);

// Only the largest product-line slices earn a direct inline label; the rest
// fall back to the legend, per the spec's "labels on larger segments" guidance.
const outerLabelThreshold = 15; // percent of the outer ring's total

// --- Chart -------------------------------------------------------------------
Highcharts.chart("container", {
  chart: {
    type: "pie",
    backgroundColor: "transparent",
    animation: false,
    style: { fontFamily: "inherit" },
  },
  credits: { enabled: false },
  colors: t.palette,
  title: {
    text: "donut-nested · javascript · highcharts · anyplot.ai",
    style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
  },
  subtitle: {
    text: "Revenue by Business Unit and Product Line ($M)",
    style: { color: t.inkSoft, fontSize: "14px" },
  },
  legend: {
    itemStyle: { color: t.inkSoft, fontSize: "14px" },
    itemHoverStyle: { color: t.ink },
    align: "center",
    verticalAlign: "bottom",
    layout: "horizontal",
  },
  tooltip: {
    backgroundColor: t.elevatedBg,
    borderColor: t.grid,
    style: { color: t.ink, fontSize: "13px" },
    pointFormat: "<b>{point.name}</b>: ${point.y}M ({point.percentage:.1f}%)",
  },
  plotOptions: {
    series: { animation: false },
    pie: {
      borderWidth: 2,
      borderColor: t.pageBg,
      borderRadius: 3,
    },
  },
  series: [
    {
      name: "Business Unit",
      innerSize: "38%",
      size: "64%",
      showInLegend: false,
      data: innerData,
      dataLabels: {
        enabled: true,
        distance: -55,
        format: "{point.name}",
        style: {
          color: t.ink,
          fontSize: "14px",
          fontWeight: "600",
          textOutline: `3px ${t.pageBg}`,
        },
      },
    },
    {
      name: "Product Line",
      innerSize: "68%",
      size: "96%",
      showInLegend: true,
      data: outerData,
      dataLabels: {
        enabled: true,
        distance: -55,
        formatter() {
          return this.percentage > outerLabelThreshold ? this.point.name : null;
        },
        style: {
          color: t.ink,
          fontSize: "11px",
          fontWeight: "600",
          textOutline: `2px ${t.pageBg}`,
        },
      },
    },
  ],
});

Part of Nested Donut Chart on anyplot.ai.

Other implementations