Skip to content

Commit bfa8042

Browse files
feat(plotnine): implement parliament-basic (#2519)
## Implementation: `parliament-basic` - plotnine Implements the **plotnine** version of `parliament-basic`. **File:** `plots/parliament-basic/implementations/plotnine.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20585401490)* --------- 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 7d134a5 commit bfa8042

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
""" pyplots.ai
2+
parliament-basic: Parliament Seat Chart
3+
Library: plotnine 0.15.2 | Python 3.13.11
4+
Quality: 92/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_rect,
13+
element_text,
14+
geom_point,
15+
ggplot,
16+
guide_legend,
17+
guides,
18+
labs,
19+
scale_color_manual,
20+
theme,
21+
theme_void,
22+
)
23+
24+
25+
# Data - fictional parliament with neutral party names
26+
parties = [
27+
{"party": "Progressive Alliance", "seats": 85, "color": "#306998"},
28+
{"party": "Center Coalition", "seats": 72, "color": "#FFD43B"},
29+
{"party": "Conservative Union", "seats": 68, "color": "#4ECDC4"},
30+
{"party": "Green Future", "seats": 42, "color": "#2ECC71"},
31+
{"party": "Liberal Democrats", "seats": 35, "color": "#9B59B6"},
32+
{"party": "Independent Group", "seats": 18, "color": "#95A5A6"},
33+
]
34+
35+
total_seats = sum(p["seats"] for p in parties)
36+
37+
# Calculate seat positions in semicircular arcs
38+
# Determine optimal row configuration
39+
n_rows = 8
40+
inner_radius = 2.0
41+
row_spacing = 1.0
42+
43+
# Calculate seats per row (more seats in outer rows)
44+
seats_per_row = []
45+
for i in range(n_rows):
46+
radius = inner_radius + i * row_spacing
47+
# Seats roughly proportional to arc length
48+
row_seats = int(np.ceil(radius * 3.5))
49+
seats_per_row.append(row_seats)
50+
51+
# Adjust to match total seats
52+
total_calc = sum(seats_per_row)
53+
scale = total_seats / total_calc
54+
seats_per_row = [max(3, int(round(s * scale))) for s in seats_per_row]
55+
56+
# Fine-tune to exact total
57+
diff = total_seats - sum(seats_per_row)
58+
for i in range(abs(diff)):
59+
idx = i % n_rows
60+
if diff > 0:
61+
seats_per_row[n_rows - 1 - idx] += 1
62+
else:
63+
seats_per_row[n_rows - 1 - idx] -= 1
64+
65+
# Generate all seat positions with angles
66+
all_seats = []
67+
for row_idx, num_seats in enumerate(seats_per_row):
68+
radius = inner_radius + row_idx * row_spacing
69+
angles = np.linspace(np.pi * 0.95, np.pi * 0.05, num_seats)
70+
for angle in angles:
71+
all_seats.append({"angle": angle, "radius": radius, "x": radius * np.cos(angle), "y": radius * np.sin(angle)})
72+
73+
# Sort seats by angle (left to right) for party assignment
74+
all_seats.sort(key=lambda s: -s["angle"])
75+
76+
# Assign parties to seats (left to right across the hemicycle)
77+
seat_data = []
78+
cumulative = 0
79+
for seat in all_seats:
80+
# Find which party this seat belongs to
81+
running_total = 0
82+
for party in parties:
83+
running_total += party["seats"]
84+
if cumulative < running_total:
85+
seat_data.append({"x": seat["x"], "y": seat["y"], "party": party["party"]})
86+
break
87+
cumulative += 1
88+
89+
df = pd.DataFrame(seat_data)
90+
91+
# Create color mapping and legend labels
92+
party_colors = {p["party"]: p["color"] for p in parties}
93+
party_order = [p["party"] for p in parties]
94+
seat_counts = {p["party"]: p["seats"] for p in parties}
95+
legend_labels = {p: f"{p} ({seat_counts[p]})" for p in party_order}
96+
97+
# Convert party to categorical with order
98+
df["party"] = pd.Categorical(df["party"], categories=party_order, ordered=True)
99+
100+
# Create plot
101+
plot = (
102+
ggplot(df, aes(x="x", y="y", color="party"))
103+
+ geom_point(size=5, alpha=0.95)
104+
+ scale_color_manual(values=party_colors, labels=lambda x: [legend_labels.get(p, p) for p in x])
105+
+ labs(title="parliament-basic · plotnine · pyplots.ai", color="")
106+
+ coord_fixed(ratio=1)
107+
+ theme_void()
108+
+ theme(
109+
figure_size=(16, 9),
110+
plot_title=element_text(size=26, ha="center", weight="bold", ma="center"),
111+
legend_title=element_text(size=16),
112+
legend_text=element_text(size=13),
113+
legend_position="bottom",
114+
legend_direction="horizontal",
115+
legend_key_size=20,
116+
plot_background=element_rect(fill="white", color="white"),
117+
panel_background=element_rect(fill="white", color="white"),
118+
plot_margin=0.1,
119+
)
120+
+ guides(color=guide_legend(nrow=2, override_aes={"size": 8}))
121+
)
122+
123+
# Save
124+
plot.save("plot.png", dpi=300, width=16, height=9, verbose=False)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
library: plotnine
2+
specification_id: parliament-basic
3+
created: '2025-12-30T00:03:48Z'
4+
updated: '2025-12-30T00:09:26Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20585401490
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/parliament-basic/plotnine/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/parliament-basic/plotnine/plot_thumb.png
12+
preview_html: null
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent semicircular seat arrangement algorithm with proper left-to-right party
17+
assignment
18+
- Clean legend with seat counts using lambda for label formatting
19+
- Good use of plotnine grammar of graphics with theme_void and coord_fixed
20+
- Proper categorical ordering for consistent legend display
21+
- Uses neutral fictional party names avoiding political controversy
22+
weaknesses:
23+
- Seat markers could be slightly larger for better visibility at full resolution
24+
- Does not include optional majority threshold line mentioned in spec

0 commit comments

Comments
 (0)