Nested Donut Chart — Apache ECharts

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 Apache ECharts

Renders

JavaScript source (Apache ECharts)

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

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

// --- Data (in-memory, deterministic) ----------------------------------------
// Company operating budget ($ thousands): department (inner ring) rolls up
// from expense category (outer ring). Each department's categories are
// listed largest-first so the color-family shading (below) tracks value.
const budget = [
  {
    department: "Engineering",
    categories: [
      { name: "Salaries & Benefits", value: 340 },
      { name: "Cloud Infrastructure", value: 180 },
      { name: "Software Licenses", value: 95 },
    ],
  },
  {
    department: "Marketing",
    categories: [
      { name: "Digital Advertising", value: 150 },
      { name: "Content Production", value: 80 },
      { name: "Events & Sponsorship", value: 60 },
    ],
  },
  {
    department: "Sales",
    categories: [
      { name: "Sales Commissions", value: 210 },
      { name: "Travel & Entertainment", value: 70 },
      { name: "CRM Tools", value: 40 },
    ],
  },
  {
    department: "Operations",
    categories: [
      { name: "Facilities", value: 120 },
      { name: "Logistics", value: 95 },
      { name: "IT Support", value: 65 },
    ],
  },
];

// --- Color families: same hue per department, lightness steps per category -
const LIGHTNESS_STEPS = [0, 0.22, 0.42];
const departmentColor = (i) => t.palette[i];
const categoryColor = (deptIndex, catIndex) =>
  echarts.color.lift(departmentColor(deptIndex), LIGHTNESS_STEPS[catIndex]);

// Fixed near-black / near-white label tones (not theme-driven — the data
// fill colors themselves never change with theme, so label contrast must be
// computed from the fill, not from ANYPLOT_THEME).
const LABEL_ON_LIGHT_FILL = "#1A1A17";
const LABEL_ON_DARK_FILL = "#F0EFE8";

function relativeLuminance(hex) {
  const c = hex.replace("#", "");
  const [r, g, b] = [0, 2, 4].map((i) => parseInt(c.substr(i, 2), 16) / 255);
  const lin = (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
function labelColorFor(hex) {
  return relativeLuminance(hex) > 0.55 ? LABEL_ON_LIGHT_FILL : LABEL_ON_DARK_FILL;
}

const innerData = budget.map((d, i) => {
  const total = d.categories.reduce((sum, c) => sum + c.value, 0);
  const color = departmentColor(i);
  return {
    name: d.department,
    value: total,
    itemStyle: { color },
    label: { color: labelColorFor(color) },
  };
});

const outerData = budget.flatMap((d, di) =>
  d.categories.map((cat, ci) => {
    const color = categoryColor(di, ci);
    return { name: cat.name, value: cat.value, itemStyle: { color } };
  })
);

// --- Title (fontsize scales down if the descriptive prefix runs long) ------
const title = "Operating Budget by Department · donut-nested · javascript · echarts · anyplot.ai";
const titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));

// --- Init --------------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

// --- Option -------------------------------------------------------------------
chart.setOption({
  animation: false,
  color: t.palette,
  backgroundColor: "transparent",
  title: {
    text: title,
    left: "center",
    top: 28,
    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 600 },
  },
  tooltip: {
    trigger: "item",
    formatter: "{b}: ${c}k ({d}%)",
  },
  legend: {
    bottom: 12,
    left: "center",
    width: "90%",
    itemWidth: 14,
    itemHeight: 14,
    itemGap: 16,
    textStyle: { color: t.inkSoft, fontSize: 13 },
    data: outerData.map((c) => c.name),
  },
  series: [
    {
      name: "Department",
      type: "pie",
      center: ["50%", "53%"],
      radius: ["0%", "30%"],
      padAngle: 2,
      label: {
        show: true,
        position: "inside",
        formatter: "{b}",
        fontSize: 16,
        fontWeight: 600,
      },
      labelLine: { show: false },
      itemStyle: { borderColor: t.pageBg, borderWidth: 3 },
      data: innerData,
    },
    {
      name: "Expense category",
      type: "pie",
      center: ["50%", "53%"],
      radius: ["40%", "68%"],
      padAngle: 1.5,
      minShowLabelAngle: 12,
      label: {
        show: true,
        position: "outside",
        formatter: "{b}",
        fontSize: 13,
        color: t.inkSoft,
      },
      labelLine: { length: 14, length2: 10, lineStyle: { color: t.grid } },
      labelLayout: { hideOverlap: true },
      itemStyle: { borderColor: t.pageBg, borderWidth: 3 },
      data: outerData,
    },
  ],
});

Part of Nested Donut Chart on anyplot.ai.

Other implementations