{"spec_id":"violin-swarm","library":"makie","language":"julia","code":"# anyplot.ai\n# violin-swarm: Violin Plot with Overlaid Swarm Points\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 85/100 | Created: 2026-09-02\n\nusing CairoMakie\nusing Colors\nusing Random\nusing Statistics\n\nRandom.seed!(42)\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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# Imprint categorical palette — 8 hues, theme-independent, hybrid-v3 sort\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green\n    colorant\"#C475FD\",  # 2 — lavender\n    colorant\"#4467A3\",  # 3 — blue\n    colorant\"#BD8233\",  # 4 — ochre\n]\n\n# Data — reaction time (ms) across 4 caffeine dosage groups, 50 trials each.\n# Right-skewed noise mimics the long slow-trial tail typical of RT data, and\n# the 300 mg group's uptick models overstimulation jitter past the optimum.\nconst DOSE_LABELS = [\"0 mg\", \"100 mg\", \"200 mg\", \"300 mg\"]\nconst BASELINE_MS = [430.0, 388.0, 356.0, 368.0]\nconst SPREAD_MS   = [46.0, 40.0, 34.0, 50.0]\nconst N_PER_GROUP = 50\n\ndose_idx         = Int[]\nreaction_time_ms = Float64[]\nfor (i, (base, spread)) in enumerate(zip(BASELINE_MS, SPREAD_MS))\n    noise      = randn(N_PER_GROUP)\n    right_tail = 0.35 .* spread .* max.(noise, 0.0)\n    trials     = clamp.(base .+ spread .* noise .+ right_tail, 180.0, 650.0)\n    append!(reaction_time_ms, trials)\n    append!(dose_idx, fill(i, N_PER_GROUP))\nend\n\n# Gaussian KDE — approximates the same density curve Makie's violin! draws,\n# so the swarm's allowed spread can be tied to the violin's actual local\n# width instead of raw histogram counts.\nfunction kde_density(values, x, bandwidth)\n    z = (x .- values) ./ bandwidth\n    return sum(exp.(-0.5 .* z .^ 2)) / (length(values) * bandwidth * sqrt(2π))\nend\n\n# Beeswarm layout — bin each category's trials by value, then stack points\n# alternating left/right of center within a bin. Each bin's half-width is\n# the local KDE density (relative to the category's peak density) scaled to\n# max_half_width, so the allowed spread narrows exactly where the violin\n# body narrows and points never escape the outline.\nfunction beeswarm_offsets(values, max_half_width, n_bins)\n    n      = length(values)\n    lo, hi = minimum(values), maximum(values)\n    # Silverman's rule of thumb bandwidth, inlined (single call site).\n    sigma  = min(std(values), (quantile(values, 0.75) - quantile(values, 0.25)) / 1.34)\n    bw     = 0.9 * sigma * n^(-0.2)\n\n    bin_edges   = range(lo, hi, length = n_bins + 1)\n    bin_centers = (bin_edges[1:(end - 1)] .+ bin_edges[2:end]) ./ 2\n    bin_density = [kde_density(values, c, bw) for c in bin_centers]\n    peak        = maximum(bin_density)\n    bin_width   = max_half_width .* bin_density ./ peak\n\n    offsets     = zeros(Float64, n)\n    span        = hi - lo\n    slot_of_bin = zeros(Int, n_bins)\n    for idx in sortperm(values)\n        b = span > 0 ? clamp(floor(Int, (values[idx] - lo) / span * n_bins), 0, n_bins - 1) + 1 : 1\n        slot = slot_of_bin[b]\n        slot_of_bin[b] += 1\n        side      = isodd(slot) ? 1 : -1\n        magnitude = slot == 0 ? 0.0 : ceil(slot / 2)\n        step      = bin_width[b] / 6.0\n        offsets[idx] = clamp(side * magnitude * step, -bin_width[b], bin_width[b])\n    end\n    return offsets\nend\n\nconst VIOLIN_WIDTH     = 0.8\nconst SWARM_HALF_WIDTH = 0.32\nconst N_BINS           = 40\n\nswarm_offset = zeros(Float64, length(reaction_time_ms))\nfor i in eachindex(DOSE_LABELS)\n    mask = dose_idx .== i\n    swarm_offset[mask] = beeswarm_offsets(reaction_time_ms[mask], SWARM_HALF_WIDTH, N_BINS)\nend\nswarm_x = Float64.(dose_idx) .+ swarm_offset\n\n# Plot — see default-style-guide.md \"Visual Sizing Defaults\" for canvas + sizing values\ntitle_str = \"violin-swarm · julia · makie · anyplot.ai\"\n\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            = \"Caffeine Dose\",\n    ylabel            = \"Reaction Time (ms)\",\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    xgridvisible      = false,\n    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    yminorgridvisible = false,\n    xminorgridvisible = false,\n    xticks            = (1:length(DOSE_LABELS), DOSE_LABELS),\n)\n\n# Translucent violins (alpha 0.4) so the swarm underneath stays legible.\n# show_median draws Makie's native per-violin median line — a built-in\n# violin! feature (not a manual overlay) that anchors each group's center.\nfor (i, col) in enumerate(IMPRINT_PALETTE)\n    mask = dose_idx .== i\n    violin!(ax, dose_idx[mask], reaction_time_ms[mask];\n        color           = (col, 0.4),\n        strokewidth     = 1.5,\n        strokecolor     = col,\n        width           = VIOLIN_WIDTH,\n        show_median     = true,\n        mediancolor     = INK,\n        medianlinewidth = 2.0,\n    )\nend\n\n# Dashed trend line through each group's median sharpens the inverted-U\n# dose-response story (reaction time drops, then rises again at 300 mg).\nmedians_ms = [median(reaction_time_ms[dose_idx .== i]) for i in eachindex(DOSE_LABELS)]\nlines!(ax, 1:length(DOSE_LABELS), medians_ms;\n    color     = INK_SOFT,\n    linewidth = 1.5,\n    linestyle = :dash,\n)\n\n# Ink-colored swarm points contrast against every translucent violin hue\nscatter!(ax, swarm_x, reaction_time_ms;\n    color       = INK,\n    markersize  = 6,\n    strokewidth = 0.5,\n    strokecolor = PAGE_BG,\n)\n\n# Save\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}