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
103 changes: 103 additions & 0 deletions plots/indicator-ema/implementations/r/ggplot2.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#' anyplot.ai
#' indicator-ema: Exponential Moving Average (EMA) Indicator Chart
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 92/100 | Created: 2026-05-19

library(ggplot2)
library(scales)
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"

OKABE_ITO <- c(
"Price" = "#009E73",
"EMA (12)" = "#D55E00",
"EMA (26)" = "#0072B2"
)

# Data
n_days <- 180
dates <- seq.Date(as.Date("2024-01-02"), by = "day", length.out = n_days)
close <- 150 * cumprod(1 + rnorm(n_days, mean = 0.0005, sd = 0.018))

# Inline EMA via Reduce — no helper function needed
k12 <- 2 / (12 + 1)
k26 <- 2 / (26 + 1)
ema12 <- Reduce(function(e, x) x * k12 + e * (1 - k12), close, accumulate = TRUE)
ema26 <- Reduce(function(e, x) x * k26 + e * (1 - k26), close, accumulate = TRUE)

df <- data.frame(date = dates, close = close, ema12 = ema12, ema26 = ema26)

# Detect EMA crossovers (sign change in ema12 - ema26)
diff_ema <- df$ema12 - df$ema26
cross_idx <- which(diff(sign(diff_ema)) != 0) + 1
crossovers <- df[cross_idx, , drop = FALSE]

# Long form for idiomatic ggplot2 multi-series mapping
df_long <- pivot_longer(df, cols = c(close, ema12, ema26),
names_to = "series", values_to = "price")
df_long$series <- factor(df_long$series,
levels = c("close", "ema12", "ema26"),
labels = c("Price", "EMA (12)", "EMA (26)"))

# Plot
p <- ggplot(df_long, aes(x = date, y = price, color = series)) +
geom_vline(data = crossovers, aes(xintercept = date),
color = INK_SOFT, linetype = "dashed",
linewidth = 0.4, alpha = 0.7) +
geom_line(aes(linewidth = series, alpha = series)) +
geom_point(data = crossovers, aes(x = date, y = ema12),
shape = 21, size = 4, fill = PAGE_BG,
color = OKABE_ITO[["EMA (12)"]], stroke = 1.5,
inherit.aes = FALSE) +
scale_color_manual(name = NULL, values = OKABE_ITO,
breaks = c("Price", "EMA (12)", "EMA (26)")) +
scale_linewidth_manual(
values = c("Price" = 1.5, "EMA (12)" = 1.1, "EMA (26)" = 1.1),
guide = "none") +
scale_alpha_manual(
values = c("Price" = 0.80, "EMA (12)" = 1.0, "EMA (26)" = 1.0),
guide = "none") +
scale_x_date(date_labels = "%b %Y", date_breaks = "2 months") +
scale_y_continuous(labels = dollar_format()) +
labs(
title = "Tech Stock EMA · indicator-ema · r · ggplot2 · anyplot.ai",
x = "Date",
y = "Closing Price (USD)"
) +
theme_minimal(base_size = 14) +
theme(
plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG),
panel.background = element_rect(fill = PAGE_BG, color = NA),
panel.grid.major.y = element_line(color = INK_SOFT, linewidth = 0.3),
panel.grid.major.x = element_blank(),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
axis.title = element_text(color = INK, size = 20),
axis.text = element_text(color = INK_SOFT, size = 16),
axis.line = element_line(color = INK_SOFT, linewidth = 0.5),
plot.title = element_text(color = INK, size = 24, face = "bold"),
legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT),
legend.text = element_text(color = INK_SOFT, size = 16),
legend.key = element_rect(fill = ELEVATED_BG, color = NA),
legend.position = "bottom",
plot.margin = margin(20, 20, 20, 20, unit = "pt")
)

ggsave(
filename = sprintf("plot-%s.png", THEME),
plot = p,
device = ragg::agg_png,
width = 16,
height = 9,
units = "in",
dpi = 300
)
242 changes: 242 additions & 0 deletions plots/indicator-ema/metadata/r/ggplot2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
library: ggplot2
language: r
specification_id: indicator-ema
created: '2026-05-19T05:00:14Z'
updated: '2026-05-19T05:20:44Z'
generated_by: claude-sonnet
workflow_run: 26077083850
issue: 3652
language_version: 4.4.1
library_version: 3.5.1
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/indicator-ema/r/ggplot2/plot-light.png
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/indicator-ema/r/ggplot2/plot-dark.png
preview_html_light: null
preview_html_dark: null
quality_score: 92
review:
strengths:
- Complete theme-adaptive chrome with zero dark-on-dark failures
- Idiomatic ggplot2 long-format pattern for clean multi-series mapping
- Technically correct EMA via Reduce with proper exponential smoothing factor
- Crossover detection and visualization adds real chart value
- Alpha and linewidth differentiation creates visual hierarchy between price and
EMA lines
weaknesses:
- DE-01 could be elevated with typography polish or a focal annotation at the most
significant crossover
- Frequent crossover dashed verticals add visual noise; limiting to significant
crossovers would improve storytelling
image_description: |-
Light render (plot-light.png):
Background: Warm off-white #FAF8F1 — correct light surface
Chrome: Title "Tech Stock EMA · indicator-ema · r · ggplot2 · anyplot.ai" in bold dark text clearly readable; axis labels "Closing Price (USD)" and "Date" readable; tick labels ($140-$180, Feb/Apr/Jun 2024) readable in INK_SOFT gray; legend box with subtle border at bottom showing Price/EMA(12)/EMA(26)
Data: Three lines — Price in teal #009E73 (slightly transparent), EMA(12) in orange #D55E00, EMA(26) in blue #0072B2; dashed vertical gray crossover lines; open circles at crossover points on EMA(12)
Legibility verdict: PASS — all text clearly readable against light background

Dark render (plot-dark.png):
Background: Warm near-black #1A1A17 — correct dark surface
Chrome: Title and axis labels rendered in light off-white (#F0EFE8); tick labels in soft light gray (#B8B7B0); legend box using elevated dark background #242420 with light text — all readable
Data: Data line colors identical to light render (teal #009E73, orange #D55E00, blue #0072B2); crossover markers and dashed verticals adapt to lighter chrome
Legibility verdict: PASS — no dark-on-dark failures; all text clearly legible against dark background
criteria_checklist:
visual_quality:
score: 30
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 8
max: 8
passed: true
comment: Title 24pt, axis labels 20pt, tick labels 16pt, legend 16pt all explicitly
set; readable in both themes
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: No text collisions; line overlaps are inherent to EMA overlay chart
type
- id: VQ-03
name: Element Visibility
score: 6
max: 6
passed: true
comment: Lines well-sized at linewidth 1.1-1.5; crossover markers visible
at size=4
- id: VQ-04
name: Color Accessibility
score: 2
max: 2
passed: true
comment: Okabe-Ito teal/orange/blue is CVD-safe; adequate contrast in both
themes
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Plot fills canvas well; 20pt margins; legend well-positioned at bottom
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Closing Price (USD) with units; Date appropriate for x-axis
- id: VQ-07
name: Palette Compliance
score: 2
max: 2
passed: true
comment: 'Price=#009E73, EMA(12)=#D55E00, EMA(26)=#0072B2; backgrounds #FAF8F1/#1A1A17;
data colors identical across themes'
design_excellence:
score: 13
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 5
max: 8
passed: true
comment: 'Above generic defaults: alpha differentiation, hollow crossover
markers; intentional design choices but not publication-grade'
- id: DE-02
name: Visual Refinement
score: 4
max: 6
passed: true
comment: L-shaped frame via axis.line + panel.border blank; y-only major grid;
no minor grid; semantic crossover verticals
- id: DE-03
name: Data Storytelling
score: 4
max: 6
passed: true
comment: Crossover highlights create clear focal points; alpha/linewidth hierarchy
keeps EMAs prominent
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct EMA overlay chart with price + two EMA periods
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: Price line prominent; EMAs overlaid; distinct colors; thinner EMAs;
legend labels; crossover highlighting
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: Date on x, closing price on y; all series visible across 180 trading
days
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Title matches {Descriptive}·{spec-id}·r·ggplot2·anyplot.ai format;
legend labels correct
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: Price volatility, EMA smoothing, crossovers, uptrend and downtrend
phases all present
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: Tech Stock EMA neutral trading scenario; standard periods 12/26;
plausible price action
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Starting price $150, range $135-$180; daily drift/volatility realistic
for tech stocks
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: 'Linear flow: imports → tokens → data → EMA → crossovers → reshape
→ plot → save'
- 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, scales, tidyr, ragg all actively used
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Idiomatic Reduce for inline EMA; pivot_longer for reshaping; named
color vector
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: sprintf plot-%s.png → plot-light/dark.png; ragg::agg_png device
library_mastery:
score: 9
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 5
max: 5
passed: true
comment: 'Exemplary: long-format data, named aesthetic vectors, scale_linewidth/alpha_manual
with guide=none, multi-dataset geoms with inherit.aes=FALSE'
- id: LM-02
name: Distinctive Features
score: 4
max: 5
passed: true
comment: Grammar-driven multi-aesthetic scaling, multi-dataset geom layering,
factor levels for legend ordering — distinctively ggplot2
verdict: APPROVED
impl_tags:
dependencies:
- scales
- tidyr
- ragg
techniques:
- layer-composition
patterns:
- data-generation
- wide-to-long
dataprep:
- time-series
styling:
- alpha-blending
Loading