{"spec_id":"sunburst-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nsunburst-basic: Basic Sunburst Chart\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-07-26\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom math import cos, pi, sin\nfrom pathlib import Path\n\n\n# Remove this script's directory from sys.path so \"bokeh\" resolves to the\n# installed package, not this file itself (which is also named bokeh.py).\nsys.path = [p for p in sys.path if os.path.abspath(p) != os.path.dirname(os.path.abspath(__file__))]\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Output directory: same folder as this script, regardless of cwd\nOUT_DIR = Path(__file__).parent\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data - Organizational budget breakdown (Department -> Team -> Project)\nhierarchy = [\n    # Engineering\n    {\"level_1\": \"Engineering\", \"level_2\": \"Backend\", \"level_3\": \"API Platform\", \"value\": 120},\n    {\"level_1\": \"Engineering\", \"level_2\": \"Backend\", \"level_3\": \"Database\", \"value\": 80},\n    {\"level_1\": \"Engineering\", \"level_2\": \"Frontend\", \"level_3\": \"Web App\", \"value\": 100},\n    {\"level_1\": \"Engineering\", \"level_2\": \"Frontend\", \"level_3\": \"Mobile\", \"value\": 60},\n    {\"level_1\": \"Engineering\", \"level_2\": \"DevOps\", \"level_3\": \"Infrastructure\", \"value\": 50},\n    # Marketing\n    {\"level_1\": \"Marketing\", \"level_2\": \"Digital\", \"level_3\": \"Social Media\", \"value\": 45},\n    {\"level_1\": \"Marketing\", \"level_2\": \"Digital\", \"level_3\": \"SEO\", \"value\": 35},\n    {\"level_1\": \"Marketing\", \"level_2\": \"Content\", \"level_3\": \"Blog\", \"value\": 30},\n    {\"level_1\": \"Marketing\", \"level_2\": \"Content\", \"level_3\": \"Video\", \"value\": 40},\n    # Sales\n    {\"level_1\": \"Sales\", \"level_2\": \"Enterprise\", \"level_3\": \"EMEA\", \"value\": 70},\n    {\"level_1\": \"Sales\", \"level_2\": \"Enterprise\", \"level_3\": \"APAC\", \"value\": 55},\n    {\"level_1\": \"Sales\", \"level_2\": \"SMB\", \"level_3\": \"Direct Sales\", \"value\": 45},\n    # Operations\n    {\"level_1\": \"Operations\", \"level_2\": \"Support\", \"level_3\": \"Tier 1\", \"value\": 40},\n    {\"level_1\": \"Operations\", \"level_2\": \"Support\", \"level_3\": \"Tier 2\", \"value\": 25},\n    {\"level_1\": \"Operations\", \"level_2\": \"HR\", \"level_3\": \"Recruiting\", \"value\": 30},\n]\n\n# Imprint categorical palette (canonical order) — departments walk positions\n# 1->N in hierarchy order (Engineering, Marketing, Sales, Operations).\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\ndept_order = [\"Engineering\", \"Marketing\", \"Sales\", \"Operations\"]\n\n\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    r, g, b = (round(a + (b - a) * t) for a, b in ((r0, r1), (g0, g1), (b0, b1)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\ndef _text_on(bg_hex):\n    \"\"\"Fixed (non theme-adaptive) black/white text pick via YIQ luminance —\n    data-layer labels sit on data colors that stay constant across themes,\n    so their text color must stay constant too.\"\"\"\n    r, g, b = (int(bg_hex[i : i + 2], 16) for i in (1, 3, 5))\n    yiq = (r * 299 + g * 587 + b * 114) / 1000\n    return \"#1A1A17\" if yiq >= 128 else \"#FFFFFF\"\n\n\n# Department color families: Imprint base tinted toward white for child rings\ndept_colors = {\n    dept: {\"base\": base, \"mid\": _lerp_hex(base, \"#FFFFFF\", 0.35), \"light\": _lerp_hex(base, \"#FFFFFF\", 0.68)}\n    for dept, base in zip(dept_order, IMPRINT_PALETTE, strict=True)\n}\n\n# Clearer abbreviations than 3-char truncation\ndept_abbrev = {\"Engineering\": \"Eng.\", \"Marketing\": \"Mktg\", \"Sales\": \"Sales\", \"Operations\": \"Ops\"}\n\n# Calculate totals\nlevel_1_totals = {}\nlevel_2_totals = {}\nfor item in hierarchy:\n    l1, l2, val = item[\"level_1\"], item[\"level_2\"], item[\"value\"]\n    level_1_totals[l1] = level_1_totals.get(l1, 0) + val\n    level_2_totals[(l1, l2)] = level_2_totals.get((l1, l2), 0) + val\n\ntotal = sum(item[\"value\"] for item in hierarchy)\n\n# Ring radii — rings touch directly (no radial gap); the white wedge borders\n# alone provide separation. A gap here would show as PAGE_BG, which reads as\n# a distracting near-black ring on the dark theme.\nr1_inner, r1_outer = 0.0, 0.30\nr2_inner, r2_outer = 0.30, 0.60\nr3_inner, r3_outer = 0.60, 0.92\n\n# Build vectorized segment data for each ring (enables HoverTool)\n_keys = [\"x\", \"y\", \"inner_radius\", \"outer_radius\", \"start_angle\", \"end_angle\", \"fill_color\", \"name\", \"budget\"]\nl1_data = {k: [] for k in _keys}\nl2_data = {k: [] for k in _keys}\nl3_data = {k: [] for k in _keys}\n\nl1_angles = {}\nl2_angles = {}\nl3_labels = []  # (mid_angle, text, fill_color, show) — computed once, reused for labels\n\n# Level 1 segments\nstart_angle = pi / 2\nfor l1 in dept_order:\n    angle_span = (level_1_totals[l1] / total) * 2 * pi\n    end_angle = start_angle - angle_span\n    for k, v in zip(\n        _keys,\n        [0, 0, r1_inner, r1_outer, end_angle, start_angle, dept_colors[l1][\"base\"], l1, f\"${level_1_totals[l1]}K\"],\n        strict=True,\n    ):\n        l1_data[k].append(v)\n    l1_angles[l1] = {\"start\": start_angle, \"end\": end_angle}\n    start_angle = end_angle\n\n# Level 2 segments\nfor l1 in dept_order:\n    l1_start = l1_angles[l1][\"start\"]\n    l1_span = l1_start - l1_angles[l1][\"end\"]\n    l2_items = [(k, v) for k, v in level_2_totals.items() if k[0] == l1]\n    l1_total = level_1_totals[l1]\n    current_start = l1_start\n    for (_, l2_name), l2_val in l2_items:\n        angle_span = (l2_val / l1_total) * l1_span\n        end_angle = current_start - angle_span\n        for k, v in zip(\n            _keys,\n            [0, 0, r2_inner, r2_outer, end_angle, current_start, dept_colors[l1][\"mid\"], l2_name, f\"${l2_val}K\"],\n            strict=True,\n        ):\n            l2_data[k].append(v)\n        l2_angles[(l1, l2_name)] = {\"start\": current_start, \"end\": end_angle}\n        current_start = end_angle\n\n# Level 3 segments — mid_angle is computed once here and reused by the label\n# loop below instead of being recomputed from the hierarchy a second time.\nfor item in hierarchy:\n    l1, l2, l3, val = item[\"level_1\"], item[\"level_2\"], item[\"level_3\"], item[\"value\"]\n    l2_start = l2_angles[(l1, l2)][\"start\"]\n    l2_span = l2_start - l2_angles[(l1, l2)][\"end\"]\n    l2_total = level_2_totals[(l1, l2)]\n    l3_items = [h for h in hierarchy if h[\"level_1\"] == l1 and h[\"level_2\"] == l2]\n    l3_idx = next(i for i, h in enumerate(l3_items) if h[\"level_3\"] == l3)\n    cumulative = sum(l3_items[i][\"value\"] for i in range(l3_idx))\n    seg_start = l2_start - (cumulative / l2_total) * l2_span\n    seg_end = seg_start - (val / l2_total) * l2_span\n    fill = dept_colors[l1][\"light\"]\n    for k, v in zip(_keys, [0, 0, r3_inner, r3_outer, seg_end, seg_start, fill, l3, f\"${val}K\"], strict=True):\n        l3_data[k].append(v)\n    l3_labels.append(((seg_start + seg_end) / 2, l3, fill, val / total > 0.04))\n\n# Plot — square canvas: a sunburst has no preferred horizontal axis.\nW = H = 2400\np = figure(\n    width=W,\n    height=H,\n    title=\"sunburst-basic · bokeh · anyplot.ai\",\n    toolbar_location=None,\n    tools=\"\",\n    x_range=(-1.45, 1.45),\n    y_range=(-1.85, 1.05),\n)\n\n# Draw rings via ColumnDataSource (required for HoverTool)\nl1_r = p.annular_wedge(\n    x=\"x\",\n    y=\"y\",\n    inner_radius=\"inner_radius\",\n    outer_radius=\"outer_radius\",\n    start_angle=\"start_angle\",\n    end_angle=\"end_angle\",\n    fill_color=\"fill_color\",\n    line_color=\"white\",\n    line_width=3,\n    source=ColumnDataSource(l1_data),\n)\nl2_r = p.annular_wedge(\n    x=\"x\",\n    y=\"y\",\n    inner_radius=\"inner_radius\",\n    outer_radius=\"outer_radius\",\n    start_angle=\"start_angle\",\n    end_angle=\"end_angle\",\n    fill_color=\"fill_color\",\n    line_color=\"white\",\n    line_width=2,\n    source=ColumnDataSource(l2_data),\n)\nl3_r = p.annular_wedge(\n    x=\"x\",\n    y=\"y\",\n    inner_radius=\"inner_radius\",\n    outer_radius=\"outer_radius\",\n    start_angle=\"start_angle\",\n    end_angle=\"end_angle\",\n    fill_color=\"fill_color\",\n    line_color=\"white\",\n    line_width=1,\n    source=ColumnDataSource(l3_data),\n)\n\n# Hover tooltips for each ring level\np.add_tools(HoverTool(renderers=[l1_r], tooltips=[(\"Department\", \"@name\"), (\"Budget\", \"@budget\")]))\np.add_tools(HoverTool(renderers=[l2_r], tooltips=[(\"Team\", \"@name\"), (\"Budget\", \"@budget\")]))\np.add_tools(HoverTool(renderers=[l3_r], tooltips=[(\"Project\", \"@name\"), (\"Budget\", \"@budget\")]))\n\n# Level 1 labels (clearer abbreviations) — fixed text color per segment,\n# never theme-adaptive INK, since the segment fill is constant across themes.\nfor l1 in dept_order:\n    angles = l1_angles[l1]\n    mid_angle = (angles[\"start\"] + angles[\"end\"]) / 2\n    label_r = (r1_inner + r1_outer) / 2 + 0.02\n    p.add_layout(\n        Label(\n            x=label_r * cos(mid_angle),\n            y=label_r * sin(mid_angle),\n            text=dept_abbrev[l1],\n            text_font_size=\"30pt\",\n            text_color=_text_on(dept_colors[l1][\"base\"]),\n            text_font_style=\"bold\",\n            text_align=\"center\",\n            text_baseline=\"middle\",\n        )\n    )\n\n# Level 2 labels (full names, no truncation)\nfor l1 in dept_order:\n    l1_start = l1_angles[l1][\"start\"]\n    l1_span = l1_start - l1_angles[l1][\"end\"]\n    l2_items = [(k, v) for k, v in level_2_totals.items() if k[0] == l1]\n    l1_total = level_1_totals[l1]\n    current_start = l1_start\n    for (_, l2_name), l2_val in l2_items:\n        angle_span = (l2_val / l1_total) * l1_span\n        end_angle = current_start - angle_span\n        if l2_val / total > 0.06:\n            mid_angle = (current_start + end_angle) / 2\n            label_r = (r2_inner + r2_outer) / 2\n            p.add_layout(\n                Label(\n                    x=label_r * cos(mid_angle),\n                    y=label_r * sin(mid_angle),\n                    text=l2_name,\n                    text_font_size=\"26pt\",\n                    text_color=_text_on(dept_colors[l1][\"mid\"]),\n                    text_align=\"center\",\n                    text_baseline=\"middle\",\n                )\n            )\n        current_start = end_angle\n\n# Level 3 labels — reuses mid_angle/fill computed during the segment build above.\nfor mid_angle, text, fill, show in l3_labels:\n    if show:\n        label_r = (r3_inner + r3_outer) / 2\n        p.add_layout(\n            Label(\n                x=label_r * cos(mid_angle),\n                y=label_r * sin(mid_angle),\n                text=text,\n                text_font_size=\"21pt\",\n                text_color=_text_on(fill),\n                text_align=\"center\",\n                text_baseline=\"middle\",\n            )\n        )\n\n# Ring level labels\nfor radius, text in [(0.14, \"Department\"), (0.44, \"Team\"), (0.76, \"Project\")]:\n    p.add_layout(\n        Label(\n            x=radius,\n            y=-1.0,\n            text=text,\n            text_font_size=\"20pt\",\n            text_color=INK_MUTED,\n            text_align=\"center\",\n            text_baseline=\"middle\",\n        )\n    )\n\n# Legend — horizontal row below the rings (square canvas has no side gutter)\nlegend_y_swatch = -1.32\nlegend_y_label = -1.55\nlegend_col_x = [-1.09, -0.36, 0.36, 1.09]\n\nfor i, l1 in enumerate(dept_order):\n    colors = dept_colors[l1]\n    cx = legend_col_x[i]\n    for shade, x_off in [(\"base\", -0.09), (\"mid\", 0.0), (\"light\", 0.09)]:\n        p.rect(\n            x=cx + x_off,\n            y=legend_y_swatch,\n            width=0.08,\n            height=0.16,\n            fill_color=colors[shade],\n            line_color=PAGE_BG,\n            line_width=1,\n        )\n    p.add_layout(\n        Label(\n            x=cx,\n            y=legend_y_label,\n            text=l1,\n            text_font_size=\"22pt\",\n            text_color=INK,\n            text_font_style=\"bold\",\n            text_align=\"center\",\n            text_baseline=\"middle\",\n        )\n    )\n    p.add_layout(\n        Label(\n            x=cx,\n            y=legend_y_label - 0.16,\n            text=f\"${level_1_totals[l1]}K\",\n            text_font_size=\"19pt\",\n            text_color=INK_SOFT,\n            text_align=\"center\",\n            text_baseline=\"middle\",\n        )\n    )\n\n# Style\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.align = \"center\"\np.axis.visible = False\np.grid.visible = False\np.outline_line_color = None\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Save HTML (use OUT_DIR so script runs correctly from any cwd)\nhtml_path = OUT_DIR / f\"plot-{THEME}.html\"\npng_path = OUT_DIR / f\"plot-{THEME}.png\"\noutput_file(str(html_path))\nsave(p)\n\n# Screenshot with headless Chrome\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{html_path.resolve()}\")\n# Headless Chrome's --window-size sets the OUTER window, which still reserves\n# a phantom title-bar height even headless — pin the viewport exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(str(png_path))\ndriver.quit()\n"}