diff --git a/plots/point-and-figure-basic/implementations/r/ggplot2.R b/plots/point-and-figure-basic/implementations/r/ggplot2.R new file mode 100644 index 0000000000..ed01e169b1 --- /dev/null +++ b/plots/point-and-figure-basic/implementations/r/ggplot2.R @@ -0,0 +1,182 @@ +#' anyplot.ai +#' point-and-figure-basic: Point and Figure Chart +#' Library: ggplot2 3.5.1 | R 4.4.1 +#' Quality: 87/100 | Created: 2026-05-20 + +library(ggplot2) +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" +BULL_COLOR <- "#009E73" # Okabe-Ito #1 — bullish X columns +BEAR_COLOR <- "#D55E00" # Okabe-Ito #2 — bearish O columns + +# --- Synthetic daily close prices (ACME Corp., 300 trading days) --- +n_days <- 300 +prices <- numeric(n_days) +prices[1] <- 52.0 + +for (i in 2:n_days) { + drift <- if (i <= 80) 0.18 else if (i <= 160) -0.15 else if (i <= 240) 0.10 else -0.06 + prices[i] <- prices[i - 1] + rnorm(1, mean = drift, sd = 1.2) +} +prices <- pmax(prices, 20) + +# --- P&F algorithm --- +box_size <- 2.0 +reversal <- 3L +floor_box <- function(p) floor(p / box_size) * box_size + +build_pf <- function(prices, box_size, reversal) { + symbols <- list() + dir <- NA_character_ + col_num <- 1L + ref <- floor_box(prices[1]) + current <- ref + + for (p in prices[-1]) { + lvl <- floor_box(p) + + if (is.na(dir)) { + if (lvl >= ref + box_size) { + dir <- "X" + for (v in seq(ref + box_size, lvl, by = box_size)) + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "X") + current <- lvl + } else if (lvl <= ref - box_size) { + dir <- "O" + for (v in seq(ref - box_size, lvl, by = -box_size)) + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "O") + current <- lvl + } + } else if (dir == "X") { + if (lvl >= current + box_size) { + for (v in seq(current + box_size, lvl, by = box_size)) + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "X") + current <- lvl + } else if (lvl <= current - reversal * box_size) { + col_num <- col_num + 1L + dir <- "O" + for (v in seq(current - box_size, lvl, by = -box_size)) + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "O") + current <- lvl + } + } else { + if (lvl <= current - box_size) { + for (v in seq(current - box_size, lvl, by = -box_size)) + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "O") + current <- lvl + } else if (lvl >= current + reversal * box_size) { + col_num <- col_num + 1L + dir <- "X" + for (v in seq(current + box_size, lvl, by = box_size)) + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "X") + current <- lvl + } + } + } + + if (length(symbols) == 0) + return(data.frame(col = integer(), price = numeric(), type = character())) + do.call(rbind, symbols) +} + +pf <- build_pf(prices, box_size, reversal) + +# --- Plot --- +n_cols <- max(pf$col) +y_lo <- min(pf$price) - box_size +y_hi <- max(pf$price) + box_size +x_end <- n_cols + 0.5 + +# --- 45-degree trend lines (1 box per column = slope of box_size) --- +x_col_ids <- sort(unique(pf$col[pf$type == "X"])) +o_col_ids <- sort(unique(pf$col[pf$type == "O"])) + +support_df <- data.frame( + x = x_col_ids, + y = sapply(x_col_ids, function(c) min(pf$price[pf$col == c])), + xend = x_end +) +support_df$yend <- support_df$y + (x_end - support_df$x) * box_size + +resist_df <- data.frame( + x = o_col_ids, + y = sapply(o_col_ids, function(c) max(pf$price[pf$col == c])), + xend = x_end +) +resist_df$yend <- resist_df$y - (x_end - resist_df$x) * box_size + +p <- ggplot(pf, aes(x = col, y = price, label = type, color = type)) + + geom_segment( + data = support_df, + aes(x = x, y = y, xend = xend, yend = yend), + color = BULL_COLOR, alpha = 0.45, linewidth = 0.55, linetype = "dashed", + inherit.aes = FALSE + ) + + geom_segment( + data = resist_df, + aes(x = x, y = y, xend = xend, yend = yend), + color = BEAR_COLOR, alpha = 0.45, linewidth = 0.55, linetype = "dashed", + inherit.aes = FALSE + ) + + geom_text(size = 3.5, fontface = "bold", family = "mono") + + scale_color_manual( + values = c("X" = BULL_COLOR, "O" = BEAR_COLOR), + labels = c("X" = "X Bullish", "O" = "O Bearish"), + name = NULL + ) + + guides(color = guide_legend( + override.aes = list(label = c("O", "X"), size = 4.5, fontface = "bold", family = "mono") + )) + + scale_x_continuous( + name = "Column (Reversal #)", + breaks = seq(2, n_cols, by = 2), + limits = c(0.5, n_cols + 0.5), + expand = expansion(0) + ) + + scale_y_continuous( + name = "Price (USD)", + breaks = seq(y_lo, y_hi, by = box_size * 2), + minor_breaks = seq(y_lo, y_hi, by = box_size), + limits = c(y_lo, y_hi), + expand = expansion(0) + ) + + labs(title = "point-and-figure-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), + panel.grid.minor = element_line(color = INK_SOFT, linewidth = 0.08), + panel.border = element_blank(), + axis.title = element_text(color = INK, size = 10), + axis.text = element_text(color = INK_SOFT, size = 8), + axis.line.x.bottom = element_line(color = INK_SOFT, linewidth = 0.4), + axis.line.y.left = element_line(color = INK_SOFT, linewidth = 0.4), + axis.ticks = element_blank(), + plot.title = element_text(color = INK, size = 12, face = "bold"), + legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3), + legend.text = element_text(color = INK_SOFT, size = 9), + legend.margin = margin(4, 6, 4, 6), + legend.key.size = unit(0.8, "lines"), + legend.position = "right", + plot.margin = margin(12, 12, 8, 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/point-and-figure-basic/metadata/r/ggplot2.yaml b/plots/point-and-figure-basic/metadata/r/ggplot2.yaml new file mode 100644 index 0000000000..66593881ae --- /dev/null +++ b/plots/point-and-figure-basic/metadata/r/ggplot2.yaml @@ -0,0 +1,247 @@ +library: ggplot2 +language: r +specification_id: point-and-figure-basic +created: '2026-05-20T03:46:50Z' +updated: '2026-05-20T04:03:49Z' +generated_by: claude-sonnet +workflow_run: 26139700905 +issue: 3755 +language_version: 4.4.1 +library_version: 3.5.1 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/r/ggplot2/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/point-and-figure-basic/r/ggplot2/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 87 +review: + strengths: + - Excellent two-theme implementation with all chrome tokens correctly applied (PAGE_BG, + INK, INK_SOFT, ELEVATED_BG) + - 'Correct Okabe-Ito palette: green (#009E73) for bullish, orange (#D55E00) for + bearish — CVD-safe with shape+color redundancy' + - Complete P&F algorithm with proper box_size, reversal, and column switching logic + - Monospace bold font for X/O symbols is thematically appropriate for a financial + chart + - Custom legend with override.aes correctly shows X/O symbols in the legend box + - Both 45-degree support (from X column lows) and resistance (from O column tops) + trend lines implemented + weaknesses: + - Helper functions (floor_box, build_pf) technically violate the KISS no-functions + criterion + - Drawing trend lines from EVERY X/O column creates many crisscrossing lines; the + spec suggests connecting ascending lows selectively for a cleaner chart + - Both major and minor grid lines active simultaneously adds subtle visual busyness; + minor grid could be removed or set near-invisible + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct light surface + Chrome: Title "point-and-figure-basic · r · ggplot2 · anyplot.ai" in bold dark text — readable. Axis labels "Column (Reversal #)" and "Price (USD)" in dark INK color — readable. Tick labels in INK_SOFT #4A4A44 — readable. Legend box with elevated background #FFFDF6 and bordered frame. + Data: X symbols in #009E73 (Okabe-Ito #1) for bullish columns; O symbols in #D55E00 (Okabe-Ito #2) for bearish columns. Dashed trend lines in matching colors at alpha=0.45. 9 columns visible across price range $42–$74. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct dark surface + Chrome: Title in light #F0EFE8 text — readable. Axis labels in light INK color — readable. Tick labels in INK_SOFT #B8B7B0 — readable. Legend box with elevated dark background #242420 and bordered frame. No dark-on-dark failures observed — all chrome correctly flips to light variants. + Data: Colors are identical to light render — X symbols in #009E73, O symbols in #D55E00. Trend lines same colors with alpha=0.45. Data colors unchanged between themes as required. + 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 sizes explicitly set; readable in both themes; minor: X/O symbols + at size=3.5 ggplot units may be slightly small at mobile scale' + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No text overlaps; X/O symbols well-spaced in column grid positions + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: X/O symbols clearly visible in both themes; many overlapping trend + lines reduce visual clarity slightly + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Okabe-Ito green/orange is CVD-safe; shape+color redundancy distinguishes + series + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Good 16:9 proportions, legend well-placed, balanced margins + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: 'X: Column (Reversal #); Y: Price (USD) — both descriptive with context' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series #009E73; second #D55E00; light bg #FAF8F1; dark bg + #1A1A17; both themes chrome-correct' + design_excellence: + score: 13 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Above default (4); monospace bold font for symbols; custom legend + with override.aes; elevated legend box; not exceptional + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Tick marks removed; spines removed; subtle major grid (0.15) and + minor grid (0.08); minor grid adds busyness + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Trend lines communicate support/resistance narrative; color coding + reinforces bullish/bearish story; lines from every column create complexity + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct P&F chart with X/O symbols in column-based layout + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 'All features present: X/O symbols, color coding, single-type columns, + trend lines, box-size grid' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X-axis = columns (reversals); Y-axis = price in USD with box-size + grid intervals + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Correct title format; legend labels O Bearish / X Bullish + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Multiple X/O columns; reversals present; both support and resistance + trend lines; realistic price progression + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: ACME Corp. stock simulation; 300 trading days; neutral financial + context + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: $52 starting price; $2 box size; 3-box reversal (standard); realistic + $42-$74 range + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: false + comment: Helper functions floor_box and build_pf defined; complexity justifies + build_pf but technically violates no-functions criterion + - 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: Only ggplot2 and ragg, both used + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean R code; no fake UI; appropriate complexity for chart type + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: plot-{THEME}.png via ragg::agg_png; modern ggplot2 3.5.1 API with + linewidth= + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Proper grammar of graphics layering; guides(override.aes=); expansion(0); + correct theme layering pattern + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: guides(override.aes=) is distinctively ggplot2; geom_text for P&F + symbols; expansion() for axis control + verdict: APPROVED +impl_tags: + dependencies: + - ragg + techniques: + - layer-composition + - custom-legend + - manual-ticks + patterns: + - data-generation + - iteration-over-groups + dataprep: + - binning + styling: + - alpha-blending + - publication-ready