Ichimoku Cloud Technical Indicator Chart — Highcharts

Displays the Ichimoku Kinko Hyo ("one glance equilibrium chart") overlay on a candlestick price chart. The indicator plots five lines — Tenkan-sen (conversion), Kijun-sen (base), Senkou Span A, Senkou Span B, and Chikou Span (lagging) — with the area between Senkou Span A and B filled as the "Kumo" (cloud). The cloud color changes depending on which span is on top, providing an at-a-glance view of trend direction, momentum, and support/resistance zones.

Ichimoku Cloud Technical Indicator Chart rendered with Highcharts

Renders

JavaScript source (Highcharts)

// anyplot.ai
// indicator-ichimoku: Ichimoku Cloud Technical Indicator Chart
// Library: highcharts 12.6.0 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-08-20
//# anyplot-orientation: landscape

const t = window.ANYPLOT_TOKENS;

// --- Data: synthetic daily OHLC for a fictional robotics stock -------------
// The core Highcharts bundle has no candlestick series (that lives in the
// Highstock/highcharts-more modules, which aren't loaded), so candles and the
// Kumo cloud fill are drawn cell-by-cell with the SVG renderer instead — the
// same technique used for the heatmap grid in heatmap-basic. The five
// Ichimoku lines (Tenkan/Kijun/Senkou A/Senkou B/Chikou) are real Highcharts
// `line` series, so hovering them still shows a native tooltip.
const TENKAN_PERIOD = 9;
const KIJUN_PERIOD = 26;
const SENKOU_B_PERIOD = 52;
const CLOUD_SHIFT = 26;

const N = 180; // visible trading candles

// Deterministic LCG — the browser has no seeded RNG.
let seed = 42;
function rand() {
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}
function gaussian() {
  const u1 = Math.max(rand(), 1e-6);
  const u2 = rand();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

const BASE_PRICE = 182;
const closes = [];
let price = BASE_PRICE;
for (let i = 0; i < N; i++) {
  // Two overlaid cycles give the price a slow bullish/bearish rhythm so the
  // cloud actually flips color a few times over the window, instead of
  // drifting in one direction the whole way.
  const cyclical = 0.35 * Math.sin(i / 26) + 0.15 * Math.sin(i / 9 + 1.3);
  const drift = 0.03 + cyclical * 0.05;
  price = Math.max(20, price + drift + gaussian() * 1.1);
  closes.push(price);
}

const opens = [];
const highs = [];
const lows = [];
for (let i = 0; i < N; i++) {
  const prevClose = i === 0 ? BASE_PRICE : closes[i - 1];
  const open = prevClose + gaussian() * 0.4;
  const range = Math.abs(gaussian()) * 1.6 + 0.6;
  opens.push(open);
  highs.push(Math.max(open, closes[i]) + range * 0.5 * rand());
  lows.push(Math.min(open, closes[i]) - range * 0.5 * rand());
}

function rollingMidpoint(period, i) {
  const start = i - period + 1;
  if (start < 0) return null;
  let hi = -Infinity;
  let lo = Infinity;
  for (let k = start; k <= i; k++) {
    if (highs[k] > hi) hi = highs[k];
    if (lows[k] < lo) lo = lows[k];
  }
  return (hi + lo) / 2;
}

const tenkan = closes.map((_, i) => rollingMidpoint(TENKAN_PERIOD, i));
const kijun = closes.map((_, i) => rollingMidpoint(KIJUN_PERIOD, i));
const senkouB = closes.map((_, i) => rollingMidpoint(SENKOU_B_PERIOD, i));
const senkouA = closes.map((_, i) => (tenkan[i] != null && kijun[i] != null ? (tenkan[i] + kijun[i]) / 2 : null));

// Most recent Tenkan/Kijun crossover — the classic Ichimoku entry/exit
// signal — gives the chart a specific focal callout beyond the cloud's
// general bullish/bearish tint.
function findLastCrossover() {
  let result = null;
  for (let i = TENKAN_PERIOD; i < N - 1; i++) {
    if (tenkan[i] == null || kijun[i] == null || tenkan[i + 1] == null || kijun[i + 1] == null) continue;
    const d0 = tenkan[i] - kijun[i];
    const d1 = tenkan[i + 1] - kijun[i + 1];
    if (d0 === 0) continue;
    if ((d0 < 0 && d1 >= 0) || (d0 > 0 && d1 <= 0)) result = { i: i + 1, bullish: d1 >= 0 };
  }
  return result;
}
const LAST_CROSS = findLastCrossover();

// Chronological x-index: candles sit at i + CLOUD_SHIFT so the chikou span
// (i - CLOUD_SHIFT) and the leading spans (i + 2*CLOUD_SHIFT) never go
// negative on the axis, while the spacing between them still reflects the
// real 26-period shift.
const candleX = (i) => i + CLOUD_SHIFT;
const chikouX = (i) => i;
const senkouX = (i) => i + 2 * CLOUD_SHIFT;
const TOTAL_X = senkouX(N - 1);

// --- Trading-day calendar for axis labels -----------------------------------
const START_DATE = new Date(2024, 0, 2);
function addTradingDays(date, n) {
  const d = new Date(date);
  let added = 0;
  while (added < n) {
    d.setDate(d.getDate() + 1);
    const day = d.getDay();
    if (day !== 0 && day !== 6) added++;
  }
  return d;
}
const DATE_LABELS = [];
for (let x = 0; x <= TOTAL_X; x++) {
  const d = addTradingDays(START_DATE, x);
  DATE_LABELS.push(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
}

// --- Colors ------------------------------------------------------------------
function hexToRgba(hex, alpha) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r},${g},${b},${alpha})`;
}
// Finance semantic exception: profit/up -> brand green, loss/down -> matte
// red (Imprint slot 5, the deferred semantic-red anchor for loss/error).
const CANDLE_UP = t.palette[0]; // #009E73
const CANDLE_DOWN = t.palette[4]; // #AE3030
const CLOUD_BULL = hexToRgba(t.palette[0], 0.22);
const CLOUD_BEAR = hexToRgba(t.palette[4], 0.22);

// Ichimoku line series follow the canonical Imprint order, skipping slot 4
// (red) since it is already spoken for by the bearish candle/cloud semantics.
const TENKAN_COLOR = t.palette[0]; // #009E73
const KIJUN_COLOR = t.palette[1]; // #C475FD
const SENKOU_A_COLOR = t.palette[2]; // #4467A3
const SENKOU_B_COLOR = t.palette[3]; // #BD8233
const CHIKOU_COLOR = t.palette[5]; // #2ABCCD

// --- Title (fontsize scaled off the 67-char baseline) -----------------------
const TITLE_TEXT = 'Solara Robotics (SLRA) · indicator-ichimoku · javascript · highcharts · anyplot.ai';
const TITLE_FS = Math.max(Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)), 14);

// --- Manual rendering: candlesticks + Kumo cloud fill ------------------------
// Neither series type ships in the core bundle (see header note), so both are
// drawn straight into chart pixel space via the axes' own toPixels() — real
// Highcharts geometry, just not a registered series type.
const drawn = [];
function clearDrawn() {
  drawn.forEach((el) => {
    try {
      el.destroy();
    } catch (_err) {
      // already removed
    }
  });
  drawn.length = 0;
}

function drawAll() {
  const chart = this;
  clearDrawn();
  const r = chart.renderer;
  const xAxis = chart.xAxis[0];
  const yAxis = chart.yAxis[0];

  // Kumo cloud: one filled quad per candle-to-candle segment, colored by
  // which span leads at that segment (bullish = Senkou A above Senkou B).
  for (let i = TENKAN_PERIOD; i < N - 1; i++) {
    const a1 = senkouA[i];
    const b1 = senkouB[i];
    const a2 = senkouA[i + 1];
    const b2 = senkouB[i + 1];
    if (a1 == null || b1 == null || a2 == null || b2 == null) continue;
    const x1 = xAxis.toPixels(senkouX(i), false);
    const x2 = xAxis.toPixels(senkouX(i + 1), false);
    const ay1 = yAxis.toPixels(a1, false);
    const by1 = yAxis.toPixels(b1, false);
    const ay2 = yAxis.toPixels(a2, false);
    const by2 = yAxis.toPixels(b2, false);
    const bullish = a1 + a2 >= b1 + b2;
    drawn.push(
      r
        .path(['M', x1, ay1, 'L', x2, ay2, 'L', x2, by2, 'L', x1, by1, 'Z'])
        .attr({ fill: bullish ? CLOUD_BULL : CLOUD_BEAR, zIndex: 2 })
        .add()
    );
  }

  // Candlesticks — conventional green (close >= open) / red (close < open).
  const pxPerUnit = Math.abs(xAxis.toPixels(1, false) - xAxis.toPixels(0, false));
  const candleWidth = Math.max(pxPerUnit * 0.62, 2);
  for (let i = 0; i < N; i++) {
    const x = xAxis.toPixels(candleX(i), false);
    const yOpen = yAxis.toPixels(opens[i], false);
    const yClose = yAxis.toPixels(closes[i], false);
    const yHigh = yAxis.toPixels(highs[i], false);
    const yLow = yAxis.toPixels(lows[i], false);
    const color = closes[i] >= opens[i] ? CANDLE_UP : CANDLE_DOWN;
    drawn.push(r.path(['M', x, yHigh, 'L', x, yLow]).attr({ stroke: color, 'stroke-width': 1.3, zIndex: 3 }).add());
    const bodyTop = Math.min(yOpen, yClose);
    const bodyHeight = Math.max(Math.abs(yClose - yOpen), 1.2);
    drawn.push(
      r
        .rect(x - candleWidth / 2, bodyTop, candleWidth, bodyHeight)
        .attr({ fill: color, zIndex: 4 })
        .add()
    );
  }

  // Callout: dot + leader line + label on the most recent Tenkan/Kijun cross.
  if (LAST_CROSS) {
    const cx = xAxis.toPixels(candleX(LAST_CROSS.i), false);
    const cy = yAxis.toPixels(kijun[LAST_CROSS.i], false);
    const dotColor = LAST_CROSS.bullish ? CANDLE_UP : CANDLE_DOWN;
    const label = LAST_CROSS.bullish ? 'Bullish cross' : 'Bearish cross';
    const minLabelY = chart.plotTop + 16;
    const labelY = Math.max(cy - 46, minLabelY);
    drawn.push(r.path(['M', cx, cy - 6, 'L', cx, labelY + 14]).attr({ stroke: dotColor, 'stroke-width': 1.2, zIndex: 6 }).add());
    drawn.push(
      r.circle(cx, cy, 5).attr({ fill: dotColor, stroke: t.elevatedBg, 'stroke-width': 1.5, zIndex: 7 }).add()
    );
    const text = r
      .text(label, cx, labelY)
      .attr({ align: 'center', zIndex: 9 })
      .css({ color: t.ink, fontSize: '12px', fontWeight: '600' })
      .add();
    const bbox = text.getBBox();
    drawn.push(
      r
        .rect(bbox.x - 6, bbox.y - 4, bbox.width + 12, bbox.height + 6, 4)
        .attr({ fill: t.elevatedBg, stroke: dotColor, 'stroke-width': 1, zIndex: 8 })
        .add()
    );
    drawn.push(text);
  }
}

// --- Chart -------------------------------------------------------------------
Highcharts.chart('container', {
  chart: {
    backgroundColor: 'transparent',
    animation: false,
    style: { fontFamily: 'inherit' },
    events: { load: drawAll, redraw: drawAll },
  },
  credits: { enabled: false },
  colors: t.palette,
  title: {
    text: TITLE_TEXT,
    style: { color: t.ink, fontSize: TITLE_FS + 'px', fontWeight: '600' },
  },
  subtitle: {
    text: 'Simulated daily OHLC price with the Ichimoku Kinko Hyo indicator (9, 26, 52 periods)',
    style: { color: t.inkSoft, fontSize: '14px' },
  },
  xAxis: {
    categories: DATE_LABELS,
    min: 0,
    max: TOTAL_X,
    lineColor: t.inkSoft,
    tickColor: t.inkSoft,
    gridLineWidth: 0,
    labels: { style: { color: t.inkSoft, fontSize: '13px' }, step: 20, rotation: -35 },
    title: { text: 'Trading Date', style: { color: t.inkSoft, fontSize: '16px' } },
  },
  yAxis: {
    lineColor: t.inkSoft,
    tickColor: t.inkSoft,
    gridLineColor: t.grid,
    labels: { style: { color: t.inkSoft, fontSize: '14px' }, format: '${value:.0f}' },
    title: { text: 'Price (USD)', style: { color: t.inkSoft, fontSize: '16px' } },
  },
  legend: {
    itemStyle: { color: t.inkSoft, fontSize: '14px' },
    itemHoverStyle: { color: t.ink },
  },
  tooltip: {
    backgroundColor: t.elevatedBg,
    borderColor: t.inkSoft,
    style: { color: t.ink, fontSize: '13px' },
    valueDecimals: 2,
    valuePrefix: '$',
  },
  plotOptions: {
    series: { animation: false, marker: { enabled: false } },
  },
  series: [
    {
      type: 'line',
      name: 'Up Candle',
      data: [],
      color: CANDLE_UP,
      lineWidth: 6,
      showInLegend: true,
      enableMouseTracking: false,
    },
    {
      type: 'line',
      name: 'Down Candle',
      data: [],
      color: CANDLE_DOWN,
      lineWidth: 6,
      showInLegend: true,
      enableMouseTracking: false,
    },
    {
      type: 'line',
      name: 'Tenkan-sen',
      data: tenkan.map((y, i) => (y == null ? null : [candleX(i), y])).filter(Boolean),
      color: TENKAN_COLOR,
      lineWidth: 2.2,
      zIndex: 5,
    },
    {
      type: 'line',
      name: 'Kijun-sen',
      data: kijun.map((y, i) => (y == null ? null : [candleX(i), y])).filter(Boolean),
      color: KIJUN_COLOR,
      lineWidth: 2.2,
      zIndex: 5,
    },
    {
      type: 'line',
      name: 'Senkou Span A',
      data: senkouA.map((y, i) => (y == null ? null : [senkouX(i), y])).filter(Boolean),
      color: SENKOU_A_COLOR,
      lineWidth: 1.6,
      zIndex: 5,
    },
    {
      type: 'line',
      name: 'Senkou Span B',
      data: senkouB.map((y, i) => (y == null ? null : [senkouX(i), y])).filter(Boolean),
      color: SENKOU_B_COLOR,
      lineWidth: 1.6,
      zIndex: 5,
    },
    {
      type: 'line',
      name: 'Chikou Span',
      data: closes.map((y, i) => [chikouX(i), y]),
      color: CHIKOU_COLOR,
      lineWidth: 1.8,
      dashStyle: 'ShortDash',
      zIndex: 5,
    },
  ],
});

Part of Ichimoku Cloud Technical Indicator Chart on anyplot.ai.

Other implementations