{"spec_id":"dendrogram-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# dendrogram-basic: Basic Dendrogram\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 89/100 | Created: 2026-06-18\n\nusing CairoMakie\nusing Colors\nusing Random\nusing RDatasets\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 ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst INK_MUTED   = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green (Setosa)\n    colorant\"#C475FD\",  # 2 — lavender (Virginica)\n    colorant\"#4467A3\",  # 3 — blue (Versicolor)\n    colorant\"#BD8233\",  # 4 — ochre\n    colorant\"#AE3030\",  # 5 — matte red\n    colorant\"#2ABCCD\",  # 6 — cyan\n    colorant\"#954477\",  # 7 — rose\n    colorant\"#99B314\",  # 8 — lime\n]\n\n# Data: 5 samples from each iris species → 15 leaf nodes\niris_data = dataset(\"datasets\", \"iris\")\nsample_idx = vcat(1:5, 51:55, 101:105)\nfeatures = Matrix{Float64}(iris_data[sample_idx, 1:4])\nspecies_vec = string.(iris_data[sample_idx, :Species])\nn = size(features, 1)\n\nsp_color = Dict(\n    \"setosa\"     => IMPRINT_PALETTE[1],\n    \"versicolor\" => IMPRINT_PALETTE[3],\n    \"virginica\"  => IMPRINT_PALETTE[2],\n)\nsp_prefix = Dict(\"setosa\" => \"Se\", \"versicolor\" => \"Ve\", \"virginica\" => \"Vi\")\nsp_count  = Dict(\"setosa\" => 0, \"versicolor\" => 0, \"virginica\" => 0)\nleaf_labels = String[]\nfor i in 1:n\n    sp = species_vec[i]\n    sp_count[sp] += 1\n    push!(leaf_labels, \"$(sp_prefix[sp])$(sp_count[sp])\")\nend\n\n# Pairwise Euclidean distances\nD = [sqrt(sum((features[i, :] .- features[j, :]).^2)) for i in 1:n, j in 1:n]\n\n# Complete-linkage agglomerative clustering\nmembers = [[i] for i in 1:n]\nactive  = collect(1:n)\nmerges  = Tuple{Int,Int,Float64}[]\n\nwhile length(active) > 1\n    best_d, best_ai, best_bi = Inf, 1, 2\n    for ai in 1:length(active), bi in (ai + 1):length(active)\n        d = maximum(D[p, q] for p in members[active[ai]] for q in members[active[bi]])\n        d < best_d && ((best_d, best_ai, best_bi) = (d, ai, bi))\n    end\n    ca, cb = active[best_ai], active[best_bi]\n    push!(merges, (ca, cb, best_d))\n    push!(members, vcat(members[ca], members[cb]))\n    deleteat!(active, sort([best_ai, best_bi]))\n    push!(active, length(members))\nend\n\nn_merges = length(merges)\n\n# Build children map and DFS leaf order\nnode_children = Dict{Int,Tuple{Int,Int}}(n + i => (ca, cb) for (i, (ca, cb, _)) in enumerate(merges))\n\nstack      = [n + n_merges]\nleaf_order = Int[]\nwhile !isempty(stack)\n    node = pop!(stack)\n    if node <= n\n        push!(leaf_order, node)\n    else\n        ca, cb = node_children[node]\n        push!(stack, cb)\n        push!(stack, ca)\n    end\nend\n\n# Node x-positions (leaves at integer positions 1..n; internals at midpoints)\nnode_x = zeros(n + n_merges)\nfor (pos, leaf_id) in enumerate(leaf_order)\n    node_x[leaf_id] = Float64(pos)\nend\nfor (i, (ca, cb, _)) in enumerate(merges)\n    node_x[n + i] = (node_x[ca] + node_x[cb]) / 2\nend\n\n# Node y-positions (merge height; leaves at 0)\nnode_y = zeros(n + n_merges)\nfor (i, (_, _, h)) in enumerate(merges)\n    node_y[n + i] = h\nend\n\n# Node colors: pure-species cluster → species color; mixed → INK_SOFT\nnode_colors = fill(INK_SOFT, n + n_merges)\nfor i in 1:n\n    node_colors[i] = sp_color[species_vec[i]]\nend\nfor (i, _) in enumerate(merges)\n    new_id = n + i\n    sp_set = Set(species_vec[l] for l in members[new_id])\n    length(sp_set) == 1 && (node_colors[new_id] = sp_color[only(sp_set)])\nend\n\nordered_labels = [leaf_labels[leaf_order[pos]] for pos in 1:n]\nmax_height     = maximum(h for (_, _, h) in merges)\n\n# Species leaf position spans for vspan! background bands\nspecies_spans = Dict{String, Tuple{Float64,Float64}}()\nfor sp in keys(sp_color)\n    positions = [Float64(pos) for (pos, lid) in enumerate(leaf_order) if species_vec[lid] == sp]\n    isempty(positions) || (species_spans[sp] = (minimum(positions) - 0.45, maximum(positions) + 0.45))\nend\n\n# Figure\ntitle_str = \"Iris Clustering · dendrogram-basic · julia · makie · anyplot.ai\"\nn_title   = length(title_str)\ntitle_fs  = n_title > 67 ? round(Int, 20 * 67 / n_title) : 20\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          = title_fs,\n    titlecolor         = INK,\n    xlabel             = \"Sample\",\n    ylabel             = \"Distance (complete linkage)\",\n    xlabelsize         = 16,\n    ylabelsize         = 16,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticks             = (collect(1:n), ordered_labels),\n    xticklabelsize     = 11,\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.15f0),\n)\n\n# Species background bands — vspan! is a Makie-specific primitive\nfor (sp, (lo, hi)) in species_spans\n    c = sp_color[sp]\n    vspan!(ax, [lo], [hi]; color = RGBAf(Float32(red(c)), Float32(green(c)), Float32(blue(c)), 0.07f0))\nend\n\n# Dendrogram branches\nfor (i, (ca, cb, h)) in enumerate(merges)\n    xa, ya = node_x[ca], node_y[ca]\n    xb, yb = node_x[cb], node_y[cb]\n    lines!(ax, [xa, xa], [ya, h]; color = node_colors[ca], linewidth = 2.2)\n    lines!(ax, [xb, xb], [yb, h]; color = node_colors[cb], linewidth = 2.2)\n    lines!(ax, [xa, xb], [h, h]; color = node_colors[n + i], linewidth = 2.2)\nend\n\n# Leaf node markers — scatter! composing with lines! showcases Makie's geom layering\nleaf_x      = [node_x[leaf_order[pos]] for pos in 1:n]\nleaf_colors = [node_colors[leaf_order[pos]] for pos in 1:n]\nscatter!(ax, leaf_x, zeros(n); color = leaf_colors, markersize = 9, strokewidth = 0)\n\n# Internal merge node markers\nmerge_x      = [node_x[n + i] for i in 1:n_merges]\nmerge_y      = [h for (_, _, h) in merges]\nmerge_colors = [node_colors[n + i] for i in 1:n_merges]\nscatter!(ax, merge_x, merge_y; color = merge_colors, markersize = 7, strokewidth = 0)\n\nxlims!(ax, 0.0, Float64(n) + 1.0)\nylims!(ax, 0.0, max_height * 1.07)\n\n# Legend\nlegend_items = [\n    LineElement(color = IMPRINT_PALETTE[1], linewidth = 3),\n    LineElement(color = IMPRINT_PALETTE[3], linewidth = 3),\n    LineElement(color = IMPRINT_PALETTE[2], linewidth = 3),\n    LineElement(color = INK_SOFT, linewidth = 3),\n]\nLegend(fig[1, 2], legend_items, [\"Setosa\", \"Versicolor\", \"Virginica\", \"Mixed\"];\n    framevisible    = true,\n    framecolor      = INK_MUTED,\n    backgroundcolor = ELEVATED_BG,\n    labelcolor      = INK,\n    labelsize       = 12,\n    rowgap          = 4,\n)\n\nsave(joinpath(@__DIR__, \"plot-$(THEME).png\"), fig; px_per_unit = 2)\n"}