{"spec_id":"indicator-bollinger","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// indicator-bollinger: Bollinger Bands Indicator Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: simulated BTC/USD daily close over 90 trading days --------------\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(1337);\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst N_DAYS = 90;\nconst WINDOW = 20;\n\nconst startDate = new Date(2024, 2, 1); // Mar 1 2024\nconst labels = Array.from({ length: N_DAYS }, (_, i) => {\n  const d = new Date(startDate);\n  d.setDate(d.getDate() + i);\n  return d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\n});\n\nconst close = [];\nlet price = 42000;\nfor (let i = 0; i < N_DAYS; i++) {\n  price *= 1 + (0.0006 + 0.028 * randNormal());\n  close.push(price);\n}\n\n// Rolling 20-day SMA + upper/lower bands (SMA ± 2 sample std dev)\nconst sma = [];\nconst upperBand = [];\nconst lowerBand = [];\nfor (let i = 0; i < N_DAYS; i++) {\n  if (i < WINDOW - 1) {\n    sma.push(null);\n    upperBand.push(null);\n    lowerBand.push(null);\n    continue;\n  }\n  const slice = close.slice(i - WINDOW + 1, i + 1);\n  const mean = slice.reduce((a, b) => a + b, 0) / WINDOW;\n  const variance = slice.reduce((a, b) => a + (b - mean) ** 2, 0) / (WINDOW - 1);\n  const std = Math.sqrt(variance);\n  sma.push(mean);\n  upperBand.push(mean + 2 * std);\n  lowerBand.push(mean - 2 * std);\n}\n\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\nconst bandColor = t.palette[2]; // blue — volatility envelope\nconst smaColor = t.palette[1]; // lavender — middle band\n\n// --- Storytelling: locate the tightest squeeze and the sharpest breakout ---\nlet squeezeIdx = -1;\nlet minWidth = Infinity;\nfor (let i = WINDOW - 1; i < N_DAYS; i++) {\n  const w = upperBand[i] - lowerBand[i];\n  if (w < minWidth) {\n    minWidth = w;\n    squeezeIdx = i;\n  }\n}\n\nlet breakoutIdx = -1;\nlet maxDeviation = -Infinity;\nfor (let i = WINDOW - 1; i < N_DAYS; i++) {\n  const dev = Math.max(close[i] - upperBand[i], lowerBand[i] - close[i]);\n  if (dev > maxDeviation) {\n    maxDeviation = dev;\n    breakoutIdx = i;\n  }\n}\n\nconst calloutPlugin = {\n  id: \"bollingerCallouts\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    const xScale = chart.scales.x;\n    const yScale = chart.scales.y;\n\n    function draw(idx, label, valueArr, color, dy) {\n      const x = xScale.getPixelForValue(labels[idx], idx);\n      const y = yScale.getPixelForValue(valueArr[idx]);\n      const textY = y + dy + (dy > 0 ? 16 : -10);\n\n      ctx.save();\n      ctx.strokeStyle = color;\n      ctx.lineWidth = 1.5;\n      ctx.setLineDash([3, 3]);\n      ctx.beginPath();\n      ctx.moveTo(x, y);\n      ctx.lineTo(x, y + dy);\n      ctx.stroke();\n\n      ctx.setLineDash([]);\n      ctx.fillStyle = color;\n      ctx.beginPath();\n      ctx.arc(x, y, 4, 0, Math.PI * 2);\n      ctx.fill();\n\n      ctx.font = \"600 14px sans-serif\";\n      ctx.fillStyle = t.ink;\n      ctx.textAlign = \"center\";\n      ctx.fillText(label, x, textY);\n      ctx.restore();\n    }\n\n    draw(squeezeIdx, \"Squeeze\", lowerBand, t.inkSoft, 36);\n    draw(breakoutIdx, \"Breakout\", close, t.palette[0], -34);\n  },\n};\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  plugins: [calloutPlugin],\n  data: {\n    labels,\n    datasets: [\n      {\n        label: \"Upper Band\",\n        data: upperBand,\n        borderColor: bandColor,\n        backgroundColor: \"transparent\",\n        borderWidth: 1.5,\n        pointRadius: 0,\n        fill: false,\n        tension: 0.15,\n      },\n      {\n        label: \"Bollinger Band (±2σ)\",\n        data: lowerBand,\n        borderColor: bandColor,\n        backgroundColor: hexToRgba(bandColor, 0.2),\n        borderWidth: 1.5,\n        pointRadius: 0,\n        fill: \"-1\",\n        tension: 0.15,\n      },\n      {\n        label: \"SMA (20-day)\",\n        data: sma,\n        borderColor: smaColor,\n        backgroundColor: \"transparent\",\n        borderWidth: 2.5,\n        borderDash: [8, 4],\n        pointRadius: 0,\n        fill: false,\n        tension: 0.15,\n      },\n      {\n        label: \"Close Price\",\n        data: close,\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        borderWidth: 3,\n        pointRadius: 0,\n        fill: false,\n        tension: 0.15,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    interaction: { intersect: false, mode: \"index\" },\n    plugins: {\n      title: {\n        display: true,\n        text: \"indicator-bollinger · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { top: 12, bottom: 8 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          boxWidth: 30,\n          padding: 20,\n          filter: (item) => item.text !== \"Upper Band\",\n        },\n      },\n    },\n    scales: {\n      x: {\n        title: { display: true, text: \"Date\", color: t.ink, font: { size: 18 } },\n        ticks: { color: t.inkSoft, font: { size: 14 }, maxTicksLimit: 10, autoSkip: true },\n        grid: { display: false },\n      },\n      y: {\n        title: { display: true, text: \"Price (USD)\", color: t.ink, font: { size: 18 } },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => \"$\" + v.toLocaleString(),\n        },\n        grid: { color: hexToRgba(t.ink, 0.1), lineWidth: 1 },\n      },\n    },\n  },\n});\n"}