diff --git a/plots/horizon-basic/implementations/r/ggplot2.R b/plots/horizon-basic/implementations/r/ggplot2.R new file mode 100644 index 0000000000..465297af3b --- /dev/null +++ b/plots/horizon-basic/implementations/r/ggplot2.R @@ -0,0 +1,131 @@ +#' anyplot.ai +#' horizon-basic: Horizon Chart +#' Library: ggplot2 3.5.1 | R 4.4.1 +#' Quality: 89/100 | Created: 2026-08-18 + +library(ggplot2) +library(dplyr) +library(scales) +library(ragg) + +set.seed(42) + +# --- Theme tokens ------------------------------------------------------------ +THEME <- Sys.getenv("ANYPLOT_THEME", "light") +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" +INK_MUTED <- if (THEME == "light") "#6B6A63" else "#A8A79F" + +# Imprint diverging endpoints (imprint_div) — folded bands climb toward these +NEG_HUE <- "#AE3030" # matte red — below baseline +POS_HUE <- "#4467A3" # blue — above baseline + +# --- Data --------------------------------------------------------------------- +# 8 microservices reporting CPU-utilization deviation from their rolling +# baseline, sampled hourly over ~8 days. Each series is z-scored so all lanes +# share the same magnitude scale, which is what makes the folded bands +# comparable across the panel. +services <- c( + "api-gateway", "auth-service", "payment-svc", "search-index", + "user-profile", "notification", "cache-layer", "recommender" +) +n_series <- length(services) +n_time <- 200 + +start_time <- as.POSIXct("2024-06-01 00:00:00", tz = "UTC") +dates <- start_time + (0:(n_time - 1)) * 3600 + +t_idx <- 0:(n_time - 1) +freqs <- 0.02 + seq_len(n_series) * 0.005 +phases <- seq_len(n_series) * 0.7 +amps <- 1 + seq_len(n_series) * 0.1 + +trend_matrix <- outer(t_idx, seq_len(n_series), function(t, i) { + sin(t * freqs[i] + phases[i]) * amps[i] +}) +noise_matrix <- matrix(rnorm(n_time * n_series, 0, 0.6), nrow = n_time) +spike_mask <- matrix(runif(n_time * n_series) > 0.97, nrow = n_time) +spike_matrix <- matrix(rnorm(n_time * n_series, 0, 3), nrow = n_time) * spike_mask +z_matrix <- scale(trend_matrix + noise_matrix + spike_matrix) # z-score per column + +df <- tibble::tibble( + date = rep(dates, times = n_series), + series = factor(rep(services, each = n_time), levels = services), + value = as.numeric(z_matrix) +) + +# --- Fold into horizon bands --------------------------------------------------- +# 3 bands per polarity: band k covers the k-th slice of |value|, clipped and +# rescaled into the fixed lane height, so a deeper band = bigger deviation. +n_bands <- 3 +row_height <- 1 +lane_gap <- 0.88 # leaves a visible gap between stacked lanes +band_height <- max(abs(df$value)) / n_bands + +df <- df %>% + mutate( + series_index = as.integer(series), + lane_base = (n_series - series_index) * row_height, + pos_1 = pmin(pmax(value - 0 * band_height, 0), band_height) / band_height * row_height * lane_gap, + pos_2 = pmin(pmax(value - 1 * band_height, 0), band_height) / band_height * row_height * lane_gap, + pos_3 = pmin(pmax(value - 2 * band_height, 0), band_height) / band_height * row_height * lane_gap, + neg_1 = pmin(pmax(-value - 0 * band_height, 0), band_height) / band_height * row_height * lane_gap, + neg_2 = pmin(pmax(-value - 1 * band_height, 0), band_height) / band_height * row_height * lane_gap, + neg_3 = pmin(pmax(-value - 2 * band_height, 0), band_height) / band_height * row_height * lane_gap + ) + +lane_lookup <- df %>% distinct(series, lane_base) + +# --- Plot ----------------------------------------------------------------- +p <- ggplot(df, aes(x = date)) + + geom_hline( + data = lane_lookup, aes(yintercept = lane_base), + color = scales::alpha(INK_MUTED, 0.35), linewidth = 0.3 + ) + + geom_ribbon(aes(ymin = lane_base, ymax = lane_base + pos_1, group = series), fill = POS_HUE, alpha = 0.35) + + geom_ribbon(aes(ymin = lane_base, ymax = lane_base + pos_2, group = series), fill = POS_HUE, alpha = 0.65) + + geom_ribbon(aes(ymin = lane_base, ymax = lane_base + pos_3, group = series), fill = POS_HUE, alpha = 1.0) + + geom_ribbon(aes(ymin = lane_base, ymax = lane_base + neg_1, group = series), fill = NEG_HUE, alpha = 0.35) + + geom_ribbon(aes(ymin = lane_base, ymax = lane_base + neg_2, group = series), fill = NEG_HUE, alpha = 0.65) + + geom_ribbon(aes(ymin = lane_base, ymax = lane_base + neg_3, group = series), fill = NEG_HUE, alpha = 1.0) + + scale_x_datetime(date_labels = "%b %d", expand = expansion(mult = c(0.01, 0.01))) + + scale_y_continuous( + breaks = lane_lookup$lane_base + row_height / 2, + labels = as.character(lane_lookup$series), + expand = expansion(mult = c(0.03, 0.08)) + ) + + labs( + title = "horizon-basic · r · ggplot2 · anyplot.ai", + subtitle = "CPU-utilization deviation (z-score) per microservice, folded into 3 bands", + x = "Date", + caption = "Blue = above baseline, red = below — deeper shade means a larger deviation", + y = NULL + ) + + theme_minimal(base_size = 8) + + theme( + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), + panel.background = element_rect(fill = PAGE_BG, color = NA), + panel.grid = element_blank(), + axis.title.x = element_text(color = INK, size = 10), + axis.text.x = element_text(color = INK_SOFT, size = 8), + axis.ticks.x = element_line(color = INK_SOFT, linewidth = 0.3), + axis.line.x = element_line(color = INK_SOFT, linewidth = 0.3), + axis.text.y = element_text(color = INK_SOFT, size = 8, hjust = 1), + axis.ticks.y = element_blank(), + plot.title = element_text(color = INK, size = 12, face = "bold"), + plot.subtitle = element_text(color = INK_SOFT, size = 8), + plot.caption = element_text(color = INK_MUTED, size = 7, hjust = 0), + plot.margin = margin(10, 16, 10, 10) + ) + +# --- Save -------------------------------------------------------------------- +ggsave( + filename = sprintf("plot-%s.png", THEME), + plot = p, + device = ragg::agg_png, + width = 8, + height = 4.5, + units = "in", + dpi = 400 +) diff --git a/plots/horizon-basic/metadata/r/ggplot2.yaml b/plots/horizon-basic/metadata/r/ggplot2.yaml new file mode 100644 index 0000000000..d0f492849f --- /dev/null +++ b/plots/horizon-basic/metadata/r/ggplot2.yaml @@ -0,0 +1,267 @@ +library: ggplot2 +language: r +specification_id: horizon-basic +created: '2026-08-18T01:02:51Z' +updated: '2026-08-18T01:07:58Z' +generated_by: claude-sonnet +workflow_run: 32086277878 +issue: 1877 +language_version: 4.4.1 +library_version: 3.5.1 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/horizon-basic/r/ggplot2/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/horizon-basic/r/ggplot2/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 89 +review: + strengths: + - 'Correct, idiomatic horizon-chart folding technique: geom_ribbon layers with per-band + alpha (0.35/0.65/1.0) faithfully encode magnitude within each of the 3 folded + bands per polarity' + - 'Correct semantic use of the Imprint diverging colormap endpoints (#AE3030 matte + red / #4467A3 blue) for below/above-baseline deviation — matches imprint_div guidance + exactly rather than misapplying the categorical palette to a continuous, diverging + quantity' + - 'Both themes render correctly: page background matches #FAF8F1/#1A1A17, all chrome + (title, subtitle, axis text, caption) uses theme-adaptive INK/INK_SOFT/INK_MUTED + tokens, and the data colors are pixel-identical between light and dark renders' + - 'Clean, reproducible, KISS-structured code: set.seed(42), a single linear dplyr + pipeline, no functions/classes, no fake interactive elements' + - Realistic, neutral dataset (8 microservice CPU-deviation z-scores over ~8 days, + hourly) that exercises all 3 fold bands, including fully-saturated spikes, and + stays within the spec's 5-50 series / 100-1000 point guidance + - No text overlap or edge clipping across either render; mandated title format 'horizon-basic + · r · ggplot2 · anyplot.ai' reproduced exactly + weaknesses: + - 'Design Excellence/storytelling is competent but not yet exceptional: the 8 lanes + are equally weighted with no annotation or callout drawing the eye to the most + notable anomaly (e.g. the fully-saturated auth-service spike) — a small highlight + or callout would raise DE-03 above the current ''visual hierarchy via alpha only'' + level' + - 'VQ-03: the lightest band (alpha=0.35) is quite subtle against the #FAF8F1 light + background for near-baseline deviations, making the finest gradation harder to + distinguish than the medium/deep bands — consider nudging the lowest alpha up + slightly (e.g. 0.42-0.48) for better separation from the page background without + disturbing the deep-band contrast' + - plot.margin = margin(10, 16, 10, 10) is quite tight at dpi=400 (~55-90 source + px on a 3200x1800 canvas); the render itself isn't cut off, but a slightly more + generous margin (especially top-right) would give the layout more breathing room + and push DE-02/VQ-05 polish further + - No small legend/swatch maps the 3 discrete alpha steps to an approximate z-score + range — the caption states 'deeper shade means a larger deviation' but a reader + can't tell what z-score each band boundary represents; a compact 3-swatch key + near the caption would strengthen VQ-06/DE-01 without adding real estate cost + image_description: |- + Light render (plot-light.png): + Background: Warm off-white, consistent with #FAF8F1 — not pure white. + Chrome: Bold dark title "horizon-basic · r · ggplot2 · anyplot.ai" top-left; medium-grey subtitle "CPU-utilization deviation (z-score) per microservice, folded into 3 bands" below it; 8 dark-grey service-name labels along the left (api-gateway, auth-service, payment-svc, search-index, user-profile, notification, cache-layer, recommender); "Date" axis title and "Jun 02/04/06/08" tick labels along the bottom in soft grey; a lighter tertiary caption at the very bottom explaining the blue/red + shade-depth encoding. All text is clearly readable against the light background — no light-on-light issues found. + Data: Each of the 8 lanes shows a jagged horizon silhouette built from stacked blue (#4467A3-family) and red (#AE3030-family) ribbons with three visible intensity steps (pale, medium, fully saturated) — deeper shade correctly reads as larger deviation. Thin pale dividers separate the lanes cleanly with no overlap between adjacent lanes. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black, consistent with #1A1A17 — not pure black. + Chrome: Identical layout to the light render; title/subtitle/axis-text/caption all switch to light off-white/grey tones (no dark-on-dark failures — checked service labels, x-axis ticks, and the bottom caption, all clearly legible against the dark background). + Data: Blue and red band colors are visually identical to the light render (only the page background and text tokens flipped, as required) — confirms correct theme-adaptive implementation with fixed data-color identity. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 28 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All font sizes explicitly set via theme(); readable in both themes; + title/subtitle/caption sized sensibly with no overflow or clipping. + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No overlap between lanes, tick labels, or title/subtitle/caption + in either render. + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Bands well adapted to 200-point density, but the lowest alpha (0.35) + band is a bit subtle against the light-theme page background. + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Blue/matte-red pairing (not red-green) is CVD-safe and clearly distinguishable. + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Plot fills the canvas well, nothing cut off, balanced margins. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: '''Date'' axis label plus descriptive subtitle/caption stating the + z-score unit and encoding.' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: Correctly uses the imprint_div diverging endpoints (#AE3030/#4467A3) + for the continuous, baseline-centered deviation; theme-correct backgrounds + and chrome in both renders. + design_excellence: + score: 15 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Thoughtful diverging-palette semantics and a genuinely well-executed + horizon-fold technique, above a configured-default look but short of full + publication polish. + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Grid removed, spines minimal, subtle lane dividers; margins are a + little tight (10-16pt @ dpi=400). + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Alpha intensity creates visual hierarchy that draws the eye to spikes, + but no explicit callout/annotation highlights the standout anomaly. + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Genuine horizon chart via folded, layered ribbon bands. + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 3 bands, mirrored positive/negative coloring, meaningful zero baseline, + magnitude-driven color intensity, 8 series all present. + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X=date, Y=lane per series, color=folded magnitude — correct and complete. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title exactly matches the mandated 'horizon-basic · r · ggplot2 · + anyplot.ai' format; series names serve as the per-lane legend. + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Shows all 3 bands including full-saturation spikes across multiple + series; not every series exhibits every band depth equally. + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Neutral, plausible microservice CPU-monitoring scenario. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Z-scored trend+noise+spike data is a sensible, well-scaled representation + of deviation from baseline. + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Linear imports -> data -> fold -> plot -> save, no functions/classes. + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: set.seed(42). + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: ggplot2, dplyr, scales, ragg all used; no unused imports. + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean vectorized fold logic, no fake UI or over-engineering. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: ggsave via ragg::agg_png at plot-{THEME}.png, correct width/height/dpi. + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Idiomatic layered geom_ribbon + scale_x_datetime/scale_y_continuous + + theme system usage. + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Creative layer-composition to build a horizon chart from primitives + ggplot2 doesn't provide natively, though the technique is replicable in + other grammar-of-graphics-style libraries. + verdict: REJECTED +impl_tags: + dependencies: [] + techniques: + - layer-composition + - manual-ticks + patterns: + - data-generation + - matrix-construction + dataprep: + - normalization + - time-series + styling: + - alpha-blending + - minimal-chrome + - publication-ready