Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions plots/horizon-basic/implementations/r/ggplot2.R
Original file line number Diff line number Diff line change
@@ -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
)
267 changes: 267 additions & 0 deletions plots/horizon-basic/metadata/r/ggplot2.yaml
Original file line number Diff line number Diff line change
@@ -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