Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions plots/sunburst-basic/implementations/python/plotnine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
""" anyplot.ai
sunburst-basic: Basic Sunburst Chart
Library: plotnine 0.15.3 | Python 3.13.13
Quality: 87/100 | Created: 2026-05-04
"""

import sys


sys.path.pop(0) # prevent this file from shadowing the installed plotnine package

import os

import numpy as np
import pandas as pd
from plotnine import (
aes,
coord_equal,
element_blank,
element_rect,
element_text,
geom_polygon,
geom_text,
ggplot,
labs,
scale_fill_identity,
scale_x_continuous,
scale_y_continuous,
theme,
)


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
# Slightly lighter than PAGE_BG in dark mode so ring boundaries remain visible
RING_SEP = PAGE_BG if THEME == "light" else "#2E2D2B"

OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"]

# Data: company annual budget by department → team ($M)
hierarchy = {
"Engineering": {"Frontend": 15, "Backend": 15, "DevOps": 10},
"Marketing": {"Digital": 10, "Brand": 8, "Events": 7},
"Operations": {"HR": 8, "Finance": 7, "Legal": 5},
"R&D": {"Product": 8, "Data Science": 7},
}

total = sum(sum(v.values()) for v in hierarchy.values())

# Ring radii
R_INNER_L1, R_OUTER_L1 = 0.35, 0.65
R_INNER_L2, R_OUTER_L2 = 0.67, 0.95
N_PTS = 80 # arc resolution

l1_rows, l2_rows, label_rows = [], [], []
cumsum = 0

for idx, (dept, teams) in enumerate(hierarchy.items()):
dept_total = sum(teams.values())
pct = round(dept_total / total * 100)
a0 = 2 * np.pi * cumsum / total - np.pi / 2
a1 = 2 * np.pi * (cumsum + dept_total) / total - np.pi / 2
color = OKABE_ITO[idx]

# L1 arc polygon: inner arc → outer arc (reversed) → closed shape
t = np.linspace(a0, a1, N_PTS)
xs = np.concatenate([R_INNER_L1 * np.cos(t), R_OUTER_L1 * np.cos(t[::-1])])
ys = np.concatenate([R_INNER_L1 * np.sin(t), R_OUTER_L1 * np.sin(t[::-1])])
for xi, yi in zip(xs, ys, strict=False):
l1_rows.append({"x": xi, "y": yi, "group": dept, "fill": color})

# L1 department name: outer half of inner ring
a_mid = (a0 + a1) / 2
r_name = 0.54
label_rows.append({"x": r_name * np.cos(a_mid), "y": r_name * np.sin(a_mid), "label": dept, "level": 1})
# Percentage annotation: inner half of inner ring
r_pct = 0.44
label_rows.append({"x": r_pct * np.cos(a_mid), "y": r_pct * np.sin(a_mid), "label": f"{pct}%", "level": 3})

# L2 arc polygons (sub-departments)
team_cumsum = cumsum
for t_idx, (team, budget) in enumerate(teams.items()):
b0 = 2 * np.pi * team_cumsum / total - np.pi / 2
b1 = 2 * np.pi * (team_cumsum + budget) / total - np.pi / 2

t2 = np.linspace(b0, b1, N_PTS)
xs2 = np.concatenate([R_INNER_L2 * np.cos(t2), R_OUTER_L2 * np.cos(t2[::-1])])
ys2 = np.concatenate([R_INNER_L2 * np.sin(t2), R_OUTER_L2 * np.sin(t2[::-1])])
grp = f"{dept}_{t_idx}"
for xi, yi in zip(xs2, ys2, strict=False):
l2_rows.append({"x": xi, "y": yi, "group": grp, "fill": color})

# Only label segments wide enough to hold text (≥8% share)
if budget / total >= 0.08:
b_mid = (b0 + b1) / 2
r_mid2 = (R_INNER_L2 + R_OUTER_L2) / 2
label_rows.append({"x": r_mid2 * np.cos(b_mid), "y": r_mid2 * np.sin(b_mid), "label": team, "level": 2})

team_cumsum += budget
cumsum += dept_total

df_l1 = pd.DataFrame(l1_rows)
df_l2 = pd.DataFrame(l2_rows)
df_labels = pd.DataFrame(label_rows)
df_l1_labels = df_labels[df_labels["level"] == 1]
df_l2_labels = df_labels[df_labels["level"] == 2]
df_pct_labels = df_labels[df_labels["level"] == 3]

# Plot
plot = (
ggplot()
+ geom_polygon(data=df_l1, mapping=aes(x="x", y="y", group="group", fill="fill"), color=RING_SEP, size=1.5)
+ geom_polygon(
data=df_l2, mapping=aes(x="x", y="y", group="group", fill="fill"), color=RING_SEP, size=0.8, alpha=0.65
)
+ geom_text(
data=df_l1_labels,
mapping=aes(x="x", y="y", label="label"),
color=INK,
size=16,
fontweight="bold",
ha="center",
va="center",
)
+ geom_text(
data=df_pct_labels, mapping=aes(x="x", y="y", label="label"), color=INK_SOFT, size=13, ha="center", va="center"
)
+ geom_text(
data=df_l2_labels, mapping=aes(x="x", y="y", label="label"), color=INK, size=16, ha="center", va="center"
)
+ scale_fill_identity()
+ coord_equal()
+ scale_x_continuous(limits=(-1.15, 1.15), breaks=[], expand=(0, 0))
+ scale_y_continuous(limits=(-1.15, 1.15), breaks=[], expand=(0, 0))
+ labs(title="sunburst-basic · plotnine · anyplot.ai")
+ theme(
figure_size=(12, 12),
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
panel_background=element_rect(fill=PAGE_BG),
panel_grid_major=element_blank(),
panel_grid_minor=element_blank(),
panel_border=element_blank(),
axis_title=element_blank(),
axis_text=element_blank(),
axis_ticks_major_x=element_blank(),
axis_ticks_major_y=element_blank(),
axis_ticks_minor_x=element_blank(),
axis_ticks_minor_y=element_blank(),
legend_position="none",
plot_title=element_text(color=INK, size=24, ha="center"),
)
)

plot.save(f"plot-{THEME}.png", dpi=300, verbose=False)
240 changes: 240 additions & 0 deletions plots/sunburst-basic/metadata/python/plotnine.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
library: plotnine
language: python
specification_id: sunburst-basic
created: '2026-05-04T22:50:30Z'
updated: '2026-05-04T23:09:52Z'
generated_by: claude-sonnet
workflow_run: 25347329138
issue: 821
python_version: 3.13.13
library_version: 0.15.3
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/sunburst-basic/python/plotnine/plot-light.png
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/sunburst-basic/python/plotnine/plot-dark.png
preview_html_light: null
preview_html_dark: null
quality_score: 87
review:
strengths:
- 'Perfectly adapted theme-adaptive chrome: both renders pass readability checks
with zero dark-on-dark issues'
- Threshold-based outer ring labeling (>=8%) elegantly prevents overcrowding
- Ring separator adapts to theme (#2E2D2B in dark vs PAGE_BG in light) — subtle
and thoughtful
- Alpha=0.65 on outer ring creates natural visual hierarchy between hierarchy levels
- 'Correct Okabe-Ito order starting with #009E73 for the first/dominant series (Engineering)'
weaknesses:
- Percentage labels at size=13 are below the 16pt minimum for tick-level text at
3600x3600 canvas — consider size=15 minimum
- Library mastery is capped by plotnines lack of native sunburst support — polygon
construction is inherently low-level
image_description: |-
Light render (plot-light.png):
Background: Warm off-white #FAF8F1 — correct, not pure white
Chrome: Title "sunburst-basic · plotnine · anyplot.ai" in dark ink (#1A1A17) at top — clearly readable. Department labels in bold dark ink within inner ring segments. Percentage labels in softer ink (#4A4A44). Team labels in dark ink on outer ring. No axis labels or ticks (correctly removed for sunburst).
Data: Engineering=#009E73 (largest, ~40%), Marketing=#D55E00 (~25%), Operations=#0072B2 (~20%), R&D=#CC79A7 (~15%). Okabe-Ito order correct, starting with #009E73 first. Outer ring at alpha=0.65 shows same color family per branch. Labeled outer segments: Frontend, Backend, DevOps (Engineering), Digital, Brand (Marketing), HR (Operations), Product (R&D).
Legibility verdict: PASS — all text dark on light background, no light-on-light issues

Dark render (plot-dark.png):
Background: Warm near-black #1A1A17 — correct, not pure black
Chrome: Title in light cream (#F0EFE8) — readable. Department names in light cream (#F0EFE8) — readable. Percentage labels in light gray (#B8B7B0) — readable. Team labels in light cream — readable. Ring separator uses #2E2D2B (slightly lighter than background) creating subtle segment boundaries. Center hole matches dark background.
Data: Colors identical to light render — Engineering=#009E73, Marketing=#D55E00, Operations=#0072B2, R&D=#CC79A7. Outer ring alpha=0.65 preserves color vibrancy. No data color changes between themes.
Legibility verdict: PASS — all text light on dark background, no dark-on-dark failures detected
criteria_checklist:
visual_quality:
score: 29
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 7
max: 8
passed: true
comment: All sizes explicitly set (title=24, labels=16, pct=13); 13pt percentages
slightly small for 3600x3600 canvas
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: Threshold-based labeling prevents crowding; all visible labels well-spaced
- id: VQ-03
name: Element Visibility
score: 6
max: 6
passed: true
comment: All ring segments clearly visible; outer ring alpha=0.65 remains
vibrant in both themes
- id: VQ-04
name: Color Accessibility
score: 2
max: 2
passed: true
comment: Okabe-Ito is CVD-safe; good luminance contrast between segment colors
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Square 12x12 at 300dpi = 3600x3600; chart fills canvas well
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Title format correct; segment labels serve as data annotations
- id: VQ-07
name: Palette Compliance
score: 2
max: 2
passed: true
comment: Engineering=#009E73 first; Okabe-Ito order preserved; correct backgrounds;
chrome adapts cleanly
design_excellence:
score: 14
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 5
max: 8
passed: true
comment: Thoughtful alpha on outer ring, adaptive ring separators, bold/soft
text hierarchy — above defaults but not publication-ready
- id: DE-02
name: Visual Refinement
score: 5
max: 6
passed: true
comment: All grid/axes/spines/ticks removed; theme-adaptive ring separator;
generous whitespace; clean minimal composition
- id: DE-03
name: Data Storytelling
score: 4
max: 6
passed: true
comment: Engineering dominance immediately clear; alpha differentiates ring
levels; bold/lighter text creates readable hierarchy
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct sunburst with concentric rings at correct hierarchy levels
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: Consistent branch colors; threshold-based labeling; clear ring-level
separation; inner segments span children
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: level_1=departments (inner ring), level_2=teams (outer ring), value=budget
angles
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: '''sunburst-basic · plotnine · anyplot.ai'' correct; no legend needed
(labels embedded)'
data_quality:
score: 14
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 5
max: 6
passed: true
comment: Shows two hierarchy levels with variable segment sizes; optional
3rd level not implemented
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: Company annual budget breakdown by department and team — neutral,
plausible business scenario
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Budget values in $M with realistic proportions; total ~100M plausible
for mid-sized company
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: 'Linear: imports → theme tokens → data → polygon construction → plot
→ save'
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: Fully deterministic (hardcoded dict, no randomness)
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: All imports used; sys for path fix, os for getenv, numpy for trig
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Polygon loops necessary for manual sunburst in plotnine; no fake
UI
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves as plot-{THEME}.png; current API throughout
library_mastery:
score: 5
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 3
max: 5
passed: true
comment: Correct ggplot grammar (geom_polygon + geom_text, coord_equal, scale_fill_identity,
theme()); sunburst not native so manual polygon construction is somewhat
low-level
- id: LM-02
name: Distinctive Features
score: 2
max: 5
passed: false
comment: coord_equal and scale_fill_identity are plotnine-specific, but core
work is manual coordinate math replicable in any polygon library
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- layer-composition
- annotations
patterns:
- iteration-over-groups
dataprep:
- cumulative-sum
styling:
- alpha-blending
- minimal-chrome
Loading