{"spec_id":"candlestick-volume","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// candlestick-volume: Stock Candlestick Chart with Volume\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n\n// The core bundle (no highcharts-more / modules/stock) has no \"candlestick\"\n// series — that type ships in modules/stock.js, which isn't loaded. Each\n// candle is built from an invisible \"Floor\" column stacked under a\n// colorByPoint \"Body\" column for the open-close range, with per-direction\n// \"line\" series (null-separated, one pair per candle) for the high-low wicks\n// — pure core series types, no add-on module. The volume pane is a second\n// yAxis (top/height split of the same plot area) sharing the chart's single\n// xAxis, so the built-in crosshair and vertical gridlines span both panes\n// automatically — no cross-pane sync code needed.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic mulberry32 PRNG) ------------------------\nfunction mulberry32(seed) {\n  return function () {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let z = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;\n    return ((z ^ (z >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(20240601);\n\nconst upColor = t.palette[0]; // #009E73 brand green — bullish (profit/up)\nconst downColor = t.palette[4]; // #AE3030 matte red — bearish (loss/down), finance semantic anchor\n\nconst dayMs = 24 * 3600 * 1000;\nconst numDays = 45; // continuous daily bars — crypto trades every calendar day\nconst startTime = Date.UTC(2024, 5, 1); // Sat 1 Jun 2024\nconst dates = Array.from({ length: numDays }, (_, i) => startTime + i * dayMs);\n\nlet price = 3200; // ETH-style token price in USD\nconst candles = dates.map((time) => {\n  const open = price;\n  const drift = (rand() - 0.5) * 90;\n  const close = Math.max(50, open + drift);\n  const swing = Math.abs(close - open) + rand() * 40 + 12;\n  const high = Math.max(open, close) + rand() * swing * 0.4;\n  const low = Math.max(10, Math.min(open, close) - rand() * swing * 0.4);\n  const bullish = close >= open;\n  const volume = Math.round(420000 + Math.abs(close - open) * 9000 + rand() * 160000);\n  price = close;\n  return {\n    time,\n    open: +open.toFixed(2),\n    high: +high.toFixed(2),\n    low: +low.toFixed(2),\n    close: +close.toFixed(2),\n    volume,\n    bullish,\n  };\n});\n\nconst candleWidth = 9;\n\nconst floorData = candles.map((c) => ({ x: c.time, y: +Math.min(c.open, c.close).toFixed(2) }));\nconst bodyData = candles.map((c) => ({\n  x: c.time,\n  y: +Math.abs(c.close - c.open).toFixed(2),\n  color: c.bullish ? upColor : downColor,\n  custom: c,\n}));\nconst wickUpData = candles\n  .filter((c) => c.bullish)\n  .flatMap((c) => [{ x: c.time, y: c.low }, { x: c.time, y: c.high }, { x: c.time, y: null }]);\nconst wickDownData = candles\n  .filter((c) => !c.bullish)\n  .flatMap((c) => [{ x: c.time, y: c.low }, { x: c.time, y: c.high }, { x: c.time, y: null }]);\nconst volumeData = candles.map((c) => ({ x: c.time, y: c.volume, color: c.bullish ? upColor : downColor }));\n\nconst allLows = candles.map((c) => c.low);\nconst allHighs = candles.map((c) => c.high);\nconst pricePad = (Math.max(...allHighs) - Math.min(...allLows)) * 0.08;\nconst priceMin = Math.floor(Math.min(...allLows) - pricePad);\nconst priceMax = Math.ceil(Math.max(...allHighs) + pricePad);\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"column\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"candlestick-volume · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    type: \"datetime\",\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    crosshair: { color: t.inkSoft, dashStyle: \"Dash\", width: 1 },\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: { text: \"Trading Date\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n  },\n  yAxis: [\n    {\n      // Price pane — top 68% of the plot area\n      top: \"0%\",\n      height: \"68%\",\n      min: priceMin,\n      max: priceMax,\n      reversedStacks: false, // keep the invisible \"Floor\" series at the bottom of the stack\n      title: { text: \"Price (USD)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n      gridLineColor: t.grid,\n      lineColor: t.inkSoft,\n      labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    },\n    {\n      // Volume pane — bottom 27%, a 5% gap separates it from the price pane\n      top: \"73%\",\n      height: \"27%\",\n      offset: 0,\n      min: 0,\n      title: { text: \"Volume\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n      gridLineColor: t.grid,\n      lineColor: t.inkSoft,\n      labels: {\n        style: { color: t.inkSoft, fontSize: \"14px\" },\n        formatter() {\n          return this.value >= 1000000\n            ? Highcharts.numberFormat(this.value / 1000000, 1).replace(/\\.0$/, \"\") + \"M\"\n            : Highcharts.numberFormat(this.value / 1000, 0) + \"k\";\n        },\n      },\n    },\n  ],\n  legend: {\n    enabled: true,\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n    symbolRadius: 6,\n    itemDistance: 24,\n    padding: 12,\n  },\n  tooltip: {\n    shared: true,\n    outside: false,\n    formatter: function () {\n      const bodyPoint = this.points && this.points.find((p) => p.series.name === \"Body\");\n      if (!bodyPoint) return false;\n      const c = bodyPoint.point.custom;\n      const volumePoint = this.points.find((p) => p.series.name === \"Volume\");\n      let html =\n        `<b>${Highcharts.dateFormat(\"%b %e, %Y\", c.time)}</b><br/>` +\n        `Open: ${c.open.toFixed(2)}<br/>High: ${c.high.toFixed(2)}<br/>` +\n        `Low: ${c.low.toFixed(2)}<br/>Close: ${c.close.toFixed(2)}`;\n      if (volumePoint) html += `<br/>Volume: ${Highcharts.numberFormat(volumePoint.y, 0)}`;\n      return html;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    column: { borderRadius: 0, animation: false },\n  },\n  series: [\n    {\n      name: \"Floor\",\n      data: floorData,\n      yAxis: 0,\n      stacking: \"normal\",\n      stack: \"price\",\n      pointWidth: candleWidth,\n      color: \"transparent\",\n      borderWidth: 0,\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"line\",\n      name: \"Wicks (up)\",\n      data: wickUpData,\n      yAxis: 0,\n      color: upColor,\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      type: \"line\",\n      name: \"Wicks (down)\",\n      data: wickDownData,\n      yAxis: 0,\n      color: downColor,\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"Body\",\n      data: bodyData,\n      yAxis: 0,\n      stacking: \"normal\",\n      stack: \"price\",\n      pointWidth: candleWidth,\n      borderColor: t.pageBg,\n      borderWidth: 1,\n      showInLegend: false,\n    },\n    {\n      name: \"Volume\",\n      data: volumeData,\n      yAxis: 1,\n      pointWidth: candleWidth,\n      borderWidth: 0,\n      showInLegend: false,\n    },\n    {\n      name: \"Bullish (Close ≥ Open)\",\n      data: [],\n      color: upColor,\n      showInLegend: true,\n    },\n    {\n      name: \"Bearish (Close < Open)\",\n      data: [],\n      color: downColor,\n      showInLegend: true,\n    },\n  ],\n});\n"}