Skip to content

Commit d99cd08

Browse files
feat(plotnine): implement donut-basic (#5341)
## Implementation: `donut-basic` - python/plotnine Implements the **python/plotnine** version of `donut-basic`. **File:** `plots/donut-basic/implementations/python/plotnine.py` **Parent Issue:** #733 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24874086016)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 75daa06 commit d99cd08

2 files changed

Lines changed: 362 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
""" anyplot.ai
2+
donut-basic: Basic Donut Chart
3+
Library: plotnine 0.15.3 | Python 3.14.4
4+
Quality: 86/100 | Created: 2026-04-24
5+
"""
6+
7+
import math
8+
import os
9+
import sys
10+
11+
12+
# Avoid name collision: drop this script's directory from sys.path
13+
# so `from plotnine import ...` resolves to the installed package.
14+
_HERE = os.path.dirname(os.path.abspath(__file__))
15+
sys.path = [p for p in sys.path if os.path.abspath(p) != _HERE]
16+
17+
import numpy as np # noqa: E402
18+
import pandas as pd # noqa: E402
19+
from plotnine import ( # noqa: E402
20+
aes,
21+
annotate,
22+
coord_fixed,
23+
element_blank,
24+
element_rect,
25+
element_text,
26+
geom_polygon,
27+
geom_text,
28+
ggplot,
29+
labs,
30+
scale_fill_identity,
31+
scale_x_continuous,
32+
scale_y_continuous,
33+
theme,
34+
)
35+
36+
37+
# Theme tokens
38+
THEME = os.getenv("ANYPLOT_THEME", "light")
39+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
40+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
41+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
42+
LABEL_ON_WEDGE = "#F0EFE8"
43+
44+
# Okabe-Ito palette (first segment is always the brand green)
45+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"]
46+
47+
# Data - Annual budget allocation by department (USD thousands)
48+
categories = ["Engineering", "Marketing", "Operations", "Sales", "Support"]
49+
values = [480, 210, 155, 125, 55]
50+
total = sum(values)
51+
52+
# Ring dimensions
53+
inner_radius = 0.62
54+
outer_radius = 1.0
55+
label_radius = 1.18
56+
pct_radius = (inner_radius + outer_radius) / 2
57+
58+
# Build annular-segment polygons for each category
59+
wedge_rows = []
60+
label_rows = []
61+
pct_rows = []
62+
63+
start_angle = math.pi / 2 # Start at 12 o'clock
64+
for category, value, color in zip(categories, values, OKABE_ITO, strict=True):
65+
sweep = (value / total) * 2 * math.pi
66+
end_angle = start_angle - sweep # Clockwise
67+
68+
# Slight gap between wedges for crisp separation
69+
gap = 0.008
70+
a0 = end_angle + gap
71+
a1 = start_angle - gap
72+
73+
n = 80
74+
inner_arc = np.linspace(a0, a1, n)
75+
outer_arc = np.linspace(a1, a0, n)
76+
77+
points = [(inner_radius * math.cos(a), inner_radius * math.sin(a)) for a in inner_arc]
78+
points += [(outer_radius * math.cos(a), outer_radius * math.sin(a)) for a in outer_arc]
79+
80+
for order, (x, y) in enumerate(points):
81+
wedge_rows.append({"x": x, "y": y, "segment": category, "order": order, "fill": color})
82+
83+
mid_angle = (start_angle + end_angle) / 2
84+
label_rows.append(
85+
{"x": label_radius * math.cos(mid_angle), "y": label_radius * math.sin(mid_angle), "label": category}
86+
)
87+
pct_rows.append(
88+
{
89+
"x": pct_radius * math.cos(mid_angle),
90+
"y": pct_radius * math.sin(mid_angle),
91+
"label": f"{value / total * 100:.1f}%",
92+
}
93+
)
94+
95+
start_angle = end_angle
96+
97+
wedge_df = pd.DataFrame(wedge_rows)
98+
label_df = pd.DataFrame(label_rows)
99+
pct_df = pd.DataFrame(pct_rows)
100+
101+
# Plot
102+
plot = (
103+
ggplot()
104+
+ geom_polygon(aes(x="x", y="y", group="segment", fill="fill"), data=wedge_df, color=PAGE_BG, size=1.2)
105+
+ geom_text(aes(x="x", y="y", label="label"), data=pct_df, size=14, fontweight="bold", color=LABEL_ON_WEDGE)
106+
+ geom_text(aes(x="x", y="y", label="label"), data=label_df, size=16, color=INK)
107+
+ annotate("text", x=0, y=0.13, label="Total budget", size=14, color=INK_SOFT)
108+
+ annotate("text", x=0, y=-0.08, label=f"${total:,}K", size=28, fontweight="bold", color=INK)
109+
+ scale_fill_identity()
110+
+ coord_fixed(ratio=1)
111+
+ scale_x_continuous(limits=(-1.45, 1.45))
112+
+ scale_y_continuous(limits=(-1.35, 1.35))
113+
+ labs(title="Budget by Department · donut-basic · plotnine · anyplot.ai")
114+
+ theme(
115+
figure_size=(12, 12),
116+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
117+
panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
118+
plot_title=element_text(size=22, color=INK, ha="center", margin={"b": 18}),
119+
axis_title=element_blank(),
120+
axis_text=element_blank(),
121+
axis_ticks=element_blank(),
122+
axis_line=element_blank(),
123+
panel_grid_major=element_blank(),
124+
panel_grid_minor=element_blank(),
125+
legend_position="none",
126+
)
127+
)
128+
129+
plot.save(f"plot-{THEME}.png", dpi=300)
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
library: plotnine
2+
language: python
3+
specification_id: donut-basic
4+
created: '2026-04-24T05:43:19Z'
5+
updated: '2026-04-24T05:49:06Z'
6+
generated_by: claude-opus
7+
workflow_run: 24874086016
8+
issue: 733
9+
python_version: 3.14.4
10+
library_version: 0.15.3
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/donut-basic/python/plotnine/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/donut-basic/python/plotnine/plot-dark.png
13+
preview_html_light: null
14+
preview_html_dark: null
15+
quality_score: 86
16+
review:
17+
strengths:
18+
- 'Perfect spec compliance: all required donut features (hollow center, percentage
19+
labels, center metric, consistent ordering) present and working'
20+
- 'Correct Okabe-Ito palette with #009E73 as first series; both light and dark themes
21+
render correctly with proper background colors and readable chrome'
22+
- Clean minimal composition — no axes, grid, or legend clutter; background-colored
23+
wedge gaps add professional separation
24+
- Realistic, neutral business data context (budget allocation by department) with
25+
plausible proportions and values
26+
weaknesses:
27+
- Title font at 22pt is below the 24pt target; percentage labels inside wedges at
28+
14pt are below the 16pt recommendation for high-resolution canvas
29+
- 'Library Mastery is low: manual polygon construction bypasses plotnine''s grammar-of-graphics
30+
strengths — coord_polar() + geom_bar() would be idiomatic'
31+
- 'Design Excellence is above average but not exceptional: typography lacks a bold/regular
32+
hierarchy between category names and percentages, and the center metric could
33+
have stronger contrast between label and value lines'
34+
image_description: |-
35+
Light render (plot-light.png):
36+
Background: Warm off-white #FAF8F1 — correct, not pure white
37+
Chrome: Title "Budget by Department · donut-basic · plotnine · anyplot.ai" in dark ink, clearly readable. No axis labels or tick labels (donut chart). Center labels ("Total budget" in INK_SOFT gray, "$1,025K" in bold INK dark) readable. Category labels outside ring in dark INK text — all clearly visible.
38+
Data: Five Okabe-Ito segments — Engineering #009E73 (46.8%), Marketing #D55E00 (20.5%), Operations #0072B2 (15.1%), Sales #CC79A7 (12.2%), Support #E69F00 (5.4%). White percentage labels inside each wedge. Background-colored gaps between segments.
39+
Legibility verdict: PASS — all text clearly readable against light background
40+
41+
Dark render (plot-dark.png):
42+
Background: Warm near-black #1A1A17 — correct, not pure black
43+
Chrome: Title in light off-white text, clearly readable against dark background. Category labels outside ring in light INK (#F0EFE8) text — all visible. Center "Total budget" in soft gray (INK_SOFT #B8B7B0), "$1,025K" in bright near-white (INK #F0EFE8) — both readable. No dark-on-dark failures observed.
44+
Data: All five segment colors identical to light render — Engineering #009E73, Marketing #D55E00, Operations #0072B2, Sales #CC79A7, Support #E69F00. White percentage labels remain visible inside wedges.
45+
Legibility verdict: PASS — all text clearly readable against dark background; no dark-on-dark issues
46+
criteria_checklist:
47+
visual_quality:
48+
score: 28
49+
max: 30
50+
items:
51+
- id: VQ-01
52+
name: Text Legibility
53+
score: 7
54+
max: 8
55+
passed: true
56+
comment: All sizes explicit; title 22pt (target 24pt), pct labels 14pt (target
57+
16pt)
58+
- id: VQ-02
59+
name: No Overlap
60+
score: 6
61+
max: 6
62+
passed: true
63+
comment: No overlaps; category labels well-spaced around ring
64+
- id: VQ-03
65+
name: Element Visibility
66+
score: 5
67+
max: 6
68+
passed: true
69+
comment: All segments visible; Support 5.4% wedge small but label fits
70+
- id: VQ-04
71+
name: Color Accessibility
72+
score: 2
73+
max: 2
74+
passed: true
75+
comment: Okabe-Ito CVD-safe; white labels on colored segments good contrast
76+
- id: VQ-05
77+
name: Layout & Canvas
78+
score: 4
79+
max: 4
80+
passed: true
81+
comment: Square format appropriate; well-centered, balanced margins
82+
- id: VQ-06
83+
name: Axis Labels & Title
84+
score: 2
85+
max: 2
86+
passed: true
87+
comment: Descriptive title; no axes needed for donut
88+
- id: VQ-07
89+
name: Palette Compliance
90+
score: 2
91+
max: 2
92+
passed: true
93+
comment: 'First series #009E73; Okabe-Ito order; correct bg both themes; chrome
94+
correct'
95+
design_excellence:
96+
score: 13
97+
max: 20
98+
items:
99+
- id: DE-01
100+
name: Aesthetic Sophistication
101+
score: 5
102+
max: 8
103+
passed: true
104+
comment: 'Above default: wedge gaps, two-tier center metric, label hierarchy.
105+
Not publication-level.'
106+
- id: DE-02
107+
name: Visual Refinement
108+
score: 4
109+
max: 6
110+
passed: true
111+
comment: Fully minimal chrome; background-colored wedge edges add crispness
112+
- id: DE-03
113+
name: Data Storytelling
114+
score: 4
115+
max: 6
116+
passed: true
117+
comment: Center $1,025K focal point; Engineering dominance immediately obvious
118+
spec_compliance:
119+
score: 15
120+
max: 15
121+
items:
122+
- id: SC-01
123+
name: Plot Type
124+
score: 5
125+
max: 5
126+
passed: true
127+
comment: Correct donut (ring chart) with hollow center
128+
- id: SC-02
129+
name: Required Features
130+
score: 4
131+
max: 4
132+
passed: true
133+
comment: Hollow center, pct labels, consistent ordering, center summary all
134+
present
135+
- id: SC-03
136+
name: Data Mapping
137+
score: 3
138+
max: 3
139+
passed: true
140+
comment: Category to segment, value to arc angle, all 5 categories present
141+
- id: SC-04
142+
name: Title & Legend
143+
score: 3
144+
max: 3
145+
passed: true
146+
comment: Title format correct; category labels directly on chart
147+
data_quality:
148+
score: 15
149+
max: 15
150+
items:
151+
- id: DQ-01
152+
name: Feature Coverage
153+
score: 6
154+
max: 6
155+
passed: true
156+
comment: Hollow center, pct labels, outer labels, center summary — all spec
157+
features shown
158+
- id: DQ-02
159+
name: Realistic Context
160+
score: 5
161+
max: 5
162+
passed: true
163+
comment: Annual budget by department; neutral business scenario, plausible
164+
proportions
165+
- id: DQ-03
166+
name: Appropriate Scale
167+
score: 4
168+
max: 4
169+
passed: true
170+
comment: Values in $K (480, 210, 155, 125, 55); 5 categories in 3-8 range
171+
code_quality:
172+
score: 10
173+
max: 10
174+
items:
175+
- id: CQ-01
176+
name: KISS Structure
177+
score: 3
178+
max: 3
179+
passed: true
180+
comment: No functions/classes; linear data -> polygon -> plot -> save
181+
- id: CQ-02
182+
name: Reproducibility
183+
score: 2
184+
max: 2
185+
passed: true
186+
comment: Fully deterministic hardcoded data
187+
- id: CQ-03
188+
name: Clean Imports
189+
score: 2
190+
max: 2
191+
passed: true
192+
comment: All imports used; math, numpy, all plotnine symbols consumed
193+
- id: CQ-04
194+
name: Code Elegance
195+
score: 2
196+
max: 2
197+
passed: true
198+
comment: Clean Pythonic loop for polygon construction; appropriate complexity
199+
- id: CQ-05
200+
name: Output & API
201+
score: 1
202+
max: 1
203+
passed: true
204+
comment: plot.save(f'plot-{THEME}.png', dpi=300) correct
205+
library_mastery:
206+
score: 5
207+
max: 10
208+
items:
209+
- id: LM-01
210+
name: Idiomatic Usage
211+
score: 3
212+
max: 5
213+
passed: true
214+
comment: Correct ggplot grammar; but coord_polar+geom_bar is the idiomatic
215+
donut approach
216+
- id: LM-02
217+
name: Distinctive Features
218+
score: 2
219+
max: 5
220+
passed: true
221+
comment: scale_fill_identity, coord_fixed, annotate layers — some distinctive
222+
ggplot features
223+
verdict: REJECTED
224+
impl_tags:
225+
dependencies: []
226+
techniques:
227+
- annotations
228+
patterns:
229+
- iteration-over-groups
230+
dataprep: []
231+
styling:
232+
- minimal-chrome
233+
- edge-highlighting

0 commit comments

Comments
 (0)