{"spec_id":"point-and-figure-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// point-and-figure-basic: Point and Figure Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: synthetic daily close prices for a fictitious stock -------------\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst numDays = 300; // ~14 trading months\nconst closes = [118];\nfor (let day = 1; day < numDays; day++) {\n  const macroTrend = Math.sin((day / numDays) * Math.PI * 22) * 3.2;\n  const dailyNoise = (rand() - 0.5) * 5.0;\n  const next = closes[day - 1] + macroTrend + dailyNoise;\n  closes.push(Math.max(70, next));\n}\n\n// --- Point and Figure construction (close-based, 3-box reversal) -----------\nconst boxSize = 1; // $1 per box\nconst reversalBoxes = 3; // classic 3-box reversal method\nconst boxIndexOf = (price) => Math.floor(price / boxSize);\n\nlet direction = 0; // 0 = undetermined, 1 = X column, -1 = O column\nlet colTop = boxIndexOf(closes[0]);\nlet colBottom = colTop;\nconst columns = [];\n\nfor (let i = 1; i < closes.length; i++) {\n  const idx = boxIndexOf(closes[i]);\n\n  if (direction === 0) {\n    if (idx > colTop) {\n      direction = 1;\n      colTop = idx;\n    } else if (idx < colBottom) {\n      direction = -1;\n      colBottom = idx;\n    }\n    continue;\n  }\n\n  if (direction === 1) {\n    if (idx > colTop) {\n      colTop = idx;\n    } else if (idx <= colTop - reversalBoxes) {\n      columns.push({ dir: 1, top: colTop, bottom: colBottom });\n      direction = -1;\n      colBottom = idx;\n      colTop = colTop - 1;\n    }\n  } else {\n    if (idx < colBottom) {\n      colBottom = idx;\n    } else if (idx >= colBottom + reversalBoxes) {\n      columns.push({ dir: -1, top: colTop, bottom: colBottom });\n      direction = 1;\n      colTop = idx;\n      colBottom = colBottom + 1;\n    }\n  }\n}\ncolumns.push({ dir: direction || 1, top: colTop, bottom: colBottom });\n\n// --- Flatten columns into per-box scatter points ----------------------------\nconst risingBoxes = [];\nconst fallingBoxes = [];\ncolumns.forEach((col, colIndex) => {\n  for (let box = col.bottom; box <= col.top; box++) {\n    const point = { x: colIndex + 1, y: box * boxSize };\n    if (col.dir === 1) risingBoxes.push(point);\n    else fallingBoxes.push(point);\n  }\n});\n\n// --- Support / resistance 45-degree trend lines -----------------------------\n// Classic P&F construction: a support (resistance) line starts at the box low\n// (high) of the first rising (falling) column reached, then advances exactly\n// one box per column — the \"45-degree\" slope the spec calls for — until a\n// later column's box range breaks through it. The next rising/falling column\n// then starts a fresh line, so trends are shown as a series of segments.\nfunction buildTrendSegments(colDir, boundaryOf, slopeSign) {\n  const segments = [];\n  let seg = null;\n  const startSegment = (colX, col) => ({\n    startCol: colX,\n    startVal: boundaryOf(col),\n    points: [{ x: colX, y: boundaryOf(col) * boxSize }],\n  });\n\n  columns.forEach((col, i) => {\n    const colX = i + 1;\n    if (!seg) {\n      if (col.dir === colDir) seg = startSegment(colX, col);\n      return;\n    }\n\n    const projected = seg.startVal + slopeSign * (colX - seg.startCol);\n    const broken = slopeSign > 0 ? boundaryOf(col) < projected : boundaryOf(col) > projected;\n    if (broken) {\n      if (seg.points.length > 1) segments.push(seg.points);\n      seg = col.dir === colDir ? startSegment(colX, col) : null;\n    } else {\n      seg.points.push({ x: colX, y: projected * boxSize });\n    }\n  });\n  if (seg && seg.points.length > 1) segments.push(seg.points);\n  return segments;\n}\n\n// Merge segments into a single dataset, breaking the line between segments\n// with a NaN point (spanGaps: false keeps the gap from being connected).\nconst withGaps = (segments) =>\n  segments.flatMap((seg, i) => (i === 0 ? seg : [{ x: seg[0].x, y: NaN }, ...seg]));\n\nconst supportPoints = withGaps(buildTrendSegments(1, (col) => col.bottom, 1));\nconst resistancePoints = withGaps(buildTrendSegments(-1, (col) => col.top, -1));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------------\nconst datasets = [\n  {\n    label: \"X — rising\",\n    data: risingBoxes,\n    pointStyle: \"crossRot\",\n    pointRadius: 11,\n    pointBorderWidth: 3,\n    pointBorderColor: t.palette[0],\n    pointBackgroundColor: t.palette[0],\n    showLine: false,\n  },\n  {\n    label: \"O — falling\",\n    data: fallingBoxes,\n    pointStyle: \"circle\",\n    pointRadius: 11,\n    pointBorderWidth: 3,\n    pointBorderColor: t.palette[4],\n    pointBackgroundColor: \"transparent\",\n    showLine: false,\n  },\n];\nif (supportPoints.length) {\n  datasets.push({\n    label: \"Support (45°)\",\n    data: supportPoints,\n    showLine: true,\n    spanGaps: false,\n    fill: false,\n    borderColor: t.inkSoft,\n    borderWidth: 2,\n    borderDash: [10, 5],\n    pointRadius: 0,\n  });\n}\nif (resistancePoints.length) {\n  datasets.push({\n    label: \"Resistance (45°)\",\n    data: resistancePoints,\n    showLine: true,\n    spanGaps: false,\n    fill: false,\n    borderColor: t.inkSoft,\n    borderWidth: 2,\n    borderDash: [3, 5],\n    pointRadius: 0,\n  });\n}\n\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"point-and-figure-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n      },\n      legend: {\n        labels: { color: t.ink, font: { size: 16 }, usePointStyle: true },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        suggestedMax: columns.length + 1,\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"Column (price reversal sequence)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      y: {\n        ticks: {\n          stepSize: 10,\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => `$${value}`,\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Price ($)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}