{"spec_id":"silhouette-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' silhouette-basic: Silhouette Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 89/100 | Created: 2026-09-09\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(cluster)\nlibrary(scales)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -----------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data --------------------------------------------------------------------\n# Cluster the iris measurements into 3 groups (species-shaped clusters) with\n# k-means, then compute the per-sample silhouette coefficient.\nfeatures <- scale(iris[, 1:4])\nkm <- kmeans(features, centers = 3, nstart = 25)\nsil <- silhouette(km$cluster, dist(features))\n\nsil_df <- tibble::tibble(\n  cluster    = factor(sil[, \"cluster\"]),\n  sil_width  = sil[, \"sil_width\"]\n) %>%\n  arrange(cluster, desc(sil_width)) %>%\n  mutate(sample_order = factor(row_number()))\n\navg_sil <- mean(sil_df$sil_width)\n\ncluster_summary <- sil_df %>%\n  mutate(row_id = row_number()) %>%\n  group_by(cluster) %>%\n  summarise(avg_width = mean(sil_width), mid_order = mean(row_id), .groups = \"drop\")\n\nsil_min      <- min(sil_df$sil_width)\nsil_max      <- max(sil_df$sil_width)\nsil_range    <- sil_max - sil_min\nlabel_x      <- sil_min - 0.02\nmean_label_x <- min(as.integer(sil_df$sample_order)) + 1\n\n# --- Plot ----------------------------------------------------------------\np <- ggplot(sil_df, aes(x = sample_order, y = sil_width, fill = cluster)) +\n  geom_col(width = 1, color = NA) +\n  geom_hline(yintercept = avg_sil, linetype = \"dashed\",\n             linewidth = 0.6, color = INK) +\n  geom_text(\n    data = cluster_summary,\n    aes(x = mid_order, y = label_x,\n        label = sprintf(\"Cluster %s\\navg = %.2f\", cluster, avg_width),\n        color = cluster),\n    inherit.aes = FALSE, hjust = 1, size = 3, lineheight = 0.9, fontface = \"bold\"\n  ) +\n  annotate(\n    \"text\", x = mean_label_x, y = avg_sil, label = sprintf(\"mean = %.2f\", avg_sil),\n    hjust = -0.1, vjust = -0.6, size = 3.3, color = INK_SOFT\n  ) +\n  scale_fill_manual(values = IMPRINT_PALETTE) +\n  scale_color_manual(values = IMPRINT_PALETTE) +\n  scale_y_continuous(limits = c(label_x - 0.55 * sil_range, sil_max + 0.05 * sil_range)) +\n  labs(\n    title = \"silhouette-basic · r · ggplot2 · anyplot.ai\",\n    x = \"Samples (sorted within cluster)\",\n    y = \"Silhouette coefficient\"\n  ) +\n  coord_flip() +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background  = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major.x = element_line(color = scales::alpha(INK, 0.15), linewidth = 0.3),\n    panel.grid.major.y = element_blank(),\n    panel.grid.minor  = element_blank(),\n    axis.title.x      = element_text(color = INK, size = 10),\n    axis.title.y      = element_text(color = INK, size = 10),\n    axis.text.x       = element_text(color = INK_SOFT, size = 8),\n    axis.text.y       = element_blank(),\n    axis.ticks.y      = element_blank(),\n    plot.title        = element_text(color = INK, size = 12),\n    legend.position   = \"none\"\n  )\n\n# --- Save --------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}