From c14e9a3d375744fdefff6fa7c8a8277c9f673b56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 May 2026 13:13:07 +0000 Subject: [PATCH 1/4] feat(makie): implement alluvial-opinion-flow --- .../implementations/julia/makie.jl | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 plots/alluvial-opinion-flow/implementations/julia/makie.jl diff --git a/plots/alluvial-opinion-flow/implementations/julia/makie.jl b/plots/alluvial-opinion-flow/implementations/julia/makie.jl new file mode 100644 index 0000000000..42c92312ff --- /dev/null +++ b/plots/alluvial-opinion-flow/implementations/julia/makie.jl @@ -0,0 +1,203 @@ +# anyplot.ai +# alluvial-opinion-flow: Opinion Flow Diagram +# Library: Makie.jl | Julia 1.11 +# Quality: pending | Created: 2026-05-30 + +using CairoMakie +using Colors +using Random + +Random.seed!(42) + +# Theme tokens +THEME = get(ENV, "ANYPLOT_THEME", "light") +PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17" +ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420" +INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8" +INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0" +INK_MUTED = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F" + +# Opinion category colors — semantic mapping: positive→green, negative→red, neutral→muted +CAT_COLORS = [ + colorant"#009E73", # Strongly Support — Imprint brand green (positive) + colorant"#2ABCCD", # Support — Imprint cyan + INK_MUTED, # Neutral — theme-adaptive muted + colorant"#BD8233", # Oppose — Imprint ochre + colorant"#AE3030", # Strongly Oppose — Imprint matte red (negative) +] +categories = ["Strongly Support", "Support", "Neutral", "Oppose", "Strongly Oppose"] +N_CATS = 5 + +# Data: public health policy opinion survey, 1000 respondents, 3 quarterly waves +waves = ["Wave 1\n(January)", "Wave 2\n(April)", "Wave 3\n(July)"] +N_WAVES = 3 + +w1 = [200, 305, 200, 190, 105] + +# Transition matrix Wave 1 → Wave 2; rows = source category, cols = target +# Row sums must equal w1 +f12 = [ + 155 28 9 5 3; + 25 205 50 18 7; + 10 40 115 30 5; + 5 15 32 120 18; + 2 5 11 18 69; +] + +w2 = vec(sum(f12; dims = 1)) # [197, 293, 217, 191, 102] + +# Transition matrix Wave 2 → Wave 3; row sums must equal w2 +f23 = [ + 148 30 12 5 2; + 25 210 40 13 5; + 12 38 132 28 7; + 4 15 28 130 14; + 2 4 10 18 68; +] + +w3 = vec(sum(f23; dims = 1)) # [191, 297, 222, 194, 96] + +wave_counts = [w1, w2, w3] +flow_matrices = [f12, f23] + +# Layout +WAVE_XS = [1.0, 3.5, 6.0] # x centre of each wave column +NODE_W = 0.20 # node rectangle half-width × 2 +NODE_GAP = 20 # gap between vertically stacked nodes (respondent units) +N_BEZ = 80 # bezier interpolation segments + +# Node y-positions: stack bottom→top, Strongly Oppose (index 5) at base +all_y_lo = [zeros(Float64, N_CATS) for _ in 1:N_WAVES] +all_y_hi = [zeros(Float64, N_CATS) for _ in 1:N_WAVES] +for w in 1:N_WAVES + y = 0.0 + for i in N_CATS:-1:1 + all_y_lo[w][i] = y + all_y_hi[w][i] = y + Float64(wave_counts[w][i]) + y += wave_counts[w][i] + NODE_GAP + end +end + +# Bezier strip: smooth S-curve flow band connecting two node edges +function bezier_strip(x0, y0_lo, y0_hi, x1, y1_lo, y1_hi) + cx = (x0 + x1) / 2.0 + ts = range(0.0, 1.0; length = N_BEZ) + bx(t, xa, xb) = (1 - t)^3 * xa + 3 * (1 - t)^2 * t * cx + 3 * (1 - t) * t^2 * cx + t^3 * xb + top = [Point2f(bx(t, x0, x1), (1 - t) * y0_hi + t * y1_hi) for t in ts] + bot = [Point2f(bx(t, x1, x0), (1 - t) * y1_lo + t * y0_lo) for t in ts] + return vcat(top, bot) +end + +# Pre-compute all flow polygons; track fill offsets per node +src_used = [zeros(Float64, N_CATS) for _ in 1:(N_WAVES - 1)] +dst_used = [zeros(Float64, N_CATS) for _ in 1:(N_WAVES - 1)] + +flow_polys = Vector{Vector{Point2f}}() +flow_colors = Vector{Tuple{RGBf, Float32}}() + +for pair in 1:(N_WAVES - 1) + x0r = WAVE_XS[pair] + NODE_W / 2 + x1l = WAVE_XS[pair + 1] - NODE_W / 2 + fmat = flow_matrices[pair] + for src in 1:N_CATS + for dst in 1:N_CATS + v = Float64(fmat[src, dst]) + v < 1 && continue + s_lo = all_y_lo[pair][src] + src_used[pair][src] + s_hi = s_lo + v + src_used[pair][src] += v + d_lo = all_y_lo[pair + 1][dst] + dst_used[pair][dst] + d_hi = d_lo + v + dst_used[pair][dst] += v + push!(flow_polys, bezier_strip(x0r, s_lo, s_hi, x1l, d_lo, d_hi)) + c = CAT_COLORS[src] + push!(flow_colors, (RGBf(Float32(red(c)), Float32(green(c)), Float32(blue(c))), 0.32f0)) + end + end +end + +y_max = maximum(all_y_hi[w][1] for w in 1:N_WAVES) + +# Figure +fig = Figure( + size = (1600, 900), + fontsize = 14, + backgroundcolor = PAGE_BG, +) + +ax = Axis( + fig[1, 1]; + title = "alluvial-opinion-flow · julia · makie · anyplot.ai", + titlesize = 20, + titlecolor = INK, + backgroundcolor = PAGE_BG, + leftspinevisible = false, + rightspinevisible = false, + topspinevisible = false, + bottomspinevisible = false, + xgridvisible = false, + ygridvisible = false, + xticksvisible = false, + yticksvisible = false, + xticklabelsvisible = false, + yticklabelsvisible = false, +) + +# Draw flows (rendered behind nodes) +for (pts, (rgb, alpha)) in zip(flow_polys, flow_colors) + poly!(ax, pts; color = RGBAf(rgb.r, rgb.g, rgb.b, alpha), strokewidth = 0) +end + +# Draw nodes and inline count labels +for w in 1:N_WAVES + for cat in 1:N_CATS + xl = WAVE_XS[w] - NODE_W / 2 + xr = WAVE_XS[w] + NODE_W / 2 + yl = all_y_lo[w][cat] + yh = all_y_hi[w][cat] + node_pts = [Point2f(xl, yl), Point2f(xr, yl), Point2f(xr, yh), Point2f(xl, yh)] + poly!(ax, node_pts; color = CAT_COLORS[cat], strokewidth = 0) + text!(ax, WAVE_XS[w], (yl + yh) / 2; + text = string(wave_counts[w][cat]), + align = (:center, :center), + fontsize = 12, + color = colorant"#FFFDF6", + font = :bold, + ) + end +end + +# Wave column headers +for w in 1:N_WAVES + text!(ax, WAVE_XS[w], y_max + 60; + text = waves[w], + align = (:center, :bottom), + fontsize = 13, + color = INK, + font = :bold, + ) +end + +# Legend +legend_x = WAVE_XS[end] + 0.55 +for (i, cat) in enumerate(categories) + y_mid = y_max - (i - 1) * 215 + swatch_pts = [ + Point2f(legend_x, y_mid - 22), + Point2f(legend_x + 0.17, y_mid - 22), + Point2f(legend_x + 0.17, y_mid + 22), + Point2f(legend_x, y_mid + 22), + ] + poly!(ax, swatch_pts; color = CAT_COLORS[i], strokewidth = 0) + text!(ax, legend_x + 0.23, y_mid; + text = cat, + align = (:left, :center), + fontsize = 12, + color = INK_SOFT, + ) +end + +xlims!(ax, 0.3, 7.7) +ylims!(ax, -60, y_max + 150) + +save(joinpath(@__DIR__, "plot-$(THEME).png"), fig; px_per_unit = 2) From 37ada26413572d18f52ee8fb8623cc49101797ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 May 2026 13:13:17 +0000 Subject: [PATCH 2/4] chore(makie): add metadata for alluvial-opinion-flow --- .../metadata/julia/makie.yaml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 plots/alluvial-opinion-flow/metadata/julia/makie.yaml diff --git a/plots/alluvial-opinion-flow/metadata/julia/makie.yaml b/plots/alluvial-opinion-flow/metadata/julia/makie.yaml new file mode 100644 index 0000000000..f1c9f2457e --- /dev/null +++ b/plots/alluvial-opinion-flow/metadata/julia/makie.yaml @@ -0,0 +1,21 @@ +# Per-library metadata for makie implementation of alluvial-opinion-flow +# Auto-generated by impl-generate.yml + +library: makie +language: julia +specification_id: alluvial-opinion-flow +created: '2026-05-30T13:13:16Z' +updated: '2026-05-30T13:13:16Z' +generated_by: claude-sonnet +workflow_run: 26684179371 +issue: 4430 +language_version: 1.11.9 +library_version: 0.22.10 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/alluvial-opinion-flow/julia/makie/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/alluvial-opinion-flow/julia/makie/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: null +review: + strengths: [] + weaknesses: [] From fa18be8da7d904fd268402c98463d2ffabe7fe35 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 May 2026 13:20:50 +0000 Subject: [PATCH 3/4] chore(makie): update quality score 80 and review feedback for alluvial-opinion-flow --- .../implementations/julia/makie.jl | 4 +- .../metadata/julia/makie.yaml | 288 +++++++++++++++++- 2 files changed, 283 insertions(+), 9 deletions(-) diff --git a/plots/alluvial-opinion-flow/implementations/julia/makie.jl b/plots/alluvial-opinion-flow/implementations/julia/makie.jl index 42c92312ff..6d094cacba 100644 --- a/plots/alluvial-opinion-flow/implementations/julia/makie.jl +++ b/plots/alluvial-opinion-flow/implementations/julia/makie.jl @@ -1,7 +1,7 @@ # anyplot.ai # alluvial-opinion-flow: Opinion Flow Diagram -# Library: Makie.jl | Julia 1.11 -# Quality: pending | Created: 2026-05-30 +# Library: makie 0.22.10 | Julia 1.11.9 +# Quality: 80/100 | Created: 2026-05-30 using CairoMakie using Colors diff --git a/plots/alluvial-opinion-flow/metadata/julia/makie.yaml b/plots/alluvial-opinion-flow/metadata/julia/makie.yaml index f1c9f2457e..5614c9f98d 100644 --- a/plots/alluvial-opinion-flow/metadata/julia/makie.yaml +++ b/plots/alluvial-opinion-flow/metadata/julia/makie.yaml @@ -1,11 +1,8 @@ -# Per-library metadata for makie implementation of alluvial-opinion-flow -# Auto-generated by impl-generate.yml - library: makie language: julia specification_id: alluvial-opinion-flow created: '2026-05-30T13:13:16Z' -updated: '2026-05-30T13:13:16Z' +updated: '2026-05-30T13:20:50Z' generated_by: claude-sonnet workflow_run: 26684179371 issue: 4430 @@ -15,7 +12,284 @@ preview_url_light: https://storage.googleapis.com/anyplot-images/plots/alluvial- preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/alluvial-opinion-flow/julia/makie/plot-dark.png preview_html_light: null preview_html_dark: null -quality_score: null +quality_score: 80 review: - strengths: [] - weaknesses: [] + strengths: + - Correct alluvial/Sankey plot type with elegant S-curve bezier flows + - 'Thoughtful semantic color mapping: green (positive), red (negative), muted grey + (neutral) creates intuitive viewer reading' + - All Imprint palette colors used correctly with valid semantic exception for opinion + polarity + - Full theme-adaptive chrome in both light and dark renders — no dark-on-dark failures + - Node count labels with white text inside colored rectangles clearly show respondent + totals + - All spines, ticks, and grid removed — appropriate minimal chrome for this diagram + type + - Semi-transparent flows (alpha=0.32) prevent overlap confusion in dense areas + - Custom bezier_strip function is idiomatic Makie low-level drawing — correctly + uses poly! for flow polygons and nodes + - Realistic public health policy survey data with plausible transition matrices + showing diagonal dominance + weaknesses: + - 'MISSING spec differentiator: spec explicitly states the variant must visually + distinguish stable respondents (same category across waves) from changers using + opacity or color intensity — all flows currently use uniform alpha=0.32 regardless + of whether src==dst. Diagonal (stable) flows should use higher opacity (~0.65) + while off-diagonal (changer) flows keep 0.25-0.30.' + - 'MISSING net flow highlight: spec requires highlighting net flows between categories + to reveal polarization trends — a simple approach is to compute net(A→B) = f_AB + - f_BA and draw an extra semi-transparent colored band on top of the dominant + direction, or annotate with net delta arrows between nodes.' + - 'DE-03 limited by missing storytelling: the plot displays data correctly but does + not guide the viewer to the insight (e.g., which categories are growing or shrinking). + Adding subtle text annotations at the right margin noting net change (+/- respondents) + per category between Wave 1 and Wave 3 would substantially raise DE-03.' + - 'VQ-01 minor: node count labels use fontsize=12 at px_per_unit=2, which renders + to effective 24px labels. For the smaller nodes (e.g., Strongly Oppose: 105, 102, + 96) the node height is ~105 units while label height is ~12 units, making labels + tight. Consider fontsize=10 for improved breathing room or auto-sizing to node + height.' + - 'VQ-05 minor: legend is positioned at legend_x = WAVE_XS[end] + 0.55 = 6.55 with + xlims=(0.3, 7.7). The longest label text (Strongly Oppose) at fontsize=12 extends + to approximately x≈7.5 — functionally OK but tight against the right xlim. Add + 0.2-0.3 to xlims upper bound for better breathing room.' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct anyplot surface color, not pure white. + Chrome: Title "alluvial-opinion-flow · julia · makie · anyplot.ai" rendered in dark ink (#1A1A17), legible at top. Wave headers "Wave 1 (January)", "Wave 2 (April)", "Wave 3 (July)" in bold dark ink above each column. No axis labels or tick labels (appropriate for flow diagram). Legend on the right with color swatches and labels in INK_SOFT (#4A4A44). + Data: Five stacked nodes per wave rendered as solid colored rectangles — brand green (#009E73) for Strongly Support, cyan (#2ABCCD) for Support, grey-muted (#6B6A63) for Neutral, ochre (#BD8233) for Oppose, matte red (#AE3030) for Strongly Oppose. S-curve bezier flow bands connect corresponding nodes across waves with alpha=0.32. White/cream respondent counts inside each node. + Legibility verdict: PASS — all text clearly readable. Node count labels (e.g. 200, 305) visible inside colored nodes. Legend labels distinct and sized appropriately. + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct anyplot dark surface, not pure black. + Chrome: Title text flips to near-white (#F0EFE8), clearly visible against dark background. Wave headers rendered in INK token (light cream on dark). Legend labels in INK_SOFT (#B8B7B0 on dark). No dark-on-dark failures detected. + Data: Data colors (green, cyan, ochre, matte red) are identical to light render — palette positions unchanged as required. Neutral category nodes appear lighter grey (#A8A79F) vs darker grey in light mode — this is by design as INK_MUTED is a theme-adaptive semantic anchor. White/cream (#FFFDF6) node count labels remain readable on all colored node backgrounds in both themes. + Legibility verdict: PASS — all chrome text is light-colored on dark background. No elements appear dark-on-dark. Brand green #009E73 is clearly visible on the dark surface. + criteria_checklist: + visual_quality: + score: 25 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 6 + max: 8 + passed: true + comment: 'All font sizes explicitly set; title, wave headers, node labels + readable in both themes. Minor: node count labels at fontsize=12 are tight + inside small nodes (e.g., 96-respondent Strongly Oppose node).' + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: No text overlaps. Flow bands overlap each other as expected for Sankey. + Minor deduction for the very dense flow crossing zone between waves. + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: 'Nodes clearly visible with solid fills. Semi-transparent flows (0.32 + alpha) preserve readability. Minor: very small flow quantities (<5 respondents) + produce thin bands that are barely visible.' + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Imprint palette colors used; green/red poles distinguish positive/negative + but category labels provide redundant encoding. CVD-safe. + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Canvas gate passed (3200x1800). Good proportions. Legend is functional + but positioned close to right xlim — Strongly Oppose label fits within canvas + but has minimal margin. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: No axis labels appropriate for flow diagram; wave headers serve as + column labels. Title is correct and descriptive. + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series (Strongly Support) = #009E73 correct. Semantic exception + mapping for opinion polarity (positive→green, neutral→muted, negative→red) + is explicitly sanctioned by style guide. Plot backgrounds #FAF8F1/#1A1A17 + correct. Theme chrome flips correctly in both renders.' + design_excellence: + score: 12 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Intentional semantic color spectrum (green→cyan→grey→ochre→red) reflects + opinion polarity. S-curve bezier flows are elegant. Above configured-default + quality, but not publication-ready (no subtle refinements, annotations, + or visual emphasis on key patterns). + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: All four spines removed, grid removed, no ticks — clean minimal chrome + appropriate for flow diagram. Semi-transparent flows prevent confusion. + White count labels inside colored nodes is a refined touch. + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: false + comment: Color gradient from green to red creates intuitive reading of opinion + polarity. But the plot does not guide the viewer toward the key insight + (which categories are growing/shrinking, polarization trend). Missing stable + vs. changer visual distinction prevents the most important story from emerging. + spec_compliance: + score: 13 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct alluvial/Sankey-style diagram with stacked nodes at each + wave and bezier flow bands between them. + - id: SC-02 + name: Required Features + score: 2 + max: 4 + passed: false + comment: 'Present: flow width proportional to count, node labels with respondent + totals, transparency for overlapping flows, chronological wave arrangement. + Missing: (1) visual distinction of stable respondents (same-category diagonal + flows) from changers — spec explicitly calls this a differentiator; (2) + net flow highlighting between categories to reveal polarization.' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Waves mapped left-to-right chronologically. Flow width proportional + to flow_count. All 5 categories visible at all 3 waves. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title is 'alluvial-opinion-flow · julia · makie · anyplot.ai' — correct + format. Legend labels match all 5 category names. + data_quality: + score: 13 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 4 + max: 6 + passed: false + comment: 'Shows 3 waves, 5 categories, varied transition patterns with realistic + diagonal dominance. Missing: the stable vs. changer visual distinction is + a key feature of this specific spec variant that is not shown in the data + visualization.' + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Public health policy opinion survey with 1000 respondents is a real, + neutral, comprehensible scenario. Opinion categories are clear and appropriate. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: 1000 total respondents, realistic transition matrices with diagonal + dominance (~75% stability), plausible opinion drift patterns across three + waves. + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'One helper function bezier_strip is justified for complex S-curve + computation. Overall KISS structure: data → layout → render → save.' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Random.seed!(42) present; data is also manually specified so fully + deterministic. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: using CairoMakie, Colors, Random — all three used (colorant"", poly!, + text!, Random.seed!). + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: No fake UI. Appropriate complexity for a manual Sankey implementation. + bezier_strip is clean and well-parameterized. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves as plot-$(THEME).png with px_per_unit=2. Uses size= (newer + Makie API replacing resolution=). Correct. + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Proper Figure/Axis construction, poly! for polygon shapes, text! + for labels, colorant strings, theme tokens via Axis parameters. Correct + Makie patterns throughout. + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Custom bezier strip polygon construction using Makie's low-level + poly! primitive is distinctive — Makie has no built-in Sankey recipe so + this implementation leverages the library's composable drawing primitives + (Point2f arrays, RGBAf colors) in a way specific to Makie's architecture. + verdict: REJECTED +impl_tags: + dependencies: [] + techniques: + - bezier-curves + - custom-legend + - annotations + - patches + patterns: + - data-generation + - matrix-construction + - iteration-over-groups + dataprep: [] + styling: + - minimal-chrome + - alpha-blending From 1a4e123f789318565f2100f08f2739e7af07c182 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 May 2026 13:24:44 +0000 Subject: [PATCH 4/4] fix(makie): address review feedback for alluvial-opinion-flow Attempt 1/3 - fixes based on AI review --- .../implementations/julia/makie.jl | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/plots/alluvial-opinion-flow/implementations/julia/makie.jl b/plots/alluvial-opinion-flow/implementations/julia/makie.jl index 6d094cacba..356ddfa715 100644 --- a/plots/alluvial-opinion-flow/implementations/julia/makie.jl +++ b/plots/alluvial-opinion-flow/implementations/julia/makie.jl @@ -111,7 +111,9 @@ for pair in 1:(N_WAVES - 1) dst_used[pair][dst] += v push!(flow_polys, bezier_strip(x0r, s_lo, s_hi, x1l, d_lo, d_hi)) c = CAT_COLORS[src] - push!(flow_colors, (RGBf(Float32(red(c)), Float32(green(c)), Float32(blue(c))), 0.32f0)) + # stable respondents (same category) get higher opacity to stand out from changers + alpha = src == dst ? 0.65f0 : 0.27f0 + push!(flow_colors, (RGBf(Float32(red(c)), Float32(green(c)), Float32(blue(c))), alpha)) end end end @@ -160,7 +162,7 @@ for w in 1:N_WAVES text!(ax, WAVE_XS[w], (yl + yh) / 2; text = string(wave_counts[w][cat]), align = (:center, :center), - fontsize = 12, + fontsize = 10, color = colorant"#FFFDF6", font = :bold, ) @@ -197,7 +199,33 @@ for (i, cat) in enumerate(categories) ) end -xlims!(ax, 0.3, 7.7) +# Net change annotations (Wave 1 → Wave 3) — reveals polarization trend +net_changes = w3 .- w1 +annotation_x = WAVE_XS[3] + NODE_W / 2 + 0.08 +text!(ax, annotation_x, y_max + 80; + text = "W1→W3", + align = (:left, :center), + fontsize = 9, + color = INK_SOFT, + font = :bold, +) +for cat in 1:N_CATS + yl = all_y_lo[3][cat] + yh = all_y_hi[3][cat] + delta = net_changes[cat] + delta_str = delta >= 0 ? "+$(delta)" : "$(delta)" + # green for growth, red for decline, muted for no change + delta_color = delta > 0 ? colorant"#009E73" : (delta < 0 ? colorant"#AE3030" : INK_SOFT) + text!(ax, annotation_x, (yl + yh) / 2; + text = delta_str, + align = (:left, :center), + fontsize = 11, + color = delta_color, + font = :bold, + ) +end + +xlims!(ax, 0.3, 8.0) ylims!(ax, -60, y_max + 150) save(joinpath(@__DIR__, "plot-$(THEME).png"), fig; px_per_unit = 2)