{"spec_id":"forest-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' forest-basic: Meta-Analysis Forest Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(scales)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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\"\n\n# Imprint palette — position 1 is ALWAYS the brand green\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nBRAND <- IMPRINT_PALETTE[1]\n\n# --- Data: RCTs of a novel antihypertensive vs. placebo on stroke risk -------\n# Effect measure is an odds ratio (log-normal, inverse-variance weighted).\nsurname <- c(\"Bergstrom\", \"Chen\", \"Diaz\", \"Okafor\", \"Kowalski\",\n             \"Nguyen\", \"Patel\", \"Silva\", \"Haddad\", \"Kim\")\nyear <- sort(sample(2010:2023, length(surname)))\nstudy <- paste0(surname, \" (\", year, \")\")\n\nsample_size <- round(runif(length(surname), 90, 640))\ntrue_log_or <- log(0.72)\nstudy_se <- 1.6 / sqrt(sample_size)\nlog_or <- rnorm(length(surname), mean = true_log_or, sd = 0.18)\n\nstudies <- tibble(\n  study       = study,\n  effect_size = exp(log_or),\n  ci_lower    = exp(log_or - 1.96 * study_se),\n  ci_upper    = exp(log_or + 1.96 * study_se),\n  inv_var     = 1 / study_se^2\n) %>%\n  mutate(weight_pct = 100 * inv_var / sum(inv_var))\n\n# Pooled estimate — inverse-variance fixed-effect model\npooled_log_or <- sum(log_or * studies$inv_var) / sum(studies$inv_var)\npooled_se     <- sqrt(1 / sum(studies$inv_var))\npooled_effect <- exp(pooled_log_or)\npooled_lower  <- exp(pooled_log_or - 1.96 * pooled_se)\npooled_upper  <- exp(pooled_log_or + 1.96 * pooled_se)\n\n# Row positions: studies stacked top-down in chronological order, pooled\n# estimate isolated near the bottom with a gap for the diamond + rule.\nn_studies <- nrow(studies)\nstudies$y_pos <- rev(seq_len(n_studies))\npooled_y <- 0\n\nwhisker_cap <- 0.15\ndiamond_half_h <- 0.32\n\ndiamond_df <- tibble(\n  x = c(pooled_lower, pooled_effect, pooled_upper, pooled_effect),\n  y = c(pooled_y, pooled_y + diamond_half_h, pooled_y, pooled_y - diamond_half_h)\n)\n\n# --- Plot ---------------------------------------------------------------\np <- ggplot(studies, aes(x = effect_size, y = y_pos)) +\n  geom_vline(xintercept = 1, linetype = \"dashed\", linewidth = 0.5, color = INK_SOFT) +\n  geom_hline(yintercept = 0.55, linewidth = 0.3, color = INK_SOFT) +\n  geom_segment(aes(x = ci_lower, xend = ci_upper, yend = y_pos),\n               color = BRAND, linewidth = 0.9) +\n  geom_segment(aes(x = ci_lower, xend = ci_lower,\n                    y = y_pos - whisker_cap, yend = y_pos + whisker_cap),\n               color = BRAND, linewidth = 0.9) +\n  geom_segment(aes(x = ci_upper, xend = ci_upper,\n                    y = y_pos - whisker_cap, yend = y_pos + whisker_cap),\n               color = BRAND, linewidth = 0.9) +\n  geom_point(aes(size = weight_pct), shape = 15, color = BRAND) +\n  geom_segment(data = tibble(x = pooled_lower, xend = pooled_upper),\n               aes(x = x, xend = xend, y = pooled_y, yend = pooled_y),\n               inherit.aes = FALSE, color = BRAND, linewidth = 0.9, alpha = 0.35) +\n  geom_polygon(data = diamond_df, aes(x = x, y = y),\n               inherit.aes = FALSE, fill = BRAND, color = INK) +\n  scale_x_log10(breaks = c(0.25, 0.5, 1, 2, 4),\n                labels = scales::label_number(accuracy = 0.01)) +\n  scale_y_continuous(\n    breaks = c(pooled_y, studies$y_pos),\n    labels = c(\"Pooled estimate\", studies$study),\n    expand = expansion(add = c(0.9, 0.9))\n  ) +\n  scale_size_continuous(range = c(3, 9), name = \"Weight (%)\") +\n  labs(\n    title = \"forest-basic · r · ggplot2 · anyplot.ai\",\n    x = \"Odds ratio (stroke, log scale)\",\n    y = NULL\n  ) +\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.y  = element_blank(),\n    panel.grid.minor.y  = element_blank(),\n    panel.grid.minor.x  = element_blank(),\n    panel.grid.major.x  = element_line(color = INK, linewidth = 0.2),\n    axis.line.x         = element_line(color = INK_SOFT, linewidth = 0.4),\n    axis.ticks          = element_blank(),\n    axis.title.x        = element_text(color = INK, size = 10),\n    axis.text           = element_text(color = INK_SOFT, size = 8),\n    plot.title          = element_text(color = INK, size = 12),\n    legend.position      = \"right\",\n    legend.background   = element_blank(),\n    legend.text         = element_text(color = INK_SOFT, size = 8),\n    legend.title        = element_text(color = INK, size = 10)\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"}