{"spec_id":"heatmap-risk-matrix","library":"makie","language":"julia","code":"# anyplot.ai\n# heatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-06-20\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\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\"\n\n# Risk colormap: Imprint green (Low) → amber (caution) → matte red (Critical)\nconst RISK_CMAP = cgrad(\n    [colorant\"#009E73\", colorant\"#DDCC77\", colorant\"#AE3030\"],\n    [0.0, 0.16, 1.0],\n)\n\n# 5×5 risk score matrix (likelihood × impact)\nrisk_matrix = Float64[i * j for i in 1:5, j in 1:5]\n\nlikelihood_labels = [\"Rare\", \"Unlikely\", \"Possible\", \"Likely\", \"Almost\\nCertain\"]\nimpact_labels     = [\"Negligible\", \"Minor\", \"Moderate\", \"Major\", \"Catastrophic\"]\n\n# Risk register: (name, likelihood 1–5, impact 1–5, category)\nrisks = [\n    (\"Data Breach\",    2, 5, \"Technical\"),\n    (\"Budget Overrun\", 4, 4, \"Financial\"),\n    (\"Scope Creep\",    4, 3, \"Operational\"),\n    (\"Vendor Failure\", 2, 4, \"Financial\"),\n    (\"System Outage\",  3, 5, \"Technical\"),\n    (\"Staff Turnover\", 4, 2, \"Operational\"),\n    (\"Compliance\",     1, 5, \"Regulatory\"),\n    (\"HW Fault\",       3, 3, \"Technical\"),\n    (\"Market Shift\",   3, 4, \"Financial\"),\n    (\"Cyber Attack\",   5, 5, \"Technical\"),\n    (\"Proc. Delay\",    5, 3, \"Operational\"),\n    (\"Reg. Change\",    2, 3, \"Regulatory\"),\n]\n\nrisk_names = [r[1] for r in risks]\nrisk_lk    = [r[2] for r in risks]   # likelihood (x-axis)\nrisk_imp   = [r[3] for r in risks]   # impact (y-axis)\nrisk_cats  = [r[4] for r in risks]   # category for shape encoding\n\n# Secondary CVD cue: distinct marker shape per risk category\ncat_markers = Dict(\n    \"Technical\"   => :circle,\n    \"Financial\"   => :diamond,\n    \"Operational\" => :utriangle,\n    \"Regulatory\"  => :rect,\n)\n\n# Jitter: deterministically offset risk items that share the same grid cell\nfunction apply_jitter(xs, ys)\n    cell_items = Dict{Tuple{Int,Int}, Vector{Int}}()\n    for i in eachindex(xs)\n        key = (xs[i], ys[i])\n        push!(get!(cell_items, key, Int[]), i)\n    end\n    xj = float.(copy(xs))\n    yj = float.(copy(ys))\n    for (_, idxs) in cell_items\n        n = length(idxs)\n        n == 1 && continue\n        for (k, i) in enumerate(idxs)\n            θ = 2π * (k - 1) / n - π / 2\n            xj[i] += 0.2 * cos(θ)\n            yj[i] += 0.2 * sin(θ)\n        end\n    end\n    return xj, yj\nend\n\nrisk_x, risk_y = apply_jitter(risk_lk, risk_imp)\n\n# ── Figure ───────────────────────────────────────────────────────────────────\nfig = Figure(\n    size            = (1200, 1200),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = \"heatmap-risk-matrix · julia · makie · anyplot.ai\",\n    titlesize          = 22,\n    titlecolor         = INK,\n    xlabel             = \"Likelihood\",\n    ylabel             = \"Consequence / Impact\",\n    xlabelsize         = 16,\n    ylabelsize         = 16,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = true,\n    rightspinevisible  = true,\n    leftspinevisible   = true,\n    bottomspinevisible = true,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    topspinecolor      = INK_SOFT,\n    rightspinecolor    = INK_SOFT,\n    xgridvisible       = false,\n    ygridvisible       = false,\n    xticks             = (1:5, likelihood_labels),\n    yticks             = (1:5, impact_labels),\n)\n\n# Background heatmap\nhm = heatmap!(ax, 1:5, 1:5, risk_matrix;\n    colormap   = RISK_CMAP,\n    colorrange = (1.0, 25.0),\n)\n\n# Cell grid lines\nfor k in 0.5:1.0:5.5\n    hlines!(ax, k; color = (INK_SOFT, 0.6), linewidth = 1.2)\n    vlines!(ax, k; color = (INK_SOFT, 0.6), linewidth = 1.2)\nend\n\n# Risk score annotation in each cell — Makie text! with sub-cell alignment control\nfor lk in 1:5, imp in 1:5\n    text!(ax, lk - 0.4, imp + 0.38;\n          text     = string(lk * imp),\n          align    = (:left, :top),\n          fontsize = 9,\n          color    = (INK_SOFT, 0.75),\n    )\nend\n\n# Category-shaped markers — label attribute feeds the Makie Legend below\nfor cat in unique(risk_cats)\n    idxs = findall(==(cat), risk_cats)\n    scatter!(ax, risk_x[idxs], risk_y[idxs];\n        color       = ELEVATED_BG,\n        marker      = cat_markers[cat],\n        markersize  = 20,\n        strokecolor = INK,\n        strokewidth = 1.8,\n        label       = cat,\n    )\nend\n\n# Risk item labels — increased fontsize for desktop and mobile readability\nfor i in eachindex(risk_names)\n    text!(ax, risk_x[i], risk_y[i] + 0.22;\n          text     = risk_names[i],\n          align    = (:center, :bottom),\n          fontsize = 11,\n          color    = INK,\n    )\nend\n\nxlims!(ax, 0.5, 5.5)\nylims!(ax, 0.5, 5.5)\n\n# Colorbar with zone labels at zone midpoints\nColorbar(fig[1, 2], hm;\n    label          = \"Risk Score  (Likelihood × Impact)\",\n    labelsize      = 14,\n    labelcolor     = INK,\n    ticklabelsize  = 11,\n    ticklabelcolor = INK_SOFT,\n    tickcolor      = INK_SOFT,\n    ticks          = (\n        [2.5, 7.0, 13.0, 22.5],\n        [\"Low (1–4)\", \"Medium (5–9)\", \"High (10–16)\", \"Critical (20–25)\"],\n    ),\n    width          = 22,\n)\n\n# Makie composable layout: category Legend in a dedicated row below the heatmap.\n# Legend(position, ax) extracts labeled scatter! plots — a layout feature\n# not available in matplotlib's Figure/Axes model.\nLegend(fig[2, 1:2], ax;\n    orientation  = :horizontal,\n    tellwidth    = false,\n    framevisible = false,\n    labelcolor   = INK_SOFT,\n    labelsize    = 11,\n)\n\nrowgap!(fig.layout, 1, 6)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}