{"spec_id":"point-and-figure-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// point-and-figure-basic: Point and Figure Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: synthetic daily closes, converted into Point & Figure columns ---\nfunction lcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (s * 1664525 + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\nconst rand = lcg(42);\nconst boxSize = 2; // $ per box\nconst reversal = 3; // boxes required to start a new column\n\nlet price = 120;\nlet momentum = 0;\nconst closes = [];\nfor (let i = 0; i < 300; i++) {\n  momentum = (momentum + (rand() - 0.5) * 0.4) * 0.85;\n  price = Math.max(20, price + momentum + (rand() - 0.5) * 5);\n  closes.push(price);\n}\n\nconst boxIndex = (p) => Math.round(p / boxSize);\nconst boxed = closes.map(boxIndex);\n\n// Whole-box reversal method: extend the current column while price keeps\n// moving in its direction, start a new column only once price reverses by\n// `reversal` boxes.\nconst columns = [];\nlet current = boxed[0];\nlet column = null;\nfor (let i = 1; i < boxed.length; i++) {\n  const idx = boxed[i];\n  if (column === null) {\n    if (idx === current) continue;\n    column = idx > current ? { type: \"X\", low: current, high: idx } : { type: \"O\", low: idx, high: current };\n    current = idx;\n    continue;\n  }\n  if (column.type === \"X\") {\n    if (idx > column.high) {\n      column.high = idx;\n      current = idx;\n    } else if (idx <= column.high - reversal) {\n      columns.push(column);\n      column = { type: \"O\", low: idx, high: column.high - 1 };\n      current = idx;\n    }\n  } else {\n    if (idx < column.low) {\n      column.low = idx;\n      current = idx;\n    } else if (idx >= column.low + reversal) {\n      columns.push(column);\n      column = { type: \"X\", low: column.low + 1, high: idx };\n      current = idx;\n    }\n  }\n}\nif (column) columns.push(column);\n\n// Classic 45-degree trend lines: a bullish support line rising from the\n// chart's lowest box, and a bearish resistance line falling from an early\n// swing high (last 30% of columns excluded so the line has room to run).\nconst minLow = d3.min(columns, (c) => c.low);\nconst supportStart = columns.findIndex((c) => c.low === minLow);\n\nconst earlyCutoff = Math.floor(columns.length * 0.7);\nconst earlyHigh = d3.max(columns.slice(0, earlyCutoff), (c) => c.high);\nconst resistanceStart = columns.findIndex((c) => c.high === earlyHigh);\n\nfunction trendLine(startIdx, startBox, slope) {\n  const points = [];\n  for (let i = startIdx; i < columns.length; i++) {\n    points.push([i, startBox + slope * (i - startIdx)]);\n  }\n  return points;\n}\n\n// --- Layout ------------------------------------------------------------\nconst margin = { top: 150, right: 70, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst minBox = d3.min(columns, (c) => c.low) - 1;\nconst maxBox = d3.max(columns, (c) => c.high) + 1;\n\nconst x = d3.scaleBand().domain(d3.range(columns.length)).range([0, iw]).paddingInner(0.08).paddingOuter(0.04);\nconst y = d3.scaleLinear().domain([minBox, maxBox]).range([ih, 0]);\nconst cellH = Math.abs(y(minBox) - y(minBox + 1));\n\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Breakout highlight: a soft band behind the single largest column ----\n// (the biggest box-count swing), giving the chart one clear focal point\n// instead of every column reading with equal weight.\nconst columnSizes = columns.map((c) => c.high - c.low + 1);\nconst breakoutIdx = columnSizes.indexOf(d3.max(columnSizes));\nconst breakoutColumn = columns[breakoutIdx];\n\ng.append(\"rect\")\n  .attr(\"x\", x(breakoutIdx) - (x.step() - x.bandwidth()) / 2)\n  .attr(\"y\", 0)\n  .attr(\"width\", x.step())\n  .attr(\"height\", ih)\n  .attr(\"fill\", breakoutColumn.type === \"X\" ? t.palette[0] : t.palette[4])\n  .attr(\"opacity\", 0.08);\n\n// --- Gridlines at box-size price intervals (every other box, kept light\n// so the box-structure glyphs stay the primary read) -----------------------\nconst boxTicks = d3.range(minBox, maxBox + 1);\nconst gridTicks = boxTicks.filter((_, i) => i % 2 === 0);\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(gridTicks)\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (b) => y(b))\n  .attr(\"y2\", (b) => y(b))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Y axis: price scale -------------------------------------------------\nconst yAxis = g.append(\"g\").call(\n  d3\n    .axisLeft(y)\n    .tickValues(boxTicks.filter((_, i) => i % 2 === 0))\n    .tickFormat((b) => `$${b * boxSize}`)\n);\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- X axis: column index (reversals), not time ---------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .tickValues(d3.range(columns.length).filter((i) => i % 2 === 0))\n      .tickFormat((i) => i + 1)\n  );\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Support / resistance trend lines ------------------------------------\nconst lineGen = d3\n  .line()\n  .x((d) => x(d[0]) + x.bandwidth() / 2)\n  .y((d) => y(d[1]));\n\ng.append(\"path\")\n  .datum(trendLine(supportStart, minLow, 1))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"9,7\")\n  .attr(\"opacity\", 0.5)\n  .attr(\"d\", lineGen);\n\ng.append(\"path\")\n  .datum(trendLine(resistanceStart, earlyHigh, -1))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"9,7\")\n  .attr(\"opacity\", 0.5)\n  .attr(\"d\", lineGen);\n\n// --- Columns of X's (rising) and O's (falling) ---------------------------\nconst bullish = t.palette[0]; // #009E73 — brand green, always first series\nconst bearish = t.palette[4]; // matte red — semantic anchor for loss / decline\n\nconst cells = columns.flatMap((c, i) => d3.range(c.low, c.high + 1).map((level) => ({ col: i, level, type: c.type })));\nconst symbolSize = Math.min(x.bandwidth(), cellH) * 0.66;\n\ng.selectAll(\"text.box\")\n  .data(cells)\n  .join(\"text\")\n  .attr(\"class\", \"box\")\n  .attr(\"x\", (d) => x(d.col) + x.bandwidth() / 2)\n  .attr(\"y\", (d) => y(d.level))\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-size\", `${symbolSize}px`)\n  .style(\"font-weight\", 700)\n  .style(\"font-family\", \"monospace\")\n  .attr(\"fill\", (d) => (d.type === \"X\" ? bullish : bearish))\n  .text((d) => d.type);\n\n// --- Axis labels -----------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Column (price reversal), not time\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Price ($)\");\n\n// --- Legend: X / O meaning --------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},92)`);\nconst legendItems = [\n  { symbol: \"X\", label: \"Rising column\", color: bullish },\n  { symbol: \"O\", label: \"Falling column\", color: bearish },\n];\nlet legendX = 0;\nfor (const item of legendItems) {\n  const entry = legend.append(\"g\").attr(\"transform\", `translate(${legendX},0)`);\n  entry\n    .append(\"text\")\n    .attr(\"fill\", item.color)\n    .style(\"font-size\", \"20px\")\n    .style(\"font-weight\", 700)\n    .style(\"font-family\", \"monospace\")\n    .text(item.symbol);\n  entry\n    .append(\"text\")\n    .attr(\"x\", 26)\n    .attr(\"y\", 0)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(item.label);\n  legendX += 26 + item.label.length * 8.5 + 40;\n}\n\n// --- Subtitle: box size + reversal setting ---------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 82)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(`Box size $${boxSize} · ${reversal}-box reversal`);\n\n// --- Title -------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", 600)\n  .text(\"point-and-figure-basic · javascript · d3 · anyplot.ai\");\n"}