{"spec_id":"slope-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# slope-basic: Basic Slope Chart (Slopegraph)\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 92/100 | Updated: 2026-07-26\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens -------------------------------------------------------\nTHEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nPAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nINK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nINK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\nIMPRINT_INCREASE = colorant\"#009E73\"  # brand green — gain (semantic exception: positive change)\nIMPRINT_DECREASE = colorant\"#AE3030\"  # matte red — loss (semantic exception: negative change)\n\n# --- Data ----------------------------------------------------------------\n# Customer satisfaction score (0-100) for 10 product lines, comparing the\n# 2023 and 2024 annual surveys.\nproducts = [\n    \"Cloud Storage\", \"Mobile App\", \"Analytics Suite\", \"Payment Gateway\",\n    \"Support Chat\", \"API Platform\", \"Video Conferencing\", \"Data Backup\",\n    \"Email Client\", \"Customer Portal\",\n]\nn = length(products)\nscore_2023 = round.(rand(n) .* 35 .+ 48, digits=1)\nchange = round.(randn(n) .* 8, digits=1)\nscore_2024 = round.(clamp.(score_2023 .+ change, 0, 100), digits=1)\n\norder = sortperm(score_2023, rev=true)\nproducts, score_2023, score_2024 = products[order], score_2023[order], score_2024[order]\n\ny_min, y_max = minimum(vcat(score_2023, score_2024)), maximum(vcat(score_2023, score_2024))\ny_pad = (y_max - y_min) * 0.12\nylim_lo, ylim_hi = y_min - y_pad, y_max + y_pad * 2.2\n\n# Nudge label y-positions apart (independent of true marker/line position)\n# so entity names don't overlap when two scores land close together.\nmin_gap = (ylim_hi - ylim_lo) * 0.052\n\nlabel_y_left = copy(score_2023)\nleft_order = sortperm(label_y_left)\nfor _ in 1:200\n    moved = false\n    for k in 1:(n - 1)\n        i, j = left_order[k], left_order[k + 1]\n        if label_y_left[j] - label_y_left[i] < min_gap\n            center = (label_y_left[i] + label_y_left[j]) / 2\n            label_y_left[i] = center - min_gap / 2\n            label_y_left[j] = center + min_gap / 2\n            moved = true\n        end\n    end\n    moved || break\nend\n\nlabel_y_right = copy(score_2024)\nright_order = sortperm(label_y_right)\nfor _ in 1:200\n    moved = false\n    for k in 1:(n - 1)\n        i, j = right_order[k], right_order[k + 1]\n        if label_y_right[j] - label_y_right[i] < min_gap\n            center = (label_y_right[i] + label_y_right[j]) / 2\n            label_y_right[i] = center - min_gap / 2\n            label_y_right[j] = center + min_gap / 2\n            moved = true\n        end\n    end\n    moved || break\nend\n\n# --- Plot ------------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\ntitle_text = \"slope-basic · julia · makie · anyplot.ai\"\n\nax = Axis(\n    fig[1, 1];\n    title           = title_text,\n    titlesize       = 20,\n    titlecolor      = INK,\n    subtitle        = \"Customer Satisfaction Score (0–100)\",\n    subtitlesize    = 14,\n    subtitlecolor   = INK_SOFT,\n    backgroundcolor = PAGE_BG,\n)\nhidedecorations!(ax)\nhidespines!(ax)\nxlims!(ax, -0.85, 1.85)\nylims!(ax, ylim_lo, ylim_hi)\n\n# The two vertical reference axes, one per survey year\nvlines!(ax, [0, 1]; color=INK_SOFT, linewidth=1.5)\n\nfor i in 1:n\n    rising = score_2024[i] >= score_2023[i]\n    color = rising ? IMPRINT_INCREASE : IMPRINT_DECREASE\n    label = rising ? \"Increase\" : \"Decrease\"\n    arrow = rising ? \"▲\" : \"▼\"\n\n    lines!(ax, [0, 1], [score_2023[i], score_2024[i]];\n           color=color, linewidth=2.8, label=label)\n    scatter!(ax, [0, 1], [score_2023[i], score_2024[i]];\n             color=color, markersize=13, strokewidth=1.5, strokecolor=PAGE_BG)\n\n    # Leader stubs: connect a nudged label back to its true marker position\n    # whenever the anti-collision loop moved it, so the label-to-line link\n    # stays traceable even when several labels cluster in a dense band.\n    if abs(label_y_left[i] - score_2023[i]) > 1e-6\n        lines!(ax, [-0.05, 0], [label_y_left[i], score_2023[i]];\n               color=INK_SOFT, linewidth=0.75, linestyle=:dot)\n    end\n    if abs(label_y_right[i] - score_2024[i]) > 1e-6\n        lines!(ax, [1, 1.05], [score_2024[i], label_y_right[i]];\n               color=INK_SOFT, linewidth=0.75, linestyle=:dot)\n    end\n\n    text!(ax, -0.05, label_y_left[i]; text=\"$(products[i])  $(score_2023[i])\",\n          align=(:right, :center), color=INK, fontsize=15)\n    text!(ax, 1.05, label_y_right[i]; text=\"$(arrow) $(score_2024[i])  $(products[i])\",\n          align=(:left, :center), color=INK, fontsize=15)\nend\n\n# Survey-year headers atop each vertical axis\ntext!(ax, 0, ylim_hi - y_pad * 0.5; text=\"2023 Survey\", align=(:center, :bottom),\n      color=INK_SOFT, fontsize=15, font=:bold)\ntext!(ax, 1, ylim_hi - y_pad * 0.5; text=\"2024 Survey\", align=(:center, :bottom),\n      color=INK_SOFT, fontsize=15, font=:bold)\n\nLegend(fig[1, 2], ax; merge=true, unique=true, framevisible=false,\n       labelcolor=INK, labelsize=13, patchsize=(22, 3), tellheight=false, valign=:center)\ncolsize!(fig.layout, 2, Fixed(105))\n\n# --- Save --------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit=2)\n"}