|
| 1 | +#' anyplot.ai |
| 2 | +#' point-and-figure-basic: Point and Figure Chart |
| 3 | +#' Library: ggplot2 3.5.1 | R 4.4.1 |
| 4 | +#' Quality: 87/100 | Created: 2026-05-20 |
| 5 | + |
| 6 | +library(ggplot2) |
| 7 | +library(ragg) |
| 8 | + |
| 9 | +set.seed(42) |
| 10 | + |
| 11 | +# --- Theme tokens --- |
| 12 | +THEME <- Sys.getenv("ANYPLOT_THEME", "light") |
| 13 | +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" |
| 14 | +ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420" |
| 15 | +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" |
| 16 | +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" |
| 17 | +BULL_COLOR <- "#009E73" # Okabe-Ito #1 — bullish X columns |
| 18 | +BEAR_COLOR <- "#D55E00" # Okabe-Ito #2 — bearish O columns |
| 19 | + |
| 20 | +# --- Synthetic daily close prices (ACME Corp., 300 trading days) --- |
| 21 | +n_days <- 300 |
| 22 | +prices <- numeric(n_days) |
| 23 | +prices[1] <- 52.0 |
| 24 | + |
| 25 | +for (i in 2:n_days) { |
| 26 | + drift <- if (i <= 80) 0.18 else if (i <= 160) -0.15 else if (i <= 240) 0.10 else -0.06 |
| 27 | + prices[i] <- prices[i - 1] + rnorm(1, mean = drift, sd = 1.2) |
| 28 | +} |
| 29 | +prices <- pmax(prices, 20) |
| 30 | + |
| 31 | +# --- P&F algorithm --- |
| 32 | +box_size <- 2.0 |
| 33 | +reversal <- 3L |
| 34 | +floor_box <- function(p) floor(p / box_size) * box_size |
| 35 | + |
| 36 | +build_pf <- function(prices, box_size, reversal) { |
| 37 | + symbols <- list() |
| 38 | + dir <- NA_character_ |
| 39 | + col_num <- 1L |
| 40 | + ref <- floor_box(prices[1]) |
| 41 | + current <- ref |
| 42 | + |
| 43 | + for (p in prices[-1]) { |
| 44 | + lvl <- floor_box(p) |
| 45 | + |
| 46 | + if (is.na(dir)) { |
| 47 | + if (lvl >= ref + box_size) { |
| 48 | + dir <- "X" |
| 49 | + for (v in seq(ref + box_size, lvl, by = box_size)) |
| 50 | + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "X") |
| 51 | + current <- lvl |
| 52 | + } else if (lvl <= ref - box_size) { |
| 53 | + dir <- "O" |
| 54 | + for (v in seq(ref - box_size, lvl, by = -box_size)) |
| 55 | + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "O") |
| 56 | + current <- lvl |
| 57 | + } |
| 58 | + } else if (dir == "X") { |
| 59 | + if (lvl >= current + box_size) { |
| 60 | + for (v in seq(current + box_size, lvl, by = box_size)) |
| 61 | + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "X") |
| 62 | + current <- lvl |
| 63 | + } else if (lvl <= current - reversal * box_size) { |
| 64 | + col_num <- col_num + 1L |
| 65 | + dir <- "O" |
| 66 | + for (v in seq(current - box_size, lvl, by = -box_size)) |
| 67 | + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "O") |
| 68 | + current <- lvl |
| 69 | + } |
| 70 | + } else { |
| 71 | + if (lvl <= current - box_size) { |
| 72 | + for (v in seq(current - box_size, lvl, by = -box_size)) |
| 73 | + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "O") |
| 74 | + current <- lvl |
| 75 | + } else if (lvl >= current + reversal * box_size) { |
| 76 | + col_num <- col_num + 1L |
| 77 | + dir <- "X" |
| 78 | + for (v in seq(current + box_size, lvl, by = box_size)) |
| 79 | + symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = "X") |
| 80 | + current <- lvl |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + if (length(symbols) == 0) |
| 86 | + return(data.frame(col = integer(), price = numeric(), type = character())) |
| 87 | + do.call(rbind, symbols) |
| 88 | +} |
| 89 | + |
| 90 | +pf <- build_pf(prices, box_size, reversal) |
| 91 | + |
| 92 | +# --- Plot --- |
| 93 | +n_cols <- max(pf$col) |
| 94 | +y_lo <- min(pf$price) - box_size |
| 95 | +y_hi <- max(pf$price) + box_size |
| 96 | +x_end <- n_cols + 0.5 |
| 97 | + |
| 98 | +# --- 45-degree trend lines (1 box per column = slope of box_size) --- |
| 99 | +x_col_ids <- sort(unique(pf$col[pf$type == "X"])) |
| 100 | +o_col_ids <- sort(unique(pf$col[pf$type == "O"])) |
| 101 | + |
| 102 | +support_df <- data.frame( |
| 103 | + x = x_col_ids, |
| 104 | + y = sapply(x_col_ids, function(c) min(pf$price[pf$col == c])), |
| 105 | + xend = x_end |
| 106 | +) |
| 107 | +support_df$yend <- support_df$y + (x_end - support_df$x) * box_size |
| 108 | + |
| 109 | +resist_df <- data.frame( |
| 110 | + x = o_col_ids, |
| 111 | + y = sapply(o_col_ids, function(c) max(pf$price[pf$col == c])), |
| 112 | + xend = x_end |
| 113 | +) |
| 114 | +resist_df$yend <- resist_df$y - (x_end - resist_df$x) * box_size |
| 115 | + |
| 116 | +p <- ggplot(pf, aes(x = col, y = price, label = type, color = type)) + |
| 117 | + geom_segment( |
| 118 | + data = support_df, |
| 119 | + aes(x = x, y = y, xend = xend, yend = yend), |
| 120 | + color = BULL_COLOR, alpha = 0.45, linewidth = 0.55, linetype = "dashed", |
| 121 | + inherit.aes = FALSE |
| 122 | + ) + |
| 123 | + geom_segment( |
| 124 | + data = resist_df, |
| 125 | + aes(x = x, y = y, xend = xend, yend = yend), |
| 126 | + color = BEAR_COLOR, alpha = 0.45, linewidth = 0.55, linetype = "dashed", |
| 127 | + inherit.aes = FALSE |
| 128 | + ) + |
| 129 | + geom_text(size = 3.5, fontface = "bold", family = "mono") + |
| 130 | + scale_color_manual( |
| 131 | + values = c("X" = BULL_COLOR, "O" = BEAR_COLOR), |
| 132 | + labels = c("X" = "X Bullish", "O" = "O Bearish"), |
| 133 | + name = NULL |
| 134 | + ) + |
| 135 | + guides(color = guide_legend( |
| 136 | + override.aes = list(label = c("O", "X"), size = 4.5, fontface = "bold", family = "mono") |
| 137 | + )) + |
| 138 | + scale_x_continuous( |
| 139 | + name = "Column (Reversal #)", |
| 140 | + breaks = seq(2, n_cols, by = 2), |
| 141 | + limits = c(0.5, n_cols + 0.5), |
| 142 | + expand = expansion(0) |
| 143 | + ) + |
| 144 | + scale_y_continuous( |
| 145 | + name = "Price (USD)", |
| 146 | + breaks = seq(y_lo, y_hi, by = box_size * 2), |
| 147 | + minor_breaks = seq(y_lo, y_hi, by = box_size), |
| 148 | + limits = c(y_lo, y_hi), |
| 149 | + expand = expansion(0) |
| 150 | + ) + |
| 151 | + labs(title = "point-and-figure-basic · r · ggplot2 · anyplot.ai") + |
| 152 | + theme_minimal(base_size = 8) + |
| 153 | + theme( |
| 154 | + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), |
| 155 | + panel.background = element_rect(fill = PAGE_BG, color = NA), |
| 156 | + panel.grid.major = element_line(color = INK_SOFT, linewidth = 0.15), |
| 157 | + panel.grid.minor = element_line(color = INK_SOFT, linewidth = 0.08), |
| 158 | + panel.border = element_blank(), |
| 159 | + axis.title = element_text(color = INK, size = 10), |
| 160 | + axis.text = element_text(color = INK_SOFT, size = 8), |
| 161 | + axis.line.x.bottom = element_line(color = INK_SOFT, linewidth = 0.4), |
| 162 | + axis.line.y.left = element_line(color = INK_SOFT, linewidth = 0.4), |
| 163 | + axis.ticks = element_blank(), |
| 164 | + plot.title = element_text(color = INK, size = 12, face = "bold"), |
| 165 | + legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3), |
| 166 | + legend.text = element_text(color = INK_SOFT, size = 9), |
| 167 | + legend.margin = margin(4, 6, 4, 6), |
| 168 | + legend.key.size = unit(0.8, "lines"), |
| 169 | + legend.position = "right", |
| 170 | + plot.margin = margin(12, 12, 8, 10) |
| 171 | + ) |
| 172 | + |
| 173 | +# --- Save --- |
| 174 | +ggsave( |
| 175 | + filename = sprintf("plot-%s.png", THEME), |
| 176 | + plot = p, |
| 177 | + device = ragg::agg_png, |
| 178 | + width = 8, |
| 179 | + height = 4.5, |
| 180 | + units = "in", |
| 181 | + dpi = 400 |
| 182 | +) |
0 commit comments