Skip to content

Commit bed9d24

Browse files
feat(plotnine): implement polar-bar (#2766)
## Implementation: `polar-bar` - plotnine Implements the **plotnine** version of `polar-bar`. **File:** `plots/polar-bar/implementations/plotnine.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20601058272)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent d7cdd4a commit bed9d24

2 files changed

Lines changed: 197 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
""" pyplots.ai
2+
polar-bar: Polar Bar Chart (Wind Rose)
3+
Library: plotnine 0.15.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from plotnine import (
10+
aes,
11+
coord_fixed,
12+
element_blank,
13+
element_text,
14+
geom_path,
15+
geom_polygon,
16+
geom_segment,
17+
geom_text,
18+
ggplot,
19+
labs,
20+
scale_fill_manual,
21+
scale_x_continuous,
22+
scale_y_continuous,
23+
theme,
24+
)
25+
26+
27+
# Data - Wind direction frequencies (8 compass directions)
28+
np.random.seed(42)
29+
directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
30+
direction_angles = [0, 45, 90, 135, 180, 225, 270, 315] # Degrees from North, clockwise
31+
frequencies = [15, 8, 12, 5, 18, 22, 10, 7]
32+
33+
# Create polygons for each bar (wedge shape)
34+
# Each bar spans ±22.5 degrees (45 degrees total width for 8 directions)
35+
bar_half_width = 18 # degrees, slightly less than 22.5 for visual gap
36+
bar_rows = []
37+
bar_id = 0
38+
39+
for direction, angle, freq in zip(directions, direction_angles, frequencies, strict=True):
40+
# Create wedge polygon points (from center outward)
41+
# Start angle and end angle
42+
start_angle = angle - bar_half_width
43+
end_angle = angle + bar_half_width
44+
45+
# Create polygon vertices: center -> arc at radius -> back to center
46+
# Convert to math convention: 0° at East, CCW positive
47+
# Polar convention: 0° at North (top), CW positive
48+
49+
# Center point
50+
points = [(0, 0)]
51+
52+
# Arc points at the outer radius
53+
arc_angles = np.linspace(start_angle, end_angle, 10)
54+
for a in arc_angles:
55+
# Convert from compass (N=0, CW) to math (E=0, CCW)
56+
theta = np.radians(90 - a)
57+
x = freq * np.cos(theta)
58+
y = freq * np.sin(theta)
59+
points.append((x, y))
60+
61+
# Close back to center
62+
points.append((0, 0))
63+
64+
# Add all points to dataframe
65+
for i, (x, y) in enumerate(points):
66+
bar_rows.append({"x": x, "y": y, "direction": direction, "bar_id": bar_id, "order": i})
67+
68+
bar_id += 1
69+
70+
bar_df = pd.DataFrame(bar_rows)
71+
72+
# Create circular gridlines (concentric circles at magnitude intervals)
73+
grid_rows = []
74+
grid_angles = np.linspace(0, 2 * np.pi, 101)
75+
max_radius = max(frequencies) + 5
76+
grid_radii = [5, 10, 15, 20, 25]
77+
78+
for radius in grid_radii:
79+
if radius <= max_radius:
80+
for angle in grid_angles:
81+
grid_rows.append({"x": radius * np.cos(angle), "y": radius * np.sin(angle), "radius": radius})
82+
83+
grid_df = pd.DataFrame(grid_rows)
84+
85+
# Create radial spokes (8 compass directions)
86+
spoke_rows = []
87+
for deg in direction_angles:
88+
angle = np.radians(90 - deg)
89+
spoke_rows.append(
90+
{"x1": 0, "y1": 0, "x2": (max_radius + 2) * np.cos(angle), "y2": (max_radius + 2) * np.sin(angle)}
91+
)
92+
93+
spoke_df = pd.DataFrame(spoke_rows)
94+
95+
# Create compass direction labels
96+
label_rows = []
97+
label_radius = max_radius + 5
98+
for deg, lbl in zip(direction_angles, directions, strict=True):
99+
angle = np.radians(90 - deg)
100+
label_rows.append({"label": lbl, "x": label_radius * np.cos(angle), "y": label_radius * np.sin(angle)})
101+
102+
label_df = pd.DataFrame(label_rows)
103+
104+
# Create radius labels (frequency values) - positioned along NNE axis
105+
radius_labels = []
106+
label_angle = np.radians(90 - 22.5) # NNE direction
107+
for r in [5, 10, 15, 20]:
108+
if r <= max_radius:
109+
radius_labels.append({"label": f"{r}", "x": r * np.cos(label_angle) + 1.5, "y": r * np.sin(label_angle)})
110+
111+
radius_label_df = pd.DataFrame(radius_labels)
112+
113+
# Color palette - alternating Python Blue and Yellow
114+
colors = {
115+
"N": "#306998",
116+
"NE": "#FFD43B",
117+
"E": "#306998",
118+
"SE": "#FFD43B",
119+
"S": "#306998",
120+
"SW": "#FFD43B",
121+
"W": "#306998",
122+
"NW": "#FFD43B",
123+
}
124+
125+
# Plot
126+
plot = (
127+
ggplot()
128+
# Circular gridlines (frequency circles)
129+
+ geom_path(
130+
aes(x="x", y="y", group="radius"), data=grid_df, color="#CCCCCC", size=0.5, alpha=0.5, linetype="dashed"
131+
)
132+
# Radial spokes (direction lines)
133+
+ geom_segment(aes(x="x1", y="y1", xend="x2", yend="y2"), data=spoke_df, color="#CCCCCC", size=0.5, alpha=0.5)
134+
# Bar wedges (wind rose bars)
135+
+ geom_polygon(
136+
aes(x="x", y="y", group="bar_id", fill="direction"),
137+
data=bar_df,
138+
color="#333333",
139+
size=0.5,
140+
alpha=0.85,
141+
show_legend=False,
142+
)
143+
# Compass direction labels
144+
+ geom_text(aes(x="x", y="y", label="label"), data=label_df, size=16, color="#333333", fontweight="bold")
145+
# Frequency labels
146+
+ geom_text(aes(x="x", y="y", label="label"), data=radius_label_df, size=10, color="#666666", ha="left")
147+
# Custom colors for directions
148+
+ scale_fill_manual(values=colors)
149+
# Equal coordinate system for proper circles
150+
+ coord_fixed(ratio=1)
151+
# Axis scaling with padding
152+
+ scale_x_continuous(limits=(-35, 35))
153+
+ scale_y_continuous(limits=(-35, 35))
154+
# Title
155+
+ labs(title="Wind Direction Frequency · polar-bar · plotnine · pyplots.ai")
156+
# Clean polar-style theme
157+
+ theme(
158+
figure_size=(12, 12),
159+
plot_title=element_text(size=24, ha="center"),
160+
axis_title=element_blank(),
161+
axis_text=element_blank(),
162+
axis_ticks=element_blank(),
163+
axis_line=element_blank(),
164+
panel_grid_major=element_blank(),
165+
panel_grid_minor=element_blank(),
166+
panel_background=element_blank(),
167+
plot_background=element_blank(),
168+
)
169+
)
170+
171+
# Save
172+
plot.save("plot.png", dpi=300, verbose=False)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: plotnine
2+
specification_id: polar-bar
3+
created: '2025-12-30T16:31:23Z'
4+
updated: '2025-12-30T16:43:38Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20601058272
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 0.15.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/polar-bar/plotnine/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/polar-bar/plotnine/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent manual construction of polar coordinate system using geom_polygon wedges
17+
- 'Clean layered composition: gridlines → spokes → wedges → labels'
18+
- Proper compass angle convention (N=0°, clockwise) correctly converted to Cartesian
19+
coordinates
20+
- Appropriate use of 1:1 aspect ratio for circular visualization
21+
- Professional appearance with subtle gridlines and balanced colors
22+
weaknesses:
23+
- Figure size is 12x12 instead of the recommended 16x9 landscape (though square
24+
is acceptable for polar plots)
25+
- Missing frequency unit in title or labels (e.g., days or %)

0 commit comments

Comments
 (0)