{"spec_id":"donut-nested","library":"makie","language":"julia","code":"# anyplot.ai\n# donut-nested: Nested Donut Chart\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 95/100 | Created: 2026-08-18\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens -------------------------------------------------------------\nTHEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nPAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nINK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nINK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint categorical palette — one hue family per department (level_1)\nIMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green\n    colorant\"#C475FD\",  # 2 — lavender\n    colorant\"#4467A3\",  # 3 — blue\n    colorant\"#BD8233\",  # 4 — ochre\n]\n\n# --- Data -----------------------------------------------------------------\n# Annual IT budget (in $ thousands): department totals (level_1, inner ring)\n# broken down into expense categories (level_2, outer ring). Outer values\n# aggregate up to the inner ring, per department.\ndepartments = [\"Engineering\", \"Sales & Marketing\", \"Operations\", \"Customer Support\"]\n\nexpense_categories = [\n    [\"Salaries\", \"Cloud Infra\", \"Tooling\"],\n    [\"Advertising\", \"Events\", \"Content\", \"Travel\"],\n    [\"Facilities\", \"Logistics\", \"Utilities\"],\n    [\"Staffing\", \"Software\", \"Training\"],\n]\n\nexpense_values = [\n    [420.0, 180.0, 60.0],\n    [140.0, 90.0, 55.0, 35.0],\n    [95.0, 70.0, 40.0],\n    [110.0, 45.0, 25.0],\n]\n\ndepartment_totals = [sum(v) for v in expense_values]\ngrand_total = sum(department_totals)\n\n# Same hue family per department, lightness spread across its children so the\n# outer ring visually nests under its parent wedge.\ndepartment_hsl = [Colors.HSL(c) for c in IMPRINT_PALETTE]\nchild_colors = [\n    [Colors.RGB(Colors.HSL(hsl.h, hsl.s, clamp(hsl.l + t, 0.18, 0.82)))\n     for t in range(-0.16, 0.24; length = length(expense_values[i]))]\n    for (i, hsl) in enumerate(department_hsl)\n]\n\nouter_values = reduce(vcat, expense_values)\nouter_labels = reduce(vcat, expense_categories)\nouter_colors = reduce(vcat, child_colors)\nouter_shares = outer_values ./ grand_total\n\n# --- Figure -----------------------------------------------------------------\nfig = Figure(\n    resolution      = (1200, 1200),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title       = \"donut-nested · julia · makie · anyplot.ai\",\n    titlesize   = 20,\n    titlecolor  = INK,\n    backgroundcolor = PAGE_BG,\n    aspect      = DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\n\nRING_START_OFFSET = pi / 2  # rotate first wedge to 12 o'clock\n\n# Inner ring — department totals\npie!(\n    ax, department_totals;\n    color         = IMPRINT_PALETTE[1:length(department_totals)],\n    radius        = 0.55,\n    inner_radius  = 0.28,\n    offset        = RING_START_OFFSET,\n    strokecolor   = PAGE_BG,\n    strokewidth   = 4,\n)\n\n# Outer ring — expense categories nested under each department\npie!(\n    ax, outer_values;\n    color         = outer_colors,\n    radius        = 1.0,\n    inner_radius  = 0.60,\n    offset        = RING_START_OFFSET,\n    strokecolor   = PAGE_BG,\n    strokewidth   = 3,\n)\n\n# --- Segment mid-angles (mirrors the pie recipe's own boundary math) --------\ninner_boundaries = cumsum([0.0; department_totals]) ./ grand_total .* 2pi\ninner_mid_angles = [\n    (inner_boundaries[i] + inner_boundaries[i + 1]) / 2 + RING_START_OFFSET\n    for i in 1:length(department_totals)\n]\ninner_label_radius = (0.28 + 0.55) / 2\ninner_label_x = [cos(a) * inner_label_radius for a in inner_mid_angles]\ninner_label_y = [sin(a) * inner_label_radius for a in inner_mid_angles]\n\nouter_boundaries = cumsum([0.0; outer_values]) ./ grand_total .* 2pi\nouter_mid_angles = [\n    (outer_boundaries[i] + outer_boundaries[i + 1]) / 2 + RING_START_OFFSET\n    for i in 1:length(outer_values)\n]\nouter_label_radius = (0.60 + 1.0) / 2\nouter_label_x = [cos(a) * outer_label_radius for a in outer_mid_angles]\nouter_label_y = [sin(a) * outer_label_radius for a in outer_mid_angles]\n\n# Pick dark or light text per wedge so labels stay legible whether their\n# background is a pale or a deep lightness variant of the department hue.\n# These are the two fixed ink anchors from the style guide (not theme-flipped\n# like INK/ELEVATED_BG) — the right anchor depends on the wedge's own\n# lightness, not on which theme is rendering.\nLABEL_ON_LIGHT = colorant\"#1A1A17\"\nLABEL_ON_DARK  = colorant\"#F0EFE8\"\ninner_wedge_colors = IMPRINT_PALETTE[1:length(department_totals)]\ninner_text_colors = [Colors.HSL(c).l >= 0.55 ? LABEL_ON_LIGHT : LABEL_ON_DARK for c in inner_wedge_colors]\nouter_text_colors = [Colors.HSL(c).l >= 0.55 ? LABEL_ON_LIGHT : LABEL_ON_DARK for c in outer_colors]\n\n# Direct labels on department wedges — 4 segments, all comfortably fit.\ntext!(\n    ax, inner_label_x, inner_label_y;\n    text        = [string(d, \"\\n\", round(Int, 100 * department_totals[i] / grand_total), \"%\")\n                    for (i, d) in enumerate(departments)],\n    align       = (:center, :center),\n    color       = inner_text_colors,\n    fontsize    = 14,\n    font        = :bold,\n)\n\n# Direct labels only on larger outer segments; smaller ones fall back to the\n# legend below so text never crowds thin wedges.\nLABEL_SHARE_THRESHOLD = 0.06\nlabeled_idx = findall(>=(LABEL_SHARE_THRESHOLD), outer_shares)\nlegend_idx = findall(<(LABEL_SHARE_THRESHOLD), outer_shares)\n\ntext!(\n    ax, outer_label_x[labeled_idx], outer_label_y[labeled_idx];\n    text        = [string(outer_labels[i], \"\\n\", round(Int, 100 * outer_shares[i]), \"%\")\n                    for i in labeled_idx],\n    align       = (:center, :center),\n    color       = outer_text_colors[labeled_idx],\n    fontsize    = 13,\n    font        = :bold,\n)\n\n# Center total\ntext!(\n    ax, 0.0, 0.0;\n    text        = string(\"Total\\n\\$\", round(Int, grand_total), \"K\"),\n    align       = (:center, :center),\n    color       = INK,\n    fontsize    = 15,\n    font        = :bold,\n)\n\n# Legend for the smaller outer-ring segments (share < threshold)\nlegend_elems = [PolyElement(color = outer_colors[i], strokecolor = PAGE_BG, strokewidth = 1)\n                for i in legend_idx]\nlegend_text = [string(outer_labels[i], \" (\", round(Int, 100 * outer_shares[i]), \"%)\")\n               for i in legend_idx]\n\nLegend(\n    fig[2, 1], legend_elems, legend_text, \"Smaller expense categories\";\n    orientation    = :horizontal,\n    nbanks         = 2,\n    framevisible   = false,\n    backgroundcolor = PAGE_BG,\n    labelcolor     = INK_SOFT,\n    titlecolor     = INK_SOFT,\n    labelsize      = 12,\n    titlesize      = 12,\n)\n\nrowsize!(fig.layout, 1, Relative(0.82))\nrowsize!(fig.layout, 2, Relative(0.18))\n\n# --- Save ---------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}