{"spec_id":"parliament-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nparliament-basic: Parliament Seat Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 82/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nimport numpy as np\n\n\n# Workaround: remove the script directory from sys.path to avoid shadowing bokeh module\nscript_dir = os.path.dirname(__file__)\nsys.path = [p for p in sys.path if p != script_dir and os.path.abspath(p) != os.path.abspath(script_dir)]\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette (first series always #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data - Political parties left-to-right by political spectrum\n# Left parties first, then center, then right\nparties = [\n    \"Green Coalition\",\n    \"Labor Party\",\n    \"Progressive Alliance\",\n    \"Center Party\",\n    \"Liberal Democrats\",\n    \"Conservative Union\",\n]\nseats = [42, 52, 85, 58, 67, 96]\ntotal_seats = sum(seats)\n\n# Calculate seat positions in semicircular arcs\nrows = 8\nbase_radius = 0.35\nradius_step = 0.12\n\n# Calculate seats per row based on arc length (outer rows have more seats)\nseats_per_row = []\nfor i in range(rows):\n    radius = base_radius + i * radius_step\n    row_capacity = int(radius * 30)\n    seats_per_row.append(row_capacity)\n\n# Normalize to match total seats\ntotal_capacity = sum(seats_per_row)\nseats_per_row = [int(s * total_seats / total_capacity) for s in seats_per_row]\n\n# Adjust to match total exactly\ndiff = total_seats - sum(seats_per_row)\nfor i in range(abs(diff)):\n    idx = (rows - 1 - i) % rows if diff > 0 else i % rows\n    seats_per_row[idx] += 1 if diff > 0 else -1\n\n# Build party assignment - each seat gets a party in order\nparty_assignments = []\nfor i, (party, seat_count) in enumerate(zip(parties, seats, strict=True)):\n    party_assignments.extend([(party, IMPRINT[i % len(IMPRINT)])] * seat_count)\n\n# Generate seat positions - fill row by row\nx_positions = []\ny_positions = []\nseat_colors = []\nseat_parties = []\n\nseat_idx = 0\nfor row in range(rows):\n    row_seat_count = seats_per_row[row]\n    if row_seat_count <= 0 or seat_idx >= total_seats:\n        continue\n\n    radius = base_radius + row * radius_step\n    angles = np.linspace(np.pi - 0.05, 0.05, row_seat_count)\n\n    for angle in angles:\n        if seat_idx >= total_seats:\n            break\n        x = radius * np.cos(angle)\n        y = radius * np.sin(angle)\n        x_positions.append(x)\n        y_positions.append(y)\n        party, color = party_assignments[seat_idx]\n        seat_colors.append(color)\n        seat_parties.append(party)\n        seat_idx += 1\n\n# Create data source\nsource = ColumnDataSource(data={\"x\": x_positions, \"y\": y_positions, \"color\": seat_colors, \"party\": seat_parties})\n\n# Create figure - landscape format to accommodate legend below\np = figure(\n    width=4800,\n    height=3200,\n    title=\"parliament-basic · bokeh · anyplot.ai\",\n    tools=\"\",\n    toolbar_location=None,\n    x_range=(-1.35, 1.35),\n    y_range=(-0.8, 1.3),\n)\n\n# Plot seats - increased size for better canvas visibility\np.scatter(x=\"x\", y=\"y\", source=source, color=\"color\", size=40, alpha=0.9, line_color=PAGE_BG, line_width=2)\n\n# Create legend with party colors and counts below the chart\nlegend_y_pos = -0.7\nfor i, (party, seat_count, color) in enumerate(zip(parties, seats, IMPRINT[: len(parties)], strict=True)):\n    # Horizontal spacing for legend items\n    x_offset = -1.1 + i * 0.37\n\n    # Plot legend dot\n    legend_dot = ColumnDataSource(data={\"x\": [x_offset], \"y\": [legend_y_pos]})\n    p.scatter(x=\"x\", y=\"y\", source=legend_dot, color=color, size=24, alpha=0.9, line_color=PAGE_BG, line_width=2)\n\n    # Add legend text\n    p.text(\n        x=[x_offset],\n        y=[legend_y_pos - 0.15],\n        text=[f\"{party}\\n({seat_count})\"],\n        text_align=\"center\",\n        text_font_size=\"16pt\",\n        text_color=INK_SOFT,\n    )\n\n# Add majority threshold annotation\nmajority = total_seats // 2 + 1\np.text(\n    x=[0],\n    y=[-0.18],\n    text=[f\"Majority threshold: {majority} seats (Total: {total_seats})\"],\n    text_align=\"center\",\n    text_font_size=\"28pt\",\n    text_color=INK_SOFT,\n)\n\n# Style the plot\np.title.text_font_size = \"36pt\"\np.title.text_color = INK\np.title.align = \"center\"\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\np.outline_line_color = INK_SOFT\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome via Selenium\nW, H = 4800, 3200  # Match figure dimensions\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\n\ntry:\n    driver = webdriver.Chrome(options=opts)\n    driver.set_window_size(W, H)\n    html_path = Path(f\"plot-{THEME}.html\").resolve()\n    driver.get(f\"file://{html_path}\")\n    time.sleep(2)  # Let bokeh's JS render the canvas\n    driver.save_screenshot(f\"plot-{THEME}.png\")\nfinally:\n    try:\n        driver.quit()\n    except Exception:\n        pass\n"}