{"spec_id":"contour-decision-boundary","library":"makie","language":"julia","code":"# anyplot.ai\n# contour-decision-boundary: Decision Boundary Classifier Visualization\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 92/100 | Created: 2026-09-04\n\nusing CairoMakie\nusing Colors\nusing Random\nusing RDatasets\nusing DataFrames\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\"\nIMPRINT_PALETTE = [colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\"]\n\n# --- Data: Fisher's iris, petal measurements for 3 species -------------------\niris = RDatasets.dataset(\"datasets\", \"iris\")\npetal_length = iris.PetalLength\npetal_width = iris.PetalWidth\nspecies_labels = String.(iris.Species)\nclass_names = sort(unique(species_labels))\nspecies_idx = [findfirst(==(s), class_names) for s in species_labels]\nn_classes = length(class_names)\n\n# --- Classifier: k-nearest-neighbours vote over a dense mesh grid ------------\nk = 9\npad_x = 0.06 * (maximum(petal_length) - minimum(petal_length))\npad_y = 0.06 * (maximum(petal_width) - minimum(petal_width))\nxs = range(minimum(petal_length) - pad_x, maximum(petal_length) + pad_x; length = 150)\nys = range(minimum(petal_width) - pad_y, maximum(petal_width) + pad_y; length = 150)\n\nregion = Matrix{Int}(undef, length(xs), length(ys))\nfor (i, gx) in enumerate(xs), (j, gy) in enumerate(ys)\n    dist_sq = (petal_length .- gx) .^ 2 .+ (petal_width .- gy) .^ 2\n    nearest = partialsortperm(dist_sq, 1:k)\n    votes = [count(==(c), species_idx[nearest]) for c in 1:n_classes]\n    region[i, j] = argmax(votes)\nend\n\n# Leave-one-out re-classification of the training points themselves, to flag\n# which ones the classifier gets wrong (used for the marker-shape encoding).\ncorrectly_classified = falses(length(species_idx))\nfor p in eachindex(species_idx)\n    dist_sq = (petal_length .- petal_length[p]) .^ 2 .+ (petal_width .- petal_width[p]) .^ 2\n    dist_sq[p] = Inf\n    nearest = partialsortperm(dist_sq, 1:k)\n    votes = [count(==(c), species_idx[nearest]) for c in 1:n_classes]\n    correctly_classified[p] = argmax(votes) == species_idx[p]\nend\n\n# Deterministic jitter for exact-duplicate (petal_length, petal_width) pairs so\n# that overlapping hit/miss markers don't collapse into a single confusing\n# glyph. Classification above uses the true coordinates; only the plotted\n# marker positions are nudged.\nplot_x = copy(petal_length)\nplot_y = copy(petal_width)\nduplicate_groups = Dict{Tuple{Float64,Float64},Vector{Int}}()\nfor (i, key) in enumerate(zip(petal_length, petal_width))\n    push!(get!(duplicate_groups, key, Int[]), i)\nend\njitter_rx = 0.018 * (maximum(petal_length) - minimum(petal_length))\njitter_ry = 0.018 * (maximum(petal_width) - minimum(petal_width))\nfor idxs in values(duplicate_groups)\n    n_dup = length(idxs)\n    if n_dup > 1\n        for (rank, i) in enumerate(idxs)\n            angle = 2π * (rank - 1) / n_dup\n            plot_x[i] += jitter_rx * cos(angle)\n            plot_y[i] += jitter_ry * sin(angle)\n        end\n    end\nend\n\n# --- Plot ---------------------------------------------------------------------\nfig = Figure(size = (1600, 900), fontsize = 14, backgroundcolor = PAGE_BG)\n\nax = Axis(\n    fig[1, 1];\n    title = \"contour-decision-boundary · julia · makie · anyplot.ai\",\n    titlesize = 20,\n    titlecolor = INK,\n    xlabel = \"Petal Length (cm)\",\n    ylabel = \"Petal Width (cm)\",\n    xlabelsize = 14,\n    ylabelsize = 14,\n    xlabelcolor = INK,\n    ylabelcolor = INK,\n    xticklabelsize = 14,\n    yticklabelsize = 14,\n    xticklabelcolor = INK_SOFT,\n    yticklabelcolor = INK_SOFT,\n    xtickcolor = INK_SOFT,\n    ytickcolor = INK_SOFT,\n    backgroundcolor = PAGE_BG,\n    topspinevisible = false,\n    rightspinevisible = false,\n    leftspinecolor = INK_SOFT,\n    bottomspinecolor = INK_SOFT,\n    xgridvisible = false,\n    ygridvisible = false,\n)\n\nregion_f = Float64.(region)\n\n# Smooth filled decision surface (interpolated boundaries, unlike a blocky\n# heatmap!) plus a thin boundary line between adjacent classes for extra\n# polish beyond a flat, unrefined region fill.\nfill_colors = [RGBAf(c.r, c.g, c.b, 0.35) for c in IMPRINT_PALETTE[1:n_classes]]\ncontourf!(\n    ax, xs, ys, region_f;\n    levels = 0.5:1:(n_classes + 0.5),\n    colormap = cgrad(fill_colors; categorical = true),\n)\ncontour!(\n    ax, xs, ys, region_f;\n    levels = collect(1.5:1:(n_classes - 0.5)),\n    color = (INK_SOFT, 0.5),\n    linewidth = 1.2,\n)\n\nfor c in 1:n_classes\n    in_class = species_idx .== c\n    hit = in_class .& correctly_classified\n    miss = in_class .& .!correctly_classified\n    scatter!(\n        ax, plot_x[hit], plot_y[hit];\n        color = IMPRINT_PALETTE[c], markersize = 16, marker = :circle,\n        strokewidth = 1.5, strokecolor = PAGE_BG, label = class_names[c],\n    )\n    scatter!(\n        ax, plot_x[miss], plot_y[miss];\n        color = IMPRINT_PALETTE[c], markersize = 20, marker = :xcross,\n        strokewidth = 2, strokecolor = INK,\n    )\nend\n\nlegend_elems = [\n    MarkerElement(\n        color = IMPRINT_PALETTE[c], marker = :circle, markersize = 16,\n        strokewidth = 1.5, strokecolor = PAGE_BG,\n    ) for c in 1:n_classes\n]\nlegend_labels = copy(class_names)\npush!(legend_elems, MarkerElement(color = INK_SOFT, marker = :xcross, markersize = 18, strokewidth = 2, strokecolor = INK_SOFT))\npush!(legend_labels, \"Misclassified (leave-one-out)\")\n\nLegend(\n    fig[1, 2], legend_elems, legend_labels;\n    framevisible = false, labelcolor = INK, labelsize = 12,\n    backgroundcolor = PAGE_BG,\n)\n\n# --- Save ----------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}