Skip to content

Commit f80c686

Browse files
feat(plotnine): implement sunburst-basic (#5681)
## Implementation: `sunburst-basic` - python/plotnine Implements the **python/plotnine** version of `sunburst-basic`. **File:** `plots/sunburst-basic/implementations/python/plotnine.py` **Parent Issue:** #821 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25347329138)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent bc31114 commit f80c686

2 files changed

Lines changed: 397 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
""" anyplot.ai
2+
sunburst-basic: Basic Sunburst Chart
3+
Library: plotnine 0.15.3 | Python 3.13.13
4+
Quality: 87/100 | Created: 2026-05-04
5+
"""
6+
7+
import sys
8+
9+
10+
sys.path.pop(0) # prevent this file from shadowing the installed plotnine package
11+
12+
import os
13+
14+
import numpy as np
15+
import pandas as pd
16+
from plotnine import (
17+
aes,
18+
coord_equal,
19+
element_blank,
20+
element_rect,
21+
element_text,
22+
geom_polygon,
23+
geom_text,
24+
ggplot,
25+
labs,
26+
scale_fill_identity,
27+
scale_x_continuous,
28+
scale_y_continuous,
29+
theme,
30+
)
31+
32+
33+
# Theme tokens
34+
THEME = os.getenv("ANYPLOT_THEME", "light")
35+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
36+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
37+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
38+
# Slightly lighter than PAGE_BG in dark mode so ring boundaries remain visible
39+
RING_SEP = PAGE_BG if THEME == "light" else "#2E2D2B"
40+
41+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"]
42+
43+
# Data: company annual budget by department → team ($M)
44+
hierarchy = {
45+
"Engineering": {"Frontend": 15, "Backend": 15, "DevOps": 10},
46+
"Marketing": {"Digital": 10, "Brand": 8, "Events": 7},
47+
"Operations": {"HR": 8, "Finance": 7, "Legal": 5},
48+
"R&D": {"Product": 8, "Data Science": 7},
49+
}
50+
51+
total = sum(sum(v.values()) for v in hierarchy.values())
52+
53+
# Ring radii
54+
R_INNER_L1, R_OUTER_L1 = 0.35, 0.65
55+
R_INNER_L2, R_OUTER_L2 = 0.67, 0.95
56+
N_PTS = 80 # arc resolution
57+
58+
l1_rows, l2_rows, label_rows = [], [], []
59+
cumsum = 0
60+
61+
for idx, (dept, teams) in enumerate(hierarchy.items()):
62+
dept_total = sum(teams.values())
63+
pct = round(dept_total / total * 100)
64+
a0 = 2 * np.pi * cumsum / total - np.pi / 2
65+
a1 = 2 * np.pi * (cumsum + dept_total) / total - np.pi / 2
66+
color = OKABE_ITO[idx]
67+
68+
# L1 arc polygon: inner arc → outer arc (reversed) → closed shape
69+
t = np.linspace(a0, a1, N_PTS)
70+
xs = np.concatenate([R_INNER_L1 * np.cos(t), R_OUTER_L1 * np.cos(t[::-1])])
71+
ys = np.concatenate([R_INNER_L1 * np.sin(t), R_OUTER_L1 * np.sin(t[::-1])])
72+
for xi, yi in zip(xs, ys, strict=False):
73+
l1_rows.append({"x": xi, "y": yi, "group": dept, "fill": color})
74+
75+
# L1 department name: outer half of inner ring
76+
a_mid = (a0 + a1) / 2
77+
r_name = 0.54
78+
label_rows.append({"x": r_name * np.cos(a_mid), "y": r_name * np.sin(a_mid), "label": dept, "level": 1})
79+
# Percentage annotation: inner half of inner ring
80+
r_pct = 0.44
81+
label_rows.append({"x": r_pct * np.cos(a_mid), "y": r_pct * np.sin(a_mid), "label": f"{pct}%", "level": 3})
82+
83+
# L2 arc polygons (sub-departments)
84+
team_cumsum = cumsum
85+
for t_idx, (team, budget) in enumerate(teams.items()):
86+
b0 = 2 * np.pi * team_cumsum / total - np.pi / 2
87+
b1 = 2 * np.pi * (team_cumsum + budget) / total - np.pi / 2
88+
89+
t2 = np.linspace(b0, b1, N_PTS)
90+
xs2 = np.concatenate([R_INNER_L2 * np.cos(t2), R_OUTER_L2 * np.cos(t2[::-1])])
91+
ys2 = np.concatenate([R_INNER_L2 * np.sin(t2), R_OUTER_L2 * np.sin(t2[::-1])])
92+
grp = f"{dept}_{t_idx}"
93+
for xi, yi in zip(xs2, ys2, strict=False):
94+
l2_rows.append({"x": xi, "y": yi, "group": grp, "fill": color})
95+
96+
# Only label segments wide enough to hold text (≥8% share)
97+
if budget / total >= 0.08:
98+
b_mid = (b0 + b1) / 2
99+
r_mid2 = (R_INNER_L2 + R_OUTER_L2) / 2
100+
label_rows.append({"x": r_mid2 * np.cos(b_mid), "y": r_mid2 * np.sin(b_mid), "label": team, "level": 2})
101+
102+
team_cumsum += budget
103+
cumsum += dept_total
104+
105+
df_l1 = pd.DataFrame(l1_rows)
106+
df_l2 = pd.DataFrame(l2_rows)
107+
df_labels = pd.DataFrame(label_rows)
108+
df_l1_labels = df_labels[df_labels["level"] == 1]
109+
df_l2_labels = df_labels[df_labels["level"] == 2]
110+
df_pct_labels = df_labels[df_labels["level"] == 3]
111+
112+
# Plot
113+
plot = (
114+
ggplot()
115+
+ geom_polygon(data=df_l1, mapping=aes(x="x", y="y", group="group", fill="fill"), color=RING_SEP, size=1.5)
116+
+ geom_polygon(
117+
data=df_l2, mapping=aes(x="x", y="y", group="group", fill="fill"), color=RING_SEP, size=0.8, alpha=0.65
118+
)
119+
+ geom_text(
120+
data=df_l1_labels,
121+
mapping=aes(x="x", y="y", label="label"),
122+
color=INK,
123+
size=16,
124+
fontweight="bold",
125+
ha="center",
126+
va="center",
127+
)
128+
+ geom_text(
129+
data=df_pct_labels, mapping=aes(x="x", y="y", label="label"), color=INK_SOFT, size=13, ha="center", va="center"
130+
)
131+
+ geom_text(
132+
data=df_l2_labels, mapping=aes(x="x", y="y", label="label"), color=INK, size=16, ha="center", va="center"
133+
)
134+
+ scale_fill_identity()
135+
+ coord_equal()
136+
+ scale_x_continuous(limits=(-1.15, 1.15), breaks=[], expand=(0, 0))
137+
+ scale_y_continuous(limits=(-1.15, 1.15), breaks=[], expand=(0, 0))
138+
+ labs(title="sunburst-basic · plotnine · anyplot.ai")
139+
+ theme(
140+
figure_size=(12, 12),
141+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
142+
panel_background=element_rect(fill=PAGE_BG),
143+
panel_grid_major=element_blank(),
144+
panel_grid_minor=element_blank(),
145+
panel_border=element_blank(),
146+
axis_title=element_blank(),
147+
axis_text=element_blank(),
148+
axis_ticks_major_x=element_blank(),
149+
axis_ticks_major_y=element_blank(),
150+
axis_ticks_minor_x=element_blank(),
151+
axis_ticks_minor_y=element_blank(),
152+
legend_position="none",
153+
plot_title=element_text(color=INK, size=24, ha="center"),
154+
)
155+
)
156+
157+
plot.save(f"plot-{THEME}.png", dpi=300, verbose=False)
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
library: plotnine
2+
language: python
3+
specification_id: sunburst-basic
4+
created: '2026-05-04T22:50:30Z'
5+
updated: '2026-05-04T23:09:52Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 25347329138
8+
issue: 821
9+
python_version: 3.13.13
10+
library_version: 0.15.3
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/sunburst-basic/python/plotnine/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/sunburst-basic/python/plotnine/plot-dark.png
13+
preview_html_light: null
14+
preview_html_dark: null
15+
quality_score: 87
16+
review:
17+
strengths:
18+
- 'Perfectly adapted theme-adaptive chrome: both renders pass readability checks
19+
with zero dark-on-dark issues'
20+
- Threshold-based outer ring labeling (>=8%) elegantly prevents overcrowding
21+
- Ring separator adapts to theme (#2E2D2B in dark vs PAGE_BG in light) — subtle
22+
and thoughtful
23+
- Alpha=0.65 on outer ring creates natural visual hierarchy between hierarchy levels
24+
- 'Correct Okabe-Ito order starting with #009E73 for the first/dominant series (Engineering)'
25+
weaknesses:
26+
- Percentage labels at size=13 are below the 16pt minimum for tick-level text at
27+
3600x3600 canvas — consider size=15 minimum
28+
- Library mastery is capped by plotnines lack of native sunburst support — polygon
29+
construction is inherently low-level
30+
image_description: |-
31+
Light render (plot-light.png):
32+
Background: Warm off-white #FAF8F1 — correct, not pure white
33+
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).
34+
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).
35+
Legibility verdict: PASS — all text dark on light background, no light-on-light issues
36+
37+
Dark render (plot-dark.png):
38+
Background: Warm near-black #1A1A17 — correct, not pure black
39+
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.
40+
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.
41+
Legibility verdict: PASS — all text light on dark background, no dark-on-dark failures detected
42+
criteria_checklist:
43+
visual_quality:
44+
score: 29
45+
max: 30
46+
items:
47+
- id: VQ-01
48+
name: Text Legibility
49+
score: 7
50+
max: 8
51+
passed: true
52+
comment: All sizes explicitly set (title=24, labels=16, pct=13); 13pt percentages
53+
slightly small for 3600x3600 canvas
54+
- id: VQ-02
55+
name: No Overlap
56+
score: 6
57+
max: 6
58+
passed: true
59+
comment: Threshold-based labeling prevents crowding; all visible labels well-spaced
60+
- id: VQ-03
61+
name: Element Visibility
62+
score: 6
63+
max: 6
64+
passed: true
65+
comment: All ring segments clearly visible; outer ring alpha=0.65 remains
66+
vibrant in both themes
67+
- id: VQ-04
68+
name: Color Accessibility
69+
score: 2
70+
max: 2
71+
passed: true
72+
comment: Okabe-Ito is CVD-safe; good luminance contrast between segment colors
73+
- id: VQ-05
74+
name: Layout & Canvas
75+
score: 4
76+
max: 4
77+
passed: true
78+
comment: Square 12x12 at 300dpi = 3600x3600; chart fills canvas well
79+
- id: VQ-06
80+
name: Axis Labels & Title
81+
score: 2
82+
max: 2
83+
passed: true
84+
comment: Title format correct; segment labels serve as data annotations
85+
- id: VQ-07
86+
name: Palette Compliance
87+
score: 2
88+
max: 2
89+
passed: true
90+
comment: Engineering=#009E73 first; Okabe-Ito order preserved; correct backgrounds;
91+
chrome adapts cleanly
92+
design_excellence:
93+
score: 14
94+
max: 20
95+
items:
96+
- id: DE-01
97+
name: Aesthetic Sophistication
98+
score: 5
99+
max: 8
100+
passed: true
101+
comment: Thoughtful alpha on outer ring, adaptive ring separators, bold/soft
102+
text hierarchy — above defaults but not publication-ready
103+
- id: DE-02
104+
name: Visual Refinement
105+
score: 5
106+
max: 6
107+
passed: true
108+
comment: All grid/axes/spines/ticks removed; theme-adaptive ring separator;
109+
generous whitespace; clean minimal composition
110+
- id: DE-03
111+
name: Data Storytelling
112+
score: 4
113+
max: 6
114+
passed: true
115+
comment: Engineering dominance immediately clear; alpha differentiates ring
116+
levels; bold/lighter text creates readable hierarchy
117+
spec_compliance:
118+
score: 15
119+
max: 15
120+
items:
121+
- id: SC-01
122+
name: Plot Type
123+
score: 5
124+
max: 5
125+
passed: true
126+
comment: Correct sunburst with concentric rings at correct hierarchy levels
127+
- id: SC-02
128+
name: Required Features
129+
score: 4
130+
max: 4
131+
passed: true
132+
comment: Consistent branch colors; threshold-based labeling; clear ring-level
133+
separation; inner segments span children
134+
- id: SC-03
135+
name: Data Mapping
136+
score: 3
137+
max: 3
138+
passed: true
139+
comment: level_1=departments (inner ring), level_2=teams (outer ring), value=budget
140+
angles
141+
- id: SC-04
142+
name: Title & Legend
143+
score: 3
144+
max: 3
145+
passed: true
146+
comment: '''sunburst-basic · plotnine · anyplot.ai'' correct; no legend needed
147+
(labels embedded)'
148+
data_quality:
149+
score: 14
150+
max: 15
151+
items:
152+
- id: DQ-01
153+
name: Feature Coverage
154+
score: 5
155+
max: 6
156+
passed: true
157+
comment: Shows two hierarchy levels with variable segment sizes; optional
158+
3rd level not implemented
159+
- id: DQ-02
160+
name: Realistic Context
161+
score: 5
162+
max: 5
163+
passed: true
164+
comment: Company annual budget breakdown by department and team — neutral,
165+
plausible business scenario
166+
- id: DQ-03
167+
name: Appropriate Scale
168+
score: 4
169+
max: 4
170+
passed: true
171+
comment: Budget values in $M with realistic proportions; total ~100M plausible
172+
for mid-sized company
173+
code_quality:
174+
score: 10
175+
max: 10
176+
items:
177+
- id: CQ-01
178+
name: KISS Structure
179+
score: 3
180+
max: 3
181+
passed: true
182+
comment: 'Linear: imports → theme tokens → data → polygon construction → plot
183+
→ save'
184+
- id: CQ-02
185+
name: Reproducibility
186+
score: 2
187+
max: 2
188+
passed: true
189+
comment: Fully deterministic (hardcoded dict, no randomness)
190+
- id: CQ-03
191+
name: Clean Imports
192+
score: 2
193+
max: 2
194+
passed: true
195+
comment: All imports used; sys for path fix, os for getenv, numpy for trig
196+
- id: CQ-04
197+
name: Code Elegance
198+
score: 2
199+
max: 2
200+
passed: true
201+
comment: Polygon loops necessary for manual sunburst in plotnine; no fake
202+
UI
203+
- id: CQ-05
204+
name: Output & API
205+
score: 1
206+
max: 1
207+
passed: true
208+
comment: Saves as plot-{THEME}.png; current API throughout
209+
library_mastery:
210+
score: 5
211+
max: 10
212+
items:
213+
- id: LM-01
214+
name: Idiomatic Usage
215+
score: 3
216+
max: 5
217+
passed: true
218+
comment: Correct ggplot grammar (geom_polygon + geom_text, coord_equal, scale_fill_identity,
219+
theme()); sunburst not native so manual polygon construction is somewhat
220+
low-level
221+
- id: LM-02
222+
name: Distinctive Features
223+
score: 2
224+
max: 5
225+
passed: false
226+
comment: coord_equal and scale_fill_identity are plotnine-specific, but core
227+
work is manual coordinate math replicable in any polygon library
228+
verdict: APPROVED
229+
impl_tags:
230+
dependencies: []
231+
techniques:
232+
- layer-composition
233+
- annotations
234+
patterns:
235+
- iteration-over-groups
236+
dataprep:
237+
- cumulative-sum
238+
styling:
239+
- alpha-blending
240+
- minimal-chrome

0 commit comments

Comments
 (0)