{"spec_id":"alluvial-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nalluvial-basic: Basic Alluvial Diagram\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.path import Path\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Background\" + \"Theme-adaptive Chrome\")\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 — use positions 1→N in order\nIMPRINT = [\n    \"#009E73\",  # 1: bluish green (brand)\n    \"#C475FD\",  # 2: vermillion\n    \"#4467A3\",  # 3: blue\n    \"#BD8233\",  # 4: reddish purple\n]\n\n# Data: Voter migration across 4 election cycles\nnp.random.seed(42)\n\ntime_points = [\"2012\", \"2016\", \"2020\", \"2024\"]\ncategories = [\"Party A\", \"Party B\", \"Party C\", \"Independent\"]\ncolors = {cat: IMPRINT[i] for i, cat in enumerate(categories)}\n\n# Node values at each time point (thousands of voters)\nnode_values = {\n    \"2012\": {\"Party A\": 450, \"Party B\": 380, \"Party C\": 120, \"Independent\": 50},\n    \"2016\": {\"Party A\": 420, \"Party B\": 350, \"Party C\": 150, \"Independent\": 80},\n    \"2020\": {\"Party A\": 380, \"Party B\": 320, \"Party C\": 200, \"Independent\": 100},\n    \"2024\": {\"Party A\": 350, \"Party B\": 280, \"Party C\": 250, \"Independent\": 120},\n}\n\n# Flow matrix between consecutive time points\nflows = [\n    # 2012 -> 2016\n    {\n        (\"Party A\", \"Party A\"): 380,\n        (\"Party A\", \"Party B\"): 20,\n        (\"Party A\", \"Party C\"): 30,\n        (\"Party A\", \"Independent\"): 20,\n        (\"Party B\", \"Party A\"): 25,\n        (\"Party B\", \"Party B\"): 310,\n        (\"Party B\", \"Party C\"): 25,\n        (\"Party B\", \"Independent\"): 20,\n        (\"Party C\", \"Party A\"): 10,\n        (\"Party C\", \"Party B\"): 15,\n        (\"Party C\", \"Party C\"): 80,\n        (\"Party C\", \"Independent\"): 15,\n        (\"Independent\", \"Party A\"): 5,\n        (\"Independent\", \"Party B\"): 5,\n        (\"Independent\", \"Party C\"): 15,\n        (\"Independent\", \"Independent\"): 25,\n    },\n    # 2016 -> 2020\n    {\n        (\"Party A\", \"Party A\"): 340,\n        (\"Party A\", \"Party B\"): 15,\n        (\"Party A\", \"Party C\"): 45,\n        (\"Party A\", \"Independent\"): 20,\n        (\"Party B\", \"Party A\"): 20,\n        (\"Party B\", \"Party B\"): 285,\n        (\"Party B\", \"Party C\"): 30,\n        (\"Party B\", \"Independent\"): 15,\n        (\"Party C\", \"Party A\"): 15,\n        (\"Party C\", \"Party B\"): 15,\n        (\"Party C\", \"Party C\"): 105,\n        (\"Party C\", \"Independent\"): 15,\n        (\"Independent\", \"Party A\"): 5,\n        (\"Independent\", \"Party B\"): 5,\n        (\"Independent\", \"Party C\"): 20,\n        (\"Independent\", \"Independent\"): 50,\n    },\n    # 2020 -> 2024\n    {\n        (\"Party A\", \"Party A\"): 310,\n        (\"Party A\", \"Party B\"): 10,\n        (\"Party A\", \"Party C\"): 40,\n        (\"Party A\", \"Independent\"): 20,\n        (\"Party B\", \"Party A\"): 20,\n        (\"Party B\", \"Party B\"): 255,\n        (\"Party B\", \"Party C\"): 30,\n        (\"Party B\", \"Independent\"): 15,\n        (\"Party C\", \"Party A\"): 15,\n        (\"Party C\", \"Party B\"): 10,\n        (\"Party C\", \"Party C\"): 160,\n        (\"Party C\", \"Independent\"): 15,\n        (\"Independent\", \"Party A\"): 5,\n        (\"Independent\", \"Party B\"): 5,\n        (\"Independent\", \"Party C\"): 20,\n        (\"Independent\", \"Independent\"): 70,\n    },\n]\n\n# Plot setup\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Layout parameters\nx_positions = [2.0, 4.0, 6.0, 8.0]\nnode_width = 0.6\ntotal_height = 7.5\nnode_gap = 0.25\n\n# Calculate node positions and store boundaries\nnode_bounds = {}\nfor t_idx, tp in enumerate(time_points):\n    values = [node_values[tp][cat] for cat in categories]\n    total = sum(values)\n    usable_height = total_height - (len(categories) - 1) * node_gap\n    heights = [v / total * usable_height for v in values]\n\n    y = 0.5\n    for c_idx, cat in enumerate(categories):\n        h = heights[c_idx]\n        node_bounds[(tp, cat)] = {\"x\": x_positions[t_idx], \"y_start\": y, \"height\": h}\n        y += h + node_gap\n\n# Draw flows between consecutive time points\nfor t_idx in range(len(time_points) - 1):\n    tp_from = time_points[t_idx]\n    tp_to = time_points[t_idx + 1]\n\n    from_offsets = dict.fromkeys(categories, 0)\n    to_offsets = dict.fromkeys(categories, 0)\n\n    flow_data = flows[t_idx]\n    for from_cat in categories:\n        for to_cat in categories:\n            flow_val = flow_data.get((from_cat, to_cat), 0)\n            if flow_val <= 0:\n                continue\n\n            from_node = node_bounds[(tp_from, from_cat)]\n            to_node = node_bounds[(tp_to, to_cat)]\n\n            from_total = sum(node_values[tp_from].values())\n            to_total = sum(node_values[tp_to].values())\n\n            usable_height = total_height - (len(categories) - 1) * node_gap\n            from_height = flow_val / from_total * usable_height\n            to_height = flow_val / to_total * usable_height\n\n            x0 = from_node[\"x\"] + node_width / 2\n            x1 = to_node[\"x\"] - node_width / 2\n            mid_x = (x0 + x1) / 2\n\n            y0_start = from_node[\"y_start\"] + from_offsets[from_cat]\n            y0_end = y0_start + from_height\n            y1_start = to_node[\"y_start\"] + to_offsets[to_cat]\n            y1_end = y1_start + to_height\n\n            # Draw curved flow band using bezier path\n            verts = [\n                (x0, y0_start),\n                (mid_x, y0_start),\n                (mid_x, y1_start),\n                (x1, y1_start),\n                (x1, y1_end),\n                (mid_x, y1_end),\n                (mid_x, y0_end),\n                (x0, y0_end),\n                (x0, y0_start),\n            ]\n            codes = [\n                Path.MOVETO,\n                Path.CURVE4,\n                Path.CURVE4,\n                Path.CURVE4,\n                Path.LINETO,\n                Path.CURVE4,\n                Path.CURVE4,\n                Path.CURVE4,\n                Path.CLOSEPOLY,\n            ]\n            path = Path(verts, codes)\n            patch = mpatches.PathPatch(path, facecolor=colors[from_cat], edgecolor=\"none\", alpha=0.4)\n            ax.add_patch(patch)\n\n            from_offsets[from_cat] += from_height\n            to_offsets[to_cat] += to_height\n\n# Draw nodes (rectangles)\nfor tp in time_points:\n    for cat in categories:\n        node = node_bounds[(tp, cat)]\n        y_start = node[\"y_start\"]\n        height = node[\"height\"]\n        x = node[\"x\"]\n\n        rect = mpatches.Rectangle(\n            (x - node_width / 2, y_start), node_width, height, facecolor=colors[cat], edgecolor=INK_SOFT, linewidth=1.5\n        )\n        ax.add_patch(rect)\n\n        # Add category label\n        value = node_values[tp][cat]\n        label = f\"{cat}\\n({value}K)\"\n        ax.text(x, y_start + height / 2, label, ha=\"center\", va=\"center\", fontsize=15, fontweight=\"bold\", color=INK)\n\n# Add time point labels\nfor t_idx, tp in enumerate(time_points):\n    ax.text(\n        x_positions[t_idx], total_height + 0.8, tp, ha=\"center\", va=\"bottom\", fontsize=22, fontweight=\"bold\", color=INK\n    )\n\n# Create legend\nlegend_handles = [mpatches.Patch(color=colors[cat], label=cat) for cat in categories]\nleg = ax.legend(handles=legend_handles, loc=\"lower left\", bbox_to_anchor=(0.01, 0.02), fontsize=16, framealpha=0.95)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nleg.get_frame().set_linewidth(1)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\n# Styling\nax.set_xlim(0.8, 9.2)\nax.set_ylim(-0.5, total_height + 1.5)\nax.set_title(\n    \"Voter Migration 2012-2024 · alluvial-basic · matplotlib · anyplot.ai\",\n    fontsize=24,\n    fontweight=\"medium\",\n    color=INK,\n    pad=20,\n)\nax.axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}