Skip to content

Commit 7eff378

Browse files
Merge remote-tracking branch 'origin/main'
2 parents e8015e5 + d99cd08 commit 7eff378

6 files changed

Lines changed: 799 additions & 343 deletions

File tree

Lines changed: 91 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
donut-basic: Basic Donut Chart
3-
Library: bokeh 3.8.1 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: bokeh 3.9.0 | Python 3.14.4
4+
Quality: 88/100 | Updated: 2026-04-24
55
"""
66

7-
from math import pi
7+
import os
8+
from math import cos, pi, sin
89

910
import numpy as np
1011
from bokeh.io import export_png, output_file, save
@@ -13,128 +14,122 @@
1314
from bokeh.transform import cumsum
1415

1516

16-
# Data - Portfolio allocation by asset class
17-
categories = ["Technology", "Healthcare", "Finance", "Energy", "Retail"]
18-
values = [35, 25, 20, 12, 8]
19-
total = sum(values)
17+
# Theme tokens
18+
THEME = os.getenv("ANYPLOT_THEME", "light")
19+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
20+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
21+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
22+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2023

21-
# Calculate angles for donut segments
22-
data = {
23-
"category": categories,
24-
"value": values,
25-
"angle": [v / total * 2 * pi for v in values],
26-
"percentage": [f"{v / total * 100:.1f}%" for v in values],
27-
}
24+
# Okabe-Ito categorical palette (first segment is always brand green)
25+
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00"]
2826

29-
# Cumulative angles for label positioning
30-
cumulative = np.cumsum([0] + data["angle"][:-1]).tolist()
31-
data["start_angle"] = cumulative
32-
data["end_angle"] = np.cumsum(data["angle"]).tolist()
27+
# Data — Annual budget allocation by department (USD thousands)
28+
categories = ["Engineering", "Operations", "Marketing", "Sales", "Support"]
29+
values = [480, 210, 155, 125, 55]
30+
total = sum(values)
3331

34-
# Colors - Python Blue as primary, then colorblind-safe palette
35-
colors = ["#306998", "#FFD43B", "#4ECDC4", "#FF6B6B", "#95E1D3"]
36-
data["color"] = colors
32+
angles = [v / total * 2 * pi for v in values]
33+
percentages = [f"{v / total * 100:.1f}%" for v in values]
3734

38-
source = ColumnDataSource(data=data)
35+
source = ColumnDataSource(
36+
data={"category": categories, "value": values, "angle": angles, "color": OKABE_ITO[: len(categories)]}
37+
)
3938

40-
# Create figure (square format for circular chart)
39+
# Plot — square canvas for circular shapes
4140
p = figure(
4241
width=3600,
4342
height=3600,
44-
title="donut-basic · bokeh · pyplots.ai",
43+
title="Budget by Department · donut-basic · bokeh · anyplot.ai",
4544
toolbar_location=None,
4645
tools="",
47-
x_range=(-1.4, 1.8),
48-
y_range=(-1.3, 1.3),
46+
x_range=(-1.4, 1.4),
47+
y_range=(-1.4, 1.4),
4948
)
5049

51-
# Draw donut using annular wedge
5250
p.annular_wedge(
5351
x=0,
5452
y=0,
55-
inner_radius=0.45,
56-
outer_radius=0.95,
53+
inner_radius=0.55,
54+
outer_radius=1.0,
5755
start_angle=cumsum("angle", include_zero=True),
5856
end_angle=cumsum("angle"),
59-
line_color="white",
60-
line_width=4,
57+
line_color=PAGE_BG,
58+
line_width=6,
6159
fill_color="color",
60+
legend_field="category",
6261
source=source,
6362
)
6463

65-
# Add percentage labels on segments
66-
for pct, start, end, color in zip(data["percentage"], data["start_angle"], data["end_angle"], colors, strict=True):
67-
mid_angle = (start + end) / 2
68-
# Position labels at middle of the ring
69-
label_radius = 0.70
70-
# Adjust angle to start from top and go clockwise
71-
x = label_radius * np.cos(mid_angle - pi / 2 + pi)
72-
y = label_radius * np.sin(mid_angle - pi / 2 + pi)
73-
74-
# Use white text for dark backgrounds, dark for light backgrounds
75-
text_color = "white" if color in ["#306998", "#FF6B6B", "#4ECDC4"] else "#333333"
76-
77-
label = Label(
78-
x=x,
79-
y=y,
80-
text=pct,
81-
text_font_size="26pt",
82-
text_color=text_color,
83-
text_font_style="bold",
64+
# Percentage labels on each segment (Bokeh: angle 0 = 3 o'clock, CCW)
65+
cumulative_starts = np.cumsum([0.0] + angles[:-1])
66+
for pct, start, ang in zip(percentages, cumulative_starts, angles, strict=True):
67+
mid = start + ang / 2
68+
label_radius = 0.78
69+
x = label_radius * cos(mid)
70+
y = label_radius * sin(mid)
71+
p.add_layout(
72+
Label(
73+
x=x,
74+
y=y,
75+
text=pct,
76+
text_font_size="44pt",
77+
text_color="#F0EFE8",
78+
text_font_style="bold",
79+
text_align="center",
80+
text_baseline="middle",
81+
)
82+
)
83+
84+
# Center metric
85+
p.add_layout(
86+
Label(
87+
x=0,
88+
y=0.13,
89+
text="Total budget",
90+
text_font_size="40pt",
91+
text_color=INK_SOFT,
8492
text_align="center",
8593
text_baseline="middle",
8694
)
87-
p.add_layout(label)
88-
89-
# Add center text showing total
90-
center_label = Label(
91-
x=0, y=0.08, text="Total", text_font_size="32pt", text_color="#555555", text_align="center", text_baseline="middle"
92-
)
93-
p.add_layout(center_label)
94-
95-
center_value = Label(
96-
x=0,
97-
y=-0.12,
98-
text=str(total),
99-
text_font_size="48pt",
100-
text_color="#306998",
101-
text_font_style="bold",
102-
text_align="center",
103-
text_baseline="middle",
10495
)
105-
p.add_layout(center_value)
106-
107-
# Add legend entries on the right
108-
legend_x = 1.20
109-
legend_y_start = 0.6
110-
legend_spacing = 0.24
111-
112-
for i, (cat, val, color) in enumerate(zip(categories, values, colors, strict=True)):
113-
y_pos = legend_y_start - i * legend_spacing
114-
# Color box
115-
p.rect(x=legend_x, y=y_pos, width=0.10, height=0.10, fill_color=color, line_color=None)
116-
# Label text
117-
legend_label = Label(
118-
x=legend_x + 0.10,
119-
y=y_pos,
120-
text=f"{cat} ({val}%)",
121-
text_font_size="22pt",
122-
text_color="#333333",
123-
text_align="left",
96+
p.add_layout(
97+
Label(
98+
x=0,
99+
y=-0.10,
100+
text=f"${total:,}K",
101+
text_font_size="96pt",
102+
text_color=INK,
103+
text_font_style="bold",
104+
text_align="center",
124105
text_baseline="middle",
125106
)
126-
p.add_layout(legend_label)
107+
)
108+
109+
# Style — theme-adaptive chrome
110+
p.background_fill_color = PAGE_BG
111+
p.border_fill_color = PAGE_BG
112+
p.outline_line_color = None
113+
114+
p.title.text_font_size = "56pt"
115+
p.title.text_color = INK
116+
p.title.align = "center"
117+
p.title.text_font_style = "normal"
127118

128-
# Style
129-
p.title.text_font_size = "36pt"
130-
p.title.text_color = "#333333"
131119
p.axis.visible = False
132120
p.grid.visible = False
133-
p.outline_line_color = None
134-
p.background_fill_color = None
135-
p.border_fill_color = None
121+
122+
p.legend.background_fill_color = ELEVATED_BG
123+
p.legend.border_line_color = None
124+
p.legend.label_text_color = INK_SOFT
125+
p.legend.label_text_font_size = "36pt"
126+
p.legend.location = "top_right"
127+
p.legend.spacing = 18
128+
p.legend.padding = 24
129+
p.legend.glyph_height = 60
130+
p.legend.glyph_width = 60
136131

137132
# Save
138-
export_png(p, filename="plot.png")
139-
output_file("plot.html")
133+
export_png(p, filename=f"plot-{THEME}.png")
134+
output_file(f"plot-{THEME}.html")
140135
save(p)

plots/donut-basic/implementations/python/plotly.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
donut-basic: Basic Donut Chart
3-
Library: plotly 6.5.0 | Python 3.13.11
4-
Quality: 92/100 | Created: 2025-12-23
3+
Library: plotly 6.7.0 | Python 3.14.4
4+
Quality: 89/100 | Updated: 2026-04-24
55
"""
66

77
import plotly.graph_objects as go
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)

0 commit comments

Comments
 (0)