{"spec_id":"residual-plot","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' residual-plot: Residual Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-05\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\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nRULE        <- scales::alpha(INK, 0.15)\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data --------------------------------------------------------------------\n# House price prediction: a simple linear model fit to a mildly non-linear,\n# heteroscedastic relationship between square footage and sale price. The\n# residual plot exposes both the curvature the linear fit misses and the\n# fanning variance that grows with home size.\nn <- 220\nsquare_footage <- runif(n, 800, 4200)\nnoise_scale <- 6000 + square_footage * 12\nsale_price <- 45000 + 95 * square_footage + 0.018 * square_footage^2 +\n  rnorm(n, mean = 0, sd = noise_scale)\n\nhomes <- tibble::tibble(square_footage, sale_price)\nmodel <- lm(sale_price ~ square_footage, data = homes)\n\nhomes <- homes %>%\n  mutate(\n    fitted    = fitted(model),\n    residual  = resid(model),\n    band      = 2 * sd(residual),\n    is_outlier = abs(residual) > band,\n    status    = factor(if_else(is_outlier, \"Outlier (>2σ)\", \"Normal\"),\n                        levels = c(\"Normal\", \"Outlier (>2σ)\"))\n  )\n\nband_width <- unique(homes$band)\n\n# --- Plot ---------------------------------------------------------------\np <- ggplot(homes, aes(x = fitted, y = residual)) +\n  geom_hline(yintercept = c(-band_width, band_width),\n             linetype = \"dashed\", linewidth = 0.5, color = INK_SOFT) +\n  geom_hline(yintercept = 0, linewidth = 0.8, color = INK) +\n  geom_smooth(method = \"loess\", formula = y ~ x, se = FALSE,\n              color = IMPRINT_PALETTE[3], linewidth = 1.0) +\n  geom_point(aes(color = status, shape = status), size = 2.5, alpha = 0.75) +\n  scale_color_manual(values = c(\"Normal\" = IMPRINT_PALETTE[1],\n                                \"Outlier (>2σ)\" = IMPRINT_PALETTE[5])) +\n  scale_shape_manual(values = c(\"Normal\" = 16, \"Outlier (>2σ)\" = 17)) +\n  labs(\n    title  = \"residual-plot · r · ggplot2 · anyplot.ai\",\n    x      = \"Fitted Sale Price ($)\",\n    y      = \"Residual ($)\",\n    color  = NULL,\n    shape  = NULL\n  ) +\n  scale_x_continuous(labels = scales::dollar_format(scale = 1e-3, suffix = \"k\")) +\n  scale_y_continuous(labels = scales::dollar_format(scale = 1e-3, suffix = \"k\")) +\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_line(color = RULE, linewidth = 0.3),\n    panel.grid.minor  = element_blank(),\n    axis.title        = element_text(color = INK, size = 10),\n    axis.text         = element_text(color = INK_SOFT, size = 8),\n    axis.line         = element_line(color = INK_SOFT),\n    plot.title        = element_text(color = INK, size = 12, face = \"bold\"),\n    legend.position   = \"top\",\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.key        = element_blank()\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"}