{"spec_id":"sunburst-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nsunburst-basic: Basic Sunburst Chart\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-07-26\n\"\"\"\n\nimport math\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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\"\n# Level 2/3 segments stay light pastels in both themes (only chrome flips), so their\n# labels are fixed dark ink for contrast rather than the theme-adaptive INK token.\nLABEL_ON_PASTEL = \"#1A1A17\"\n\n# Data - Organizational budget by department, team, and project (3 levels)\ndata = [\n    # Engineering branch\n    {\"level_1\": \"Eng\", \"level_2\": \"Backend\", \"level_3\": \"API\", \"value\": 15},\n    {\"level_1\": \"Eng\", \"level_2\": \"Backend\", \"level_3\": \"Database\", \"value\": 10},\n    {\"level_1\": \"Eng\", \"level_2\": \"Frontend\", \"level_3\": \"Web App\", \"value\": 12},\n    {\"level_1\": \"Eng\", \"level_2\": \"Frontend\", \"level_3\": \"Mobile\", \"value\": 8},\n    # Sales branch\n    {\"level_1\": \"Sales\", \"level_2\": \"North\", \"level_3\": \"Enterprise\", \"value\": 18},\n    {\"level_1\": \"Sales\", \"level_2\": \"North\", \"level_3\": \"SMB\", \"value\": 7},\n    {\"level_1\": \"Sales\", \"level_2\": \"South\", \"level_3\": \"Retail\", \"value\": 9},\n    # Marketing branch\n    {\"level_1\": \"Mktg\", \"level_2\": \"Digital\", \"level_3\": \"SEO\", \"value\": 6},\n    {\"level_1\": \"Mktg\", \"level_2\": \"Digital\", \"level_3\": \"Ads\", \"value\": 8},\n    {\"level_1\": \"Mktg\", \"level_2\": \"Brand\", \"level_3\": \"Events\", \"value\": 7},\n]\n\ndf = pd.DataFrame(data)\ntotal_value = df[\"value\"].sum()\n\n\ndef create_wedge(inner_r, outer_r, start_angle, end_angle, n_points=30):\n    angles_outer = [start_angle + (end_angle - start_angle) * i / n_points for i in range(n_points + 1)]\n    angles_inner = angles_outer[::-1]\n    x_outer = [outer_r * math.cos(a) for a in angles_outer]\n    y_outer = [outer_r * math.sin(a) for a in angles_outer]\n    x_inner = [inner_r * math.cos(a) for a in angles_inner]\n    y_inner = [inner_r * math.sin(a) for a in angles_inner]\n    return x_outer + x_inner, y_outer + y_inner\n\n\n# Imprint palette branch colors for level 1; graduated lighter shades for levels 2-3\nbranch_colors = {\n    \"Eng\": \"#009E73\",  # Imprint palette position 1\n    \"Sales\": \"#C475FD\",  # Imprint palette position 2\n    \"Mktg\": \"#4467A3\",  # Imprint palette position 3\n}\nlevel2_colors = {\n    \"Backend\": \"#66C5AB\",  # Eng, ~40% lighter\n    \"Frontend\": \"#80CFB9\",  # Eng, ~50% lighter\n    \"North\": \"#E69E66\",  # Sales, ~40% lighter\n    \"South\": \"#EEAF80\",  # Sales, ~50% lighter\n    \"Digital\": \"#66AAD1\",  # Mktg, ~40% lighter\n    \"Brand\": \"#80B9D9\",  # Mktg, ~50% lighter\n}\nlevel3_colors = {\n    \"API\": \"#A6DDCE\",  # Eng, ~65% lighter\n    \"Database\": \"#B3E2D5\",  # Eng, ~70% lighter\n    \"Web App\": \"#BFE7DC\",  # Eng, ~75% lighter\n    \"Mobile\": \"#CCECE3\",  # Eng, ~80% lighter\n    \"Enterprise\": \"#F0C7A6\",  # Sales, ~65% lighter\n    \"SMB\": \"#F2CFB3\",  # Sales, ~70% lighter\n    \"Retail\": \"#F5D7BF\",  # Sales, ~75% lighter\n    \"SEO\": \"#A6CEE4\",  # Mktg, ~65% lighter\n    \"Ads\": \"#B3D5E8\",  # Mktg, ~70% lighter\n    \"Events\": \"#BFDCEC\",  # Mktg, ~75% lighter\n}\nall_colors = {**branch_colors, **level2_colors, **level3_colors}\n\n# Ring radii\nr_inner_1, r_outer_1 = 0, 28  # Level 1 (innermost, filled center)\nr_inner_2, r_outer_2 = 31, 52  # Level 2\nr_inner_3, r_outer_3 = 55, 80  # Level 3 (outermost)\n\n# Calculate proportions\nlevel1_agg = df.groupby(\"level_1\")[\"value\"].sum().reset_index()\nlevel1_agg[\"pct\"] = level1_agg[\"value\"] / total_value\nlevel1_agg = level1_agg.sort_values(\"level_1\").reset_index(drop=True)\n\nlevel2_agg = df.groupby([\"level_1\", \"level_2\"])[\"value\"].sum().reset_index()\nlevel2_agg[\"pct\"] = level2_agg[\"value\"] / total_value\n\ndf[\"pct\"] = df[\"value\"] / total_value\n\n# Build polygon and label data\npolygon_rows = []\nlabel_rows = []\nsegment_id = 0\n\nstart_angle = math.pi / 2  # Start at top, go clockwise\nlevel1_angles = {}\n\nfor _, row in level1_agg.iterrows():\n    end_angle = start_angle - row[\"pct\"] * 2 * math.pi\n    level1_angles[row[\"level_1\"]] = {\"start\": start_angle, \"end\": end_angle}\n\n    x_pts, y_pts = create_wedge(r_inner_1, r_outer_1, end_angle, start_angle)\n    for x, y in zip(x_pts, y_pts, strict=True):\n        polygon_rows.append(\n            {\n                \"x\": x,\n                \"y\": y,\n                \"segment_id\": segment_id,\n                \"color\": row[\"level_1\"],\n                \"value\": row[\"value\"],\n                \"pct\": row[\"pct\"],\n            }\n        )\n\n    mid_angle = (start_angle + end_angle) / 2\n    label_r = r_outer_1 * 0.55\n    label_rows.append(\n        {\"x\": label_r * math.cos(mid_angle), \"y\": label_r * math.sin(mid_angle), \"label\": row[\"level_1\"], \"level\": 1}\n    )\n\n    segment_id += 1\n    start_angle = end_angle\n\nlevel2_angles = {}\n\nfor level1_name in level1_agg[\"level_1\"]:\n    l1_angles = level1_angles[level1_name]\n    l2_data = level2_agg[level2_agg[\"level_1\"] == level1_name].sort_values(\"level_2\")\n    cur_angle = l1_angles[\"start\"]\n\n    for _, row in l2_data.iterrows():\n        end_angle = cur_angle - row[\"pct\"] * 2 * math.pi\n        level2_angles[(row[\"level_1\"], row[\"level_2\"])] = {\"start\": cur_angle, \"end\": end_angle}\n\n        x_pts, y_pts = create_wedge(r_inner_2, r_outer_2, end_angle, cur_angle)\n        for x, y in zip(x_pts, y_pts, strict=True):\n            polygon_rows.append(\n                {\n                    \"x\": x,\n                    \"y\": y,\n                    \"segment_id\": segment_id,\n                    \"color\": row[\"level_2\"],\n                    \"value\": row[\"value\"],\n                    \"pct\": row[\"pct\"],\n                }\n            )\n\n        mid_angle = (cur_angle + end_angle) / 2\n        label_r = (r_inner_2 + r_outer_2) / 2\n        label_rows.append(\n            {\n                \"x\": label_r * math.cos(mid_angle),\n                \"y\": label_r * math.sin(mid_angle),\n                \"label\": row[\"level_2\"],\n                \"level\": 2,\n            }\n        )\n\n        segment_id += 1\n        cur_angle = end_angle\n\nfor level1_name in level1_agg[\"level_1\"]:\n    l2_data = level2_agg[level2_agg[\"level_1\"] == level1_name].sort_values(\"level_2\")\n\n    for _, l2_row in l2_data.iterrows():\n        l2_key = (l2_row[\"level_1\"], l2_row[\"level_2\"])\n        l2_angles_range = level2_angles[l2_key]\n        l3_data = df[(df[\"level_1\"] == l2_row[\"level_1\"]) & (df[\"level_2\"] == l2_row[\"level_2\"])].sort_values(\"level_3\")\n        cur_angle = l2_angles_range[\"start\"]\n\n        for _, row in l3_data.iterrows():\n            end_angle = cur_angle - row[\"pct\"] * 2 * math.pi\n\n            x_pts, y_pts = create_wedge(r_inner_3, r_outer_3, end_angle, cur_angle)\n            for x, y in zip(x_pts, y_pts, strict=True):\n                polygon_rows.append(\n                    {\n                        \"x\": x,\n                        \"y\": y,\n                        \"segment_id\": segment_id,\n                        \"color\": row[\"level_3\"],\n                        \"value\": row[\"value\"],\n                        \"pct\": row[\"pct\"],\n                    }\n                )\n\n            mid_angle = (cur_angle + end_angle) / 2\n            label_r = (r_inner_3 + r_outer_3) / 2\n            label_rows.append(\n                {\n                    \"x\": label_r * math.cos(mid_angle),\n                    \"y\": label_r * math.sin(mid_angle),\n                    \"label\": row[\"level_3\"],\n                    \"level\": 3,\n                }\n            )\n\n            segment_id += 1\n            cur_angle = end_angle\n\npolygon_df = pd.DataFrame(polygon_rows)\nlabel_df = pd.DataFrame(label_rows)\n\n# Plot\nplot = (\n    ggplot(polygon_df)\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", fill=\"color\", group=\"segment_id\"),\n        color=PAGE_BG,\n        size=0.75,\n        alpha=0.9,\n        tooltips=layer_tooltips()\n        .line(\"@color\")\n        .line(\"Budget|@value K\")\n        .format(\"@pct\", \".1%\")\n        .line(\"Share of total|@pct\"),\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=label_df[label_df[\"level\"] == 1],\n        size=6,\n        color=\"#FFFFFF\",\n        fontface=\"bold\",\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df[label_df[\"level\"] == 2], size=5.5, color=LABEL_ON_PASTEL\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df[label_df[\"level\"] == 3], size=4.5, color=LABEL_ON_PASTEL\n    )\n    + scale_fill_manual(values=all_colors)\n    + coord_fixed(ratio=1)\n    + scale_x_continuous(limits=(-100, 100))\n    + scale_y_continuous(limits=(-100, 100))\n    + labs(\n        title=\"sunburst-basic · letsplot · anyplot.ai\",\n        subtitle=\"Budget allocation by department, team, and project ($K)\",\n    )\n    + ggsize(600, 600)\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=13, hjust=0.5, color=INK),\n        plot_subtitle=element_text(size=9, hjust=0.5, color=INK),\n        legend_position=\"none\",\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid=element_blank(),\n    )\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}