Skip to content

Commit 3ee48f1

Browse files
feat(plotnine): implement rose-basic (#5600)
## Implementation: `rose-basic` - python/plotnine Implements the **python/plotnine** version of `rose-basic`. **File:** `plots/rose-basic/implementations/python/plotnine.py` **Parent Issue:** #1003 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25151963204)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent bf36445 commit 3ee48f1

2 files changed

Lines changed: 209 additions & 208 deletions

File tree

Lines changed: 49 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
rose-basic: Basic Rose Chart
3-
Library: plotnine 0.15.2 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: plotnine 0.15.3 | Python 3.13.13
4+
Quality: 85/100 | Updated: 2026-04-30
55
"""
66

77
import math
8+
import os
9+
import sys
10+
11+
12+
# Remove this file's own directory from sys.path so that "plotnine" resolves
13+
# to the installed library rather than this file (Python 3.13 naming-collision fix).
14+
_script_dir = os.path.dirname(os.path.abspath(__file__))
15+
sys.path = [p for p in sys.path if os.path.realpath(p) != os.path.realpath(_script_dir)]
816

917
import numpy as np
1018
import pandas as pd
19+
from matplotlib import colormaps
20+
from matplotlib.colors import Normalize, to_hex
1121
from plotnine import (
1222
aes,
1323
element_blank,
24+
element_rect,
1425
element_text,
1526
geom_line,
1627
geom_polygon,
@@ -24,133 +35,97 @@
2435
)
2536

2637

38+
# Theme tokens
39+
THEME = os.getenv("ANYPLOT_THEME", "light")
40+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
41+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
42+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
43+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
44+
2745
# Data - Monthly rainfall (mm) for a temperate climate
2846
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
2947
rainfall = [78, 62, 55, 48, 52, 68, 82, 85, 72, 88, 95, 82]
3048

3149
n = len(months)
3250

51+
# Viridis colors per month based on rainfall value (continuous colormap for value-encoded data)
52+
_norm = Normalize(vmin=min(rainfall), vmax=max(rainfall))
53+
_cmap = colormaps["viridis"]
54+
month_colors = {month: to_hex(_cmap(_norm(val))) for month, val in zip(months, rainfall, strict=True)}
55+
3356
# Create wedge polygons for each month
34-
# Each wedge is a triangle from center to the arc
57+
n_arc_points = 30
3558
wedge_rows = []
36-
n_arc_points = 30 # Points along the arc for smooth edges
37-
3859
for i, (month, value) in enumerate(zip(months, rainfall, strict=True)):
39-
# Starting angle (12 o'clock = pi/2, going clockwise)
40-
# Adjust so January is at top (index 0 starts at pi/2)
41-
# Use negative direction for clockwise motion
4260
start_angle = math.pi / 2 - (i * 2 * math.pi / n)
4361
end_angle = math.pi / 2 - ((i + 1) * 2 * math.pi / n)
44-
45-
# Small gap between wedges
4662
gap = 0.02
4763
start_angle += gap
4864
end_angle -= gap
49-
50-
# Center point
5165
wedge_rows.append({"x": 0, "y": 0, "month": month, "order": 0})
52-
53-
# Arc points
54-
arc_angles = np.linspace(start_angle, end_angle, n_arc_points)
55-
for j, angle in enumerate(arc_angles):
56-
x = value * math.cos(angle)
57-
y = value * math.sin(angle)
58-
wedge_rows.append({"x": x, "y": y, "month": month, "order": j + 1})
59-
60-
# Close back to center
66+
for j, angle in enumerate(np.linspace(start_angle, end_angle, n_arc_points)):
67+
wedge_rows.append({"x": value * math.cos(angle), "y": value * math.sin(angle), "month": month, "order": j + 1})
6168
wedge_rows.append({"x": 0, "y": 0, "month": month, "order": n_arc_points + 1})
6269

6370
df = pd.DataFrame(wedge_rows)
64-
65-
# Preserve month order
6671
df["month"] = pd.Categorical(df["month"], categories=months, ordered=True)
6772

68-
# Colors - gradient based on rainfall values
69-
# Using Python Blue (#306998) as base with varying intensity
70-
base_blue = np.array([48, 105, 152]) # RGB for #306998
71-
python_yellow = np.array([255, 212, 59]) # RGB for #FFD43B
72-
73-
# Normalize rainfall for color mapping
74-
min_val, max_val = min(rainfall), max(rainfall)
75-
colors = []
76-
for value in rainfall:
77-
t = (value - min_val) / (max_val - min_val) # 0 to 1
78-
# Interpolate from blue to yellow
79-
rgb = (1 - t) * base_blue + t * python_yellow
80-
colors.append(f"#{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}")
81-
82-
color_dict = dict(zip(months, colors, strict=True))
83-
84-
# Create radial gridlines (circles at 20, 40, 60, 80, 100)
73+
# Radial gridlines (circles)
8574
grid_rows = []
8675
grid_angles = np.linspace(0, 2 * math.pi, 101)
8776
for radius in [20, 40, 60, 80, 100]:
8877
for angle in grid_angles:
8978
grid_rows.append({"x": radius * math.cos(angle), "y": radius * math.sin(angle), "radius": radius})
90-
9179
grid_df = pd.DataFrame(grid_rows)
9280

93-
# Create spoke lines (one for each month boundary)
81+
# Spoke lines from center to edge
9482
spoke_rows = []
9583
for i in range(n):
9684
angle = math.pi / 2 - (i * 2 * math.pi / n)
9785
spoke_rows.append({"x": 0, "y": 0, "spoke_id": i})
9886
spoke_rows.append({"x": 105 * math.cos(angle), "y": 105 * math.sin(angle), "spoke_id": i})
99-
10087
spoke_df = pd.DataFrame(spoke_rows)
10188

102-
# Create month labels positioned outside the chart
89+
# Month labels positioned outside the chart
10390
label_rows = []
10491
for i, month in enumerate(months):
105-
# Center angle of each month's wedge
10692
center_angle = math.pi / 2 - ((i + 0.5) * 2 * math.pi / n)
107-
label_rows.append({"label": month, "x": 115 * math.cos(center_angle), "y": 115 * math.sin(center_angle)})
108-
93+
label_rows.append({"label": month, "x": 120 * math.cos(center_angle), "y": 120 * math.sin(center_angle)})
10994
label_df = pd.DataFrame(label_rows)
11095

111-
# Create value labels on gridlines
112-
value_label_rows = []
113-
for radius in [20, 40, 60, 80, 100]:
114-
value_label_rows.append({"label": str(radius), "x": 5, "y": radius + 3})
115-
96+
# Value labels positioned along the top-right spoke
97+
value_label_rows = [{"label": str(r), "x": 6, "y": r + 4} for r in [20, 40, 60, 80, 100]]
11698
value_label_df = pd.DataFrame(value_label_rows)
11799

118100
# Plot
119101
plot = (
120102
ggplot()
121-
# Gridlines (circles)
122103
+ geom_line(
123-
aes(x="x", y="y", group="radius"), data=grid_df, color="#CCCCCC", size=0.5, alpha=0.6, linetype="dashed"
104+
aes(x="x", y="y", group="radius"), data=grid_df, color=INK_SOFT, size=0.4, alpha=0.15, linetype="dashed"
124105
)
125-
# Spoke lines
126-
+ geom_line(aes(x="x", y="y", group="spoke_id"), data=spoke_df, color="#DDDDDD", size=0.3, alpha=0.5)
127-
# Rose wedges
128-
+ geom_polygon(aes(x="x", y="y", fill="month", group="month"), data=df, color="#2C3E50", size=0.3, alpha=0.85)
129-
# Month labels
130-
+ geom_text(aes(x="x", y="y", label="label"), data=label_df, size=14, fontweight="bold", color="#333333")
131-
# Value labels
132-
+ geom_text(aes(x="x", y="y", label="label"), data=value_label_df, size=10, color="#666666")
133-
# Colors
134-
+ scale_fill_manual(values=color_dict)
135-
# Axis scaling
136-
+ scale_x_continuous(limits=(-135, 135))
137-
+ scale_y_continuous(limits=(-135, 135))
138-
# Labels and title
139-
+ labs(title="Monthly Rainfall (mm) · rose-basic · plotnine · pyplots.ai")
140-
# Theme for clean rose chart appearance
106+
+ geom_line(aes(x="x", y="y", group="spoke_id"), data=spoke_df, color=INK_SOFT, size=0.3, alpha=0.12)
107+
+ geom_polygon(aes(x="x", y="y", fill="month", group="month"), data=df, color=PAGE_BG, size=0.3, alpha=0.88)
108+
+ geom_text(aes(x="x", y="y", label="label"), data=label_df, size=14, fontweight="bold", color=INK)
109+
+ geom_text(aes(x="x", y="y", label="label"), data=value_label_df, size=10, color=INK_MUTED)
110+
+ scale_fill_manual(values=month_colors)
111+
+ scale_x_continuous(limits=(-142, 142))
112+
+ scale_y_continuous(limits=(-142, 142))
113+
+ labs(title="Monthly Rainfall (mm) · rose-basic · plotnine · anyplot.ai")
141114
+ theme(
142115
figure_size=(12, 12),
143-
plot_title=element_text(size=22, ha="center"),
116+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
117+
panel_background=element_rect(fill=PAGE_BG),
118+
plot_title=element_text(size=22, ha="center", color=INK),
144119
axis_title=element_blank(),
145120
axis_text=element_blank(),
146121
axis_ticks=element_blank(),
147122
axis_line=element_blank(),
148123
panel_grid_major=element_blank(),
149124
panel_grid_minor=element_blank(),
150-
panel_background=element_blank(),
125+
panel_border=element_blank(),
151126
legend_position="none",
152127
)
153128
)
154129

155130
# Save
156-
plot.save("plot.png", dpi=300)
131+
plot.save(f"plot-{THEME}.png", dpi=300)

0 commit comments

Comments
 (0)