{"spec_id":"span-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// span-basic: Basic Span Plot (Highlighted Region)\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 92/100 | Created: 2026-07-25\n\nconst t = window.ANYPLOT_TOKENS;\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\n// Tiny fixed-seed LCG — the browser has no seeded RNG\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(20260725);\n\n// --- Data: illustrative equity index, monthly, 2006-2012 -------------------\nconst MONTH_NAMES = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nconst START_YEAR = 2006;\nconst MONTHS = 84; // Jan 2006 - Dec 2012\n\n// Two vertical spans: recession periods marking market downturns\nconst verticalSpans = [\n  { startIdx: 24, endIdx: 41, label: \"Financial Crisis\" }, // Jan 2008 - Jun 2009\n  { startIdx: 67, endIdx: 70, label: \"Debt-Ceiling Crisis\" }, // Aug 2011 - Nov 2011\n];\nconst inAnySpan = (i) => verticalSpans.some((s) => i >= s.startIdx && i <= s.endIdx);\n\n// One horizontal span: an index-value threshold band (the \"horizontal\" direction).\n// The upper bound sits just above the deepest crisis dip so the line visibly\n// enters the zone during the Financial Crisis, tying the two directions together.\nconst horizontalSpans = [{ startVal: 70, endVal: 95, label: \"Elevated Risk Zone (Index < 95)\" }];\n\nconst labels = [];\nconst indexValues = [];\nlet index = 100;\nfor (let i = 0; i < MONTHS; i++) {\n  const year = START_YEAR + Math.floor(i / 12);\n  labels.push(`${MONTH_NAMES[i % 12]} '${String(year).slice(2)}`);\n\n  const drift = inAnySpan(i) ? -0.018 : 0.0075;\n  const noise = (rand() - 0.5) * 0.05;\n  index *= 1 + drift + noise;\n  indexValues.push(Math.round(index * 100) / 100);\n}\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Inline plugin: shade the crisis periods (vertical) and the risk-zone threshold\n// (horizontal) behind the line, covering both `direction` values from the spec.\n// Drawn in beforeDatasetsDraw so the line renders on top of the fills.\nconst spanHighlight = {\n  id: \"spanHighlight\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const xScale = scales.x;\n    const yScale = scales.y;\n    const step = xScale.getPixelForValue(1) - xScale.getPixelForValue(0);\n    const half = step / 2;\n\n    ctx.save();\n\n    // Horizontal span first (full width, thin band) so the vertical spans stay\n    // the visually dominant fill where the two overlap.\n    const amberFill = hexToRgba(t.amber, 0.16);\n    const amberEdge = hexToRgba(t.amber, 0.8);\n    horizontalSpans.forEach((span) => {\n      // Clamp to the plot area: the band's outer edge may fall outside the\n      // auto-scaled axis range, but the fill must never bleed past the frame.\n      const top = Math.max(chartArea.top, yScale.getPixelForValue(span.endVal));\n      const bottom = Math.min(chartArea.bottom, yScale.getPixelForValue(span.startVal));\n\n      ctx.fillStyle = amberFill;\n      ctx.fillRect(chartArea.left, top, chartArea.right - chartArea.left, bottom - top);\n\n      // Dashed edges distinguish the threshold band from the solid-edged crisis spans.\n      ctx.strokeStyle = amberEdge;\n      ctx.lineWidth = 1.5;\n      ctx.setLineDash([6, 4]);\n      ctx.beginPath();\n      ctx.moveTo(chartArea.left, top);\n      ctx.lineTo(chartArea.right, top);\n      ctx.moveTo(chartArea.left, bottom);\n      ctx.lineTo(chartArea.right, bottom);\n      ctx.stroke();\n      ctx.setLineDash([]);\n\n      ctx.fillStyle = t.inkSoft;\n      ctx.font = \"600 13px sans-serif\";\n      ctx.textAlign = \"right\";\n      ctx.textBaseline = \"bottom\";\n      ctx.fillText(span.label, chartArea.right - 10, bottom - 6);\n    });\n\n    // Vertical crisis spans, with a solid edge line at each boundary.\n    const redFill = hexToRgba(t.palette[4], 0.22); // matte red — semantic anchor for a \"bad\" period\n    const redEdge = hexToRgba(t.palette[4], 0.6);\n    verticalSpans.forEach((span) => {\n      const left = xScale.getPixelForValue(span.startIdx) - half;\n      const right = xScale.getPixelForValue(span.endIdx) + half;\n\n      ctx.fillStyle = redFill;\n      ctx.fillRect(left, chartArea.top, right - left, chartArea.bottom - chartArea.top);\n\n      ctx.strokeStyle = redEdge;\n      ctx.lineWidth = 1.5;\n      ctx.beginPath();\n      ctx.moveTo(left, chartArea.top);\n      ctx.lineTo(left, chartArea.bottom);\n      ctx.moveTo(right, chartArea.top);\n      ctx.lineTo(right, chartArea.bottom);\n      ctx.stroke();\n\n      ctx.fillStyle = t.inkSoft;\n      ctx.font = \"600 13px sans-serif\";\n      ctx.textAlign = \"left\";\n      ctx.textBaseline = \"top\";\n      ctx.save();\n      ctx.translate(left + 10, chartArea.top + 12);\n      ctx.rotate(Math.PI / 2);\n      ctx.fillText(span.label, 0, 0);\n      ctx.restore();\n    });\n\n    ctx.restore();\n  },\n};\n\n// --- Chart -----------------------------------------------------------------\nconst title = \"Equity Index · span-basic · javascript · chartjs · anyplot.ai\";\nconst titleFontSize = Math.round(27 * (title.length > 67 ? 67 / title.length : 1));\n\nnew Chart(canvas, {\n  type: \"line\",\n  plugins: [spanHighlight],\n  data: {\n    labels,\n    datasets: [\n      {\n        label: \"Equity Index\",\n        data: indexValues,\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        borderWidth: 3,\n        pointRadius: 0,\n        tension: 0.15,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 8 } },\n    plugins: {\n      title: {\n        display: true,\n        text: title,\n        color: t.ink,\n        font: { size: titleFontSize },\n        padding: { top: 12, bottom: 16 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 14 }, maxTicksLimit: 12, autoSkip: true },\n        grid: { display: false },\n        title: { display: true, text: \"Date\", color: t.ink, font: { size: 18 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Index Value (Jan 2006 = 100)\", color: t.ink, font: { size: 18 } },\n      },\n    },\n  },\n});\n"}