{"spec_id":"silhouette-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# silhouette-basic: Silhouette Plot\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 87/100 | Created: 2026-09-09\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: synthetic clustering result, one pair of clusters overlapping ---\n# to produce a realistic mix of strong and borderline silhouette scores.\nn_per_cluster = 50\nn_clusters = 3\ncenters = [(0.0, 0.0), (4.2, 0.0), (2.0, 3.2)]\nspreads = [1.0, 1.0, 1.3]\n\nfeature_x = Float64[]\nfeature_y = Float64[]\ncluster_labels = Int[]\nfor c in 1:n_clusters\n    cx, cy = centers[c]\n    append!(feature_x, cx .+ spreads[c] .* randn(n_per_cluster))\n    append!(feature_y, cy .+ spreads[c] .* randn(n_per_cluster))\n    append!(cluster_labels, fill(c - 1, n_per_cluster))\nend\nn_samples = length(feature_x)\n\n# --- Silhouette coefficient per sample (computed directly: a(i), b(i)) -----\neuclidean(i, j) = sqrt((feature_x[i] - feature_x[j])^2 + (feature_y[i] - feature_y[j])^2)\n\nsilhouette_values = zeros(n_samples)\nfor i in 1:n_samples\n    own_cluster = cluster_labels[i]\n    same_idx = [j for j in 1:n_samples if cluster_labels[j] == own_cluster && j != i]\n    a_i = isempty(same_idx) ? 0.0 : mean(euclidean(i, j) for j in same_idx)\n\n    b_i = Inf\n    for c in 0:(n_clusters - 1)\n        c == own_cluster && continue\n        other_idx = [j for j in 1:n_samples if cluster_labels[j] == c]\n        b_i = min(b_i, mean(euclidean(i, j) for j in other_idx))\n    end\n\n    silhouette_values[i] = isempty(same_idx) ? 0.0 : (b_i - a_i) / max(a_i, b_i)\nend\navg_silhouette = mean(silhouette_values)\n\n# --- Arrange bars: grouped by cluster, ascending within cluster, gapped ----\ncluster_gap = 8\ny_positions = Float64[]\nbar_values = Float64[]\nbar_colors = RGB[]\ncluster_center_y = Float64[]\ncluster_avg_silhouette = Float64[]\ncluster_max_silhouette = Float64[]\n\ny_cursor = cluster_gap\nfor c in 0:(n_clusters - 1)\n    global y_cursor\n    idx = findall(==(c), cluster_labels)\n    sorted_vals = sort(silhouette_values[idx])\n    size_c = length(sorted_vals)\n\n    append!(y_positions, y_cursor:(y_cursor + size_c - 1))\n    append!(bar_values, sorted_vals)\n    append!(bar_colors, fill(IMPRINT_PALETTE[c + 1], size_c))\n\n    push!(cluster_center_y, y_cursor + size_c / 2 - 0.5)\n    push!(cluster_avg_silhouette, mean(sorted_vals))\n    push!(cluster_max_silhouette, maximum(sorted_vals))\n\n    y_cursor += size_c + cluster_gap\nend\n\nx_upper = maximum(cluster_max_silhouette) + 0.18\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              = \"silhouette-basic · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    xlabel             = \"Silhouette Coefficient\",\n    ylabel             = \"Cluster\",\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinevisible   = false,\n    yticksvisible      = false,\n    bottomspinecolor   = INK_SOFT,\n    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    ygridvisible       = false,\n    yticks             = (cluster_center_y, [\"Cluster $(c)\" for c in 0:(n_clusters - 1)]),\n)\n\nbarplot!(ax, y_positions, bar_values;\n    direction = :x, color = bar_colors, gap = 0.0, strokewidth = 0)\n\nvlines!(ax, [avg_silhouette]; color = INK_SOFT, linestyle = :dash, linewidth = 2)\n\nfor c in 0:(n_clusters - 1)\n    text!(ax, cluster_max_silhouette[c + 1] + 0.03, cluster_center_y[c + 1];\n        text = \"avg = $(round(cluster_avg_silhouette[c + 1], digits = 2))\",\n        align = (:left, :center),\n        color = INK,\n        fontsize = 13,\n    )\nend\n\nxlims!(ax, min(-0.15, minimum(bar_values) - 0.05), x_upper)\nylims!(ax, 0, y_cursor)\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}