{"spec_id":"sunburst-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nsunburst-basic: Basic Sunburst Chart\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-07-26\n\"\"\"\n\nimport sys\n\n\nsys.path.pop(0)  # prevent this file from shadowing the installed plotnine package\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_equal,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_size_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n# Slightly lighter than PAGE_BG in dark mode so ring boundaries remain visible\nRING_SEP = PAGE_BG if THEME == \"light\" else \"#2E2D2B\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: company annual budget by department -> team -> subproject ($M), 3 hierarchy levels\nhierarchy = {\n    \"Engineering\": {\n        \"Frontend\": {\"Web\": 9, \"Mobile\": 6},\n        \"Backend\": {\"API\": 8, \"Data Pipeline\": 7},\n        \"DevOps\": {\"Infra\": 6, \"CI/CD\": 4},\n    },\n    \"Marketing\": {\n        \"Digital\": {\"Paid Ads\": 6, \"SEO\": 4},\n        \"Brand\": {\"Design\": 5, \"Content\": 3},\n        \"Events\": {\"Conferences\": 4, \"Webinars\": 3},\n    },\n    \"Operations\": {\n        \"HR\": {\"Recruiting\": 5, \"Benefits\": 3},\n        \"Finance\": {\"Accounting\": 4, \"Payroll\": 3},\n        \"Legal\": {\"Contracts\": 3, \"Compliance\": 2},\n    },\n    \"R&D\": {\"Product\": {\"Roadmap\": 5, \"UX Research\": 3}, \"Data Science\": {\"ML\": 4, \"Analytics\": 3}},\n}\n\ndept_colors = dict(zip(hierarchy, IMPRINT, strict=False))\n\ndept_totals = {dept: sum(sum(sub.values()) for sub in teams.values()) for dept, teams in hierarchy.items()}\ntotal = sum(dept_totals.values())\nfocal_dept = max(dept_totals, key=dept_totals.get)  # largest segment gets a focal-point accent\n\n# Ring radii: 3 concentric bands (department -> team -> subproject). Generous gaps between\n# bands (0.10-0.12) keep long horizontally-oriented labels (e.g. \"Engineering\" at a near-0deg\n# mid-angle) from bleeding radially into the next ring's label.\nR1_IN, R1_OUT = 0.28, 0.48\nR2_IN, R2_OUT = 0.60, 0.80\nR3_IN, R3_OUT = 0.90, 1.08\nN_PTS = 60  # arc resolution\n\nl1_rows, l2_rows, l3_rows, label_rows = [], [], [], []\ncumsum = 0\n\n\ndef arc_polygon(a0, a1, r_in, r_out):\n    t = np.linspace(a0, a1, N_PTS)\n    xs = np.concatenate([r_in * np.cos(t), r_out * np.cos(t[::-1])])\n    ys = np.concatenate([r_in * np.sin(t), r_out * np.sin(t[::-1])])\n    return xs, ys\n\n\nfor dept, teams in hierarchy.items():\n    dept_total = dept_totals[dept]\n    pct = round(dept_total / total * 100)\n    a0 = 2 * np.pi * cumsum / total - np.pi / 2\n    a1 = 2 * np.pi * (cumsum + dept_total) / total - np.pi / 2\n    is_focal = dept == focal_dept\n    edge = \"focal\" if is_focal else \"normal\"\n\n    # L1 arc polygon: inner arc -> outer arc (reversed) -> closed shape\n    xs, ys = arc_polygon(a0, a1, R1_IN, R1_OUT)\n    for xi, yi in zip(xs, ys, strict=False):\n        l1_rows.append({\"x\": xi, \"y\": yi, \"group\": dept, \"dept\": dept, \"edge\": f\"L1_{edge}\"})\n\n    # L1 department name + percentage: shared anchor at the ring's mid-radius, split into a\n    # two-line stack via va=\"bottom\"/\"top\" (screen-space, not radial) so the pair never collides\n    # for sections whose mid-angle runs near-horizontal (radial offsetting alone would overlap there).\n    a_mid = (a0 + a1) / 2\n    r_label = R1_IN + 0.28 * (R1_OUT - R1_IN)\n    lx, ly = r_label * np.cos(a_mid), r_label * np.sin(a_mid)\n    label_rows.append({\"x\": lx, \"y\": ly, \"label\": dept, \"level\": 1})\n    label_rows.append({\"x\": lx, \"y\": ly, \"label\": f\"{pct}%\", \"level\": 4})\n\n    team_cumsum = cumsum\n    for team, subprojects in teams.items():\n        team_total = sum(subprojects.values())\n        b0 = 2 * np.pi * team_cumsum / total - np.pi / 2\n        b1 = 2 * np.pi * (team_cumsum + team_total) / total - np.pi / 2\n\n        xs2, ys2 = arc_polygon(b0, b1, R2_IN, R2_OUT)\n        grp2 = f\"{dept}_{team}\"\n        for xi, yi in zip(xs2, ys2, strict=False):\n            l2_rows.append({\"x\": xi, \"y\": yi, \"group\": grp2, \"dept\": dept, \"edge\": f\"L2_{edge}\"})\n\n        if team_total / total >= 0.08:\n            b_mid = (b0 + b1) / 2\n            r_mid2 = (R2_IN + R2_OUT) / 2\n            label_rows.append({\"x\": r_mid2 * np.cos(b_mid), \"y\": r_mid2 * np.sin(b_mid), \"label\": team, \"level\": 2})\n\n        # L3 arc polygons (subprojects) — outermost ring\n        sub_cumsum = team_cumsum\n        for subproject, value in subprojects.items():\n            c0 = 2 * np.pi * sub_cumsum / total - np.pi / 2\n            c1 = 2 * np.pi * (sub_cumsum + value) / total - np.pi / 2\n\n            xs3, ys3 = arc_polygon(c0, c1, R3_IN, R3_OUT)\n            grp3 = f\"{dept}_{team}_{subproject}\"\n            for xi, yi in zip(xs3, ys3, strict=False):\n                l3_rows.append({\"x\": xi, \"y\": yi, \"group\": grp3, \"dept\": dept, \"edge\": f\"L3_{edge}\"})\n\n            # Only label subprojects wide enough to hold text (>=6% share) to keep the fine\n            # outer ring free of clutter — matches the spec's \"label major segments\" guidance.\n            if value / total >= 0.06:\n                c_mid = (c0 + c1) / 2\n                r_mid3 = (R3_IN + R3_OUT) / 2\n                label_rows.append(\n                    {\"x\": r_mid3 * np.cos(c_mid), \"y\": r_mid3 * np.sin(c_mid), \"label\": subproject, \"level\": 3}\n                )\n\n            sub_cumsum += value\n        team_cumsum += team_total\n    cumsum += dept_total\n\ndf_l1 = pd.DataFrame(l1_rows)\ndf_l2 = pd.DataFrame(l2_rows)\ndf_l3 = pd.DataFrame(l3_rows)\ndf_labels = pd.DataFrame(label_rows)\ndf_l1_labels = df_labels[df_labels[\"level\"] == 1]\ndf_l2_labels = df_labels[df_labels[\"level\"] == 2]\ndf_l3_labels = df_labels[df_labels[\"level\"] == 3]\ndf_pct_labels = df_labels[df_labels[\"level\"] == 4]\n\n# Center KPI label inside the donut hole — a focal anchor for the whole chart\ndf_center = pd.DataFrame(\n    [\n        {\"x\": 0, \"y\": 0.045, \"label\": f\"${total}M\", \"level\": \"value\"},\n        {\"x\": 0, \"y\": -0.06, \"label\": \"Total Budget\", \"level\": \"caption\"},\n    ]\n)\n\n# Stroke accents: the focal department (largest share) gets a bolder outline on every\n# ring it touches, sharpening the visual hierarchy beyond wedge size + percentage alone.\nEDGE_COLORS = {\n    \"L1_focal\": INK,\n    \"L1_normal\": RING_SEP,\n    \"L2_focal\": INK,\n    \"L2_normal\": RING_SEP,\n    \"L3_focal\": INK,\n    \"L3_normal\": RING_SEP,\n}\nEDGE_SIZES = {\"L1_focal\": 1.3, \"L1_normal\": 0.75, \"L2_focal\": 0.9, \"L2_normal\": 0.4, \"L3_focal\": 0.7, \"L3_normal\": 0.3}\n\n# Plot\nplot = (\n    ggplot()\n    + geom_polygon(data=df_l1, mapping=aes(x=\"x\", y=\"y\", group=\"group\", fill=\"dept\", color=\"edge\", size=\"edge\"))\n    + geom_polygon(\n        data=df_l2, mapping=aes(x=\"x\", y=\"y\", group=\"group\", fill=\"dept\", color=\"edge\", size=\"edge\"), alpha=0.72\n    )\n    + geom_polygon(\n        data=df_l3, mapping=aes(x=\"x\", y=\"y\", group=\"group\", fill=\"dept\", color=\"edge\", size=\"edge\"), alpha=0.48\n    )\n    + geom_text(\n        data=df_l1_labels,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        color=INK,\n        size=5.5,\n        fontweight=\"bold\",\n        ha=\"center\",\n        va=\"bottom\",\n    )\n    + geom_text(\n        data=df_pct_labels, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK_SOFT, size=5, ha=\"center\", va=\"top\"\n    )\n    + geom_text(\n        data=df_l2_labels, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK, size=6.5, ha=\"center\", va=\"center\"\n    )\n    + geom_text(\n        data=df_l3_labels, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), color=INK, size=5.5, ha=\"center\", va=\"center\"\n    )\n    + geom_text(\n        data=df_center[df_center[\"level\"] == \"value\"],\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        color=INK,\n        size=10,\n        fontweight=\"bold\",\n        ha=\"center\",\n        va=\"center\",\n    )\n    + geom_text(\n        data=df_center[df_center[\"level\"] == \"caption\"],\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        color=INK_SOFT,\n        size=6.5,\n        ha=\"center\",\n        va=\"center\",\n    )\n    + scale_fill_manual(values=dept_colors, guide=None)\n    + scale_color_manual(values=EDGE_COLORS, guide=None)\n    + scale_size_manual(values=EDGE_SIZES, guide=None)\n    + coord_equal()\n    + scale_x_continuous(limits=(-1.30, 1.30), breaks=[], expand=(0, 0))\n    + scale_y_continuous(limits=(-1.30, 1.30), breaks=[], expand=(0, 0))\n    + labs(title=\"sunburst-basic · plotnine · anyplot.ai\")\n    + theme(\n        figure_size=(6, 6),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_border=element_blank(),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks_major_x=element_blank(),\n        axis_ticks_major_y=element_blank(),\n        axis_ticks_minor_x=element_blank(),\n        axis_ticks_minor_y=element_blank(),\n        legend_position=\"none\",\n        plot_title=element_text(color=INK, size=12, ha=\"center\"),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\", verbose=False)\n"}