{"spec_id":"indicator-ichimoku","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// indicator-ichimoku: Ichimoku Cloud Technical Indicator Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-20\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: synthetic daily OHLC for a fictional robotics stock -------------\n// The core Highcharts bundle has no candlestick series (that lives in the\n// Highstock/highcharts-more modules, which aren't loaded), so candles and the\n// Kumo cloud fill are drawn cell-by-cell with the SVG renderer instead — the\n// same technique used for the heatmap grid in heatmap-basic. The five\n// Ichimoku lines (Tenkan/Kijun/Senkou A/Senkou B/Chikou) are real Highcharts\n// `line` series, so hovering them still shows a native tooltip.\nconst TENKAN_PERIOD = 9;\nconst KIJUN_PERIOD = 26;\nconst SENKOU_B_PERIOD = 52;\nconst CLOUD_SHIFT = 26;\n\nconst N = 180; // visible trading candles\n\n// Deterministic LCG — the browser has no seeded RNG.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction gaussian() {\n  const u1 = Math.max(rand(), 1e-6);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst BASE_PRICE = 182;\nconst closes = [];\nlet price = BASE_PRICE;\nfor (let i = 0; i < N; i++) {\n  // Two overlaid cycles give the price a slow bullish/bearish rhythm so the\n  // cloud actually flips color a few times over the window, instead of\n  // drifting in one direction the whole way.\n  const cyclical = 0.35 * Math.sin(i / 26) + 0.15 * Math.sin(i / 9 + 1.3);\n  const drift = 0.03 + cyclical * 0.05;\n  price = Math.max(20, price + drift + gaussian() * 1.1);\n  closes.push(price);\n}\n\nconst opens = [];\nconst highs = [];\nconst lows = [];\nfor (let i = 0; i < N; i++) {\n  const prevClose = i === 0 ? BASE_PRICE : closes[i - 1];\n  const open = prevClose + gaussian() * 0.4;\n  const range = Math.abs(gaussian()) * 1.6 + 0.6;\n  opens.push(open);\n  highs.push(Math.max(open, closes[i]) + range * 0.5 * rand());\n  lows.push(Math.min(open, closes[i]) - range * 0.5 * rand());\n}\n\nfunction rollingMidpoint(period, i) {\n  const start = i - period + 1;\n  if (start < 0) return null;\n  let hi = -Infinity;\n  let lo = Infinity;\n  for (let k = start; k <= i; k++) {\n    if (highs[k] > hi) hi = highs[k];\n    if (lows[k] < lo) lo = lows[k];\n  }\n  return (hi + lo) / 2;\n}\n\nconst tenkan = closes.map((_, i) => rollingMidpoint(TENKAN_PERIOD, i));\nconst kijun = closes.map((_, i) => rollingMidpoint(KIJUN_PERIOD, i));\nconst senkouB = closes.map((_, i) => rollingMidpoint(SENKOU_B_PERIOD, i));\nconst senkouA = closes.map((_, i) => (tenkan[i] != null && kijun[i] != null ? (tenkan[i] + kijun[i]) / 2 : null));\n\n// Most recent Tenkan/Kijun crossover — the classic Ichimoku entry/exit\n// signal — gives the chart a specific focal callout beyond the cloud's\n// general bullish/bearish tint.\nfunction findLastCrossover() {\n  let result = null;\n  for (let i = TENKAN_PERIOD; i < N - 1; i++) {\n    if (tenkan[i] == null || kijun[i] == null || tenkan[i + 1] == null || kijun[i + 1] == null) continue;\n    const d0 = tenkan[i] - kijun[i];\n    const d1 = tenkan[i + 1] - kijun[i + 1];\n    if (d0 === 0) continue;\n    if ((d0 < 0 && d1 >= 0) || (d0 > 0 && d1 <= 0)) result = { i: i + 1, bullish: d1 >= 0 };\n  }\n  return result;\n}\nconst LAST_CROSS = findLastCrossover();\n\n// Chronological x-index: candles sit at i + CLOUD_SHIFT so the chikou span\n// (i - CLOUD_SHIFT) and the leading spans (i + 2*CLOUD_SHIFT) never go\n// negative on the axis, while the spacing between them still reflects the\n// real 26-period shift.\nconst candleX = (i) => i + CLOUD_SHIFT;\nconst chikouX = (i) => i;\nconst senkouX = (i) => i + 2 * CLOUD_SHIFT;\nconst TOTAL_X = senkouX(N - 1);\n\n// --- Trading-day calendar for axis labels -----------------------------------\nconst START_DATE = new Date(2024, 0, 2);\nfunction addTradingDays(date, n) {\n  const d = new Date(date);\n  let added = 0;\n  while (added < n) {\n    d.setDate(d.getDate() + 1);\n    const day = d.getDay();\n    if (day !== 0 && day !== 6) added++;\n  }\n  return d;\n}\nconst DATE_LABELS = [];\nfor (let x = 0; x <= TOTAL_X; x++) {\n  const d = addTradingDays(START_DATE, x);\n  DATE_LABELS.push(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));\n}\n\n// --- Colors ------------------------------------------------------------------\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r},${g},${b},${alpha})`;\n}\n// Finance semantic exception: profit/up -> brand green, loss/down -> matte\n// red (Imprint slot 5, the deferred semantic-red anchor for loss/error).\nconst CANDLE_UP = t.palette[0]; // #009E73\nconst CANDLE_DOWN = t.palette[4]; // #AE3030\nconst CLOUD_BULL = hexToRgba(t.palette[0], 0.22);\nconst CLOUD_BEAR = hexToRgba(t.palette[4], 0.22);\n\n// Ichimoku line series follow the canonical Imprint order, skipping slot 4\n// (red) since it is already spoken for by the bearish candle/cloud semantics.\nconst TENKAN_COLOR = t.palette[0]; // #009E73\nconst KIJUN_COLOR = t.palette[1]; // #C475FD\nconst SENKOU_A_COLOR = t.palette[2]; // #4467A3\nconst SENKOU_B_COLOR = t.palette[3]; // #BD8233\nconst CHIKOU_COLOR = t.palette[5]; // #2ABCCD\n\n// --- Title (fontsize scaled off the 67-char baseline) -----------------------\nconst TITLE_TEXT = 'Solara Robotics (SLRA) · indicator-ichimoku · javascript · highcharts · anyplot.ai';\nconst TITLE_FS = Math.max(Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)), 14);\n\n// --- Manual rendering: candlesticks + Kumo cloud fill ------------------------\n// Neither series type ships in the core bundle (see header note), so both are\n// drawn straight into chart pixel space via the axes' own toPixels() — real\n// Highcharts geometry, just not a registered series type.\nconst drawn = [];\nfunction clearDrawn() {\n  drawn.forEach((el) => {\n    try {\n      el.destroy();\n    } catch (_err) {\n      // already removed\n    }\n  });\n  drawn.length = 0;\n}\n\nfunction drawAll() {\n  const chart = this;\n  clearDrawn();\n  const r = chart.renderer;\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n\n  // Kumo cloud: one filled quad per candle-to-candle segment, colored by\n  // which span leads at that segment (bullish = Senkou A above Senkou B).\n  for (let i = TENKAN_PERIOD; i < N - 1; i++) {\n    const a1 = senkouA[i];\n    const b1 = senkouB[i];\n    const a2 = senkouA[i + 1];\n    const b2 = senkouB[i + 1];\n    if (a1 == null || b1 == null || a2 == null || b2 == null) continue;\n    const x1 = xAxis.toPixels(senkouX(i), false);\n    const x2 = xAxis.toPixels(senkouX(i + 1), false);\n    const ay1 = yAxis.toPixels(a1, false);\n    const by1 = yAxis.toPixels(b1, false);\n    const ay2 = yAxis.toPixels(a2, false);\n    const by2 = yAxis.toPixels(b2, false);\n    const bullish = a1 + a2 >= b1 + b2;\n    drawn.push(\n      r\n        .path(['M', x1, ay1, 'L', x2, ay2, 'L', x2, by2, 'L', x1, by1, 'Z'])\n        .attr({ fill: bullish ? CLOUD_BULL : CLOUD_BEAR, zIndex: 2 })\n        .add()\n    );\n  }\n\n  // Candlesticks — conventional green (close >= open) / red (close < open).\n  const pxPerUnit = Math.abs(xAxis.toPixels(1, false) - xAxis.toPixels(0, false));\n  const candleWidth = Math.max(pxPerUnit * 0.62, 2);\n  for (let i = 0; i < N; i++) {\n    const x = xAxis.toPixels(candleX(i), false);\n    const yOpen = yAxis.toPixels(opens[i], false);\n    const yClose = yAxis.toPixels(closes[i], false);\n    const yHigh = yAxis.toPixels(highs[i], false);\n    const yLow = yAxis.toPixels(lows[i], false);\n    const color = closes[i] >= opens[i] ? CANDLE_UP : CANDLE_DOWN;\n    drawn.push(r.path(['M', x, yHigh, 'L', x, yLow]).attr({ stroke: color, 'stroke-width': 1.3, zIndex: 3 }).add());\n    const bodyTop = Math.min(yOpen, yClose);\n    const bodyHeight = Math.max(Math.abs(yClose - yOpen), 1.2);\n    drawn.push(\n      r\n        .rect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight)\n        .attr({ fill: color, zIndex: 4 })\n        .add()\n    );\n  }\n\n  // Callout: dot + leader line + label on the most recent Tenkan/Kijun cross.\n  if (LAST_CROSS) {\n    const cx = xAxis.toPixels(candleX(LAST_CROSS.i), false);\n    const cy = yAxis.toPixels(kijun[LAST_CROSS.i], false);\n    const dotColor = LAST_CROSS.bullish ? CANDLE_UP : CANDLE_DOWN;\n    const label = LAST_CROSS.bullish ? 'Bullish cross' : 'Bearish cross';\n    const minLabelY = chart.plotTop + 16;\n    const labelY = Math.max(cy - 46, minLabelY);\n    drawn.push(r.path(['M', cx, cy - 6, 'L', cx, labelY + 14]).attr({ stroke: dotColor, 'stroke-width': 1.2, zIndex: 6 }).add());\n    drawn.push(\n      r.circle(cx, cy, 5).attr({ fill: dotColor, stroke: t.elevatedBg, 'stroke-width': 1.5, zIndex: 7 }).add()\n    );\n    const text = r\n      .text(label, cx, labelY)\n      .attr({ align: 'center', zIndex: 9 })\n      .css({ color: t.ink, fontSize: '12px', fontWeight: '600' })\n      .add();\n    const bbox = text.getBBox();\n    drawn.push(\n      r\n        .rect(bbox.x - 6, bbox.y - 4, bbox.width + 12, bbox.height + 6, 4)\n        .attr({ fill: t.elevatedBg, stroke: dotColor, 'stroke-width': 1, zIndex: 8 })\n        .add()\n    );\n    drawn.push(text);\n  }\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart('container', {\n  chart: {\n    backgroundColor: 'transparent',\n    animation: false,\n    style: { fontFamily: 'inherit' },\n    events: { load: drawAll, redraw: drawAll },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: TITLE_TEXT,\n    style: { color: t.ink, fontSize: TITLE_FS + 'px', fontWeight: '600' },\n  },\n  subtitle: {\n    text: 'Simulated daily OHLC price with the Ichimoku Kinko Hyo indicator (9, 26, 52 periods)',\n    style: { color: t.inkSoft, fontSize: '14px' },\n  },\n  xAxis: {\n    categories: DATE_LABELS,\n    min: 0,\n    max: TOTAL_X,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: '13px' }, step: 20, rotation: -35 },\n    title: { text: 'Trading Date', style: { color: t.inkSoft, fontSize: '16px' } },\n  },\n  yAxis: {\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: '14px' }, format: '${value:.0f}' },\n    title: { text: 'Price (USD)', style: { color: t.inkSoft, fontSize: '16px' } },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: '14px' },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    style: { color: t.ink, fontSize: '13px' },\n    valueDecimals: 2,\n    valuePrefix: '$',\n  },\n  plotOptions: {\n    series: { animation: false, marker: { enabled: false } },\n  },\n  series: [\n    {\n      type: 'line',\n      name: 'Up Candle',\n      data: [],\n      color: CANDLE_UP,\n      lineWidth: 6,\n      showInLegend: true,\n      enableMouseTracking: false,\n    },\n    {\n      type: 'line',\n      name: 'Down Candle',\n      data: [],\n      color: CANDLE_DOWN,\n      lineWidth: 6,\n      showInLegend: true,\n      enableMouseTracking: false,\n    },\n    {\n      type: 'line',\n      name: 'Tenkan-sen',\n      data: tenkan.map((y, i) => (y == null ? null : [candleX(i), y])).filter(Boolean),\n      color: TENKAN_COLOR,\n      lineWidth: 2.2,\n      zIndex: 5,\n    },\n    {\n      type: 'line',\n      name: 'Kijun-sen',\n      data: kijun.map((y, i) => (y == null ? null : [candleX(i), y])).filter(Boolean),\n      color: KIJUN_COLOR,\n      lineWidth: 2.2,\n      zIndex: 5,\n    },\n    {\n      type: 'line',\n      name: 'Senkou Span A',\n      data: senkouA.map((y, i) => (y == null ? null : [senkouX(i), y])).filter(Boolean),\n      color: SENKOU_A_COLOR,\n      lineWidth: 1.6,\n      zIndex: 5,\n    },\n    {\n      type: 'line',\n      name: 'Senkou Span B',\n      data: senkouB.map((y, i) => (y == null ? null : [senkouX(i), y])).filter(Boolean),\n      color: SENKOU_B_COLOR,\n      lineWidth: 1.6,\n      zIndex: 5,\n    },\n    {\n      type: 'line',\n      name: 'Chikou Span',\n      data: closes.map((y, i) => [chikouX(i), y]),\n      color: CHIKOU_COLOR,\n      lineWidth: 1.8,\n      dashStyle: 'ShortDash',\n      zIndex: 5,\n    },\n  ],\n});\n"}