{"spec_id":"kagi-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// kagi-basic: Basic Kagi Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 150, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: deterministic daily closes (mulberry32 PRNG, fixed seed) --------\nfunction mulberry32(seed) {\n  return function () {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(20260108);\n\nconst numDays = 320;\nconst regimeDrift = [0.0012, -0.001, 0.002, -0.0016, 0.0008, -0.0013, 0.0018]; // rotating trend legs\nconst closes = [84];\nfor (let i = 1; i < numDays; i++) {\n  const regime = regimeDrift[Math.floor(i / 38) % regimeDrift.length];\n  const shock = (rand() - 0.5) * 0.055;\n  closes.push(Math.max(20, closes[i - 1] * (1 + regime + shock)));\n}\n\n// --- Kagi construction: reversal-threshold zigzag over the close series ----\nconst REVERSAL_PCT = 0.032;\n\nfunction buildKagiPivots(prices, reversalPct) {\n  const pivots = [prices[0]];\n  let direction = 0; // 0 = undecided, 1 = up-tracking, -1 = down-tracking\n  let extreme = prices[0];\n\n  for (let i = 1; i < prices.length; i++) {\n    const p = prices[i];\n    if (direction === 0) {\n      // Undecided: wait for the first breakout from the starting anchor.\n      if (p >= prices[0] * (1 + reversalPct)) {\n        direction = 1;\n        extreme = p;\n      } else if (p <= prices[0] * (1 - reversalPct)) {\n        direction = -1;\n        extreme = p;\n      }\n      continue;\n    }\n    if (direction === 1) {\n      if (p > extreme) {\n        extreme = p;\n      } else if (p <= extreme * (1 - reversalPct)) {\n        pivots.push(extreme);\n        direction = -1;\n        extreme = p;\n      }\n    } else {\n      if (p < extreme) {\n        extreme = p;\n      } else if (p >= extreme * (1 + reversalPct)) {\n        pivots.push(extreme);\n        direction = 1;\n        extreme = p;\n      }\n    }\n  }\n  pivots.push(extreme);\n  return pivots;\n}\n\nconst pivots = buildKagiPivots(closes, REVERSAL_PCT);\nconst numColumns = pivots.length - 1;\n\n// Each pivot-to-pivot transition is one step-line segment: a vertical move\n// to the new pivot, then (except for the final segment) a horizontal\n// shoulder/waist carrying it to the next column.\nconst kagiSegments = [];\nfor (let i = 0; i < numColumns; i++) {\n  const hasShoulder = i < numColumns - 1;\n  kagiSegments.push({\n    points: [\n      { x: i, y: pivots[i] },\n      { x: hasShoulder ? i + 1 : i, y: pivots[i + 1] },\n    ],\n    dir: pivots[i + 1] >= pivots[i] ? \"up\" : \"down\",\n  });\n}\n\n// --- Scales -------------------------------------------------------------\nconst x = d3\n  .scaleLinear()\n  .domain([0, numColumns - 1])\n  .range([0, iw]);\nconst yExtent = d3.extent(pivots);\nconst y = d3\n  .scaleLinear()\n  .domain([yExtent[0] * 0.97, yExtent[1] * 1.03])\n  .nice()\n  .range([ih, 0]);\n\n// --- SVG mount -------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Kagi lines: thick/green for yang (up), thin/red for yin (down) --------\n// Semantic exception (default-style-guide.md): profit/up -> green, loss/down -> red.\nconst THICK = 6;\nconst THIN = 2.5;\nconst upColor = t.palette[0]; // brand green\nconst downColor = t.palette[4]; // matte red\n\n// Each vertical-then-shoulder segment is a d3-shape path (curveStepBefore),\n// not a raw SVG <line>, so the yang/yin step geometry comes from d3.line().\nconst kagiLine = d3\n  .line()\n  .x((d) => x(d.x))\n  .y((d) => y(d.y))\n  .curve(d3.curveStepBefore);\n\ng.selectAll(\".kagi-segment\")\n  .data(kagiSegments)\n  .join(\"path\")\n  .attr(\"class\", \"kagi-segment\")\n  .attr(\"fill\", \"none\")\n  .attr(\"d\", (d) => kagiLine(d.points))\n  .attr(\"stroke\", (d) => (d.dir === \"up\" ? upColor : downColor))\n  .attr(\"stroke-width\", (d) => (d.dir === \"up\" ? THICK : THIN))\n  .attr(\"stroke-linecap\", \"round\")\n  .attr(\"stroke-linejoin\", \"round\");\n\n// --- Axes --------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(10).tickFormat(d3.format(\"d\")));\nconst yAxis = g\n  .append(\"g\")\n  .call(d3.axisLeft(y).tickFormat((d) => `$${d3.format(\",.0f\")(d)}`));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels -------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(`Kagi Line Index (${d3.format(\".1%\")(REVERSAL_PCT)} Reversal Threshold)`);\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Closing Price ($)\");\n\n// --- Legend (rounded color chips, no frame per style guide) ----------------\nconst legendItems = [\n  { label: \"Yang (Up)\", color: upColor },\n  { label: \"Yin (Down)\", color: downColor },\n];\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${width - margin.right - 190}, 24)`);\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0, ${i * 28})`);\n  row\n    .append(\"rect\")\n    .attr(\"x\", 0)\n    .attr(\"y\", -8)\n    .attr(\"width\", 16)\n    .attr(\"height\", 16)\n    .attr(\"rx\", 4)\n    .attr(\"fill\", item.color);\n  row\n    .append(\"text\")\n    .attr(\"x\", 26)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n});\n\n// --- End-of-line price callout --------------------------------------------\nconst lastPivot = pivots[pivots.length - 1];\nconst lastDir = kagiSegments[kagiSegments.length - 1].dir;\nconst calloutColor = lastDir === \"up\" ? upColor : downColor;\nconst callout = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(${x(numColumns - 1)},${y(lastPivot)})`);\ncallout\n  .append(\"circle\")\n  .attr(\"r\", 4.5)\n  .attr(\"fill\", calloutColor)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\ncallout\n  .append(\"rect\")\n  .attr(\"x\", 12)\n  .attr(\"y\", -13)\n  .attr(\"width\", 96)\n  .attr(\"height\", 26)\n  .attr(\"rx\", 6)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", calloutColor)\n  .attr(\"stroke-width\", 1.5);\ncallout\n  .append(\"text\")\n  .attr(\"x\", 60)\n  .attr(\"y\", 4)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(`Last: $${lastPivot.toFixed(2)}`);\n\n// --- Title -------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"kagi-basic · javascript · d3 · anyplot.ai\");\n"}