Nested Donut Chart — Matplotlib

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 Matplotlib

Renders

Python source (Matplotlib)

""" anyplot.ai
donut-nested: Nested Donut Chart
Library: matplotlib 3.11.1 | Python 3.13.15
Quality: 90/100 | Updated: 2026-08-18
"""

import os

import matplotlib.patheffects as pe
import matplotlib.pyplot as plt
import numpy as np


# Theme tokens (see prompts/default-style-guide.md "Background" + "Theme-adaptive Chrome")
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Imprint palette (positions 1-4 for departments)
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233"]

# Data - Budget allocation: departments (inner ring) and expense categories (outer ring)
departments = ["Engineering", "Marketing", "Operations", "Sales"]
categories = {
    "Engineering": ["Salaries", "Equipment", "Software"],
    "Marketing": ["Advertising", "Events", "Content"],
    "Operations": ["Facilities", "Utilities", "Maintenance"],
    "Sales": ["Travel", "Commissions", "Training"],
}
values = {
    "Engineering": [450, 120, 80],
    "Marketing": [200, 150, 100],
    "Operations": [180, 90, 60],
    "Sales": [140, 220, 70],
}

# Department totals for the inner ring (values aggregate outer -> inner)
dept_totals = [sum(values[dept]) for dept in departments]

# Flatten outer ring data while maintaining department order
outer_labels = []
outer_values = []
for dept in departments:
    outer_labels.extend(categories[dept])
    outer_values.extend(values[dept])

inner_colors = [IMPRINT[i] for i in range(len(departments))]

# Outer ring reuses each department's hue with varying opacity per subcategory,
# so a category visually belongs to its parent while staying distinguishable.
outer_colors_with_alpha = []
for dept, color in zip(departments, inner_colors, strict=True):
    rgb = tuple(int(color[j : j + 2], 16) / 255.0 for j in (1, 3, 5))
    alphas = np.linspace(0.5, 1.0, len(categories[dept]))
    outer_colors_with_alpha.extend((*rgb, alpha) for alpha in alphas)

# Square canvas for symmetric circular shapes: figsize=(6, 6) x dpi=400 -> 2400x2400 px
fig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Inner ring (departments)
inner_wedge_props = {"width": 0.4, "edgecolor": PAGE_BG, "linewidth": 2}
wedges_inner, _ = ax.pie(dept_totals, radius=0.6, colors=inner_colors, wedgeprops=inner_wedge_props, startangle=90)

# Outer ring (categories within departments)
outer_wedge_props = {"width": 0.35, "edgecolor": PAGE_BG, "linewidth": 1.5}
wedges_outer, _ = ax.pie(
    outer_values, radius=1.0, colors=outer_colors_with_alpha, wedgeprops=outer_wedge_props, startangle=90
)

# A thin halo behind wedge labels keeps them crisp against every segment shade
halo = [pe.withStroke(linewidth=3, foreground=PAGE_BG)]

# Department labels (inner ring) - bold, the largest data-label tier
for wedge, dept, total in zip(wedges_inner, departments, dept_totals, strict=True):
    angle = np.radians((wedge.theta2 + wedge.theta1) / 2)
    x, y = 0.42 * np.cos(angle), 0.42 * np.sin(angle)
    ax.text(
        x,
        y,
        f"{dept}\n${total}K",
        ha="center",
        va="center",
        fontsize=12,
        fontweight="bold",
        color=INK,
        path_effects=halo,
    )

# Category labels (outer ring) - regular weight, larger segments only
for wedge, label, value in zip(wedges_outer, outer_labels, outer_values, strict=True):
    if value >= 100:  # only label segments >= $100K to avoid clutter
        angle = np.radians((wedge.theta2 + wedge.theta1) / 2)
        x, y = 0.82 * np.cos(angle), 0.82 * np.sin(angle)
        ax.text(x, y, f"{label}\n${value}K", ha="center", va="center", fontsize=8, color=INK, path_effects=halo)

# Custom legend: every department + category pair, colored by parent hue
legend_elements = [
    plt.Rectangle((0, 0), 1, 1, facecolor=IMPRINT[i], edgecolor=INK_SOFT, label=f"{dept}: {cat}")
    for i, dept in enumerate(departments)
    for cat in categories[dept]
]
leg = ax.legend(
    handles=legend_elements,
    loc="center left",
    bbox_to_anchor=(1.0, 0.5),
    fontsize=8,
    frameon=True,
    edgecolor=INK_SOFT,
    facecolor=ELEVATED_BG,
    handlelength=1.2,
    handletextpad=0.6,
    borderaxespad=0.6,
)
leg.get_frame().set_linewidth(0.5)
plt.setp(leg.get_texts(), color=INK_SOFT)

# fig.suptitle (not ax.set_title) so the mandated title centers on the full canvas,
# not just the donut's axes, which is shifted left to leave room for the legend.
fig.suptitle(
    "donut-nested · python · matplotlib · anyplot.ai", x=0.5, y=0.965, fontsize=12, fontweight="medium", color=INK
)

ax.set_aspect("equal")
# Reserve room on the right for the legend instead of bbox_inches="tight" (which drifts canvas size).
# Height span matches the width span (both 0.64) and is centered vertically (top/bottom margins
# both 0.18) so aspect="equal" doesn't leave an asymmetric dead zone below the donut.
fig.subplots_adjust(left=0.02, right=0.66, top=0.82, bottom=0.18)
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/donut-nested/matplotlib/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "donut-nested",
  "language": "python",
  "library": "matplotlib",
  "page": "https://anyplot.ai/donut-nested/python/matplotlib",
  "hub": "https://anyplot.ai/donut-nested",
  "code_json": "https://api.anyplot.ai/specs/donut-nested/matplotlib/code",
  "spec_json": "https://api.anyplot.ai/specs/donut-nested",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/donut-nested/python/matplotlib/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/donut-nested/python/matplotlib/plot-dark.png",
  "quality_score": 90.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Nested Donut Chart on anyplot.ai.

Other implementations