{"spec_id":"sankey-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nsankey-basic: Basic Sankey Diagram\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-07-25\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_polygon,\n    geom_rect,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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# Imprint palette for source categories (canonical order, first = #009E73)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Energy flow data: sources -> sectors (realistic energy distribution)\nflows = [\n    (\"Coal\", \"Industrial\", 28),\n    (\"Coal\", \"Residential\", 8),\n    (\"Natural Gas\", \"Industrial\", 22),\n    (\"Natural Gas\", \"Residential\", 35),\n    (\"Natural Gas\", \"Commercial\", 18),\n    (\"Nuclear\", \"Industrial\", 16),\n    (\"Nuclear\", \"Commercial\", 12),\n    (\"Renewable\", \"Residential\", 14),\n    (\"Renewable\", \"Commercial\", 10),\n    (\"Renewable\", \"Industrial\", 6),\n]\n\nsources = [\"Coal\", \"Natural Gas\", \"Nuclear\", \"Renewable\"]\ntargets = [\"Industrial\", \"Residential\", \"Commercial\"]\nsource_color_map = dict(zip(sources, IMPRINT, strict=True))\n\n# The single largest flow gets a callout to give the diagram a point of view\ndominant_source, dominant_target, dominant_value = max(flows, key=lambda f: f[2])\n\n# Calculate totals for each node\nsource_totals = {}\nfor src, _, val in flows:\n    source_totals[src] = source_totals.get(src, 0) + val\n\ntarget_totals = {}\nfor _, tgt, val in flows:\n    target_totals[tgt] = target_totals.get(tgt, 0) + val\n\n# Layout parameters\ntotal_flow = sum(v for _, _, v in flows)\nnode_gap = 0.04\nx_left = 0.18\nx_right = 0.82\n\n# Position source nodes (left side)\nsource_positions = {}\ny_offset = 0.05\nfor src in sources:\n    height = source_totals.get(src, 0) / total_flow * 0.85\n    source_positions[src] = {\"y0\": y_offset, \"y1\": y_offset + height, \"x\": x_left}\n    y_offset += height + node_gap\n\n# Position target nodes (right side)\ntarget_positions = {}\ny_offset = 0.05\nfor tgt in targets:\n    height = target_totals.get(tgt, 0) / total_flow * 0.85\n    target_positions[tgt] = {\"y0\": y_offset, \"y1\": y_offset + height, \"x\": x_right}\n    y_offset += height + node_gap\n\n# Track flow offsets within each node\nsource_offsets = dict.fromkeys(sources, 0)\ntarget_offsets = dict.fromkeys(targets, 0)\n\n# Build flow polygons with smooth cubic bezier curves\nflow_data = []\n\nfor src, tgt, val in flows:\n    flow_height = val / total_flow * 0.85\n\n    src_y0 = source_positions[src][\"y0\"] + source_offsets[src]\n    src_y1 = src_y0 + flow_height\n    source_offsets[src] += flow_height\n\n    tgt_y0 = target_positions[tgt][\"y0\"] + target_offsets[tgt]\n    tgt_y1 = tgt_y0 + flow_height\n    target_offsets[tgt] += flow_height\n\n    n_points = 40\n    x_vals_top, y_vals_top = [], []\n    x_vals_bottom, y_vals_bottom = [], []\n\n    for i in range(n_points + 1):\n        t = i / n_points\n        x = x_left + t * (x_right - x_left)\n        ease = t * t * (3 - 2 * t)\n        x_vals_top.append(x)\n        y_vals_top.append(src_y1 + ease * (tgt_y1 - src_y1))\n        x_vals_bottom.append(x)\n        y_vals_bottom.append(src_y0 + ease * (tgt_y0 - src_y0))\n\n    x_polygon = x_vals_top + x_vals_bottom[::-1]\n    y_polygon = y_vals_top + y_vals_bottom[::-1]\n    is_dominant = src == dominant_source and tgt == dominant_target and val == dominant_value\n\n    for x, y in zip(x_polygon, y_polygon, strict=False):\n        flow_data.append(\n            {\n                \"x\": x,\n                \"y\": y,\n                \"flow_id\": f\"{src} → {tgt}\",\n                \"source\": src,\n                \"target\": tgt,\n                \"value\": val,\n                \"highlight\": is_dominant,\n            }\n        )\n\ndf_flows = pd.DataFrame(flow_data)\n\n# Build node rectangles\nnode_rects = []\nnode_width = 0.025\n\nfor src in sources:\n    pos = source_positions[src]\n    node_rects.append(\n        {\n            \"xmin\": pos[\"x\"] - node_width / 2,\n            \"xmax\": pos[\"x\"] + node_width / 2,\n            \"ymin\": pos[\"y0\"],\n            \"ymax\": pos[\"y1\"],\n            \"name\": src,\n            \"total\": source_totals[src],\n        }\n    )\n\nfor tgt in targets:\n    pos = target_positions[tgt]\n    node_rects.append(\n        {\n            \"xmin\": pos[\"x\"] - node_width / 2,\n            \"xmax\": pos[\"x\"] + node_width / 2,\n            \"ymin\": pos[\"y0\"],\n            \"ymax\": pos[\"y1\"],\n            \"name\": tgt,\n            \"total\": target_totals[tgt],\n        }\n    )\n\ndf_nodes = pd.DataFrame(node_rects)\n\n# Build labels with flow totals\nlabels = []\nfor src in sources:\n    pos = source_positions[src]\n    labels.append(\n        {\n            \"x\": pos[\"x\"] - node_width - 0.015,\n            \"y\": (pos[\"y0\"] + pos[\"y1\"]) / 2,\n            \"label\": f\"{src}\\n({source_totals[src]} TWh)\",\n            \"side\": \"left\",\n        }\n    )\n\nfor tgt in targets:\n    pos = target_positions[tgt]\n    labels.append(\n        {\n            \"x\": pos[\"x\"] + node_width + 0.015,\n            \"y\": (pos[\"y0\"] + pos[\"y1\"]) / 2,\n            \"label\": f\"{tgt}\\n({target_totals[tgt]} TWh)\",\n            \"side\": \"right\",\n        }\n    )\n\ndf_labels = pd.DataFrame(labels)\n\nflow_tooltips = layer_tooltips().line(\"@flow_id\").line(\"@value TWh\")\nnode_tooltips = layer_tooltips().line(\"@name\").line(\"@total TWh\")\n\n# Plot\nplot = (\n    ggplot()\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"flow_id\", fill=\"source\"),\n        data=df_flows[~df_flows[\"highlight\"]],\n        alpha=0.6,\n        color=PAGE_BG,\n        size=0.2,\n        tooltips=flow_tooltips,\n    )\n    + geom_polygon(\n        aes(x=\"x\", y=\"y\", group=\"flow_id\", fill=\"source\"),\n        data=df_flows[df_flows[\"highlight\"]],\n        alpha=0.9,\n        color=INK,\n        size=0.6,\n        tooltips=flow_tooltips,\n    )\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=df_nodes,\n        fill=INK,\n        color=INK,\n        size=1.5,\n        tooltips=node_tooltips,\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=df_labels[df_labels[\"side\"] == \"left\"],\n        size=5.5,\n        hjust=1,\n        color=INK_SOFT,\n        family=\"sans-serif\",\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=df_labels[df_labels[\"side\"] == \"right\"],\n        size=5.5,\n        hjust=0,\n        color=INK_SOFT,\n        family=\"sans-serif\",\n    )\n    + scale_fill_manual(values=[source_color_map[s] for s in sources], name=\"Energy Source   \")\n    + labs(\n        title=\"sankey-basic · python · letsplot · anyplot.ai\",\n        subtitle=f\"Largest flow: {dominant_source} → {dominant_target} ({dominant_value} TWh)\",\n    )\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=20, face=\"bold\", color=INK),\n        plot_subtitle=element_text(size=13, color=INK_SOFT),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        panel_grid=element_blank(),\n        legend_text=element_text(size=13, color=INK_SOFT),\n        legend_title=element_text(size=14, face=\"bold\", color=INK),\n        legend_position=\"bottom\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    )\n    + scale_x_continuous(limits=[-0.22, 1.22])\n    + scale_y_continuous(limits=[-0.02, 1.02])\n    + ggsize(800, 450)\n)\n\n# Save PNG (3200 × 1800 px) and interactive HTML (hover tooltips on flows and nodes)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}