Categorical Scatter Plot — Makie.jl

A scatter plot where points are colored according to a categorical variable. Each category has a distinct color, allowing visual comparison of patterns across groups. A legend maps colors to category names.

Categorical Scatter Plot rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# scatter-categorical: Categorical Scatter Plot
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 89/100 | Created: 2026-09-05

using CairoMakie
using Colors
using Random
using Statistics

Random.seed!(42)

# --- Theme tokens ------------------------------------------------------------
const THEME    = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG  = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const INK      = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
const IMPRINT_PALETTE = [
    colorant"#009E73", colorant"#C475FD", colorant"#4467A3", colorant"#BD8233",
    colorant"#AE3030", colorant"#2ABCCD", colorant"#954477", colorant"#99B314",
]

# --- Data ---------------------------------------------------------------------
# Soil nutrient survey across three land-use types: fertilized cropland runs
# nitrogen-rich, grassland sits in between, and undisturbed forest soil is
# nutrient-poor but more variable plot to plot.
land_uses   = ["Cropland", "Grassland", "Forest"]
n_means     = [45.0, 26.0, 12.0]
n_sds       = [6.0, 5.0, 4.5]
p_means     = [31.0, 19.0, 8.0]
p_sds       = [4.5, 4.0, 3.5]
n_samples   = 60
markers     = [:circle, :utriangle, :diamond]

nitrogen   = Float64[]
phosphorus = Float64[]
group_idx  = Int[]

for i in eachindex(land_uses)
    append!(nitrogen, n_means[i] .+ n_sds[i] .* randn(n_samples))
    append!(phosphorus, p_means[i] .+ p_sds[i] .* randn(n_samples) .+ 0.3 .* (nitrogen[end-n_samples+1:end] .- n_means[i]))
    append!(group_idx, fill(i, n_samples))
end

title_str = "scatter-categorical · julia · makie · anyplot.ai"

# --- Plot ----------------------------------------------------------------------
fig = Figure(
    resolution      = (1600, 900),
    fontsize        = 14,
    backgroundcolor = PAGE_BG,
)

ax = Axis(
    fig[1, 1];
    title             = title_str,
    titlesize         = 20,
    titlecolor        = INK,
    xlabel            = "Soil Nitrogen (mg/kg)",
    ylabel            = "Soil Phosphorus (mg/kg)",
    xlabelsize        = 14,
    ylabelsize        = 14,
    xlabelcolor       = INK,
    ylabelcolor       = INK,
    xticklabelsize    = 12,
    yticklabelsize    = 12,
    xticklabelcolor   = INK_SOFT,
    yticklabelcolor   = INK_SOFT,
    xtickcolor        = INK_SOFT,
    ytickcolor        = INK_SOFT,
    backgroundcolor   = PAGE_BG,
    topspinevisible   = false,
    rightspinevisible = false,
    leftspinecolor    = INK_SOFT,
    bottomspinecolor  = INK_SOFT,
    xgridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),
    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),
    xminorgridvisible = false,
    yminorgridvisible = false,
)

for i in eachindex(land_uses)
    mask = group_idx .== i
    fill_color = RGBAf(IMPRINT_PALETTE[i].r, IMPRINT_PALETTE[i].g, IMPRINT_PALETTE[i].b, 0.85)
    scatter!(ax, nitrogen[mask], phosphorus[mask];
             color = fill_color, marker = markers[i],
             markersize = 16, strokewidth = 1, strokecolor = PAGE_BG,
             label = land_uses[i])
end

# Direct group labels above each cluster's core — offset from the centroid so
# the label sits in the open space above the point cloud, with a soft
# background-matched glow as a safety net against any stray overlapping point.
for i in eachindex(land_uses)
    mask = group_idx .== i
    label_x = mean(nitrogen[mask])
    label_y = mean(phosphorus[mask]) + 1.3 * std(phosphorus[mask])
    text!(ax, label_x, label_y; text = land_uses[i], color = INK,
          fontsize = 15, font = :bold, align = (:center, :center),
          glowcolor = PAGE_BG, glowwidth = 4)
end

axislegend(ax, position = :rb, labelcolor = INK, framevisible = false, labelsize = 13)

# --- Save -----------------------------------------------------------------------
save("plot-$(THEME).png", fig; px_per_unit = 2)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/scatter-categorical/makie/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "scatter-categorical",
  "language": "julia",
  "library": "makie",
  "page": "https://anyplot.ai/scatter-categorical/julia/makie",
  "hub": "https://anyplot.ai/scatter-categorical",
  "code_json": "https://api.anyplot.ai/specs/scatter-categorical/makie/code",
  "spec_json": "https://api.anyplot.ai/specs/scatter-categorical",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/scatter-categorical/julia/makie/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/scatter-categorical/julia/makie/plot-dark.png",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Categorical Scatter Plot on anyplot.ai.

Other implementations