{"spec_id":"ohlc-bar","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// ohlc-bar: OHLC Bar Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny fixed-seed LCG — the browser has no seeded RNG.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(7);\n\nconst sessionCount = 45;\nconst labels = [];\nconst opens = [];\nconst highs = [];\nconst lows = [];\nconst closes = [];\nconst barColors = [];\n\nlet price = 62; // crude oil futures, USD per barrel\nconst date = new Date(Date.UTC(2024, 2, 1));\nfor (let i = 0; i < sessionCount; i++) {\n  while (date.getUTCDay() === 0 || date.getUTCDay() === 6) {\n    date.setUTCDate(date.getUTCDate() + 1);\n  }\n\n  const open = price;\n  const drift = (rand() - 0.48) * 1.6;\n  const close = Math.max(open + drift, 1);\n  const high = Math.max(open, close) + rand() * 0.9;\n  const low = Math.min(open, close) - rand() * 0.9;\n  const isUp = close >= open;\n\n  labels.push(date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" }));\n  opens.push(open);\n  highs.push(high);\n  lows.push(low);\n  closes.push(close);\n  barColors.push(isUp ? t.palette[0] : t.palette[4]);\n\n  price = close;\n  date.setUTCDate(date.getUTCDate() + 1);\n}\n\nconst priceMin = Math.min(...lows);\nconst priceMax = Math.max(...highs);\nconst pricePad = (priceMax - priceMin) * 0.12;\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Open/close tick + storytelling plugin -------------------------------------\n// Draws the left (open) and right (close) horizontal ticks per bar, a small\n// direction glyph (redundant to color, for CVD readers), and dashed period\n// high/low reference lines — all directly on the canvas via core Chart.js\n// plugin hooks. No external financial plugin.\nconst tickHalfWidth = 8;\nconst ohlcTicksPlugin = {\n  id: \"ohlcTicks\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const highY = scales.y.getPixelForValue(priceMax);\n    const lowY = scales.y.getPixelForValue(priceMin);\n\n    ctx.save();\n    ctx.lineWidth = 1;\n    ctx.strokeStyle = t.inkSoft;\n    ctx.setLineDash([5, 4]);\n    ctx.beginPath();\n    ctx.moveTo(chartArea.left, highY);\n    ctx.lineTo(chartArea.right, highY);\n    ctx.stroke();\n    ctx.beginPath();\n    ctx.moveTo(chartArea.left, lowY);\n    ctx.lineTo(chartArea.right, lowY);\n    ctx.stroke();\n    ctx.setLineDash([]);\n\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"13px sans-serif\";\n    ctx.textAlign = \"right\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(`Period high: $${priceMax.toFixed(2)}`, chartArea.right - 6, highY - 4);\n    ctx.textBaseline = \"top\";\n    ctx.fillText(`Period low: $${priceMin.toFixed(2)}`, chartArea.right - 6, lowY + 4);\n    ctx.restore();\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    ctx.lineWidth = 3;\n    for (let i = 0; i < sessionCount; i++) {\n      const x = scales.x.getPixelForValue(i);\n      const openY = scales.y.getPixelForValue(opens[i]);\n      const closeY = scales.y.getPixelForValue(closes[i]);\n      const highY = scales.y.getPixelForValue(highs[i]);\n      const lowY = scales.y.getPixelForValue(lows[i]);\n      const isUp = closes[i] >= opens[i];\n      ctx.strokeStyle = barColors[i];\n\n      ctx.beginPath();\n      ctx.moveTo(x - tickHalfWidth, openY);\n      ctx.lineTo(x, openY);\n      ctx.stroke();\n\n      ctx.beginPath();\n      ctx.moveTo(x, closeY);\n      ctx.lineTo(x + tickHalfWidth, closeY);\n      ctx.stroke();\n\n      // Direction glyph — redundant (non-color) cue for CVD readers: a small\n      // triangle above the high (up days) or below the low (down days),\n      // pointing in the direction of the move.\n      const glyphHalfWidth = 4;\n      const glyphHeight = 7;\n      const baseY = isUp ? highY - 4 : lowY + 4;\n      const apexY = isUp ? baseY - glyphHeight : baseY + glyphHeight;\n      ctx.beginPath();\n      ctx.moveTo(x, apexY);\n      ctx.lineTo(x - glyphHalfWidth, baseY);\n      ctx.lineTo(x + glyphHalfWidth, baseY);\n      ctx.closePath();\n      ctx.fillStyle = barColors[i];\n      ctx.fill();\n      ctx.lineWidth = 1;\n      ctx.strokeStyle = t.ink;\n      ctx.stroke();\n    }\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\n// Core Chart.js has no dedicated OHLC type, so the high-low range is a thin\n// floating bar (one core \"bar\" dataset, data=[low, high]) and the open/close\n// ticks are drawn with the plugin above. Only core Chart.js bar-chart and\n// plugin-hook features are used — no chartjs-chart-financial or other package.\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels,\n    datasets: [\n      {\n        label: \"Range\",\n        data: lows.map((low, i) => [low, highs[i]]),\n        backgroundColor: barColors,\n        borderWidth: 0,\n        barThickness: 3,\n      },\n    ],\n  },\n  plugins: [ohlcTicksPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"ohlc-bar · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          generateLabels: () => [\n            { text: \"Up ▲ (close ≥ open)\", fillStyle: t.palette[0], strokeStyle: t.ink, lineWidth: 1 },\n            { text: \"Down ▼ (close < open)\", fillStyle: t.palette[4], strokeStyle: t.ink, lineWidth: 1 },\n          ],\n        },\n        onClick: () => {},\n      },\n      tooltip: {\n        mode: \"index\",\n        intersect: false,\n        callbacks: {\n          label: (item) => {\n            const i = item.dataIndex;\n            return `O: $${opens[i].toFixed(2)}  H: $${highs[i].toFixed(2)}  L: $${lows[i].toFixed(2)}  C: $${closes[i].toFixed(2)}`;\n          },\n        },\n      },\n    },\n    scales: {\n      x: {\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          maxRotation: 45,\n          minRotation: 45,\n          autoSkip: true,\n          maxTicksLimit: 14,\n        },\n        grid: { display: false },\n        title: { display: true, text: \"Date\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        min: priceMin - pricePad,\n        max: priceMax + pricePad,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => \"$\" + value.toFixed(2),\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Price (USD/barrel)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}