{"spec_id":"heatmap-risk-matrix","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' heatmap-risk-matrix: Risk Assessment Matrix (Probability vs Impact)\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 84/100 | Created: 2026-06-20\n\nlibrary(ggplot2)\nlibrary(dplyr)\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\"\n\n# Semantic risk zone colors from the Imprint palette\nZONE_COLORS <- c(\n  \"Low (1–4)\"       = \"#009E73\",  # Imprint green — low risk\n  \"Medium (5–9)\"    = \"#DDCC77\",  # amber — caution\n  \"High (10–16)\"    = \"#BD8233\",  # ochre — elevated risk\n  \"Critical (20–25)\" = \"#AE3030\"  # matte red — critical risk\n)\n\n# Axis label text for each level\nlikelihood_labels <- c(\"Rare\", \"Unlikely\", \"Possible\", \"Likely\", \"Almost\\nCertain\")\nimpact_labels     <- c(\"Negligible\", \"Minor\", \"Moderate\", \"Major\", \"Catastrophic\")\n\n# 5x5 risk matrix grid cells\ngrid_df <- expand.grid(likelihood = 1:5, impact = 1:5) %>%\n  mutate(\n    score = likelihood * impact,\n    zone  = case_when(\n      score >= 20 ~ \"Critical (20–25)\",\n      score >= 10 ~ \"High (10–16)\",\n      score >= 5  ~ \"Medium (5–9)\",\n      TRUE        ~ \"Low (1–4)\"\n    ),\n    zone = factor(zone, levels = names(ZONE_COLORS))\n  )\n\n# Software project risk register — 12 risks placed in distinct cells\nrisks <- data.frame(\n  risk_name  = c(\n    \"Budget\\nOverrun\", \"Staff\\nTurnover\", \"Scope Creep\",\n    \"Data Breach\", \"Ransomware\", \"Tech Debt\",\n    \"Regulatory\\nChange\", \"Integration\\nIssues\", \"User Adoption\",\n    \"Vendor Delay\", \"Network\\nOutage\", \"Critical\\nDependency\"\n  ),\n  likelihood = c(4, 3, 5, 2, 3, 3, 2, 3, 4, 4, 2, 4),\n  impact     = c(4, 4, 3, 5, 5, 3, 3, 2, 2, 3, 4, 5)\n)\n\n# Small reproducible jitter so markers sit slightly off cell centres\nrisks$jx <- risks$likelihood + runif(nrow(risks), -0.22, 0.22)\nrisks$jy <- risks$impact     + runif(nrow(risks), -0.22, 0.22)\n\nplot_title <- \"heatmap-risk-matrix · r · ggplot2 · anyplot.ai\"\n\np <- ggplot() +\n  # Zone-coloured background cells\n  geom_tile(\n    data = grid_df,\n    aes(x = likelihood, y = impact, fill = zone),\n    color = PAGE_BG, linewidth = 1.2, alpha = 0.82\n  ) +\n  # Risk score in bottom-left corner of each cell (subtle reference)\n  geom_text(\n    data = grid_df,\n    aes(x = likelihood - 0.38, y = impact - 0.38, label = score),\n    color = INK_MUTED, size = 2.4, hjust = 0, vjust = 0, fontface = \"bold\"\n  ) +\n  # Risk item markers — white-filled circle with ink border\n  geom_point(\n    data = risks,\n    aes(x = jx, y = jy),\n    shape = 21, size = 4.5,\n    fill = PAGE_BG, color = INK, stroke = 1.8\n  ) +\n  # Risk item labels (above marker)\n  geom_text(\n    data = risks,\n    aes(x = jx, y = jy, label = risk_name),\n    color = INK, size = 2.8, vjust = -0.55, hjust = 0.5, lineheight = 0.88\n  ) +\n  # Bold border around the Critical zone cells (score ≥ 20)\n  annotate(\n    \"rect\",\n    xmin = 3.5, xmax = 5.5, ymin = 3.5, ymax = 5.5,\n    fill = NA, color = \"#AE3030\", linewidth = 1.5\n  ) +\n  # Axis scales\n  scale_x_continuous(\n    name   = \"Likelihood\",\n    breaks = 1:5,\n    labels = likelihood_labels,\n    limits = c(0.45, 5.55),\n    expand = c(0, 0)\n  ) +\n  scale_y_continuous(\n    name   = \"Impact\",\n    breaks = 1:5,\n    labels = impact_labels,\n    limits = c(0.42, 5.88),  # extra room above for top-row labels\n    expand = c(0, 0)\n  ) +\n  scale_fill_manual(\n    values = ZONE_COLORS,\n    name   = \"Risk Level\",\n    guide  = guide_legend(reverse = TRUE)\n  ) +\n  labs(title = plot_title) +\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  = element_blank(),\n    panel.grid.minor  = element_blank(),\n    panel.border      = element_blank(),\n    axis.title        = element_text(color = INK,      size = 10),\n    axis.text         = element_text(color = INK_SOFT, size = 7.5),\n    axis.text.x       = element_text(angle = 20, hjust = 1),\n    plot.title        = element_text(\n                          color = INK, size = 12, face = \"bold\",\n                          margin = margin(b = 14)\n                        ),\n    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT,\n                                     linewidth = 0.3),\n    legend.text       = element_text(color = INK_SOFT, size = 7.5),\n    legend.title      = element_text(color = INK,      size = 9),\n    legend.position   = \"right\",\n    legend.key.size   = unit(1.0, \"lines\"),\n    legend.key        = element_rect(color = INK_SOFT, linewidth = 0.3),\n    plot.margin       = margin(18, 18, 18, 18)\n  )\n\n# Save — square canvas: width=6, height=6, dpi=400 → 2400 x 2400 px\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}