{"spec_id":"point-and-figure-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' point-and-figure-basic: Point and Figure Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 87/100 | Created: 2026-05-20\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens ---\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nBULL_COLOR  <- \"#009E73\"   # Okabe-Ito #1 — bullish X columns\nBEAR_COLOR  <- \"#AE3030\"   # imprint red — bearish O columns\n\n# --- Synthetic daily close prices (ACME Corp., 300 trading days) ---\nn_days <- 300\nprices <- numeric(n_days)\nprices[1] <- 52.0\n\nfor (i in 2:n_days) {\n  drift <- if (i <= 80) 0.18 else if (i <= 160) -0.15 else if (i <= 240) 0.10 else -0.06\n  prices[i] <- prices[i - 1] + rnorm(1, mean = drift, sd = 1.2)\n}\nprices <- pmax(prices, 20)\n\n# --- P&F algorithm ---\nbox_size <- 2.0\nreversal  <- 3L\nfloor_box <- function(p) floor(p / box_size) * box_size\n\nbuild_pf <- function(prices, box_size, reversal) {\n  symbols <- list()\n  dir     <- NA_character_\n  col_num <- 1L\n  ref     <- floor_box(prices[1])\n  current <- ref\n\n  for (p in prices[-1]) {\n    lvl <- floor_box(p)\n\n    if (is.na(dir)) {\n      if (lvl >= ref + box_size) {\n        dir <- \"X\"\n        for (v in seq(ref + box_size, lvl, by = box_size))\n          symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = \"X\")\n        current <- lvl\n      } else if (lvl <= ref - box_size) {\n        dir <- \"O\"\n        for (v in seq(ref - box_size, lvl, by = -box_size))\n          symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = \"O\")\n        current <- lvl\n      }\n    } else if (dir == \"X\") {\n      if (lvl >= current + box_size) {\n        for (v in seq(current + box_size, lvl, by = box_size))\n          symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = \"X\")\n        current <- lvl\n      } else if (lvl <= current - reversal * box_size) {\n        col_num <- col_num + 1L\n        dir <- \"O\"\n        for (v in seq(current - box_size, lvl, by = -box_size))\n          symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = \"O\")\n        current <- lvl\n      }\n    } else {\n      if (lvl <= current - box_size) {\n        for (v in seq(current - box_size, lvl, by = -box_size))\n          symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = \"O\")\n        current <- lvl\n      } else if (lvl >= current + reversal * box_size) {\n        col_num <- col_num + 1L\n        dir <- \"X\"\n        for (v in seq(current + box_size, lvl, by = box_size))\n          symbols[[length(symbols) + 1L]] <- data.frame(col = col_num, price = v, type = \"X\")\n        current <- lvl\n      }\n    }\n  }\n\n  if (length(symbols) == 0)\n    return(data.frame(col = integer(), price = numeric(), type = character()))\n  do.call(rbind, symbols)\n}\n\npf <- build_pf(prices, box_size, reversal)\n\n# --- Plot ---\nn_cols <- max(pf$col)\ny_lo   <- min(pf$price) - box_size\ny_hi   <- max(pf$price) + box_size\nx_end  <- n_cols + 0.5\n\n# --- 45-degree trend lines (1 box per column = slope of box_size) ---\nx_col_ids <- sort(unique(pf$col[pf$type == \"X\"]))\no_col_ids <- sort(unique(pf$col[pf$type == \"O\"]))\n\nsupport_df <- data.frame(\n  x    = x_col_ids,\n  y    = sapply(x_col_ids, function(c) min(pf$price[pf$col == c])),\n  xend = x_end\n)\nsupport_df$yend <- support_df$y + (x_end - support_df$x) * box_size\n\nresist_df <- data.frame(\n  x    = o_col_ids,\n  y    = sapply(o_col_ids, function(c) max(pf$price[pf$col == c])),\n  xend = x_end\n)\nresist_df$yend <- resist_df$y - (x_end - resist_df$x) * box_size\n\np <- ggplot(pf, aes(x = col, y = price, label = type, color = type)) +\n  geom_segment(\n    data        = support_df,\n    aes(x = x, y = y, xend = xend, yend = yend),\n    color       = BULL_COLOR, alpha = 0.45, linewidth = 0.55, linetype = \"dashed\",\n    inherit.aes = FALSE\n  ) +\n  geom_segment(\n    data        = resist_df,\n    aes(x = x, y = y, xend = xend, yend = yend),\n    color       = BEAR_COLOR, alpha = 0.45, linewidth = 0.55, linetype = \"dashed\",\n    inherit.aes = FALSE\n  ) +\n  geom_text(size = 3.5, fontface = \"bold\", family = \"mono\") +\n  scale_color_manual(\n    values = c(\"X\" = BULL_COLOR, \"O\" = BEAR_COLOR),\n    labels = c(\"X\" = \"X  Bullish\", \"O\" = \"O  Bearish\"),\n    name   = NULL\n  ) +\n  guides(color = guide_legend(\n    override.aes = list(label = c(\"O\", \"X\"), size = 4.5, fontface = \"bold\", family = \"mono\")\n  )) +\n  scale_x_continuous(\n    name   = \"Column (Reversal #)\",\n    breaks = seq(2, n_cols, by = 2),\n    limits = c(0.5, n_cols + 0.5),\n    expand = expansion(0)\n  ) +\n  scale_y_continuous(\n    name         = \"Price (USD)\",\n    breaks       = seq(y_lo, y_hi, by = box_size * 2),\n    minor_breaks = seq(y_lo, y_hi, by = box_size),\n    limits       = c(y_lo, y_hi),\n    expand       = expansion(0)\n  ) +\n  labs(title = \"point-and-figure-basic · r · ggplot2 · anyplot.ai\") +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background    = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background   = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid.major   = element_line(color = INK_SOFT, linewidth = 0.15),\n    panel.grid.minor   = element_line(color = INK_SOFT, linewidth = 0.08),\n    panel.border       = element_blank(),\n    axis.title         = element_text(color = INK, size = 10),\n    axis.text          = element_text(color = INK_SOFT, size = 8),\n    axis.line.x.bottom = element_line(color = INK_SOFT, linewidth = 0.4),\n    axis.line.y.left   = element_line(color = INK_SOFT, linewidth = 0.4),\n    axis.ticks         = element_blank(),\n    plot.title         = element_text(color = INK, size = 12, face = \"bold\"),\n    legend.background  = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),\n    legend.text        = element_text(color = INK_SOFT, size = 9),\n    legend.margin      = margin(4, 6, 4, 6),\n    legend.key.size    = unit(0.8, \"lines\"),\n    legend.position    = \"right\",\n    plot.margin        = margin(12, 12, 8, 10)\n  )\n\n# --- Save ---\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}