{"spec_id":"alluvial-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nalluvial-basic: Basic Alluvial Diagram\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 74/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent current directory from shadowing the plotnine package\nsys.path = [p for p in sys.path if p and not p.endswith(\"implementations\") and not p.endswith(\"python\")]\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_cartesian,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_rect,\n    geom_text,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    theme,\n    theme_minimal,\n)\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Data - Voter migration between parties across 4 election cycles\ntransitions = pd.DataFrame(\n    {\n        \"from_time\": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2],\n        \"to_time\": [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3],\n        \"from_party\": [\n            \"Democrats\",\n            \"Democrats\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Republicans\",\n            \"Republicans\",\n            \"Independent\",\n            \"Independent\",\n            \"Independent\",\n            \"Democrats\",\n            \"Democrats\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Republicans\",\n            \"Republicans\",\n            \"Independent\",\n            \"Independent\",\n            \"Independent\",\n            \"Democrats\",\n            \"Democrats\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Republicans\",\n            \"Republicans\",\n            \"Independent\",\n            \"Independent\",\n            \"Independent\",\n        ],\n        \"to_party\": [\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n            \"Democrats\",\n            \"Republicans\",\n            \"Independent\",\n        ],\n        \"voters\": [38, 4, 3, 3, 36, 2, 2, 3, 9, 35, 5, 5, 4, 33, 4, 4, 4, 6, 32, 6, 7, 5, 30, 6, 6, 5, 3],\n    }\n)\n\n# Party colors using Okabe-Ito palette\nparty_colors = {\n    \"Democrats\": IMPRINT[0],  # #009E73 (bluish green)\n    \"Republicans\": IMPRINT[1],  # #C475FD (vermillion)\n    \"Independent\": IMPRINT[2],  # #4467A3 (blue)\n}\n\nparties = [\"Democrats\", \"Republicans\", \"Independent\"]\ntime_points = [0, 1, 2, 3]\ntime_labels = [\"2016\", \"2018\", \"2020\", \"2022\"]\n\n# Layout parameters\nx_positions = {0: 0.15, 1: 0.38, 2: 0.62, 3: 0.85}\nnode_width = 0.06\nnode_gap = 0.04\ntotal_height = 0.85\ny_start = 0.95\n\n# Calculate node sizes at each time point\nnode_positions = {}\nfor t in time_points:\n    if t == 0:\n        totals = transitions[transitions[\"from_time\"] == t].groupby(\"from_party\")[\"voters\"].sum()\n    else:\n        totals = transitions[transitions[\"to_time\"] == t].groupby(\"to_party\")[\"voters\"].sum()\n\n    total_voters = totals.sum()\n    current_y = y_start\n\n    for party in parties:\n        count = totals.get(party, 0)\n        height = (count / total_voters) * total_height\n\n        node_positions[(t, party)] = {\n            \"x\": x_positions[t],\n            \"y_top\": current_y,\n            \"y_bottom\": current_y - height,\n            \"height\": height,\n            \"count\": count,\n            \"flow_offset\": 0,\n        }\n        current_y = current_y - height - node_gap\n\n# Build node rectangles\nnode_data = []\nfor (t, party), pos in node_positions.items():\n    node_data.append(\n        {\n            \"time\": t,\n            \"party\": party,\n            \"xmin\": pos[\"x\"] - node_width / 2,\n            \"xmax\": pos[\"x\"] + node_width / 2,\n            \"ymin\": pos[\"y_bottom\"],\n            \"ymax\": pos[\"y_top\"],\n            \"label_y\": (pos[\"y_top\"] + pos[\"y_bottom\"]) / 2,\n            \"count\": pos[\"count\"],\n        }\n    )\nnodes_df = pd.DataFrame(node_data)\n\n# Build flow polygons between adjacent time points\nflow_polygons = []\nmin_voters_for_flow = 3\n\nfor _, row in transitions.iterrows():\n    from_t = row[\"from_time\"]\n    to_t = row[\"to_time\"]\n    from_party = row[\"from_party\"]\n    to_party = row[\"to_party\"]\n    voters = row[\"voters\"]\n\n    if voters < min_voters_for_flow:\n        src_pos = node_positions[(from_t, from_party)]\n        tgt_pos = node_positions[(to_t, to_party)]\n\n        total_src = sum(\n            transitions[(transitions[\"from_time\"] == from_t) & (transitions[\"from_party\"] == from_party)][\"voters\"]\n        )\n        flow_height_src = (voters / total_src) * src_pos[\"height\"] if total_src > 0 else 0\n\n        total_tgt = sum(transitions[(transitions[\"to_time\"] == to_t) & (transitions[\"to_party\"] == to_party)][\"voters\"])\n        flow_height_tgt = (voters / total_tgt) * tgt_pos[\"height\"] if total_tgt > 0 else 0\n\n        src_pos[\"flow_offset\"] += flow_height_src\n        tgt_pos[\"flow_offset\"] += flow_height_tgt\n        continue\n\n    src_pos = node_positions[(from_t, from_party)]\n    tgt_pos = node_positions[(to_t, to_party)]\n\n    total_src = sum(\n        transitions[(transitions[\"from_time\"] == from_t) & (transitions[\"from_party\"] == from_party)][\"voters\"]\n    )\n    flow_height_src = (voters / total_src) * src_pos[\"height\"] if total_src > 0 else 0\n\n    total_tgt = sum(transitions[(transitions[\"to_time\"] == to_t) & (transitions[\"to_party\"] == to_party)][\"voters\"])\n    flow_height_tgt = (voters / total_tgt) * tgt_pos[\"height\"] if total_tgt > 0 else 0\n\n    src_y_top = src_pos[\"y_top\"] - src_pos[\"flow_offset\"]\n    src_y_bottom = src_y_top - flow_height_src\n    src_pos[\"flow_offset\"] += flow_height_src\n\n    tgt_y_top = tgt_pos[\"y_top\"] - tgt_pos[\"flow_offset\"]\n    tgt_y_bottom = tgt_y_top - flow_height_tgt\n    tgt_pos[\"flow_offset\"] += flow_height_tgt\n\n    flow_x_left = x_positions[from_t] + node_width / 2\n    flow_x_right = x_positions[to_t] - node_width / 2\n    n_points = 40\n\n    t_param = np.linspace(0, 1, n_points)\n    x_top = flow_x_left + (flow_x_right - flow_x_left) * t_param\n    y_top = src_y_top + (tgt_y_top - src_y_top) * (3 * t_param**2 - 2 * t_param**3)\n\n    x_bottom = flow_x_right + (flow_x_left - flow_x_right) * t_param\n    y_bottom = tgt_y_bottom + (src_y_bottom - tgt_y_bottom) * (3 * t_param**2 - 2 * t_param**3)\n\n    x_polygon = np.concatenate([x_top, x_bottom])\n    y_polygon = np.concatenate([y_top, y_bottom])\n\n    flow_id = f\"{from_t}_{to_t}_{from_party}_{to_party}\"\n    for i in range(len(x_polygon)):\n        flow_polygons.append({\"x\": x_polygon[i], \"y\": y_polygon[i], \"flow_id\": flow_id, \"from_party\": from_party})\n\nflows_df = pd.DataFrame(flow_polygons)\n\n# Create the plot\nplot = (\n    ggplot()\n    + geom_polygon(flows_df, aes(x=\"x\", y=\"y\", group=\"flow_id\", fill=\"from_party\"), alpha=0.5)\n    + geom_rect(\n        nodes_df, aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"party\"), color=\"white\", size=0.5\n    )\n    + geom_text(\n        nodes_df[nodes_df[\"count\"] >= 10],\n        aes(x=(nodes_df[\"xmin\"] + nodes_df[\"xmax\"]) / 2, y=\"label_y\", label=\"count\"),\n        ha=\"center\",\n        va=\"center\",\n        size=12,\n        color=ELEVATED_BG,\n        fontweight=\"bold\",\n    )\n    + scale_fill_manual(\n        values=party_colors, name=\"Party\", breaks=parties, labels=[\"Democrats\", \"Republicans\", \"Independent\"]\n    )\n    + labs(title=\"Voter Migration · alluvial-basic · plotnine · pyplots.ai\", x=\"\", y=\"\")\n    + coord_cartesian(xlim=(0, 1), ylim=(-0.05, 1.05))\n    + theme_minimal()\n    + theme(\n        figure_size=(16, 9),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_grid=element_blank(),\n        plot_title=element_text(size=24, ha=\"center\", weight=\"bold\", color=INK),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_title=element_blank(),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_title=element_text(size=16, weight=\"bold\", color=INK),\n        legend_text=element_text(size=14, color=INK_SOFT),\n        legend_position=\"right\",\n    )\n)\n\n# Add time point labels at bottom\nfor t, label in zip(time_points, time_labels, strict=True):\n    plot = plot + annotate(\n        \"text\", x=x_positions[t], y=0.02, label=label, size=18, color=INK, fontweight=\"bold\", ha=\"center\"\n    )\n\n# Add party name labels on the left side of first column nodes\nfor party in parties:\n    pos = node_positions[(0, party)]\n    label_y = (pos[\"y_top\"] + pos[\"y_bottom\"]) / 2\n    plot = plot + annotate(\n        \"text\",\n        x=x_positions[0] - node_width / 2 - 0.015,\n        y=label_y,\n        label=party,\n        size=11,\n        color=INK,\n        fontweight=\"bold\",\n        ha=\"right\",\n        va=\"center\",\n    )\n\n# Add party name labels on the right side of last column nodes\nfor party in parties:\n    pos = node_positions[(3, party)]\n    label_y = (pos[\"y_top\"] + pos[\"y_bottom\"]) / 2\n    plot = plot + annotate(\n        \"text\",\n        x=x_positions[3] + node_width / 2 + 0.015,\n        y=label_y,\n        label=party,\n        size=11,\n        color=INK,\n        fontweight=\"bold\",\n        ha=\"left\",\n        va=\"center\",\n    )\n\nplot.save(f\"plot-{THEME}.png\", dpi=300, verbose=False)\n"}