{"spec_id":"alluvial-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nalluvial-basic: Basic Alluvial Diagram\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\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 = [\n    \"#009E73\",  # 1. bluish green (brand)\n    \"#C475FD\",  # 2. vermillion\n    \"#4467A3\",  # 3. blue\n    \"#BD8233\",  # 4. reddish purple\n]\n\n# Data: Employment sector transitions across 4 years\nnp.random.seed(42)\n\nyears = [\"2021\", \"2022\", \"2023\", \"2024\"]\nsectors = [\"Technology\", \"Finance\", \"Healthcare\", \"Manufacturing\"]\n\n# Employment counts (thousands) at each time point\nsector_counts = np.array(\n    [\n        [450, 520, 580, 620],  # Technology\n        [280, 290, 310, 315],  # Finance\n        [320, 340, 360, 380],  # Healthcare\n        [250, 230, 210, 190],  # Manufacturing\n    ]\n)\n\n# Flow transitions between consecutive years\nflows = [\n    # 2021 -> 2022\n    {\n        (\"Technology\", \"Technology\"): 400,\n        (\"Technology\", \"Finance\"): 25,\n        (\"Technology\", \"Healthcare\"): 15,\n        (\"Technology\", \"Manufacturing\"): 10,\n        (\"Finance\", \"Technology\"): 20,\n        (\"Finance\", \"Finance\"): 250,\n        (\"Finance\", \"Healthcare\"): 5,\n        (\"Finance\", \"Manufacturing\"): 5,\n        (\"Healthcare\", \"Technology\"): 10,\n        (\"Healthcare\", \"Finance\"): 5,\n        (\"Healthcare\", \"Healthcare\"): 300,\n        (\"Healthcare\", \"Manufacturing\"): 5,\n        (\"Manufacturing\", \"Technology\"): 70,\n        (\"Manufacturing\", \"Finance\"): 10,\n        (\"Manufacturing\", \"Healthcare\"): 40,\n        (\"Manufacturing\", \"Manufacturing\"): 130,\n    },\n    # 2022 -> 2023\n    {\n        (\"Technology\", \"Technology\"): 480,\n        (\"Technology\", \"Finance\"): 15,\n        (\"Technology\", \"Healthcare\"): 20,\n        (\"Technology\", \"Manufacturing\"): 5,\n        (\"Finance\", \"Technology\"): 35,\n        (\"Finance\", \"Finance\"): 245,\n        (\"Finance\", \"Healthcare\"): 5,\n        (\"Finance\", \"Manufacturing\"): 5,\n        (\"Healthcare\", \"Technology\"): 30,\n        (\"Healthcare\", \"Finance\"): 10,\n        (\"Healthcare\", \"Healthcare\"): 295,\n        (\"Healthcare\", \"Manufacturing\"): 5,\n        (\"Manufacturing\", \"Technology\"): 50,\n        (\"Manufacturing\", \"Finance\"): 5,\n        (\"Manufacturing\", \"Healthcare\"): 25,\n        (\"Manufacturing\", \"Manufacturing\"): 150,\n    },\n    # 2023 -> 2024\n    {\n        (\"Technology\", \"Technology\"): 550,\n        (\"Technology\", \"Finance\"): 10,\n        (\"Technology\", \"Healthcare\"): 15,\n        (\"Technology\", \"Manufacturing\"): 5,\n        (\"Finance\", \"Technology\"): 30,\n        (\"Finance\", \"Finance\"): 280,\n        (\"Finance\", \"Healthcare\"): 3,\n        (\"Finance\", \"Manufacturing\"): 2,\n        (\"Healthcare\", \"Technology\"): 50,\n        (\"Healthcare\", \"Finance\"): 8,\n        (\"Healthcare\", \"Healthcare\"): 295,\n        (\"Healthcare\", \"Manufacturing\"): 7,\n        (\"Manufacturing\", \"Technology\"): 25,\n        (\"Manufacturing\", \"Finance\"): 3,\n        (\"Manufacturing\", \"Healthcare\"): 30,\n        (\"Manufacturing\", \"Manufacturing\"): 152,\n    },\n]\n\n# Create mapping from sector to color\nsector_colors = {sector: IMPRINT[i] for i, sector in enumerate(sectors)}\n\n# Create figure\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Calculate positions for each time point\nn_years = len(years)\nx_positions = np.linspace(0, 10, n_years)\nbar_width = 0.6\ntotal_height = 100\n\n# Track positions for each node\nnode_positions = {}\n\n# Draw nodes (stacked bars) at each time point\nfor year_idx, year in enumerate(years):\n    x = x_positions[year_idx]\n    year_total = sector_counts[:, year_idx].sum()\n\n    y_bottom = 0\n    for sector_idx, sector in enumerate(sectors):\n        height = (sector_counts[sector_idx, year_idx] / year_total) * total_height\n        y_top = y_bottom + height\n\n        node_positions[(year_idx, sector)] = (y_bottom, y_top)\n\n        # Draw the bar segment\n        rect = mpatches.Rectangle(\n            (x - bar_width / 2, y_bottom),\n            bar_width,\n            height,\n            facecolor=sector_colors[sector],\n            edgecolor=PAGE_BG,\n            linewidth=1.5,\n        )\n        ax.add_patch(rect)\n\n        # Add sector labels on first and last columns\n        count = sector_counts[sector_idx, year_idx]\n        label_text = f\"{sector} ({count}k)\"\n        font_size = 14\n\n        if year_idx == 0:\n            ax.text(\n                x - bar_width / 2 - 0.2,\n                (y_bottom + y_top) / 2,\n                label_text,\n                ha=\"right\",\n                va=\"center\",\n                fontsize=font_size,\n                fontweight=\"bold\",\n                color=sector_colors[sector],\n            )\n        elif year_idx == n_years - 1:\n            ax.text(\n                x + bar_width / 2 + 0.2,\n                (y_bottom + y_top) / 2,\n                label_text,\n                ha=\"left\",\n                va=\"center\",\n                fontsize=font_size,\n                fontweight=\"bold\",\n                color=sector_colors[sector],\n            )\n\n        y_bottom = y_top\n\n    # Add year labels\n    year_total_display = sector_counts[:, year_idx].sum()\n    ax.text(\n        x,\n        total_height + 3,\n        f\"{year}\\n({year_total_display}k total)\",\n        ha=\"center\",\n        va=\"bottom\",\n        fontsize=18,\n        fontweight=\"bold\",\n        color=INK,\n    )\n\n# Draw flows between consecutive time points\nfor flow_idx, flow_dict in enumerate(flows):\n    x0 = x_positions[flow_idx]\n    x1 = x_positions[flow_idx + 1]\n\n    year0_total = sector_counts[:, flow_idx].sum()\n    year1_total = sector_counts[:, flow_idx + 1].sum()\n\n    source_offsets = {sector: node_positions[(flow_idx, sector)][0] for sector in sectors}\n    target_offsets = {sector: node_positions[(flow_idx + 1, sector)][0] for sector in sectors}\n\n    for (source_sector, target_sector), flow_value in flow_dict.items():\n        if flow_value <= 0:\n            continue\n\n        source_height = (flow_value / year0_total) * total_height\n        target_height = (flow_value / year1_total) * total_height\n\n        y0_bot = source_offsets[source_sector]\n        y0_top = y0_bot + source_height\n        y1_bot = target_offsets[target_sector]\n        y1_top = y1_bot + target_height\n\n        # Draw curved band\n        band_x0 = x0 + bar_width / 2\n        band_x1 = x1 - bar_width / 2\n        cx0 = band_x0 + 0.4 * (band_x1 - band_x0)\n        cx1 = band_x0 + 0.6 * (band_x1 - band_x0)\n\n        verts = [\n            (band_x0, y0_bot),\n            (cx0, y0_bot),\n            (cx1, y1_bot),\n            (band_x1, y1_bot),\n            (band_x1, y1_top),\n            (cx1, y1_top),\n            (cx0, y0_top),\n            (band_x0, y0_top),\n            (band_x0, y0_bot),\n        ]\n        codes = [\n            mpatches.Path.MOVETO,\n            mpatches.Path.CURVE4,\n            mpatches.Path.CURVE4,\n            mpatches.Path.CURVE4,\n            mpatches.Path.LINETO,\n            mpatches.Path.CURVE4,\n            mpatches.Path.CURVE4,\n            mpatches.Path.CURVE4,\n            mpatches.Path.CLOSEPOLY,\n        ]\n        path = mpatches.Path(verts, codes)\n        min_height = min(source_height, target_height)\n        alpha = 0.5 if min_height < 3 else 0.35\n        patch = mpatches.PathPatch(\n            path,\n            facecolor=sector_colors[source_sector],\n            edgecolor=sector_colors[source_sector],\n            linewidth=0.5,\n            alpha=alpha,\n        )\n        ax.add_patch(patch)\n\n        source_offsets[source_sector] = y0_top\n        target_offsets[target_sector] = y1_top\n\n# Add legend\nlegend_patches = [\n    mpatches.Patch(facecolor=sector_colors[sector], edgecolor=PAGE_BG, label=sector) for sector in sectors\n]\nax.legend(\n    handles=legend_patches,\n    loc=\"upper right\",\n    fontsize=14,\n    frameon=True,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n    framealpha=0.95,\n)\n\n# Styling\nax.set_xlim(-2.8, 13.3)\nax.set_ylim(-8, 120)\nax.set_aspect(\"auto\")\n\nax.set_xticks([])\nax.set_yticks([])\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"bottom\"].set_visible(False)\nax.spines[\"left\"].set_visible(False)\n\n# Title\nax.set_title(\"alluvial-basic · seaborn · anyplot.ai\", fontsize=24, fontweight=\"medium\", color=INK, pad=25)\n\n# Subtitle with data context\nax.text(\n    5,\n    -5,\n    \"Employment Sector Transitions 2021-2024 | Values in thousands | Flow width proportional to transitions\",\n    ha=\"center\",\n    va=\"top\",\n    fontsize=14,\n    color=INK_SOFT,\n    style=\"italic\",\n)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}