Skip to content

Commit cd3793f

Browse files
feat(ggplot2): implement point-and-figure-basic (#7472)
## Implementation: `point-and-figure-basic` - r/ggplot2 Implements the **r/ggplot2** version of `point-and-figure-basic`. **File:** `plots/point-and-figure-basic/implementations/r/ggplot2.R` **Parent Issue:** #3755 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26139700905)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 61b3704 commit cd3793f

2 files changed

Lines changed: 429 additions & 0 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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

Comments
 (0)