{"spec_id":"sankey-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# sankey-basic: Basic Sankey Diagram\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 87/100 | Created: 2026-07-25\n\nusing CairoMakie\nusing Colors\n\n# --- Theme tokens ------------------------------------------------------------\nconst THEME        = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG       = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK           = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst ANYPLOT_MUTED = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\n\n# --- Data: national electricity supply routed to end-use sectors (GWh) ------\nsources = [\"Coal\", \"Natural Gas\", \"Nuclear\", \"Wind\", \"Solar\"]\ntargets = [\"Residential\", \"Commercial\", \"Industrial\", \"Transportation\"]\n\nflows = [\n    (\"Coal\", \"Industrial\", 45.0), (\"Coal\", \"Commercial\", 15.0),\n    (\"Natural Gas\", \"Residential\", 38.0), (\"Natural Gas\", \"Commercial\", 30.0),\n    (\"Natural Gas\", \"Industrial\", 25.0), (\"Natural Gas\", \"Transportation\", 22.0),\n    (\"Nuclear\", \"Residential\", 32.0), (\"Nuclear\", \"Commercial\", 18.0),\n    (\"Nuclear\", \"Industrial\", 10.0),\n    (\"Wind\", \"Residential\", 20.0), (\"Wind\", \"Industrial\", 12.0),\n    (\"Solar\", \"Residential\", 18.0), (\"Solar\", \"Commercial\", 10.0),\n    (\"Solar\", \"Transportation\", 8.0),\n]\n\nsource_color = Dict(name => IMPRINT_PALETTE[i] for (i, name) in enumerate(sources))\n\n# --- Barycenter node ordering: minimizes ribbon crossings independently of the\n# palette-assignment order above (which stays fixed so Coal keeps the brand green).\nfunction barycenter_order(names, other_order, edges; as_source::Bool)\n    rank = Dict(n => i for (i, n) in enumerate(other_order))\n    score(name) = begin\n        pairs = as_source ? [(t, v) for (s, t, v) in edges if s == name] :\n                             [(s, v) for (s, t, v) in edges if t == name]\n        sum(rank[p] * v for (p, v) in pairs) / sum(v for (_, v) in pairs)\n    end\n    return sort(names; by = score)\nend\n\nsource_order = barycenter_order(sources, targets, flows; as_source = true)\ntarget_order = barycenter_order(targets, source_order, flows; as_source = false)\nsource_order = barycenter_order(sources, target_order, flows; as_source = true)\n\n# --- Node column layout: stacked bars, gap proportional to total flow -------\ngap = sum(v for (_, _, v) in flows) * 0.025\n\nsource_totals = [sum(v for (s, _, v) in flows if s == name) for name in source_order]\ntarget_totals = [sum(v for (_, t, v) in flows if t == name) for name in target_order]\n\nsource_col_h = sum(source_totals) + gap * (length(source_order) - 1)\ntarget_col_h = sum(target_totals) + gap * (length(target_order) - 1)\ncol_h = max(source_col_h, target_col_h)\n\nsource_offsets = cumsum(vcat(0.0, (source_totals .+ gap)[1:end-1]))\ntarget_offsets = cumsum(vcat(0.0, (target_totals .+ gap)[1:end-1]))\n\nsource_top = (col_h - source_col_h) / 2 .+ (source_col_h .- source_offsets)\ntarget_top = (col_h - target_col_h) / 2 .+ (target_col_h .- target_offsets)\n\nsource_pos = Dict(name => (top - h, top) for (name, top, h) in zip(source_order, source_top, source_totals))\ntarget_pos = Dict(name => (top - h, top) for (name, top, h) in zip(target_order, target_top, target_totals))\n\n# --- Per-node link segment allocation (stacked in the order of the other column)\nsource_seg = Dict{Tuple{String,String},Tuple{Float64,Float64}}()\nfor name in source_order\n    outgoing = sort([(t, v) for (s, t, v) in flows if s == name],\n                     by = pair -> findfirst(==(pair[1]), target_order))\n    cur = source_pos[name][2]\n    for (t, v) in outgoing\n        source_seg[(name, t)] = (cur - v, cur)\n        cur -= v\n    end\nend\n\ntarget_seg = Dict{Tuple{String,String},Tuple{Float64,Float64}}()\nfor name in target_order\n    incoming = sort([(s, v) for (s, t, v) in flows if t == name],\n                     by = pair -> findfirst(==(pair[1]), source_order))\n    cur = target_pos[name][2]\n    for (s, v) in incoming\n        target_seg[(s, name)] = (cur - v, cur)\n        cur -= v\n    end\nend\n\n# --- Figure -------------------------------------------------------------------\ntitle_str = \"sankey-basic · julia · makie · anyplot.ai\"\n\nfig = Figure(resolution = (1600, 900), fontsize = 14, backgroundcolor = PAGE_BG)\n\nax = Axis(\n    fig[1, 1];\n    title           = title_str,\n    titlesize       = 52,\n    titlecolor      = INK,\n    backgroundcolor = PAGE_BG,\n)\n\nhidedecorations!(ax)\nhidespines!(ax)\n\nnode_width = 0.35\nx_right = 10.0\nx0 = node_width\nx1 = x_right - node_width\nxm = (x0 + x1) / 2\n\nxlims!(ax, -3.5, x_right + 3.5)\nylims!(ax, -col_h * 0.05, col_h * 1.05)\n\n# --- Links: cubic-bezier ribbons, filled polygon between top/bottom curves --\ntt = range(0.0, 1.0; length = 40)\nfor (s, t, v) in flows\n    y0b, y0t = source_seg[(s, t)]\n    y1b, y1t = target_seg[(s, t)]\n    xc = @. (1 - tt)^3 * x0 + 3 * (1 - tt)^2 * tt * xm + 3 * (1 - tt) * tt^2 * xm + tt^3 * x1\n    yc_top = @. (1 - tt)^3 * y0t + 3 * (1 - tt)^2 * tt * y0t + 3 * (1 - tt) * tt^2 * y1t + tt^3 * y1t\n    yc_bot = @. (1 - tt)^3 * y0b + 3 * (1 - tt)^2 * tt * y0b + 3 * (1 - tt) * tt^2 * y1b + tt^3 * y1b\n    ribbon = vcat(Point2f.(xc, yc_top), Point2f.(reverse(xc), reverse(yc_bot)))\n    poly!(ax, ribbon; color = (source_color[s], 0.55), strokewidth = 0)\nend\n\n# --- Nodes: rectangles + direct labels ---------------------------------------\nfor name in source_order\n    b, t = source_pos[name]\n    rect = Point2f[(0.0, b), (node_width, b), (node_width, t), (0.0, t)]\n    poly!(ax, rect; color = source_color[name], strokewidth = 1.5, strokecolor = PAGE_BG)\n    text!(ax, -0.25, (b + t) / 2; text = name, align = (:right, :center),\n          color = INK, fontsize = 15)\nend\n\nfor name in target_order\n    b, t = target_pos[name]\n    rect = Point2f[(x_right - node_width, b), (x_right, b), (x_right, t), (x_right - node_width, t)]\n    poly!(ax, rect; color = ANYPLOT_MUTED, strokewidth = 1.5, strokecolor = PAGE_BG)\n    text!(ax, x_right + 0.25, (b + t) / 2; text = name, align = (:left, :center),\n          color = INK, fontsize = 15)\nend\n\n# --- Save ---------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}