{"spec_id":"heatmap-loss-triangle","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' heatmap-loss-triangle: Actuarial Loss Development Triangle\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 85/100 | Created: 2026-06-03\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(scales)\nlibrary(ragg)\n\nset.seed(42)\n\n# Theme tokens (Imprint palette, theme-adaptive chrome)\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# Data: actuarial loss development triangle (chain-ladder method)\naccident_years <- 2015:2024\ndev_periods    <- 1:10\n\n# Cumulative percent of ultimate paid by development period\npct_paid <- c(0.40, 0.65, 0.80, 0.88, 0.93, 0.96, 0.98, 0.990, 0.995, 1.000)\n\n# Ultimate claims per accident year (in $M)\nultimates <- c(42.5, 38.2, 51.3, 47.8, 55.1, 49.6, 61.2, 57.4, 63.8, 68.5)\n\n# Age-to-age development factors (period p -> period p+1)\nata_factors <- pct_paid[2:10] / pct_paid[1:9]\n\n# y-axis levels: accident years + ATA row at bottom\ny_levels <- c(as.character(accident_years), \"ATA Factor\")\ny_limits <- c(\"ATA Factor\", rev(as.character(accident_years)))\n\n# Build full 10 x 10 triangle grid\ndf <- expand.grid(\n  accident_year = accident_years,\n  dev_period    = dev_periods,\n  stringsAsFactors = FALSE\n) %>%\n  mutate(\n    ay_idx     = accident_year - 2014L,\n    is_actual  = dev_period <= (11L - ay_idx),\n    cumulative = ultimates[ay_idx] * pct_paid[dev_period],\n    cumulative = ifelse(\n      is_actual,\n      round(cumulative * runif(n(), 0.97, 1.03), 1),\n      round(cumulative, 1)\n    ),\n    label   = sprintf(\"$%.1fM\", cumulative),\n    dev_fac = factor(dev_period, levels = 1:10),\n    acc_fac = factor(as.character(accident_year), levels = y_levels)\n  )\n\ndf_proj <- filter(df, !is_actual)\n\n# ATA factor row: background tiles and text annotations\ndf_ata_bg <- data.frame(\n  acc_fac = factor(rep(\"ATA Factor\", 10), levels = y_levels),\n  dev_fac = factor(1:10, levels = 1:10)\n)\n\n# ATA factors at periods 2-10 (multiplicative factor from prior period)\ndf_ata <- data.frame(\n  acc_fac = factor(rep(\"ATA Factor\", 9), levels = y_levels),\n  dev_fac = factor(2:10, levels = 1:10),\n  label   = sprintf(\"×%.3f\", ata_factors)\n)\n\n# Semi-transparent red tint for projected cells (~15% opacity) + opaque border\nproj_fill <- \"#AE303026\"\n\n# Plot\np <- ggplot(df, aes(x = dev_fac, y = acc_fac)) +\n  # ATA factor row: muted elevated background\n  geom_tile(data = df_ata_bg,\n            fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.1) +\n  # Main triangle: colored by cumulative claims (Imprint sequential: green -> blue)\n  geom_tile(aes(fill = cumulative), color = INK_SOFT, linewidth = 0.15) +\n  # Projected cells: semi-transparent red tint + opaque border for stronger contrast\n  geom_tile(data = df_proj,\n            fill = proj_fill, color = \"#AE3030\", linewidth = 0.5) +\n  # Cell value labels\n  geom_text(aes(label = label),\n            color = \"white\", size = 2.2, fontface = \"bold\") +\n  # ATA factor annotations in bottom row\n  geom_text(data = df_ata, aes(label = label),\n            color = INK_MUTED, size = 1.8) +\n  # Imprint sequential colormap: brand green -> blue\n  scale_fill_gradient(\n    low    = \"#009E73\",\n    high   = \"#4467A3\",\n    name   = \"Claims ($M)\",\n    labels = function(x) sprintf(\"%.0f\", x)\n  ) +\n  # Oldest accident year at top (actuarial convention); ATA row at bottom\n  scale_y_discrete(limits = y_limits) +\n  labs(\n    title   = \"heatmap-loss-triangle · r · ggplot2 · anyplot.ai\",\n    x       = \"Development Period (Years)\",\n    y       = \"Accident Year\",\n    caption = \"▪ Actual (observed)   ▫ Projected / IBNR estimate (red tint + border)\"\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        = element_blank(),\n    axis.title        = 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, face = \"bold\"),\n    plot.caption      = element_text(color = INK_MUTED, size = 7, hjust = 0),\n    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),\n    legend.text       = element_text(color = INK_SOFT,  size = 8),\n    legend.title      = element_text(color = INK,       size = 9),\n    legend.position   = \"right\",\n    plot.margin       = margin(t = 10, r = 10, b = 10, l = 10)\n  )\n\n# Save: square canvas (2400 x 2400 px at 400 dpi)\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"}