Skip to content
Merged
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
117 changes: 117 additions & 0 deletions plots/quiver-basic/implementations/r/ggplot2.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#' anyplot.ai
#' quiver-basic: Basic Quiver Plot
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 92/100 | Created: 2026-07-24

library(ggplot2)
library(dplyr)
library(tidyr)
library(ragg)

set.seed(42)

# --- Theme tokens -------------------------------------------------------
THEME <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0"

# Imprint sequential cmap (single-polarity: wind speed magnitude)
IMPRINT_SEQ_LOW <- "#009E73"
IMPRINT_SEQ_HIGH <- "#4467A3"

# --- Data: cyclonic wind field around a low-pressure eye ----------------
# Rotational flow (u = -y, v = x) modulated by a radial Gaussian decay so
# speed peaks in a ring around the calm eye and fades toward the outskirts.
# WIND_SCALE converts the unitless rotation field into storm-force m/s so the
# "Cyclonic" framing matches the numbers on the legend (peak ~34 m/s, low-end
# Category 1 hurricane strength).
WIND_SCALE <- 12
grid <- expand_grid(
x = seq(-8, 8, by = 1),
y = seq(-4.5, 4.5, by = 1)
)

wind <- grid %>%
mutate(
radius = sqrt(x^2 + y^2),
decay = exp(-radius^2 / 40),
u = -y * decay * WIND_SCALE,
v = x * decay * WIND_SCALE,
speed = sqrt(u^2 + v^2)
)

# Scale arrow length so the fastest vector spans ~80% of the 1-unit grid
# spacing, keeping neighbouring arrows from overlapping.
arrow_scale <- 0.8 / max(wind$speed)
wind <- wind %>%
mutate(
xend = x + u * arrow_scale,
yend = y + v * arrow_scale
)

# Dashed radial guide marking the ring of peak tangential wind speed, i.e.
# the radius that maximises r * exp(-r^2/40) (found analytically at r = sqrt(20)).
eyewall_radius <- sqrt(20)
eyewall_theta <- seq(0, 2 * pi, length.out = 200)
eyewall <- tibble::tibble(
x = eyewall_radius * cos(eyewall_theta),
y = eyewall_radius * sin(eyewall_theta)
)

# --- Plot ----------------------------------------------------------------
p <- ggplot(wind, aes(x = x, y = y, xend = xend, yend = yend, color = speed)) +
geom_path(
data = eyewall, aes(x = x, y = y), inherit.aes = FALSE,
linetype = "dashed", linewidth = 0.4, color = INK_SOFT, alpha = 0.5
) +
geom_segment(
linewidth = 0.7,
arrow = arrow(length = unit(2, "mm"), type = "closed", angle = 20)
) +
annotate(
"point", x = 0, y = 0,
shape = 21, size = 3, stroke = 1, color = INK_SOFT, fill = PAGE_BG
) +
annotate(
"text", x = 0, y = -0.9, label = "eye",
color = INK_SOFT, size = 2.6, fontface = "italic"
) +
coord_fixed(ratio = 1, expand = TRUE) +
scale_color_gradient(
low = IMPRINT_SEQ_LOW, high = IMPRINT_SEQ_HIGH,
name = "Wind speed (m/s)"
) +
labs(
x = "Distance east of eye (km)",
y = "Distance north of eye (km)",
title = "Cyclonic Wind Field · quiver-basic · r · ggplot2 · anyplot.ai"
) +
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.major = element_line(color = INK_SOFT, linewidth = 0.15, linetype = "dotted"),
panel.grid.minor = element_blank(),
axis.title = element_text(color = INK, size = 10),
axis.text = element_text(color = INK_SOFT, size = 8),
axis.ticks = element_blank(),
plot.title = element_text(color = INK, size = 12),
legend.background = element_rect(fill = ELEVATED_BG, color = NA),
legend.text = element_text(color = INK_SOFT, size = 8),
legend.title = element_text(color = INK, size = 10),
legend.key = element_rect(fill = PAGE_BG, color = NA),
plot.margin = margin(10, 14, 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
)
257 changes: 257 additions & 0 deletions plots/quiver-basic/metadata/r/ggplot2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
library: ggplot2
language: r
specification_id: quiver-basic
created: '2026-07-24T18:20:51Z'
updated: '2026-07-24T18:37:05Z'
generated_by: claude-sonnet
workflow_run: 30116255071
issue: 1014
language_version: 4.4.1
library_version: 3.5.1
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/quiver-basic/r/ggplot2/plot-light.png
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/quiver-basic/r/ggplot2/plot-dark.png
preview_html_light: null
preview_html_dark: null
quality_score: 92
review:
strengths:
- Wind speed range now genuinely cyclone-force (~5-34 m/s, peaking right at Category
1 hurricane strength) instead of the previous light-breeze scale, so the 'Cyclonic
Wind Field' title now matches the data (fixes prior DQ-03 weakness)
- New dashed eyewall guide ring at the radius of peak tangential speed plus an annotated
open-circle 'eye' marker give the plot a clear focal narrative device beyond color/length
redundancy alone (fixes prior DE-03 weakness)
- Faint dotted reference grid now helps read exact arrow positions across the 170-arrow
field without competing visually with the data (fixes prior DE-02 weakness)
- 'Correct grammar-of-graphics quiver approach: geom_segment() + arrow(type=''closed'')
with coord_fixed(ratio = 1) so directions are not visually distorted'
- Arrow direction AND magnitude are doubly encoded via length and imprint_seq color,
giving a clear visual gradient from calm eye to fast outer ring
- 170-arrow 17x10 grid sits squarely in the spec's recommended 100-400 arrow / 10x10-20x20
range, uniformly spaced with no overlap (confirmed by close-up crop around the
eye marker on both renders)
- Theme-adaptive chrome correctly threaded through title/axis/tick/legend/grid colors
and both plot + legend backgrounds for light and dark renders
- 'Clean, reproducible, single-pass script: set.seed(42), only used imports (ggplot2,
dplyr, tidyr, ragg), no functions/classes, correct ragg::agg_png save at the exact
landscape canvas size (3200x1800)'
weaknesses:
- Still no library-distinctive technique beyond the baseline geom_segment+arrow()+coord_fixed
pattern for the core vector field itself — the new guide ring and eye marker add
storytelling but don't showcase a further ggplot2-specific quiver capability.
- 'Aesthetic sophistication is strong but not exceptional: the color+length redundant
encoding, eyewall ring, and eye annotation cover the main polish opportunities,
leaving little further headroom without added complexity.'
image_description: |-
Light render (plot-light.png):
Background: Warm off-white (#FAF8F1-class), not pure white, not dark.
Chrome: Title "Cyclonic Wind Field · quiver-basic · r · ggplot2 · anyplot.ai" in dark ink, ~60% of plot width, comfortably sized and fully within the canvas; x-axis label "Distance east of eye (km)" and y-axis label "Distance north of eye (km)" in dark ink, both similarly sized and clearly legible; tick labels (-5, 0, 5 / -5.0 to 5.0) in a softer gray, all readable; a new faint dotted major grid (both axes) is visible without competing with the data; legend "Wind speed (m/s)" with a vertical color-bar and numeric ticks (10-30), housed in a subtly elevated off-white box.
Data: 170 arrows on a 17x10 grid form a clear rotational (cyclonic) pattern — tangential flow circling a calm "eye" near the origin, now marked with an open dark-ink circle and an italic "eye" label directly below it, plus a dashed light-gray eyewall ring at the radius of peak tangential speed. Arrow color runs from brand green (#009E73, slow, near the eye) to blue (~#4467A3, fast, outer ring, ~30-34 m/s), matching the imprint_seq sequential colormap — now genuinely cyclone-force rather than light-breeze speeds. Close-up inspection around the eye marker confirms no overlap between the circle, the "eye" label, the dashed ring, and the nearby arrows.
Legibility verdict: PASS — all text has strong contrast against the light background; no light-on-light issues.

Dark render (plot-dark.png):
Background: Warm near-black (#1A1A17-class), not pure black, not light.
Chrome: Same title, axis labels, tick labels, and new dotted grid, now rendered in light ink (off-white for title/axis titles, softer light gray for tick labels and grid) — fully legible against the dark background, no dark-on-dark failures observed. Legend box uses a barely-lighter dark elevated fill with light text, also legible.
Data: Same green-to-blue gradient, same arrow geometry/positions, same eye marker (light-ink circle) and dashed eyewall ring as the light render — data colors and annotation geometry are identical between themes, confirming only chrome flipped. Close-up crop around the eye marker shows no overlap issues on this theme either.
Legibility verdict: PASS — title, axis labels, tick labels, grid, and legend text are all clearly readable against the dark surface; no black-on-black or washed-out text detected; brand green (#009E73) remains clearly visible near the eye.
criteria_checklist:
visual_quality:
score: 29
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 7
max: 8
passed: true
comment: Explicit font sizes throughout; all text (incl. new grid/eye label)
readable in both themes at full resolution
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: 170 uniformly spaced arrows with no collisions; eye marker + label
+ eyewall ring do not overlap arrows (verified via close-up crop)
- id: VQ-03
name: Element Visibility
score: 6
max: 6
passed: true
comment: Arrow length/color appropriately sized for medium-density 17x10 grid
- id: VQ-04
name: Color Accessibility
score: 2
max: 2
passed: true
comment: Sequential green-blue imprint_seq gradient is CVD-safe; no red-green
sole signal
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Canvas gate passed (3200x1800); title ~60% width; coord_fixed keeps
proportions correct; nothing clipped
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Descriptive axis labels with units (km); legend labeled with units
(m/s)
- id: VQ-07
name: Palette Compliance
score: 2
max: 2
passed: true
comment: 'imprint_seq (#009E73 -> #4467A3) used for continuous magnitude;
correct theme backgrounds both renders'
design_excellence:
score: 15
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 6
max: 8
passed: true
comment: Redundant length+color encoding plus the new eyewall guide ring and
eye annotation create noticeably more polish than the prior attempt
- id: DE-02
name: Visual Refinement
score: 4
max: 6
passed: true
comment: New faint dotted reference grid aids readability without adding clutter;
spines still removed, margins generous
- id: DE-03
name: Data Storytelling
score: 5
max: 6
passed: true
comment: Eye marker + label + dashed eyewall ring now give an explicit focal
narrative device beyond the calm-eye vs. fast-ring color/length gradient
alone
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: geom_segment + arrow() is the correct/only idiomatic ggplot2 quiver
approach
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: Uniform spacing, scaled arrow length, magnitude via color — all spec
notes covered
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: x/y grid positions correct; coord_fixed shows all data without distortion
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Title matches mandated format exactly; legend labeled with units
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: Position, direction, and magnitude (length + color) all shown
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: Neutral meteorological framing (cyclonic wind field around a low-pressure
eye)
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Speeds now ~5-34 m/s, peaking at genuine Category-1-hurricane strength,
matching the 'Cyclonic' title framing — prior light-breeze mismatch fixed
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: No functions/classes, single linear pipeline
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: set.seed(42) present
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: ggplot2, dplyr, tidyr, ragg all used
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Appropriate complexity, no fake UI/interactivity, comments explain
non-obvious derivations (WIND_SCALE, eyewall radius)
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves plot-{THEME}.png via ragg::agg_png at correct canvas size
library_mastery:
score: 8
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 5
max: 5
passed: true
comment: geom_segment+arrow() with coord_fixed is the correct idiomatic ggplot2
pattern for vector fields
- id: LM-02
name: Distinctive Features
score: 3
max: 5
passed: false
comment: Layering geom_path guide + dual annotate() calls over the quiver
shows more compositional skill than baseline, but no further library-distinctive
technique beyond that
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- colorbar
- annotations
- layer-composition
patterns:
- data-generation
dataprep: []
styling:
- custom-colormap
- minimal-chrome
- alpha-blending
Loading