{"spec_id":"kagi-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// kagi-basic: Basic Kagi Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Daily closing prices via a fixed-seed LCG random walk with mild upward drift.\nconst makeRng = (seed) => {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n};\nconst rand = makeRng(42);\n\nconst numObservations = 260;\nconst closePrices = [128.5];\nfor (let i = 1; i < numObservations; i++) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  const drift = 0.0005;\n  const volatility = 0.017;\n  closePrices.push(closePrices[i - 1] * (1 + drift + volatility * z));\n}\n\n// --- Kagi construction: reversal-threshold swing filter ---------------------\n// A new Kagi column only forms once price moves `reversalPct` against the\n// current trend; small moves within the threshold are absorbed into the\n// running high/low, filtering out time-based noise.\nconst reversalPct = 0.04;\n\nconst kagiColumns = [];\nlet columnBase = closePrices[0];\nlet direction = null;\nlet extreme = closePrices[0];\nfor (let i = 1; i < closePrices.length; i++) {\n  const price = closePrices[i];\n  if (direction === null) {\n    if (price >= columnBase * (1 + reversalPct)) {\n      direction = \"up\";\n      extreme = price;\n    } else if (price <= columnBase * (1 - reversalPct)) {\n      direction = \"down\";\n      extreme = price;\n    }\n    continue;\n  }\n  if (direction === \"up\") {\n    if (price > extreme) {\n      extreme = price;\n    } else if (price <= extreme * (1 - reversalPct)) {\n      kagiColumns.push({ dir: \"up\", from: columnBase, to: extreme });\n      columnBase = extreme;\n      direction = \"down\";\n      extreme = price;\n    }\n  } else {\n    if (price < extreme) {\n      extreme = price;\n    } else if (price >= extreme * (1 + reversalPct)) {\n      kagiColumns.push({ dir: \"down\", from: columnBase, to: extreme });\n      columnBase = extreme;\n      direction = \"up\";\n      extreme = price;\n    }\n  }\n}\nkagiColumns.push({ dir: direction || \"up\", from: columnBase, to: extreme });\n\n// Flatten columns into drawable segments: one vertical bar per column (yang\n// thick/green on up-swings, yin thin/red on down-swings) plus the horizontal\n// shoulder/waist connecting each column to the next at the reversal price.\nconst segments = [];\nfor (let i = 0; i < kagiColumns.length; i++) {\n  const y0 = i === 0 ? kagiColumns[0].from : kagiColumns[i - 1].to;\n  const y1 = kagiColumns[i].to;\n  segments.push({ kind: \"v\", x: i, y0, y1, dir: kagiColumns[i].dir });\n  if (i < kagiColumns.length - 1) {\n    segments.push({ kind: \"h\", x0: i, x1: i + 1, y: y1, dir: kagiColumns[i].dir });\n  }\n}\n\nconst allPrices = kagiColumns.flatMap((c) => [c.from, c.to]);\nconst priceMin = Math.min(...allPrices);\nconst priceMax = Math.max(...allPrices);\nconst pricePad = (priceMax - priceMin) * 0.08;\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"kagi-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 40,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    data: [\"Yang (Uptrend)\", \"Yin (Downtrend)\"],\n    top: 95,\n    left: \"center\",\n    itemGap: 48,\n    itemWidth: 26,\n    itemHeight: 14,\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  grid: { left: 120, right: 70, top: 165, bottom: 100 },\n  xAxis: {\n    type: \"value\",\n    name: \"Kagi Line Index\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    min: -0.5,\n    max: kagiColumns.length - 0.5,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Price ($)\",\n    nameLocation: \"middle\",\n    nameGap: 70,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    min: priceMin - pricePad,\n    max: priceMax + pricePad,\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: (v) => `$${v.toFixed(0)}` },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Yang (Uptrend)\",\n      type: \"line\",\n      data: [],\n      showSymbol: false,\n      lineStyle: { color: t.palette[0], width: 7, cap: \"round\" },\n      itemStyle: { color: t.palette[0] },\n    },\n    {\n      name: \"Yin (Downtrend)\",\n      type: \"line\",\n      data: [],\n      showSymbol: false,\n      lineStyle: { color: t.palette[4], width: 2.5, cap: \"round\" },\n      itemStyle: { color: t.palette[4] },\n    },\n    {\n      name: \"Kagi\",\n      type: \"custom\",\n      encode: { x: 0, y: 1 },\n      data: segments.map((s) =>\n        s.kind === \"v\" ? [s.x, (s.y0 + s.y1) / 2] : [(s.x0 + s.x1) / 2, s.y]\n      ),\n      renderItem: (params, api) => {\n        const seg = segments[params.dataIndex];\n        const color = seg.dir === \"up\" ? t.palette[0] : t.palette[4];\n        const lineWidth = seg.dir === \"up\" ? 7 : 2.5;\n        const p0 = seg.kind === \"v\" ? api.coord([seg.x, seg.y0]) : api.coord([seg.x0, seg.y]);\n        const p1 = seg.kind === \"v\" ? api.coord([seg.x, seg.y1]) : api.coord([seg.x1, seg.y]);\n        return {\n          type: \"line\",\n          shape: { x1: p0[0], y1: p0[1], x2: p1[0], y2: p1[1] },\n          style: { stroke: color, lineWidth, lineCap: \"round\" },\n        };\n      },\n    },\n  ],\n});\n\n// --- Deliberate flourish: callout on the largest single-column reversal -----\n// Marks the column with the biggest shoulder/waist swing so the chart tells a\n// story at a glance instead of leaving every column visually equal-weight.\nlet calloutIdx = 0;\nlet calloutSwing = 0;\nkagiColumns.forEach((c, i) => {\n  const swing = Math.abs(c.to - c.from);\n  if (swing > calloutSwing) {\n    calloutSwing = swing;\n    calloutIdx = i;\n  }\n});\nconst calloutCol = kagiColumns[calloutIdx];\nconst calloutPct = Math.round(Math.abs(calloutCol.to / calloutCol.from - 1) * 100);\nconst calloutY = Math.max(calloutCol.from, calloutCol.to);\nconst [calloutPx, calloutPy] = chart.convertToPixel({ xAxisIndex: 0, yAxisIndex: 0 }, [\n  calloutIdx,\n  calloutY,\n]);\nconst calloutColor = calloutCol.dir === \"up\" ? t.palette[0] : t.palette[4];\nconst size = window.ANYPLOT_SIZE;\nconst calloutLeft = Math.min(Math.max(calloutPx - 70, 130), size.width - 230);\nconst calloutTop = Math.max(calloutPy - 34, 172);\nchart.setOption({\n  animation: false,\n  graphic: [\n    {\n      type: \"circle\",\n      left: calloutPx - 6,\n      top: calloutPy - 6,\n      shape: { cx: 6, cy: 6, r: 6 },\n      style: { fill: \"transparent\", stroke: calloutColor, lineWidth: 1.5 },\n      z: 10,\n    },\n    {\n      type: \"text\",\n      left: calloutLeft,\n      top: calloutTop,\n      style: {\n        text: `Largest swing: ${calloutPct}%`,\n        fill: t.ink,\n        fontSize: 13,\n        fontWeight: 600,\n      },\n      z: 10,\n    },\n  ],\n});\n"}