{"spec_id":"horizon-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' horizon-basic: Horizon Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 89/100 | Created: 2026-08-19\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\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\"\n\n# Imprint diverging colormap stops (bad/loss red <-> midpoint <-> blue) — used\n# here as two sequential ramps (midpoint -> red, midpoint -> blue) so each\n# horizon band gets a progressively more saturated color with magnitude.\nIMPRINT_DIV_LOW  <- \"#AE3030\"\nIMPRINT_DIV_HIGH <- \"#4467A3\"\n\n# --- Data -----------------------------------------------------------------\n# CPU load deviation (percentage points) from each server's 7-day rolling\n# baseline, sampled hourly. Mean-centered per server so 0 = \"typical load\".\nn_points <- 168 # 7 days of hourly readings\nn_bands  <- 3   # fold width — anyplot.ai default 2-4 bands\n\nservers <- c(\n  \"web-01\", \"web-02\", \"web-03\", \"web-04\",\n  \"api-01\", \"api-02\", \"api-03\",\n  \"db-01\", \"db-02\", \"cache-01\"\n)\n\ntimestamps <- seq(as.POSIXct(\"2026-06-01\", tz = \"UTC\"), by = \"hour\", length.out = n_points)\n\ndf <- bind_rows(lapply(servers, function(server) {\n  drift    <- cumsum(rnorm(n_points, 0, 0.12))\n  seasonal <- sin(seq_len(n_points) / 24 * 2 * pi + runif(1, 0, 2 * pi)) * runif(1, 0.6, 1.6)\n  noise    <- rnorm(n_points, 0, 0.25)\n  raw      <- drift * 0.3 + seasonal + noise\n  tibble::tibble(timestamp = timestamps, server = server, deviation = raw - mean(raw))\n}))\n\n# --- Fold into horizon bands ----------------------------------------------\n# Each band re-uses the same [0, band_size] vertical space; magnitude beyond\n# a band spills into the next one, drawn on top in a darker shade. Negative\n# deviations are mirrored (abs value) into their own red-shaded bands so the\n# whole panel stays a single band_size tall regardless of sign.\nband_size <- max(abs(df$deviation)) / n_bands\n\nfold_bands <- function(magnitude) {\n  sapply(seq_len(n_bands), function(k) {\n    top     <- k * band_size\n    clipped <- pmin(pmax(magnitude, 0), top)\n    pmax(clipped - (k - 1) * band_size, 0)\n  })\n}\n\npos_bands <- as_tibble(fold_bands(pmax(df$deviation, 0)), .name_repair = ~ paste0(\"pos_\", seq_len(n_bands)))\nneg_bands <- as_tibble(fold_bands(pmax(-df$deviation, 0)), .name_repair = ~ paste0(\"neg_\", seq_len(n_bands)))\n\nfolded_df <- bind_cols(df %>% select(timestamp, server), pos_bands, neg_bands) %>%\n  pivot_longer(\n    cols      = c(starts_with(\"pos_\"), starts_with(\"neg_\")),\n    names_to  = \"tier\",\n    values_to = \"folded\"\n  ) %>%\n  mutate(\n    server = factor(server, levels = servers),\n    # Draw order (ascending): widest/lightest band first, narrowest/darkest last.\n    tier   = factor(tier, levels = c(\"neg_1\", \"neg_2\", \"neg_3\", \"pos_1\", \"pos_2\", \"pos_3\"))\n  ) %>%\n  arrange(server, tier, timestamp)\n\npos_colors <- grDevices::colorRampPalette(c(PAGE_BG, IMPRINT_DIV_HIGH))(n_bands + 1)[-1]\nneg_colors <- grDevices::colorRampPalette(c(PAGE_BG, IMPRINT_DIV_LOW))(n_bands + 1)[-1]\ntier_colors <- setNames(\n  c(neg_colors[1], neg_colors[2], neg_colors[3], pos_colors[1], pos_colors[2], pos_colors[3]),\n  c(\"neg_1\", \"neg_2\", \"neg_3\", \"pos_1\", \"pos_2\", \"pos_3\")\n)\n\n# --- Title (fontsize scales with title length, see plot-generator.md) -----\nplot_title  <- \"Server Load Deviation · horizon-basic · r · ggplot2 · anyplot.ai\"\ntitle_ratio <- if (nchar(plot_title) > 67) 67 / nchar(plot_title) else 1.0\ntitle_size  <- max(8, round(12 * title_ratio))\n\n# Numeric band-boundary key for the caption, e.g. \"1.2 / 2.4 / 3.6 pp\".\nband_bounds  <- round(seq_len(n_bands) * band_size, 1)\nplot_caption <- sprintf(\n  \"Band color intensity = deviation magnitude (bounds %s pp) · blue = above baseline · red = below baseline\",\n  paste(band_bounds, collapse = \" / \")\n)\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot(folded_df, aes(x = timestamp, y = folded, fill = tier, group = tier)) +\n  geom_area(position = \"identity\") +\n  facet_wrap(~server, ncol = 1, strip.position = \"left\") +\n  scale_fill_manual(values = tier_colors, guide = \"none\") +\n  scale_x_datetime(date_breaks = \"1 day\", date_labels = \"%b %d\", expand = c(0, 0)) +\n  scale_y_continuous(expand = c(0, 0)) +\n  labs(\n    title   = plot_title,\n    x       = \"Date\",\n    y       = \"Deviation (pp)\",\n    caption = plot_caption\n  ) +\n  theme_minimal(base_size = 7) +\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    panel.spacing.y   = unit(2, \"pt\"),\n    strip.background  = element_blank(),\n    strip.placement   = \"outside\",\n    strip.text.y.left = element_text(color = INK_SOFT, size = 8, angle = 0, hjust = 1),\n    axis.title.x      = element_text(color = INK, size = 10),\n    axis.title.y      = element_text(color = INK, size = 10),\n    axis.text.x       = element_text(color = INK_SOFT, size = 8),\n    axis.text.y       = element_blank(),\n    axis.ticks.y      = element_blank(),\n    axis.ticks.x      = element_line(color = INK_SOFT, linewidth = 0.3),\n    axis.line.x       = element_line(color = INK_SOFT, linewidth = 0.3),\n    plot.title        = element_text(color = INK, size = title_size, face = \"bold\"),\n    plot.caption      = element_text(color = INK_SOFT, size = 7, hjust = 0)\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"}