{"spec_id":"alluvial-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# alluvial-basic: Basic Alluvial Diagram\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 90/100 | Created: 2026-09-02\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 INK_SOFT  = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst INK_MUTED = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green\n    colorant\"#C475FD\",  # 2 — lavender\n    colorant\"#4467A3\",  # 3 — blue\n]\n\n# --- Data: academic track transitions across four semesters -------------------\n# Undecided is mapped to the theme-adaptive \"muted\" semantic anchor rather than\n# a categorical slot — it literally represents a neutral / uncommitted state.\ncategories      = [\"STEM\", \"Business\", \"Humanities\", \"Undecided\"]\ncategory_colors = [IMPRINT_PALETTE[1], IMPRINT_PALETTE[2], IMPRINT_PALETTE[3], INK_MUTED]\ntime_labels     = [\"Semester 1\", \"Semester 2\", \"Semester 3\", \"Semester 4\"]\n\nnt   = length(time_labels)\nncat = length(categories)\n\n# values[t, k] — enrolled students in category k at time point t\nvalues = Float64[\n    140 100  80 60\n    145 110  95 30\n    145 113  95 27\n    146 112  98 24\n]\n\n# flows[t][i, j] — students moving from category i (at time t) to category j (at time t+1)\nflows = [\n    Float64[110 15 10  5; 10 70 15  5;  5 10 60  5; 20 15 10 15],\n    Float64[120 15  5  5; 10 80 15  5;  5 10 70 10; 10  8  5  7],\n    Float64[130 10  3  2;  8 90 10  5;  3  7 80  5;  5  5  5 12],\n]\n\n# --- Layout: stack each column's categories top-to-bottom with a fixed gap ---\ngap = 6.0\nnode_top = zeros(Float64, nt, ncat)\nnode_bot = zeros(Float64, nt, ncat)\nfor t in 1:nt\n    cursor = 0.0\n    for k in 1:ncat\n        node_top[t, k] = cursor\n        node_bot[t, k] = cursor - values[t, k]\n        cursor = node_bot[t, k] - gap\n    end\nend\n\nnode_halfwidth = 0.06\nnpts = 40\n\n# --- Plot -----------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n    figure_padding  = (130, 130, 10, 10),\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = \"alluvial-basic · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    xticks             = (1:nt, time_labels),\n    xticklabelsize     = 16,\n    xticklabelcolor    = INK,\n    xticksvisible      = false,\n    yticksvisible      = false,\n    yticklabelsvisible = false,\n    ylabelvisible      = false,\n    xgridvisible       = false,\n    ygridvisible       = false,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinevisible   = false,\n    bottomspinecolor   = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n)\n\n# Flow bands (drawn first, sit beneath the nodes). Sub-segments within a source\n# node are ordered by destination category, and within a destination node by\n# source category, so bands never cross their own siblings.\nfor t in 1:(nt - 1)\n    out_cursor = copy(node_top[t, :])\n    in_cursor  = copy(node_top[t + 1, :])\n    for i in 1:ncat, j in 1:ncat\n        f = flows[t][i, j]\n        f <= 0 && continue\n\n        src_top = out_cursor[i]\n        src_bot = src_top - f\n        out_cursor[i] = src_bot\n\n        dst_top = in_cursor[j]\n        dst_bot = dst_top - f\n        in_cursor[j] = dst_bot\n\n        x_left  = t + node_halfwidth\n        x_right = (t + 1) - node_halfwidth\n        xs = range(x_left, x_right, length = npts)\n        ease = [(1 - cos(pi * (x - x_left) / (x_right - x_left))) / 2 for x in xs]\n        ys_top = src_top .+ (dst_top - src_top) .* ease\n        ys_bot = src_bot .+ (dst_bot - src_bot) .* ease\n\n        band_pts = vcat(Point2f.(xs, ys_top), Point2f.(reverse(xs), reverse(ys_bot)))\n        poly!(ax, band_pts; color = (category_colors[i], 0.55), strokewidth = 0)\n    end\nend\n\n# Nodes (stacked category blocks per time point)\nfor t in 1:nt, k in 1:ncat\n    corners = Point2f[\n        (t - node_halfwidth, node_top[t, k]),\n        (t + node_halfwidth, node_top[t, k]),\n        (t + node_halfwidth, node_bot[t, k]),\n        (t - node_halfwidth, node_bot[t, k]),\n    ]\n    poly!(ax, corners; color = category_colors[k], strokecolor = PAGE_BG, strokewidth = 2)\nend\n\n# Category labels at the first and last column, with enrollment counts\nfor k in 1:ncat\n    y_first = (node_top[1, k] + node_bot[1, k]) / 2\n    text!(ax, 1 - node_halfwidth - 0.04, y_first;\n          text = \"$(categories[k]) ($(Int(values[1, k])))\",\n          align = (:right, :center), color = INK, fontsize = 14)\n\n    y_last = (node_top[nt, k] + node_bot[nt, k]) / 2\n    text!(ax, nt + node_halfwidth + 0.04, y_last;\n          text = \"$(categories[k]) ($(Int(values[nt, k])))\",\n          align = (:left, :center), color = INK, fontsize = 14)\nend\n\nxlims!(ax, 1 - 0.9, nt + 0.9)\n\nlegend_elems = [PolyElement(color = c) for c in category_colors]\nLegend(\n    fig[2, 1], legend_elems, categories, \"Track\";\n    orientation = :horizontal, tellwidth = false, framevisible = false,\n    labelcolor = INK, titlecolor = INK,\n)\nrowsize!(fig.layout, 2, Relative(0.08))\n\n# --- Save -----------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}