{"spec_id":"biplot-pca","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' biplot-pca: PCA Biplot with Scores and Loading Vectors\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 97/100 | Created: 2026-05-17\n\nlibrary(ggplot2)\nlibrary(dplyr)\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\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\nIMPRINT   <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                 \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# --- Data: Perform PCA on iris dataset ----\npca_result <- prcomp(iris[, 1:4], scale. = TRUE)\n\n# Extract and format scores\npca_scores <- as.data.frame(pca_result$x[, 1:2])\npca_scores$species <- iris$Species\n\n# Extract variance explained (as percentages)\nvar_explained <- summary(pca_result)$importance[2, 1:2] * 100\n\n# Extract and format loadings\nloadings <- as.data.frame(pca_result$rotation[, 1:2])\nloadings$variable <- rownames(pca_result$rotation)\n\n# Scale loadings for visibility alongside score points\nloadings_scaled <- loadings\nloadings_scaled$PC1 <- loadings_scaled$PC1 * 3.2\nloadings_scaled$PC2 <- loadings_scaled$PC2 * 3.2\n\n# --- Custom theme ----\nanyplot_theme <- theme_minimal(base_size = 14) +\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  = element_line(color = INK, linewidth = 0.25),\n    panel.grid.minor  = element_blank(),\n    axis.title        = element_text(color = INK, size = 20),\n    axis.text         = element_text(color = INK_SOFT, size = 16),\n    plot.title        = element_text(color = INK, size = 24, hjust = 0.5),\n    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT),\n    legend.text       = element_text(color = INK_SOFT, size = 16),\n    legend.title      = element_text(color = INK, size = 18),\n    panel.border      = element_rect(color = INK_SOFT, fill = NA, linewidth = 0.8)\n  )\n\n# --- Plot ----\np <- ggplot() +\n  # Observation scores as points\n  geom_point(\n    data = pca_scores,\n    aes(x = PC1, y = PC2, color = species),\n    size = 3.5,\n    alpha = 0.65\n  ) +\n  # Loading vectors as arrows from origin\n  geom_segment(\n    data = loadings_scaled,\n    aes(x = 0, y = 0, xend = PC1, yend = PC2),\n    arrow = arrow(length = unit(0.18, \"inches\"), type = \"closed\"),\n    color = INK_SOFT,\n    linewidth = 0.7,\n    alpha = 0.75\n  ) +\n  # Variable labels on loading arrows\n  geom_text(\n    data = loadings_scaled,\n    aes(x = PC1 * 1.15, y = PC2 * 1.15, label = variable),\n    color = INK_SOFT,\n    size = 5,\n    fontface = \"italic\"\n  ) +\n  # Reference circle (unit circle for correlation scaling)\n  annotate(\n    \"path\",\n    x = cos(seq(0, 2*pi, length.out = 100)),\n    y = sin(seq(0, 2*pi, length.out = 100)),\n    color = INK_MUTED,\n    linewidth = 0.35,\n    alpha = 0.4,\n    linetype = \"dashed\"\n  ) +\n  # Color scale: species (first group uses #009E73)\n  scale_color_manual(\n    values = c(\n      \"setosa\" = IMPRINT[1],\n      \"versicolor\" = IMPRINT[2],\n      \"virginica\" = IMPRINT[3]\n    ),\n    name = \"Species\"\n  ) +\n  # Labels with variance explained\n  labs(\n    title = \"biplot-pca · ggplot2 · anyplot.ai\",\n    x = sprintf(\"PC1 (%.1f%%)\", var_explained[1]),\n    y = sprintf(\"PC2 (%.1f%%)\", var_explained[2])\n  ) +\n  # Equal aspect ratio for visual fairness\n  coord_fixed() +\n  anyplot_theme\n\n# --- Save ----\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot = p,\n  device = ragg::agg_png,\n  width = 16,\n  height = 9,\n  units = \"in\",\n  dpi = 300\n)\n"}