{"spec_id":"kagi-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// kagi-basic: Basic Kagi Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst YANG = t.palette[0]; // #009E73 brand green — up/gain (semantic exception)\nconst YIN = t.palette[4]; // #AE3030 matte red — down/loss (semantic exception)\n\n// --- Data: synthetic daily closing prices (deterministic LCG) --------------\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) & 0x7fffffff;\n    return state / 0x7fffffff;\n  };\n}\nconst rand = lcg(42);\n\nconst nDays = 300;\nconst closes = [128];\nfor (let i = 1; i < nDays; i++) {\n  const drift = 0.05;\n  const shock = (rand() - 0.5) * 5;\n  closes.push(Math.max(20, closes[i - 1] + drift + shock));\n}\n\n// --- Kagi construction: zigzag pivots at the reversal threshold ------------\nconst reversalPct = 0.04;\nlet direction = null; // null | \"up\" | \"down\"\nlet extreme = closes[0];\nlet extremeIdx = 0;\nconst pivots = [];\n\nfor (let i = 1; i < closes.length; i++) {\n  const price = closes[i];\n  if (direction === null) {\n    if (price - extreme >= extreme * reversalPct) {\n      pivots.push({ idx: extremeIdx, price: extreme });\n      direction = \"up\";\n      extreme = price;\n      extremeIdx = i;\n    } else if (extreme - price >= extreme * reversalPct) {\n      pivots.push({ idx: extremeIdx, price: extreme });\n      direction = \"down\";\n      extreme = price;\n      extremeIdx = i;\n    } else if (price > extreme) {\n      extreme = price;\n      extremeIdx = i;\n    }\n  } else if (direction === \"up\") {\n    if (price > extreme) {\n      extreme = price;\n      extremeIdx = i;\n    } else if (extreme - price >= extreme * reversalPct) {\n      pivots.push({ idx: extremeIdx, price: extreme });\n      direction = \"down\";\n      extreme = price;\n      extremeIdx = i;\n    }\n  } else {\n    if (price < extreme) {\n      extreme = price;\n      extremeIdx = i;\n    } else if (price - extreme >= extreme * reversalPct) {\n      pivots.push({ idx: extremeIdx, price: extreme });\n      direction = \"up\";\n      extreme = price;\n      extremeIdx = i;\n    }\n  }\n}\npivots.push({ idx: extremeIdx, price: extreme });\n\n// Yang/yin: a column thickens on breaking the prior same-type pivot (shoulder\n// for up columns, waist for down columns) and thins on the opposite break.\n// The first column has no prior pivot to compare against, so it starts thick\n// (yang) by convention — harmless here since the series' first move is up.\nlet thick = true;\nconst columns = [];\nfor (let k = 0; k < pivots.length - 1; k++) {\n  const from = pivots[k];\n  const to = pivots[k + 1];\n  const up = to.price > from.price;\n  const prevSamePivot = k - 1 >= 0 ? pivots[k - 1] : null;\n  if (prevSamePivot) {\n    if (up && to.price > prevSamePivot.price) thick = true;\n    if (!up && to.price < prevSamePivot.price) thick = false;\n  }\n  columns.push({ up, thick });\n}\n\n// Explicit vertical (price move) + horizontal (column shift) points, since a\n// Kagi chart's x-axis is the line index, not time.\nconst kagiPoints = [{ x: 0, y: pivots[0].price }];\nfor (let k = 0; k < columns.length; k++) {\n  const col = columns[k];\n  kagiPoints.push({ x: k, y: pivots[k + 1].price, thick: col.thick });\n  if (k < columns.length - 1) {\n    kagiPoints.push({ x: k + 1, y: pivots[k + 1].price, thick: col.thick });\n  }\n}\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart -------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: \"Price\",\n        data: kagiPoints,\n        borderColor: YANG,\n        pointRadius: 0,\n        tension: 0,\n        segment: {\n          borderColor: (ctx) => (kagiPoints[ctx.p1DataIndex].thick ? YANG : YIN),\n          borderWidth: (ctx) => (kagiPoints[ctx.p1DataIndex].thick ? 7 : 2.5),\n        },\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"kagi-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: `${reversalPct * 100}% reversal threshold · ${nDays} daily closes`,\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 16 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          boxPadding: 6,\n          padding: 14,\n          generateLabels: () => [\n            { text: \"Yang (uptrend)\", fillStyle: YANG, strokeStyle: YANG, lineWidth: 7, pointStyle: \"line\" },\n            { text: \"Yin (downtrend)\", fillStyle: YIN, strokeStyle: YIN, lineWidth: 2.5, pointStyle: \"line\" },\n          ],\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: columns.length - 1,\n        bounds: \"data\",\n        ticks: { color: t.inkSoft, font: { size: 14 }, stepSize: 5 },\n        grid: { display: false },\n        title: { display: true, text: \"Kagi Line Index\", color: t.ink, font: { size: 18 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid.replace(\"0.15\", \"0.1\") },\n        title: { display: true, text: \"Price ($)\", color: t.ink, font: { size: 18 } },\n      },\n    },\n  },\n});\n"}