{"spec_id":"sunburst-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nsunburst-basic: Basic Sunburst Chart\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-07-26\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path to avoid importing local altair.py\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != _script_dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme-adaptive chrome tokens (Imprint)\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# Data - Company budget breakdown by department, team, and project\n# Hierarchical structure: Department > Team > Project\ndata = [\n    # Engineering Department\n    {\"level_1\": \"Engineering\", \"level_2\": \"Backend\", \"level_3\": \"API\", \"value\": 180},\n    {\"level_1\": \"Engineering\", \"level_2\": \"Backend\", \"level_3\": \"Database\", \"value\": 120},\n    {\"level_1\": \"Engineering\", \"level_2\": \"Frontend\", \"level_3\": \"Web App\", \"value\": 150},\n    {\"level_1\": \"Engineering\", \"level_2\": \"Frontend\", \"level_3\": \"Mobile\", \"value\": 100},\n    {\"level_1\": \"Engineering\", \"level_2\": \"DevOps\", \"level_3\": \"Cloud\", \"value\": 90},\n    {\"level_1\": \"Engineering\", \"level_2\": \"DevOps\", \"level_3\": \"CI/CD\", \"value\": 60},\n    # Marketing Department\n    {\"level_1\": \"Marketing\", \"level_2\": \"Digital\", \"level_3\": \"SEO\", \"value\": 80},\n    {\"level_1\": \"Marketing\", \"level_2\": \"Digital\", \"level_3\": \"Social\", \"value\": 70},\n    {\"level_1\": \"Marketing\", \"level_2\": \"Content\", \"level_3\": \"Blog\", \"value\": 50},\n    {\"level_1\": \"Marketing\", \"level_2\": \"Content\", \"level_3\": \"Video\", \"value\": 60},\n    # Operations Department\n    {\"level_1\": \"Operations\", \"level_2\": \"Support\", \"level_3\": \"Tier 1\", \"value\": 70},\n    {\"level_1\": \"Operations\", \"level_2\": \"Support\", \"level_3\": \"Tier 2\", \"value\": 50},\n    {\"level_1\": \"Operations\", \"level_2\": \"HR\", \"level_3\": \"Recruiting\", \"value\": 60},\n    {\"level_1\": \"Operations\", \"level_2\": \"HR\", \"level_3\": \"Training\", \"value\": 40},\n    # Sales Department\n    {\"level_1\": \"Sales\", \"level_2\": \"Enterprise\", \"level_3\": \"APAC\", \"value\": 100},\n    {\"level_1\": \"Sales\", \"level_2\": \"Enterprise\", \"level_3\": \"EMEA\", \"value\": 85},\n    {\"level_1\": \"Sales\", \"level_2\": \"SMB\", \"level_3\": \"Direct\", \"value\": 65},\n    {\"level_1\": \"Sales\", \"level_2\": \"SMB\", \"level_3\": \"Partners\", \"value\": 45},\n]\n\ndf = pd.DataFrame(data)\n\n# Imprint palette - first series is always #009E73 (brand green)\nlevel1_colors = {\n    \"Engineering\": \"#009E73\",  # brand green\n    \"Marketing\": \"#C475FD\",  # lavender\n    \"Operations\": \"#4467A3\",  # blue\n    \"Sales\": \"#BD8233\",  # ochre\n}\n\n# Helper to lighten colors for child levels\nhex_to_lighter = {}\nfor name, hex_color in level1_colors.items():\n    r, g, b = int(hex_color[1:3], 16), int(hex_color[3:5], 16), int(hex_color[5:7], 16)\n    # Level 2: 25% lighter\n    r2, g2, b2 = int(r + (255 - r) * 0.25), int(g + (255 - g) * 0.25), int(b + (255 - b) * 0.25)\n    # Level 3: 50% lighter\n    r3, g3, b3 = int(r + (255 - r) * 0.5), int(g + (255 - g) * 0.5), int(b + (255 - b) * 0.5)\n    hex_to_lighter[name] = {\"l1\": hex_color, \"l2\": f\"#{r2:02x}{g2:02x}{b2:02x}\", \"l3\": f\"#{r3:02x}{g3:02x}{b3:02x}\"}\n\n\ndef text_color_for_bg(hex_color):\n    \"\"\"Pick black/white ink for readable contrast against a lightened wedge color.\"\"\"\n    r, g, b = int(hex_color[1:3], 16) / 255, int(hex_color[3:5], 16) / 255, int(hex_color[5:7], 16) / 255\n    luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b\n    return \"#1A1A17\" if luminance > 0.55 else \"#ffffff\"\n\n\n# Level 1: Department totals\nlevel1_df = df.groupby(\"level_1\")[\"value\"].sum().reset_index()\nlevel1_df.columns = [\"name\", \"value\"]\nlevel1_df = level1_df.sort_values(\"value\", ascending=False).reset_index(drop=True)\ntotal_value = level1_df[\"value\"].sum()\n\n# Calculate level 1 angles (full circle distribution)\nlevel1_df[\"theta\"] = 0.0\nlevel1_df[\"theta2\"] = 0.0\ncurrent_angle = 0\nfor idx in level1_df.index:\n    fraction = level1_df.loc[idx, \"value\"] / total_value\n    level1_df.loc[idx, \"theta\"] = current_angle\n    level1_df.loc[idx, \"theta2\"] = current_angle + fraction * 2 * np.pi\n    current_angle = level1_df.loc[idx, \"theta2\"]\n\nlevel1_df[\"color\"] = level1_df[\"name\"].map(level1_colors)\n\n# Level 2: Team totals\nlevel2_df = df.groupby([\"level_1\", \"level_2\"])[\"value\"].sum().reset_index()\nlevel2_df.columns = [\"parent\", \"name\", \"value\"]\nlevel2_df = level2_df.sort_values([\"parent\", \"value\"], ascending=[True, False]).reset_index(drop=True)\n\n# Calculate level 2 angles (within parent's arc)\nlevel2_df[\"theta\"] = 0.0\nlevel2_df[\"theta2\"] = 0.0\nparent_cumulative = {row[\"name\"]: row[\"theta\"] for _, row in level1_df.iterrows()}\n\nfor idx in level2_df.index:\n    parent = level2_df.loc[idx, \"parent\"]\n    parent_row = level1_df[level1_df[\"name\"] == parent].iloc[0]\n    parent_start, parent_end = parent_row[\"theta\"], parent_row[\"theta2\"]\n    parent_total = parent_row[\"value\"]\n\n    fraction = level2_df.loc[idx, \"value\"] / parent_total\n    segment_angle = (parent_end - parent_start) * fraction\n\n    level2_df.loc[idx, \"theta\"] = parent_cumulative[parent]\n    level2_df.loc[idx, \"theta2\"] = parent_cumulative[parent] + segment_angle\n    parent_cumulative[parent] = level2_df.loc[idx, \"theta2\"]\n\n# Assign lighter colors for level 2\nlevel2_df[\"color\"] = level2_df[\"parent\"].apply(lambda p: hex_to_lighter[p][\"l2\"])\nlevel2_df[\"label_color\"] = level2_df[\"color\"].apply(text_color_for_bg)\n\n# Level 3: Project values\nlevel3_df = df.copy()\nlevel3_df[\"parent_l1\"] = level3_df[\"level_1\"]\nlevel3_df[\"parent_l2\"] = level3_df[\"level_2\"]\nlevel3_df[\"name\"] = level3_df[\"level_3\"]\nlevel3_df = level3_df.sort_values([\"level_1\", \"level_2\", \"value\"], ascending=[True, True, False]).reset_index(drop=True)\n\n# Calculate level 3 angles (within parent's arc in level 2)\nlevel3_df[\"theta\"] = 0.0\nlevel3_df[\"theta2\"] = 0.0\nl2_cumulative = {}\nfor _, row in level2_df.iterrows():\n    key = f\"{row['parent']}|{row['name']}\"\n    l2_cumulative[key] = {\"start\": row[\"theta\"], \"current\": row[\"theta\"], \"end\": row[\"theta2\"], \"total\": row[\"value\"]}\n\nfor idx in level3_df.index:\n    key = f\"{level3_df.loc[idx, 'level_1']}|{level3_df.loc[idx, 'level_2']}\"\n    l2_data = l2_cumulative[key]\n    fraction = level3_df.loc[idx, \"value\"] / l2_data[\"total\"]\n    segment_angle = (l2_data[\"end\"] - l2_data[\"start\"]) * fraction\n\n    level3_df.loc[idx, \"theta\"] = l2_data[\"current\"]\n    level3_df.loc[idx, \"theta2\"] = l2_data[\"current\"] + segment_angle\n    l2_data[\"current\"] = level3_df.loc[idx, \"theta2\"]\n\n# Assign lightest colors for level 3\nlevel3_df[\"color\"] = level3_df[\"level_1\"].apply(lambda p: hex_to_lighter[p][\"l3\"])\n\n# Ring radii (absolute pixels, centered in the 500x460 view)\ninner_r1, outer_r1 = 60, 98  # Level 1 (innermost)\ninner_r2, outer_r2 = 104, 142  # Level 2 (middle)\ninner_r3, outer_r3 = 148, 186  # Level 3 (outermost)\n\n# Overlay coordinate system (label/legend layer only, 1:1 with view pixels so\n# radii computed above line up exactly with the arc marks' own pixel radii).\n# range is explicit (not the Vega-Lite default flipped-Y range) so that\n# label_y = -radius*cos(angle) (standard y-down screen convention) lines up\n# with the arc marks' own top-origin, clockwise angle convention.\nX_DOMAIN = [-250, 250]  # matches half of properties(width=500)\nY_DOMAIN = [-230, 230]  # matches half of properties(height=460)\nX_SCALE = alt.Scale(domain=X_DOMAIN, range=[0, 500])\nY_SCALE = alt.Scale(domain=Y_DOMAIN, range=[0, 460])\n\n# Level 1 - innermost ring (Departments)\nchart_l1 = (\n    alt.Chart(level1_df)\n    .mark_arc(innerRadius=inner_r1, outerRadius=outer_r1, stroke=PAGE_BG, strokeWidth=2)\n    .encode(\n        theta=alt.Theta(\"theta:Q\", scale=alt.Scale(domain=[0, 2 * np.pi])),\n        theta2=\"theta2:Q\",\n        color=alt.Color(\"color:N\", scale=None, legend=None),\n        tooltip=[alt.Tooltip(\"name:N\", title=\"Department\"), alt.Tooltip(\"value:Q\", title=\"Budget ($K)\")],\n    )\n)\n\n# Level 2 - middle ring (Teams)\nchart_l2 = (\n    alt.Chart(level2_df)\n    .mark_arc(innerRadius=inner_r2, outerRadius=outer_r2, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(\n        theta=alt.Theta(\"theta:Q\", scale=alt.Scale(domain=[0, 2 * np.pi])),\n        theta2=\"theta2:Q\",\n        color=alt.Color(\"color:N\", scale=None, legend=None),\n        tooltip=[\n            alt.Tooltip(\"parent:N\", title=\"Department\"),\n            alt.Tooltip(\"name:N\", title=\"Team\"),\n            alt.Tooltip(\"value:Q\", title=\"Budget ($K)\"),\n        ],\n    )\n)\n\n# Level 3 - outer ring (Projects)\nchart_l3 = (\n    alt.Chart(level3_df)\n    .mark_arc(innerRadius=inner_r3, outerRadius=outer_r3, stroke=PAGE_BG, strokeWidth=1)\n    .encode(\n        theta=alt.Theta(\"theta:Q\", scale=alt.Scale(domain=[0, 2 * np.pi])),\n        theta2=\"theta2:Q\",\n        color=alt.Color(\"color:N\", scale=None, legend=None),\n        tooltip=[\n            alt.Tooltip(\"level_1:N\", title=\"Department\"),\n            alt.Tooltip(\"level_2:N\", title=\"Team\"),\n            alt.Tooltip(\"name:N\", title=\"Project\"),\n            alt.Tooltip(\"value:Q\", title=\"Budget ($K)\"),\n        ],\n    )\n)\n\n# Add labels for level 1 segments (department names at arc center)\nlevel1_df[\"label_angle\"] = (level1_df[\"theta\"] + level1_df[\"theta2\"]) / 2\nlevel1_df[\"label_radius\"] = (inner_r1 + outer_r1) / 2\nlevel1_df[\"label_x\"] = level1_df[\"label_radius\"] * np.sin(level1_df[\"label_angle\"])\nlevel1_df[\"label_y\"] = -level1_df[\"label_radius\"] * np.cos(level1_df[\"label_angle\"])\n\ntext_l1 = (\n    alt.Chart(level1_df)\n    .mark_text(fontSize=12, fontWeight=\"bold\", color=\"#ffffff\")\n    .encode(\n        x=alt.X(\"label_x:Q\", scale=X_SCALE, axis=None), y=alt.Y(\"label_y:Q\", scale=Y_SCALE, axis=None), text=\"name:N\"\n    )\n)\n\n# Add labels for level 2 segments (team names at arc center) — every team wedge\n# spans >=24 degrees here, wide enough for short text, so all are labeled.\nlevel2_df[\"label_angle\"] = (level2_df[\"theta\"] + level2_df[\"theta2\"]) / 2\nlevel2_df[\"label_radius\"] = (inner_r2 + outer_r2) / 2\nlevel2_df[\"label_x\"] = level2_df[\"label_radius\"] * np.sin(level2_df[\"label_angle\"])\nlevel2_df[\"label_y\"] = -level2_df[\"label_radius\"] * np.cos(level2_df[\"label_angle\"])\n\ntext_l2 = (\n    alt.Chart(level2_df)\n    .mark_text(fontSize=8, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"label_x:Q\", scale=X_SCALE, axis=None),\n        y=alt.Y(\"label_y:Q\", scale=Y_SCALE, axis=None),\n        text=\"name:N\",\n        color=alt.Color(\"label_color:N\", scale=None, legend=None),\n    )\n)\n\n# Focal-point emphasis (DE-03): outline + callout label on the single largest\n# leaf segment so the chart has one clear \"headline\" data point beyond the\n# radial nesting itself.\ntop_leaf = level3_df.loc[[level3_df[\"value\"].idxmax()]].copy()\ntop_leaf[\"label_angle\"] = (top_leaf[\"theta\"] + top_leaf[\"theta2\"]) / 2\ntop_leaf[\"label_radius\"] = outer_r3 + 38\ntop_leaf[\"label_x\"] = top_leaf[\"label_radius\"] * np.sin(top_leaf[\"label_angle\"])\ntop_leaf[\"label_y\"] = -top_leaf[\"label_radius\"] * np.cos(top_leaf[\"label_angle\"])\ntop_leaf[\"label_text\"] = top_leaf[\"parent_l1\"] + \" · \" + top_leaf[\"name\"] + \" ($\" + top_leaf[\"value\"].astype(str) + \"K)\"\n\nchart_highlight = (\n    alt.Chart(top_leaf)\n    .mark_arc(innerRadius=inner_r3 - 2, outerRadius=outer_r3 + 6, stroke=INK, strokeWidth=2, fillOpacity=0)\n    .encode(theta=alt.Theta(\"theta:Q\", scale=alt.Scale(domain=[0, 2 * np.pi])), theta2=\"theta2:Q\")\n)\n\n# Anchor the callout away from the ring instead of centering it over the\n# wedge's radial edge (which sits near the top of the chart, close to x=0).\n_label_align = \"left\" if float(top_leaf[\"label_x\"].iloc[0]) >= 0 else \"right\"\n_label_dx = 6 if _label_align == \"left\" else -6\n\ntext_highlight = (\n    alt.Chart(top_leaf)\n    .mark_text(fontSize=11, fontWeight=\"bold\", color=INK, align=_label_align, dx=_label_dx)\n    .encode(\n        x=alt.X(\"label_x:Q\", scale=X_SCALE, axis=None),\n        y=alt.Y(\"label_y:Q\", scale=Y_SCALE, axis=None),\n        text=\"label_text:N\",\n    )\n)\n\n# Add legend as a horizontal row below the rings (kept left/right-balanced\n# around x=0 so the donut itself stays centered on the padded canvas)\nLEGEND_SLOT_W = 120\nlegend_items = []\nfor i, (_, row) in enumerate(level1_df.iterrows()):\n    slot_x = -LEGEND_SLOT_W * len(level1_df) / 2 + i * LEGEND_SLOT_W\n    legend_items.append({\"dept\": row[\"name\"], \"color\": level1_colors[row[\"name\"]], \"x\": slot_x, \"y\": 205})\nlegend_df = pd.DataFrame(legend_items)\n\nlegend_rects = (\n    alt.Chart(legend_df)\n    .mark_rect(width=14, height=14)\n    .encode(\n        x=alt.X(\"x:Q\", scale=X_SCALE, axis=None),\n        y=alt.Y(\"y:Q\", scale=Y_SCALE, axis=None),\n        color=alt.Color(\"color:N\", scale=None),\n    )\n)\n\nlegend_text = (\n    alt.Chart(legend_df)\n    .mark_text(fontSize=10, align=\"left\", dx=8, color=INK_SOFT)\n    .encode(x=alt.X(\"x:Q\", scale=X_SCALE, axis=None), y=alt.Y(\"y:Q\", scale=Y_SCALE, axis=None), text=\"dept:N\")\n)\n\n# Combine all layers\nchart = (\n    alt.layer(\n        chart_l1, chart_l2, chart_l3, chart_highlight, text_l1, text_l2, text_highlight, legend_rects, legend_text\n    )\n    .properties(\n        width=500,\n        height=460,\n        background=PAGE_BG,\n        title=alt.Title(text=\"sunburst-basic · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\", color=INK),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n)\n\n# Save outputs — canonical square target 2400x2400 (see prompts/library/altair.md \"Canvas\")\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 2400, 2400\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}