{"spec_id":"indicator-ichimoku","library":"d3","language":"javascript","code":"// anyplot.ai\n// indicator-ichimoku: Ichimoku Cloud Technical Indicator Chart\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-08\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 80, right: 188, bottom: 58, left: 80 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Imprint palette — semantic color choices for financial data\nconst BULL   = \"#009E73\";  // Imprint pos 1, bullish / up candles\nconst BEAR   = \"#AE3030\";  // Imprint semantic red, bearish / down candles\nconst TENKAN = \"#C475FD\";  // Imprint pos 2, Tenkan-sen (conversion line)\nconst KIJUN  = \"#4467A3\";  // Imprint pos 3, Kijun-sen (base line)\nconst CHIKOU = \"#BD8233\";  // Imprint pos 4, Chikou Span (lagging)\n\n// Deterministic LCG — no seeded RNG in the browser\nlet seed = 42;\nfunction lcg() {\n    seed = (seed * 1664525 + 1013904223) >>> 0;\n    return seed / 0x100000000;\n}\n\n// Generate 252 raw OHLC periods (200 display + 52 lookback for Span B)\nconst N_RAW = 252, N_DISPLAY = 200, LOOKBACK = 52;\nlet price = 140;\nlet drift = 0;\nconst raw = [];\nfor (let i = 0; i < N_RAW; i++) {\n    drift = drift * 0.93 + (lcg() - 0.5) * 1.3;\n    price = Math.max(85, price + drift + (lcg() - 0.5) * 3);\n    const spread = 1 + lcg() * 3.5;\n    const o = price;\n    const h = o + lcg() * spread;\n    const l = o - lcg() * spread;\n    const c = l + lcg() * (h - l);\n    raw.push({ o, h, l, c });\n}\n\n// Period high / low helpers\nfunction pHigh(arr, end, n) {\n    let m = -Infinity;\n    for (let j = Math.max(0, end - n + 1); j <= end; j++) if (arr[j].h > m) m = arr[j].h;\n    return m;\n}\nfunction pLow(arr, end, n) {\n    let m = Infinity;\n    for (let j = Math.max(0, end - n + 1); j <= end; j++) if (arr[j].l < m) m = arr[j].l;\n    return m;\n}\n\n// Build display data and Ichimoku components\nconst candles = [], tenkanLine = [], kijunLine = [], cloudPts = [], chikou = [];\n\nfor (let di = 0; di < N_DISPLAY; di++) {\n    const ri = di + LOOKBACK;\n    const { o, h, l, c } = raw[ri];\n    candles.push({ i: di, o, h, l, c });\n\n    const tk = (pHigh(raw, ri, 9)  + pLow(raw, ri, 9))  / 2;  // Tenkan-sen\n    const kj = (pHigh(raw, ri, 26) + pLow(raw, ri, 26)) / 2;  // Kijun-sen\n    tenkanLine.push({ i: di, v: tk });\n    kijunLine.push({ i: di, v: kj });\n\n    // Senkou Span A and B: plotted 26 periods into the future\n    const spanA = (tk + kj) / 2;\n    const spanB = (pHigh(raw, ri, 52) + pLow(raw, ri, 52)) / 2;\n    cloudPts.push({ i: di + 26, a: spanA, b: spanB });\n}\n\n// Chikou Span: current close shifted 26 periods into the past\nfor (let di = 26; di < N_DISPLAY; di++) {\n    chikou.push({ i: di - 26, v: candles[di].c });\n}\n\n// Scales — x covers display range plus 26 future cloud periods\nconst X_TOTAL = N_DISPLAY + 26;\nconst x = d3.scaleLinear().domain([0, X_TOTAL]).range([0, iw]);\nconst xMid = i => x(i + 0.5);\nconst candleW = Math.max(1.5, (x(1) - x(0)) * 0.72);\n\nconst allY = [\n    ...candles.flatMap(d => [d.h, d.l]),\n    ...cloudPts.flatMap(d => [d.a, d.b]),\n    ...tenkanLine.map(d => d.v),\n    ...kijunLine.map(d => d.v),\n    ...chikou.map(d => d.v),\n];\nconst ySpan = d3.max(allY) - d3.min(allY);\nconst y = d3.scaleLinear()\n    .domain([d3.min(allY) - ySpan * 0.04, d3.max(allY) + ySpan * 0.04])\n    .range([ih, 0])\n    .nice();\n\n// SVG root\nconst svg = d3.select(\"#container\")\n    .append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Clip path keeps candles and indicator lines within the chart area\nsvg.append(\"defs\").append(\"clipPath\").attr(\"id\", \"chartClip\")\n    .append(\"rect\").attr(\"width\", iw).attr(\"height\", ih + 1);\n\n// Y gridlines (subtle)\ny.ticks(6).forEach(v => {\n    g.append(\"line\")\n        .attr(\"x1\", 0).attr(\"x2\", iw)\n        .attr(\"y1\", y(v)).attr(\"y2\", y(v))\n        .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\n\n// Cloud — segment into bullish (Span A > B) and bearish (Span B > A) regions\nconst bullSegs = [], bearSegs = [];\nif (cloudPts.length > 1) {\n    let seg = [cloudPts[0]];\n    let wasBull = cloudPts[0].a >= cloudPts[0].b;\n    for (let i = 1; i < cloudPts.length; i++) {\n        const pt = cloudPts[i];\n        const isBull = pt.a >= pt.b;\n        if (isBull !== wasBull) {\n            const prev = cloudPts[i - 1];\n            const tCross = (prev.a - prev.b) / ((prev.a - prev.b) - (pt.a - pt.b));\n            const cv = prev.a + tCross * (pt.a - prev.a);\n            seg.push({ i: prev.i + tCross, a: cv, b: cv });\n            (wasBull ? bullSegs : bearSegs).push(seg);\n            seg = [{ i: prev.i + tCross, a: cv, b: cv }, pt];\n            wasBull = isBull;\n        } else {\n            seg.push(pt);\n        }\n    }\n    (wasBull ? bullSegs : bearSegs).push(seg);\n}\n\nconst areaGen = d3.area().x(d => xMid(d.i)).y0(d => y(d.a)).y1(d => y(d.b));\n\nfor (const seg of bullSegs) {\n    g.append(\"path\").datum(seg).attr(\"d\", areaGen)\n        .attr(\"fill\", BULL).attr(\"fill-opacity\", 0.22).attr(\"stroke\", \"none\");\n}\nfor (const seg of bearSegs) {\n    g.append(\"path\").datum(seg).attr(\"d\", areaGen)\n        .attr(\"fill\", BEAR).attr(\"fill-opacity\", 0.22).attr(\"stroke\", \"none\");\n}\n\n// Span A and Span B boundary lines (dashed, inside cloud)\nconst lineGen = d3.line().x(d => xMid(d.i)).y(d => y(d.v));\n\ng.append(\"path\")\n    .datum(cloudPts.map(d => ({ i: d.i, v: d.a })))\n    .attr(\"d\", lineGen).attr(\"fill\", \"none\")\n    .attr(\"stroke\", BULL).attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-opacity\", 0.65).attr(\"stroke-dasharray\", \"5,3\");\n\ng.append(\"path\")\n    .datum(cloudPts.map(d => ({ i: d.i, v: d.b })))\n    .attr(\"d\", lineGen).attr(\"fill\", \"none\")\n    .attr(\"stroke\", BEAR).attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-opacity\", 0.65).attr(\"stroke-dasharray\", \"5,3\");\n\n// Chikou Span — close price shifted 26 periods back\ng.append(\"path\")\n    .datum(chikou).attr(\"d\", lineGen).attr(\"fill\", \"none\")\n    .attr(\"stroke\", CHIKOU).attr(\"stroke-width\", 1.5).attr(\"stroke-opacity\", 0.85);\n\n// Candlesticks (clipped to chart area)\nconst candleG = g.append(\"g\").attr(\"clip-path\", \"url(#chartClip)\");\n\ncandleG.selectAll(\"line.wick\").data(candles).join(\"line\")\n    .attr(\"x1\", d => xMid(d.i)).attr(\"x2\", d => xMid(d.i))\n    .attr(\"y1\", d => y(d.h)).attr(\"y2\", d => y(d.l))\n    .attr(\"stroke\", d => d.c >= d.o ? BULL : BEAR)\n    .attr(\"stroke-width\", 1);\n\ncandleG.selectAll(\"rect.body\").data(candles).join(\"rect\")\n    .attr(\"x\", d => xMid(d.i) - candleW / 2)\n    .attr(\"width\", candleW)\n    .attr(\"y\", d => y(Math.max(d.o, d.c)))\n    .attr(\"height\", d => Math.max(1, Math.abs(y(d.o) - y(d.c))))\n    .attr(\"fill\", d => d.c >= d.o ? BULL : BEAR);\n\n// Tenkan-sen and Kijun-sen\ng.append(\"path\")\n    .datum(tenkanLine).attr(\"d\", lineGen).attr(\"fill\", \"none\")\n    .attr(\"stroke\", TENKAN).attr(\"stroke-width\", 1.5);\n\ng.append(\"path\")\n    .datum(kijunLine).attr(\"d\", lineGen).attr(\"fill\", \"none\")\n    .attr(\"stroke\", KIJUN).attr(\"stroke-width\", 2);\n\n// Dashed vertical line marking the forecast boundary\nconst futureX = x(N_DISPLAY);\ng.append(\"line\")\n    .attr(\"x1\", futureX).attr(\"x2\", futureX)\n    .attr(\"y1\", 0).attr(\"y2\", ih)\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-dasharray\", \"4,4\")\n    .attr(\"stroke-width\", 1).attr(\"stroke-opacity\", 0.4);\n\ng.append(\"text\")\n    .attr(\"x\", futureX + 5).attr(\"y\", 16)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\").style(\"font-style\", \"italic\")\n    .text(\"Forecast →\");\n\n// Axes\nconst xAxisG = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(\n    d3.axisBottom(x).ticks(8).tickFormat(d => `${Math.round(d)}`)\n);\nxAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nxAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAxisG = g.append(\"g\").call(\n    d3.axisLeft(y).ticks(6).tickFormat(d => `$${d.toFixed(0)}`)\n);\nyAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nyAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nyAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// Axis labels\ng.append(\"text\")\n    .attr(\"x\", iw / 2).attr(\"y\", ih + 48)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\")\n    .text(\"Trading Period\");\n\ng.append(\"text\")\n    .attr(\"transform\", `translate(${-margin.left + 18},${ih / 2}) rotate(-90)`)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\")\n    .text(\"Price ($)\");\n\n// Legend\nconst legendItems = [\n    { type: \"rect\", color: BULL,   label: \"Bullish Candle\",  alpha: 1 },\n    { type: \"rect\", color: BEAR,   label: \"Bearish Candle\",  alpha: 1 },\n    { type: \"line\", color: TENKAN, label: \"Tenkan-sen (9)\",   w: 1.5, alpha: 1,    dash: null },\n    { type: \"line\", color: KIJUN,  label: \"Kijun-sen (26)\",   w: 2,   alpha: 1,    dash: null },\n    { type: \"line\", color: CHIKOU, label: \"Chikou Span\",      w: 1.5, alpha: 0.85, dash: null },\n    { type: \"line\", color: BULL,   label: \"Span A\",           w: 1.5, alpha: 0.65, dash: \"5,3\" },\n    { type: \"line\", color: BEAR,   label: \"Span B\",           w: 1.5, alpha: 0.65, dash: \"5,3\" },\n    { type: \"fill\", color: BULL,   label: \"Bullish Cloud\",   alpha: 0.22 },\n    { type: \"fill\", color: BEAR,   label: \"Bearish Cloud\",   alpha: 0.22 },\n];\n\nconst lg = g.append(\"g\").attr(\"transform\", `translate(${iw + 14}, 8)`);\nconst LSP = 23;\n\nlegendItems.forEach((item, i) => {\n    const ly = i * LSP;\n    if (item.type === \"line\") {\n        const ln = lg.append(\"line\")\n            .attr(\"x1\", 0).attr(\"x2\", 22).attr(\"y1\", ly).attr(\"y2\", ly)\n            .attr(\"stroke\", item.color).attr(\"stroke-width\", item.w || 1.5)\n            .attr(\"stroke-opacity\", item.alpha || 1);\n        if (item.dash) ln.attr(\"stroke-dasharray\", item.dash);\n    } else {\n        lg.append(\"rect\")\n            .attr(\"x\", 0).attr(\"y\", ly - 6)\n            .attr(\"width\", 22).attr(\"height\", 12)\n            .attr(\"fill\", item.color)\n            .attr(\"fill-opacity\", item.alpha);\n    }\n    lg.append(\"text\")\n        .attr(\"x\", 28).attr(\"y\", ly + 5)\n        .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n        .text(item.label);\n});\n\n// Title\nsvg.append(\"text\")\n    .attr(\"x\", margin.left + iw / 2).attr(\"y\", 50)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n    .text(\"indicator-ichimoku · javascript · d3 · anyplot.ai\");\n"}