{"spec_id":"point-and-figure-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// point-and-figure-basic: Point and Figure Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: synthetic daily closes over ~1 trading year (deterministic LCG) -\nfunction lcg(seed) {\n  let state = seed;\n  return function rand() {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\n// Short alternating-direction legs (strict sign flip each leg) so the box\n// conversion below produces a genuine zigzag of many columns instead of one\n// long trend per macro regime.\nconst NUM_LEGS = 20;\nlet dir = 1;\nconst segments = [];\nfor (let s = 0; s < NUM_LEGS; s++) {\n  const days = 8 + Math.floor(rand() * 8); // 8-15 trading days per leg\n  const magnitude = 0.7 + rand() * 0.9; // 0.7-1.6 drift per day\n  segments.push({ days, drift: dir * magnitude, vol: 1.6 + rand() * 0.8 });\n  dir *= -1;\n}\n\nlet price = 148;\nconst closes = [];\nsegments.forEach(({ days, drift, vol }) => {\n  for (let i = 0; i < days; i++) {\n    price += drift + (rand() - 0.5) * vol;\n    closes.push(Math.max(price, 5));\n  }\n});\n\n// --- Point & figure box conversion (close-only, classic 1-box / 3-box rule) -\nconst BOX_SIZE = 2;\nconst REVERSAL_BOXES = 3;\nconst boxOf = (p) => Math.round(p / BOX_SIZE);\n\nfunction buildColumns(prices) {\n  const boxes = prices.map(boxOf);\n  const columns = [];\n  let direction = 0; // 0 undetermined, 1 = X column (rising), -1 = O column (falling)\n  let currentBox = boxes[0];\n  let column = { direction: 0, boxes: [currentBox] };\n\n  for (let i = 1; i < boxes.length; i++) {\n    const box = boxes[i];\n    if (direction === 0) {\n      if (box > currentBox) {\n        for (let b = currentBox + 1; b <= box; b++) column.boxes.push(b);\n        column.direction = 1;\n        direction = 1;\n        currentBox = box;\n      } else if (box < currentBox) {\n        for (let b = currentBox - 1; b >= box; b--) column.boxes.push(b);\n        column.direction = -1;\n        direction = -1;\n        currentBox = box;\n      }\n    } else if (direction === 1) {\n      if (box > currentBox) {\n        for (let b = currentBox + 1; b <= box; b++) column.boxes.push(b);\n        currentBox = box;\n      } else if (box <= currentBox - REVERSAL_BOXES) {\n        columns.push(column);\n        column = { direction: -1, boxes: [] };\n        for (let b = currentBox - 1; b >= box; b--) column.boxes.push(b);\n        direction = -1;\n        currentBox = box;\n      }\n    } else {\n      if (box < currentBox) {\n        for (let b = currentBox - 1; b >= box; b--) column.boxes.push(b);\n        currentBox = box;\n      } else if (box >= currentBox + REVERSAL_BOXES) {\n        columns.push(column);\n        column = { direction: 1, boxes: [] };\n        for (let b = currentBox + 1; b <= box; b++) column.boxes.push(b);\n        direction = 1;\n        currentBox = box;\n      }\n    }\n  }\n  columns.push(column);\n  return columns.filter((c) => c.boxes.length > 0);\n}\n\nconst columns = buildColumns(closes);\n\n// --- Series points: one scatter series per symbol; dataLabels ARE the glyph -\nconst xPoints = [];\nconst oPoints = [];\ncolumns.forEach((col, colIndex) => {\n  col.boxes.forEach((box) => {\n    const point = [colIndex, box * BOX_SIZE];\n    if (col.direction === 1) xPoints.push(point);\n    else oPoints.push(point);\n  });\n});\n\n// --- 45-degree support / resistance projections (1 box per column) ---------\nlet support = { col: 0, box: Infinity };\nlet resistance = { col: 0, box: -Infinity };\ncolumns.forEach((col, colIndex) => {\n  const low = Math.min(...col.boxes);\n  const high = Math.max(...col.boxes);\n  if (low < support.box) support = { col: colIndex, box: low };\n  if (high > resistance.box) resistance = { col: colIndex, box: high };\n});\nconst lastCol = columns.length - 1;\nconst span = Math.min(12, lastCol - Math.min(support.col, resistance.col));\nconst supportLine = Array.from({ length: span + 1 }, (_, i) => [support.col + i, (support.box + i) * BOX_SIZE]).filter(\n  ([x]) => x <= lastCol,\n);\nconst resistanceLine = Array.from({ length: span + 1 }, (_, i) => [\n  resistance.col + i,\n  (resistance.box - i) * BOX_SIZE,\n]).filter(([x]) => x <= lastCol);\n\n// --- Chart -------------------------------------------------------------------\n// Semantic exception (default-style-guide.md): bullish/bearish reads as\n// green/red to any trader, so X columns keep the brand green (palette\n// position 1) and O columns use the matte-red semantic anchor rather than\n// the next ordinal palette slot.\nconst RISING = t.palette[0];\nconst FALLING = \"#AE3030\";\nconst tickStep = Math.max(1, Math.ceil((lastCol + 1) / 15));\n\n// --- Zebra row shading every other box: keeps dense columns (many stacked\n// X/O glyphs at the box interval) reading as discrete symbols rather than a\n// solid block, without shrinking the data-label font below VQ-01 comfort ---\nconst allBoxes = columns.flatMap((c) => c.boxes);\nconst minBoxAll = Math.min(...allBoxes);\nconst maxBoxAll = Math.max(...allBoxes);\nconst rowBands = [];\nfor (let b = minBoxAll; b <= maxBoxAll; b += 2) {\n  rowBands.push({\n    from: (b - 0.5) * BOX_SIZE,\n    to: (b + 0.5) * BOX_SIZE,\n    color: Highcharts.color(t.grid).setOpacity(0.15).get(),\n  });\n}\n\n// --- Current reversal-zone highlight: shades the most recent column so the\n// reader can spot the live reversal at a glance (Highcharts xAxis.plotBands,\n// a core capability distinct from a plain scatter+dataLabels rendering) ----\nconst lastColumn = columns[lastCol];\nconst reversalZoneColor = Highcharts.color(lastColumn.direction === 1 ? RISING : FALLING)\n  .setOpacity(0.1)\n  .get();\n\n// --- Price target via the classic vertical count method (spec Applications):\n// target = extreme of the active column + column height x box size x\n// reversal count, projected in the column's own direction --------------------\nconst lastBoxCount = lastColumn.boxes.length;\nconst priceTarget =\n  lastColumn.direction === 1\n    ? Math.min(...lastColumn.boxes) * BOX_SIZE + lastBoxCount * BOX_SIZE * REVERSAL_BOXES\n    : Math.max(...lastColumn.boxes) * BOX_SIZE - lastBoxCount * BOX_SIZE * REVERSAL_BOXES;\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"point-and-figure-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: `Box size $${BOX_SIZE} · ${REVERSAL_BOXES}-box reversal`,\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: {\n      text: \"Column (price reversals — not time)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    min: -0.5,\n    max: lastCol + 0.5,\n    tickInterval: tickStep,\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotBands: [\n      {\n        from: lastCol - 0.5,\n        to: lastCol + 0.5,\n        color: reversalZoneColor,\n        zIndex: 0,\n      },\n    ],\n  },\n  yAxis: {\n    title: { text: \"Price ($)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    tickInterval: BOX_SIZE,\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotBands: rowBands,\n    plotLines: [\n      {\n        value: priceTarget,\n        color: t.amber,\n        dashStyle: \"ShortDot\",\n        width: 2,\n        zIndex: 5,\n        label: {\n          text: `Price target (vertical count): $${priceTarget.toFixed(0)}`,\n          align: \"right\",\n          x: -6,\n          style: { color: t.amber, fontSize: \"12px\", fontWeight: \"600\" },\n        },\n      },\n    ],\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    headerFormat: \"\",\n    pointFormat: \"Column {point.x} · ${point.y}\",\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      marker: { enabled: false },\n      dataLabels: {\n        enabled: true,\n        allowOverlap: true,\n        verticalAlign: \"middle\",\n        align: \"center\",\n        y: 1,\n        padding: 0,\n        style: { fontWeight: \"700\", fontSize: \"14px\", textOutline: \"none\" },\n      },\n    },\n  },\n  series: [\n    {\n      name: \"Rising (X)\",\n      type: \"scatter\",\n      data: xPoints,\n      color: RISING,\n      dataLabels: { format: \"X\", style: { color: RISING } },\n    },\n    {\n      name: \"Falling (O)\",\n      type: \"scatter\",\n      data: oPoints,\n      color: FALLING,\n      dataLabels: { format: \"O\", style: { color: FALLING } },\n    },\n    {\n      name: \"Support (45°)\",\n      type: \"line\",\n      data: supportLine,\n      color: t.inkSoft,\n      dashStyle: \"Dash\",\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"Resistance (45°)\",\n      type: \"line\",\n      data: resistanceLine,\n      color: t.inkSoft,\n      dashStyle: \"Dash\",\n      lineWidth: 2,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n  ],\n});\n"}