Skip to content

Commit 34e5fbe

Browse files
feat(altair): implement parliament-basic (#2514)
## Implementation: `parliament-basic` - altair Implements the **altair** version of `parliament-basic`. **File:** `plots/parliament-basic/implementations/altair.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20585401343)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent ed602e3 commit 34e5fbe

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
""" pyplots.ai
2+
parliament-basic: Parliament Seat Chart
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 90/100 | Created: 2025-12-30
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
np.random.seed(42)
13+
14+
# Data: Fictional parliament with 300 seats
15+
parties = [
16+
{"party": "Progressive", "seats": 95, "color": "#306998"},
17+
{"party": "Conservative", "seats": 82, "color": "#FFD43B"},
18+
{"party": "Green", "seats": 45, "color": "#2CA02C"},
19+
{"party": "Liberal", "seats": 38, "color": "#FF7F0E"},
20+
{"party": "Social Dem.", "seats": 28, "color": "#9467BD"},
21+
{"party": "Independent", "seats": 12, "color": "#8C564B"},
22+
]
23+
24+
total_seats = sum(p["seats"] for p in parties)
25+
26+
# Generate seat positions in semicircular arcs
27+
# Calculate number of rows based on total seats (more seats = more rows)
28+
n_rows = 5 # For 300 seats
29+
inner_radius = 3.0
30+
row_spacing = 1.0
31+
32+
# Calculate seats per row (more seats in outer rows)
33+
row_weights = [(inner_radius + i * row_spacing) for i in range(n_rows)]
34+
total_weight = sum(row_weights)
35+
seats_per_row = [max(1, int(total_seats * w / total_weight)) for w in row_weights]
36+
37+
# Adjust to match total exactly
38+
diff = total_seats - sum(seats_per_row)
39+
for i in range(abs(diff)):
40+
if diff > 0:
41+
seats_per_row[-(i % n_rows) - 1] += 1
42+
else:
43+
seats_per_row[i % n_rows] -= 1
44+
45+
# Generate positions for each seat
46+
all_seats = []
47+
seat_idx = 0
48+
49+
for row_idx, n_seats_in_row in enumerate(seats_per_row):
50+
radius = inner_radius + row_idx * row_spacing
51+
angles = np.linspace(np.pi, 0, n_seats_in_row)
52+
for angle in angles:
53+
x = radius * np.cos(angle)
54+
y = radius * np.sin(angle)
55+
all_seats.append({"x": x, "y": y, "seat_idx": seat_idx, "angle": angle})
56+
seat_idx += 1
57+
58+
# Sort seats by angle descending (pi to 0 = left to right)
59+
seats_sorted = sorted(all_seats, key=lambda s: -s["angle"])
60+
61+
# Assign parties to seats in order
62+
seat_data = []
63+
current_idx = 0
64+
for party_info in parties:
65+
party_name = party_info["party"]
66+
party_color = party_info["color"]
67+
n_seats = party_info["seats"]
68+
for _ in range(n_seats):
69+
if current_idx < len(seats_sorted):
70+
seat = seats_sorted[current_idx]
71+
seat_data.append(
72+
{"x": seat["x"], "y": seat["y"], "party": party_name, "color": party_color, "seats_count": n_seats}
73+
)
74+
current_idx += 1
75+
76+
df = pd.DataFrame(seat_data)
77+
78+
# Create color scale from party data
79+
party_colors = {p["party"]: p["color"] for p in parties}
80+
color_domain = list(party_colors.keys())
81+
color_range = list(party_colors.values())
82+
83+
# Create legend labels with seat counts
84+
legend_labels = {p["party"]: f"{p['party']} ({p['seats']})" for p in parties}
85+
df["party_label"] = df["party"].map(legend_labels)
86+
87+
# Create the parliament chart
88+
chart = (
89+
alt.Chart(df)
90+
.mark_circle(size=250, opacity=0.9)
91+
.encode(
92+
x=alt.X("x:Q", axis=None),
93+
y=alt.Y("y:Q", axis=None, scale=alt.Scale(domain=[-0.5, inner_radius + n_rows * row_spacing])),
94+
color=alt.Color(
95+
"party_label:N",
96+
scale=alt.Scale(domain=[legend_labels[p["party"]] for p in parties], range=color_range),
97+
legend=alt.Legend(
98+
title="Parties",
99+
titleFontSize=20,
100+
labelFontSize=16,
101+
labelLimit=200,
102+
symbolSize=300,
103+
orient="bottom",
104+
direction="horizontal",
105+
columns=3,
106+
titleOrient="top",
107+
titleAnchor="middle",
108+
),
109+
),
110+
tooltip=["party:N", "seats_count:Q"],
111+
)
112+
.properties(
113+
width=1500, height=800, title=alt.Title("parliament-basic · altair · pyplots.ai", fontSize=28, anchor="middle")
114+
)
115+
.configure_view(strokeWidth=0)
116+
.configure_legend(padding=20, offset=0)
117+
)
118+
119+
# Save as PNG and HTML
120+
chart.save("plot.png", scale_factor=3.0)
121+
chart.save("plot.html")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: altair
2+
specification_id: parliament-basic
3+
created: '2025-12-30T00:03:33Z'
4+
updated: '2025-12-30T00:20:19Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20585401343
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/parliament-basic/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/parliament-basic/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/parliament-basic/altair/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Excellent semicircular parliament visualization with proper arc arrangement
17+
- Seats correctly distributed with more seats in outer rows (realistic parliament
18+
layout)
19+
- Well-configured legend with party names and seat counts
20+
- Good use of Altair declarative grammar with custom color scales
21+
- Proper title format and good color accessibility
22+
weaknesses:
23+
- Large gap between the parliament chart and the legend creates visual disconnection
24+
- Uses political party theme which content policy suggests avoiding for neutral
25+
topics

0 commit comments

Comments
 (0)