{"spec_id":"donut-nested","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\ndonut-nested: Nested Donut Chart\nLibrary: seaborn 0.13.2 | Python 3.13.15\nQuality: 84/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport sys\n\n\nsys.path.pop(0)\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\nfrom matplotlib.colors import to_rgb\nfrom matplotlib.patches import Patch\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette - positions 1-4 for parent categories\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\nDARK_TEXT = (\n    \"#1A1A17\"  # fixed ink for label text on light wedges - data colors don't flip with theme, so neither should this\n)\n\n\ndef _luminance(color):\n    \"\"\"Relative (WCAG) luminance of a matplotlib color spec.\"\"\"\n    r, g, b = to_rgb(color)\n\n    def channel(c):\n        return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4\n\n    r, g, b = channel(r), channel(g), channel(b)\n    return 0.2126 * r + 0.7152 * g + 0.0722 * b\n\n\ndef _label_color(wedge_color):\n    \"\"\"White reads well on most Imprint hues, but lighter wedges (e.g. lavender) need dark text for AA contrast.\"\"\"\n    return DARK_TEXT if _luminance(wedge_color) > 0.3 else \"white\"\n\n\n# Data: Regional budget allocation with expense categories\nregions = [\"North America\", \"Europe\", \"Asia Pacific\", \"Latin America\"]\ncategories = [\"Salaries\", \"Marketing\", \"Operations\", \"R&D\"]\n\ndata = {\n    \"North America\": [45, 22, 18, 35],\n    \"Europe\": [38, 18, 15, 24],\n    \"Asia Pacific\": [28, 20, 25, 32],\n    \"Latin America\": [18, 12, 10, 10],\n}\n\n# Calculate totals for inner ring\ninner_values = [sum(data[r]) for r in regions]\nouter_values = []\nfor r in regions:\n    outer_values.extend(data[r])\n\ntotal_budget = sum(inner_values)\n\n# Set seaborn style with theme-adaptive colors (Imprint chrome mapping)\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Create figure (square for symmetric donut) — canonical 2400x2400 px\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Create outer colors - lighter shades of each parent category color\nouter_colors = []\nfor i, _region in enumerate(regions):\n    parent_color = IMPRINT[i]\n    shades = sns.light_palette(parent_color, n_colors=5, reverse=True)[:-1]\n    outer_colors.extend(shades)\n\n# Donut geometry - shrunk 10% from a full radius=1.0 ring to leave clearance\n# between the outer wedges and the \"Categories (Outer)\" legend box\nOUTER_RADIUS = 0.9\nOUTER_WIDTH = 0.315\nINNER_RADIUS = 0.54\nINNER_WIDTH = 0.27\nINNER_LABEL_R = 0.49  # biased toward the outer edge of the inner ring for center-text clearance\nOUTER_LABEL_R = OUTER_RADIUS - OUTER_WIDTH / 2  # midpoint of the outer ring\n\n# The legend column occupies the axes' left margin, so the donut is shifted right\n# of the data origin to balance the canvas whitespace evenly left-to-right.\nCX, CY = 0.31, 0.0\n\n# Outer ring (categories within regions)\nouter_wedges, _ = ax.pie(\n    outer_values,\n    radius=OUTER_RADIUS,\n    colors=outer_colors,\n    wedgeprops={\"width\": OUTER_WIDTH, \"edgecolor\": PAGE_BG, \"linewidth\": 2.5},\n    startangle=90,\n    center=(CX, CY),\n)\n\n# Inner ring (regions)\ninner_wedges, inner_texts = ax.pie(\n    inner_values,\n    radius=INNER_RADIUS,\n    colors=IMPRINT,\n    wedgeprops={\"width\": INNER_WIDTH, \"edgecolor\": PAGE_BG, \"linewidth\": 2.5},\n    startangle=90,\n    labels=None,\n    center=(CX, CY),\n)\n\n# Pin the viewport to the un-shifted symmetric range so the CX offset above\n# actually moves the donut within the axes instead of being auto-recentered.\nax.set_xlim(-1.25, 1.25)\nax.set_ylim(-1.25, 1.25)\n\n# Add center text\nax.text(CX, CY, f\"Total\\nBudget\\n${total_budget}M\", ha=\"center\", va=\"center\", fontsize=11, fontweight=\"bold\", color=INK)\n\n# Add labels for inner ring (regions with values)\ncumsum = 0\nfor region, val, color in zip(regions, inner_values, IMPRINT, strict=True):\n    # matplotlib pie() sweeps counterclockwise from startangle by default — match that direction\n    angle = 90 + (cumsum + val / 2) / total_budget * 360\n    angle_rad = np.radians(angle)\n    x = CX + INNER_LABEL_R * np.cos(angle_rad)\n    y = CY + INNER_LABEL_R * np.sin(angle_rad)\n    ax.text(\n        x, y, f\"{region}\\n${val}M\", ha=\"center\", va=\"center\", fontsize=7, fontweight=\"bold\", color=_label_color(color)\n    )\n    cumsum += val\n\n# Add a direct label to the single largest outer (category) wedge per region,\n# so viewers aren't limited to the legend/position-parity to read the biggest segments\ncumsum = 0\nfor r_idx, region in enumerate(regions):\n    region_values = data[region]\n    max_idx = region_values.index(max(region_values))\n    for c_idx, val in enumerate(region_values):\n        mid_angle = 90 + (cumsum + val / 2) / total_budget * 360\n        if c_idx == max_idx:\n            angle_rad = np.radians(mid_angle)\n            x = CX + OUTER_LABEL_R * np.cos(angle_rad)\n            y = CY + OUTER_LABEL_R * np.sin(angle_rad)\n            wedge_color = outer_colors[r_idx * len(categories) + c_idx]\n            ax.text(\n                x,\n                y,\n                f\"{categories[c_idx]}\\n${val}M\",\n                ha=\"center\",\n                va=\"center\",\n                fontsize=7,\n                fontweight=\"bold\",\n                color=_label_color(wedge_color),\n            )\n        cumsum += val\n\n# Create legend for regions (inner ring)\nregion_patches = [\n    Patch(facecolor=IMPRINT[i], label=f\"{regions[i]}\", edgecolor=INK_SOFT, linewidth=1) for i in range(len(regions))\n]\n\n# Create legend for categories, shown in the North America shade family\n# (each region's outer wedges use its own tint of the same 4 categories, in this order)\ncategory_patches = [\n    Patch(\n        facecolor=sns.light_palette(IMPRINT[0], n_colors=5, reverse=True)[:-1][i],\n        label=categories[i],\n        edgecolor=INK_SOFT,\n        linewidth=1,\n    )\n    for i in range(len(categories))\n]\n\n# Add legends\nlegend1 = ax.legend(\n    handles=region_patches,\n    title=\"Regions (Inner)\",\n    loc=\"upper left\",\n    bbox_to_anchor=(-0.02, 1.0),\n    fontsize=8,\n    title_fontsize=9,\n    framealpha=0.95,\n    edgecolor=INK_SOFT,\n    facecolor=ELEVATED_BG,\n    labelcolor=INK,\n)\nfor text in legend1.get_texts():\n    text.set_color(INK)\nlegend1.get_title().set_fontsize(9)\nlegend1.get_title().set_weight(\"bold\")\nlegend1.get_title().set_color(INK)\nax.add_artist(legend1)\n\nlegend2 = ax.legend(\n    handles=category_patches,\n    title=\"Categories (Outer)\\n(N. Am. shades)\",\n    loc=\"lower left\",\n    bbox_to_anchor=(-0.02, 0.0),\n    fontsize=8,\n    title_fontsize=9,\n    framealpha=0.95,\n    edgecolor=INK_SOFT,\n    facecolor=ELEVATED_BG,\n    labelcolor=INK,\n)\nfor text in legend2.get_texts():\n    text.set_color(INK)\nlegend2.get_title().set_fontsize(9)\nlegend2.get_title().set_weight(\"bold\")\nlegend2.get_title().set_color(INK)\n\n# Title\nax.set_title(\"donut-nested · python · seaborn · anyplot.ai\", fontsize=14, fontweight=\"bold\", pad=16, color=INK)\n\nax.set_aspect(\"equal\")\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}