{"spec_id":"sunburst-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nsunburst-basic: Basic Sunburst Chart\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 84/100 | Updated: 2026-07-26\n\"\"\"\n\nimport os\n\nimport matplotlib.colors as mcolors\nimport matplotlib.patheffects as path_effects\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport seaborn.objects as so\n\n\n# Theme tokens\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# Imprint palette — first series always #009E73; abstract folders get canonical order\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Ring-label text colors, chosen per-wedge from the wedge's own fill luminance rather than\n# a fixed theme token — wedge fills are data colors (branch tints), not page chrome, so their\n# brightness varies independently of THEME and a single INK/INK_SOFT choice can't stay legible\n# across the whole range from pale tints to fully-saturated branch colors.\nWEDGE_LABEL_LIGHT = \"#F0EFE8\"  # for text on saturated/dark wedge fills\nWEDGE_LABEL_DARK = \"#1A1A17\"  # for text on pale wedge tints\n\n\ndef _label_color(hex_color):\n    r, g, b = mcolors.to_rgb(hex_color)\n    luminance = 0.299 * r + 0.587 * g + 0.114 * b\n    return WEDGE_LABEL_DARK if luminance > 0.5 else WEDGE_LABEL_LIGHT\n\n\ndef _label_halo(fill_color):\n    # Opposite-tone halo so a label stays legible even where it overhangs its wedge\n    # onto the page background — PAGE_BG flips between light/dark per THEME, but the\n    # wedge-derived fill color above doesn't, so a plain fill can vanish into PAGE_BG.\n    halo_color = WEDGE_LABEL_LIGHT if fill_color == WEDGE_LABEL_DARK else WEDGE_LABEL_DARK\n    return [path_effects.withStroke(linewidth=2.5, foreground=halo_color)]\n\n\n# Hierarchical data: a repository's disk usage (in MB)\n# Level 1: top-level directories, Level 2: subdirectories, Level 3: leaf folders\ndata = {\n    \"src/\": {\n        \"components/\": {\"widgets/\": 42, \"forms/\": 28},\n        \"services/\": {\"api-client/\": 35, \"auth/\": 18},\n        \"utils/\": {\"helpers/\": 15, \"validators/\": 10},\n    },\n    \"assets/\": {\"images/\": {\"icons/\": 22, \"photos/\": 48}, \"fonts/\": {\"sans/\": 8, \"serif/\": 6}},\n    \"tests/\": {\"integration/\": {\"api/\": 24, \"e2e/\": 30}, \"unit/\": {\"components/\": 20, \"services/\": 16}},\n    \"docs/\": {\"guides/\": {\"getting-started/\": 5, \"tutorials/\": 9}, \"reference/\": {\"api/\": 12, \"changelog/\": 3}},\n}\n\nsns.set_theme(\n    style=\"white\",\n    context=\"notebook\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.12,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Build hierarchical structure and branch-coherent colors\nlevel1_names, level1_values, level1_colors = [], [], []\nlevel2_names, level2_values, level2_colors = [], [], []\nlevel3_names, level3_values, level3_colors = [], [], []\n\nfor i, (top_dir, subdirs) in enumerate(data.items()):\n    dir_total = sum(sum(leaves.values()) for leaves in subdirs.values())\n    level1_names.append(top_dir)\n    level1_values.append(dir_total)\n    base_color = IMPRINT[i % len(IMPRINT)]\n    level1_colors.append(base_color)\n\n    # Light tints of the branch color carry the parent/child relationship visually\n    branch_tints = sns.light_palette(base_color, n_colors=6)\n\n    for j, (subdir, leaves) in enumerate(subdirs.items()):\n        subdir_total = sum(leaves.values())\n        level2_names.append(subdir)\n        level2_values.append(subdir_total)\n        level2_colors.append(branch_tints[3 + j % 2])\n\n        for k, (leaf, size) in enumerate(leaves.items()):\n            level3_names.append(leaf)\n            level3_values.append(size)\n            level3_colors.append(branch_tints[4 + k % 2])\n\n\n# Figure layout: sunburst left, branch-size summary right\nfig = plt.figure(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax_sun = fig.add_axes((0.02, 0.05, 0.54, 0.86))\nax_bar = fig.add_axes((0.60, 0.18, 0.36, 0.58))\nax_sun.set_facecolor(PAGE_BG)\nax_bar.set_facecolor(PAGE_BG)\n\nring_width = 0.32\ninner_radius = 0.22\n\n# Level 3 (outermost ring — leaf folders)\nwedges3, _ = ax_sun.pie(\n    level3_values,\n    radius=inner_radius + 3 * ring_width,\n    colors=level3_colors,\n    startangle=90,\n    counterclock=False,\n    wedgeprops={\"width\": ring_width, \"edgecolor\": PAGE_BG, \"linewidth\": 2},\n)\n\n# Level 2 (middle ring — subdirectories)\nwedges2, _ = ax_sun.pie(\n    level2_values,\n    radius=inner_radius + 2 * ring_width,\n    colors=level2_colors,\n    startangle=90,\n    counterclock=False,\n    wedgeprops={\"width\": ring_width, \"edgecolor\": PAGE_BG, \"linewidth\": 2},\n)\n\n# Level 1 (innermost ring — top-level directories)\nwedges1, texts1 = ax_sun.pie(\n    level1_values,\n    radius=inner_radius + ring_width,\n    colors=level1_colors,\n    labels=level1_names,\n    labeldistance=0.6,\n    startangle=90,\n    counterclock=False,\n    wedgeprops={\"width\": ring_width, \"edgecolor\": PAGE_BG, \"linewidth\": 2},\n    textprops={\"fontsize\": 14, \"fontweight\": \"bold\"},\n)\nfor text, color in zip(texts1, level1_colors, strict=True):\n    fill = _label_color(color)\n    text.set_color(fill)\n    text.set_path_effects(_label_halo(fill))\n\n# Level 2 labels — horizontal, shown only where the wedge is wide enough to hold text\nlevel2_label_positions = []  # (x, y) of rendered labels, checked by level-3 labels below\nfor i, wedge in enumerate(wedges2):\n    ang = (wedge.theta2 + wedge.theta1) / 2\n    span = wedge.theta2 - wedge.theta1\n    if span <= 15:\n        continue\n    r = inner_radius + 1.5 * ring_width\n    x = r * np.cos(np.radians(ang))\n    y = r * np.sin(np.radians(ang))\n    fill = _label_color(level2_colors[i])\n    ax_sun.text(\n        x,\n        y,\n        level2_names[i].rstrip(\"/\"),\n        ha=\"center\",\n        va=\"center\",\n        fontsize=12,\n        color=fill,\n        path_effects=_label_halo(fill),\n    )\n    level2_label_positions.append((x, y))\n\n# Level 3 labels — higher threshold to avoid crowding the outermost, narrowest wedges\nfor i, wedge in enumerate(wedges3):\n    ang = (wedge.theta2 + wedge.theta1) / 2\n    span = wedge.theta2 - wedge.theta1\n    if span <= 10:\n        continue\n    r = inner_radius + 2.6 * ring_width\n    x = r * np.cos(np.radians(ang))\n    y = r * np.sin(np.radians(ang))\n    # Skip if a level-2 label already sits on almost the same horizontal line — at this\n    # radius scale their text would run together (e.g. \"componentsforms\") with no visible gap.\n    if any(abs(y - ly) < 0.08 and abs(x - lx) < 0.9 for lx, ly in level2_label_positions):\n        continue\n    fill = _label_color(level3_colors[i])\n    ax_sun.text(\n        x,\n        y,\n        level3_names[i].rstrip(\"/\"),\n        ha=\"center\",\n        va=\"center\",\n        fontsize=10,\n        color=fill,\n        path_effects=_label_halo(fill),\n    )\n\n# Center text showing repository total\ntotal_size = sum(level1_values)\nax_sun.text(0, 0, f\"{total_size} MB\", ha=\"center\", va=\"center\", fontsize=15, fontweight=\"bold\", color=INK)\nax_sun.set_aspect(\"equal\")\nouter_radius = inner_radius + 3 * ring_width\nsun_lim = outer_radius * 1.08\nax_sun.set_xlim(-sun_lim, sun_lim)\nax_sun.set_ylim(-sun_lim, sun_lim)\n\n# Companion panel: directory totals via seaborn's Objects interface (so.Plot),\n# colored with the same branch hues as the sunburst rings\ndf_dir = pd.DataFrame({\"Directory\": level1_names, \"Size (MB)\": level1_values}).sort_values(\"Size (MB)\", ascending=True)\n(\n    so.Plot(df_dir, x=\"Size (MB)\", y=\"Directory\", color=\"Directory\")\n    .add(so.Bar(edgecolor=PAGE_BG, edgewidth=2))\n    .scale(color=so.Nominal(dict(zip(level1_names, IMPRINT, strict=True))))\n    .on(ax_bar)\n    .plot()\n)\nfor legend in fig.legends:\n    legend.set_visible(False)\n\nax_bar.set_xlabel(\"Size (MB)\", fontsize=20, color=INK)\nax_bar.set_ylabel(\"\", color=INK)\nax_bar.tick_params(axis=\"both\", labelsize=16, colors=INK_SOFT)\nax_bar.set_title(\"Directory Totals\", fontsize=18, fontweight=\"bold\", pad=12, color=INK)\nax_bar.xaxis.grid(True, alpha=0.15, color=INK)\nsns.despine(ax=ax_bar)\n\nfor i, v in enumerate(df_dir[\"Size (MB)\"]):\n    ax_bar.text(v + 4, i, f\"{v} MB\", va=\"center\", fontsize=13, fontweight=\"normal\", color=INK)\nax_bar.set_xlim(0, max(level1_values) * 1.22)\n\nfig.suptitle(\"sunburst-basic · python · seaborn · anyplot.ai\", fontsize=18, fontweight=\"bold\", y=0.98, color=INK)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}