{"spec_id":"ternary-density","library":"makie","language":"julia","code":"# anyplot.ai\n# ternary-density: Ternary Density Plot\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 91/100 | Created: 2026-09-02\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 GRID_RGBA = RGBAf(INK.r, INK.g, INK.b, 0.35)\n\n# Imprint sequential colormap — single-polarity continuous data (density)\nconst IMPRINT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\n\n# --- Data: simulated sediment composition (sand / silt / clay), two facies --\nn_samples = 2400\nn_sandy = round(Int, 0.55 * n_samples)\n\ncomponent_a = zeros(n_samples)  # sand\ncomponent_b = zeros(n_samples)  # silt\ncomponent_c = zeros(n_samples)  # clay\n\nfor i in 1:n_samples\n    shape_a, shape_b, shape_c = i <= n_sandy ? (7, 3, 2) : (2, 3, 7)\n    gamma_a = -sum(log.(rand(shape_a)))\n    gamma_b = -sum(log.(rand(shape_b)))\n    gamma_c = -sum(log.(rand(shape_c)))\n    total = gamma_a + gamma_b + gamma_c\n    component_a[i] = gamma_a / total\n    component_b[i] = gamma_b / total\n    component_c[i] = gamma_c / total\nend\n\n# Barycentric -> Cartesian (A at (0,0), B at (1,0), C at (0.5, sqrt(3)/2))\ntri_height = sqrt(3) / 2\nx_coords = component_b .+ component_c .* 0.5\ny_coords = component_c .* tri_height\n\n# --- Kernel density estimate on a fine grid, masked to the triangle --------\nbandwidth = mean([std(x_coords), std(y_coords)]) * n_samples^(-1 / 6)\n\n# Bin edges exactly at the triangle's bounding box, so the heatmap below has no\n# half-cell overhang past [0, 1] x [0, tri_height] (Makie centers cells otherwise).\nedge_xs = range(0, 1; length = 161)\nedge_ys = range(0, tri_height; length = 141)\ncenter_xs = (edge_xs[1:end-1] .+ edge_xs[2:end]) ./ 2\ncenter_ys = (edge_ys[1:end-1] .+ edge_ys[2:end]) ./ 2\n\ndensity_grid = Array{Float64}(undef, length(center_xs), length(center_ys))\nfor (i, gx) in enumerate(center_xs), (j, gy) in enumerate(center_ys)\n    weights = exp.(-((gx .- x_coords) .^ 2 .+ (gy .- y_coords) .^ 2) ./ (2 * bandwidth^2))\n    density_grid[i, j] = sum(weights) / (n_samples * 2 * pi * bandwidth^2)\nend\n\ncontour_levels = maximum(density_grid) .* [0.15, 0.35, 0.55, 0.75]\n\n# --- Figure -------------------------------------------------------------------\ntitle_str = \"Sediment Composition · ternary-density · julia · makie · anyplot.ai\"\n\nfig = Figure(\n    size            = (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    backgroundcolor = PAGE_BG,\n    aspect          = DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\n\n# Ternary grid lines (beneath the density layer), every 20% of each component\nfor k in (0.2, 0.4, 0.6, 0.8)\n    # constant component_a = k, parallel to edge B-C\n    lines!(ax, [1 - k, (1 - k) * 0.5], [0, (1 - k) * tri_height]; color = GRID_RGBA, linewidth = 1.2)\n    # constant component_b = k, parallel to edge A-C\n    lines!(ax, [k, k + (1 - k) * 0.5], [0, (1 - k) * tri_height]; color = GRID_RGBA, linewidth = 1.2)\n    # constant component_c = k, parallel to edge A-B\n    lines!(ax, [k * 0.5, 1 - k * 0.5], [k * tri_height, k * tri_height]; color = GRID_RGBA, linewidth = 1.2)\nend\n\n# Density heatmap over the grid's bounding box (Imprint sequential)\nhm = heatmap!(ax, edge_xs, edge_ys, density_grid; colormap = IMPRINT_SEQ, alpha = 0.92)\n\n# Contour lines at key density levels for easier interpretation\ncontour!(ax, center_xs, center_ys, density_grid; levels = contour_levels, color = INK, linewidth = 1.3, alpha = 0.55)\n\n# Mask the two bounding-box corners outside the triangle with crisp vector edges\n# (avoids the staircase artifact a rectilinear heatmap grid would leave on the diagonals)\npoly!(ax, Point2f[(0, 0), (0, tri_height), (0.5, tri_height)]; color = PAGE_BG, strokewidth = 0)\npoly!(ax, Point2f[(1, 0), (1, tri_height), (0.5, tri_height)]; color = PAGE_BG, strokewidth = 0)\n\n# Triangle outline\nlines!(ax, [0, 1, 0.5, 0], [0, 0, tri_height, 0]; color = INK_SOFT, linewidth = 2.5)\n\n# Vertex labels\ntext!(ax, 0, -0.05; text = \"Sand\", align = (:center, :top), color = INK, fontsize = 16)\ntext!(ax, 1, -0.05; text = \"Silt\", align = (:center, :top), color = INK, fontsize = 16)\ntext!(ax, 0.5, tri_height + 0.05; text = \"Clay\", align = (:center, :bottom), color = INK, fontsize = 16)\n\n# Explicit limits — automatic axis limits don't account for text bounding boxes,\n# which would otherwise clip the vertex labels against the scene edge.\nxlims!(ax, -0.1, 1.1)\nylims!(ax, -0.13, tri_height + 0.13)\n\nColorbar(fig[1, 2], hm; label = \"Density\", labelcolor = INK, ticklabelcolor = INK_SOFT, labelsize = 14, ticklabelsize = 12)\ncolsize!(fig.layout, 1, Relative(0.88))\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}