{"spec_id":"point-and-figure-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// point-and-figure-basic: Point and Figure Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: simulated daily closes for a mid-cap tech stock -----------------\n// Deterministic LCG (no network, no Math.random) driving a regime-switching\n// random walk so the price alternates between multi-week up/down swings.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rnd = makeLcg(391);\n\nconst CLOSE_COUNT = 280;\nconst closes = [];\nlet price = 100;\nlet regime = 1;\nlet regimeDaysLeft = 0;\nfor (let i = 0; i < CLOSE_COUNT; i++) {\n  regimeDaysLeft -= 1;\n  if (regimeDaysLeft <= 0) {\n    regime = (rnd() - 0.5) * 2;\n    regimeDaysLeft = 8 + Math.floor(rnd() * 12);\n  }\n  price += regime * 0.35 + (rnd() - 0.5) * 3.2;\n  price = Math.max(20, price);\n  closes.push(Math.round(price * 100) / 100);\n}\n\n// --- Point-and-figure column construction -----------------------------------\n// Box size: $2 per box. Reversal: 3 boxes required to start a new column.\nconst BOX_SIZE = 2;\nconst REVERSAL = 3;\nconst priceToBox = (p) => Math.round(p / BOX_SIZE);\n\nconst columns = [];\nlet direction = null; // \"up\" | \"down\"\nlet boxes = [priceToBox(closes[0])];\nfor (let i = 1; i < closes.length; i++) {\n  const box = priceToBox(closes[i]);\n  const lastBox = boxes[boxes.length - 1];\n  if (direction === null) {\n    if (box > lastBox) {\n      for (let b = lastBox + 1; b <= box; b++) boxes.push(b);\n      direction = \"up\";\n    } else if (box < lastBox) {\n      for (let b = lastBox - 1; b >= box; b--) boxes.push(b);\n      direction = \"down\";\n    }\n  } else if (direction === \"up\") {\n    if (box > lastBox) {\n      for (let b = lastBox + 1; b <= box; b++) boxes.push(b);\n    } else if (box <= lastBox - REVERSAL) {\n      columns.push({ direction, boxes });\n      const start = lastBox - 1;\n      boxes = [];\n      for (let b = start; b >= box; b--) boxes.push(b);\n      direction = \"down\";\n    }\n  } else {\n    if (box < lastBox) {\n      for (let b = lastBox - 1; b >= box; b--) boxes.push(b);\n    } else if (box >= lastBox + REVERSAL) {\n      columns.push({ direction, boxes });\n      const start = lastBox + 1;\n      boxes = [];\n      for (let b = start; b <= box; b++) boxes.push(b);\n      direction = \"up\";\n    }\n  }\n}\ncolumns.push({ direction, boxes });\n\n// --- Chart-ready points -------------------------------------------------\nconst risingPoints = []; // X — rising columns\nconst fallingPoints = []; // O — falling columns\nlet globalMinBox = Infinity;\nlet globalMaxBox = -Infinity;\nlet bottomColumn = 0;\nlet topColumn = 0;\n// Category-axis x-values must match the category strings exactly — a plain\n// number is treated as a 0-based category *index*, which silently drops the\n// last column and shifts every other column by one.\ncolumns.forEach((col, colIndex) => {\n  const colNumber = colIndex + 1;\n  const target = col.direction === \"up\" ? risingPoints : fallingPoints;\n  col.boxes.forEach((b) => {\n    target.push([String(colNumber), b * BOX_SIZE]);\n    if (b < globalMinBox) {\n      globalMinBox = b;\n      bottomColumn = colNumber;\n    }\n    if (b > globalMaxBox) {\n      globalMaxBox = b;\n      topColumn = colNumber;\n    }\n  });\n});\n\n// Bullish support line: a classic P&F trend line anchored on the deepest\n// reversal low, rising at the canonical 45-degree rate of one box per column.\nconst supportStart = [String(bottomColumn), globalMinBox * BOX_SIZE];\nconst supportEnd = [\n  String(columns.length),\n  (globalMinBox + (columns.length - bottomColumn)) * BOX_SIZE,\n];\n\n// Bearish resistance line: mirrors the support line's construction, anchored\n// on the highest reversal high and descending at the same 45-degree rate.\nconst resistanceStart = [String(topColumn), globalMaxBox * BOX_SIZE];\nconst resistanceEnd = [\n  String(columns.length),\n  (globalMaxBox - (columns.length - topColumn)) * BOX_SIZE,\n];\n\n// --- Option -------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"point-and-figure-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    top: 56,\n    // Series markers are transparent (only the X/O label glyph is visible),\n    // so the legend needs its own swatch colors rather than inheriting them.\n    data: [\n      { name: \"Rising (X)\", icon: \"rect\", itemStyle: { color: t.palette[0] } },\n      { name: \"Falling (O)\", icon: \"rect\", itemStyle: { color: t.palette[4] } },\n      { name: \"Bullish support (45°)\", icon: \"rect\", itemStyle: { color: t.inkSoft } },\n      { name: \"Bearish resistance (45°)\", icon: \"rect\", itemStyle: { color: t.inkSoft } },\n    ],\n    textStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  grid: { left: 110, right: 60, top: 130, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    name: \"Column (price reversal)\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.inkSoft, fontSize: 15 },\n    data: columns.map((_, i) => String(i + 1)),\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Price ($)\",\n    nameTextStyle: { color: t.inkSoft, fontSize: 15 },\n    min: (globalMinBox - 2) * BOX_SIZE,\n    max: (globalMaxBox + 1) * BOX_SIZE,\n    interval: BOX_SIZE,\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: \"${value}\" },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Rising (X)\",\n      type: \"scatter\",\n      data: risingPoints,\n      symbol: \"circle\",\n      symbolSize: 30,\n      itemStyle: { color: \"transparent\" },\n      label: {\n        show: true,\n        position: \"inside\",\n        formatter: \"X\",\n        fontSize: 26,\n        fontWeight: 700,\n        color: t.palette[0],\n      },\n      tooltip: { valueFormatter: (v) => `$${v}` },\n      z: 3,\n    },\n    {\n      name: \"Falling (O)\",\n      type: \"scatter\",\n      data: fallingPoints,\n      symbol: \"circle\",\n      symbolSize: 30,\n      itemStyle: { color: \"transparent\" },\n      label: {\n        show: true,\n        position: \"inside\",\n        formatter: \"O\",\n        fontSize: 26,\n        fontWeight: 700,\n        color: t.palette[4],\n      },\n      tooltip: { valueFormatter: (v) => `$${v}` },\n      z: 3,\n    },\n    {\n      name: \"Bullish support (45°)\",\n      type: \"line\",\n      data: [supportStart, supportEnd],\n      showSymbol: false,\n      lineStyle: { color: t.inkSoft, width: 2, type: \"dashed\" },\n      z: 2,\n      tooltip: { show: false },\n    },\n    {\n      name: \"Bearish resistance (45°)\",\n      type: \"line\",\n      data: [resistanceStart, resistanceEnd],\n      showSymbol: false,\n      lineStyle: { color: t.inkSoft, width: 2, type: \"dotted\" },\n      z: 2,\n      tooltip: { show: false },\n    },\n  ],\n});\n"}