|
| 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) |
0 commit comments