{"spec_id":"scatter-categorical","library":"makie","language":"julia","code":"# anyplot.ai\n# scatter-categorical: Categorical Scatter Plot\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 89/100 | Created: 2026-09-05\n\nusing CairoMakie\nusing Colors\nusing Random\nusing Statistics\n\nRandom.seed!(42)\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\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\n\n# --- Data ---------------------------------------------------------------------\n# Soil nutrient survey across three land-use types: fertilized cropland runs\n# nitrogen-rich, grassland sits in between, and undisturbed forest soil is\n# nutrient-poor but more variable plot to plot.\nland_uses   = [\"Cropland\", \"Grassland\", \"Forest\"]\nn_means     = [45.0, 26.0, 12.0]\nn_sds       = [6.0, 5.0, 4.5]\np_means     = [31.0, 19.0, 8.0]\np_sds       = [4.5, 4.0, 3.5]\nn_samples   = 60\nmarkers     = [:circle, :utriangle, :diamond]\n\nnitrogen   = Float64[]\nphosphorus = Float64[]\ngroup_idx  = Int[]\n\nfor i in eachindex(land_uses)\n    append!(nitrogen, n_means[i] .+ n_sds[i] .* randn(n_samples))\n    append!(phosphorus, p_means[i] .+ p_sds[i] .* randn(n_samples) .+ 0.3 .* (nitrogen[end-n_samples+1:end] .- n_means[i]))\n    append!(group_idx, fill(i, n_samples))\nend\n\ntitle_str = \"scatter-categorical · julia · makie · anyplot.ai\"\n\n# --- Plot ----------------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title             = title_str,\n    titlesize         = 20,\n    titlecolor        = INK,\n    xlabel            = \"Soil Nitrogen (mg/kg)\",\n    ylabel            = \"Soil Phosphorus (mg/kg)\",\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    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    xgridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n)\n\nfor i in eachindex(land_uses)\n    mask = group_idx .== i\n    fill_color = RGBAf(IMPRINT_PALETTE[i].r, IMPRINT_PALETTE[i].g, IMPRINT_PALETTE[i].b, 0.85)\n    scatter!(ax, nitrogen[mask], phosphorus[mask];\n             color = fill_color, marker = markers[i],\n             markersize = 16, strokewidth = 1, strokecolor = PAGE_BG,\n             label = land_uses[i])\nend\n\n# Direct group labels above each cluster's core — offset from the centroid so\n# the label sits in the open space above the point cloud, with a soft\n# background-matched glow as a safety net against any stray overlapping point.\nfor i in eachindex(land_uses)\n    mask = group_idx .== i\n    label_x = mean(nitrogen[mask])\n    label_y = mean(phosphorus[mask]) + 1.3 * std(phosphorus[mask])\n    text!(ax, label_x, label_y; text = land_uses[i], color = INK,\n          fontsize = 15, font = :bold, align = (:center, :center),\n          glowcolor = PAGE_BG, glowwidth = 4)\nend\n\naxislegend(ax, position = :rb, labelcolor = INK, framevisible = false, labelsize = 13)\n\n# --- Save -----------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}