{"spec_id":"confusion-matrix","library":"makie","language":"julia","code":"# anyplot.ai\n# confusion-matrix: Confusion Matrix Heatmap\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 87/100 | Created: 2026-09-04\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\"\n\n# --- Data: bird species classifier confusion matrix -------------------------\n# Rows = true species, columns = predicted species. Small songbirds\n# (Sparrow, Finch, Robin) are visually similar and get confused with each\n# other more often; raptors (Owl, Hawk) are visually distinct and are rarely\n# confused with songbirds or with each other.\nclass_names = [\"Sparrow\", \"Finch\", \"Robin\", \"Owl\", \"Hawk\"]\nn_classes = length(class_names)\nsample_sizes = [180, 150, 165, 90, 95]\nconfusion_rates = [\n    0.90 0.05 0.04 0.00 0.01\n    0.06 0.87 0.06 0.00 0.01\n    0.05 0.07 0.86 0.01 0.01\n    0.00 0.01 0.01 0.93 0.05\n    0.01 0.00 0.01 0.06 0.92\n]\n\ncounts = zeros(Int, n_classes, n_classes)\nfor i in 1:n_classes\n    counts[i, :] = round.(Int, sample_sizes[i] .* confusion_rates[i, :])\nend\nmax_count = maximum(counts)\n\n# Row-normalized percentages (recall): each cell as a share of its true-class\n# row total — satisfies the spec's \"support normalization by row\" requirement\n# alongside the raw counts.\nrow_sums = vec(sum(counts; dims = 2))\nrow_pct = [round(Int, 100 * counts[i, j] / row_sums[i]) for i in 1:n_classes, j in 1:n_classes]\ndiag_counts = [counts[i, i] for i in 1:n_classes]\noverall_accuracy = 100 * sum(diag_counts) / sum(counts)\n\n# --- Colormap: Imprint sequential (single-polarity count data) --------------\nconst IMPRINT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\n\n# --- Plot ---------------------------------------------------------------\nfig = Figure(\n    resolution      = (1200, 1200),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = rich(\n        \"confusion-matrix · julia · makie · anyplot.ai\",\n        \"\\n\",\n        rich(\n            \"Overall accuracy: $(round(overall_accuracy, digits = 1))% · cells show count and row-normalized recall\";\n            fontsize = 13, color = INK_SOFT,\n        ),\n    ),\n    titlesize          = 20,\n    titlecolor         = INK,\n    xlabel             = \"Predicted Label\",\n    ylabel             = \"True Label\",\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xticks             = (1:n_classes, class_names),\n    yticks             = (1:n_classes, class_names),\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    aspect             = DataAspect(),\n    yreversed          = true,\n    leftspinecolor     = INK_SOFT,\n    rightspinecolor    = INK_SOFT,\n    topspinecolor      = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = false,\n    ygridvisible       = false,\n)\n\nzmatrix = permutedims(counts)\nhm = heatmap!(\n    ax, 1:n_classes, 1:n_classes, zmatrix;\n    colormap = IMPRINT_SEQ, colorrange = (0, max_count),\n)\n\n# Cell annotations — raw count above the row-normalized recall percentage,\n# combined into a single two-line text! call per cell (a separate text!\n# call per line risked being dropped); text color adapts to cell luminance\n# so it stays legible across the full green-to-blue sequential range.\nfor i in 1:n_classes, j in 1:n_classes\n    value = counts[i, j]\n    t = max_count == 0 ? 0.0 : value / max_count\n    cell_color = IMPRINT_SEQ[t]\n    luminance = 0.2126 * red(cell_color) + 0.7152 * green(cell_color) + 0.0722 * blue(cell_color)\n    text_color = luminance > 0.55 ? INK : colorant\"#FFFFFF\"\n    text!(\n        ax, j, i, text = \"$(value)\\n$(row_pct[i, j])%\",\n        align = (:center, :center), color = text_color, fontsize = 15,\n    )\nend\n\n# Highlight the diagonal (correct predictions)\nfor i in 1:n_classes\n    xs = [i - 0.5, i + 0.5, i + 0.5, i - 0.5, i - 0.5]\n    ys = [i - 0.5, i - 0.5, i + 0.5, i + 0.5, i - 0.5]\n    lines!(ax, xs, ys; color = INK, linewidth = 3)\nend\n\nColorbar(\n    fig[1, 2], hm;\n    label = \"Sample Count\", labelsize = 14, labelcolor = INK,\n    ticklabelsize = 12, ticklabelcolor = INK_SOFT, width = 25,\n)\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}