{"spec_id":"line-stock-comparison","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// line-stock-comparison: Stock Price Comparison Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-08-26\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// One year of simulated daily closes for four tickers, each with its own\n// drift/volatility so the rebased comparison shows real divergence between\n// winners and laggards. SPY stands in as the lower-volatility benchmark index.\nlet seed = 42;\nconst nextRand = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n};\n\nconst DAYS = 252;\nconst START_DATE = new Date(\"2024-01-02T00:00:00Z\");\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n\nconst dateLabels = [];\nconst cursor = new Date(START_DATE);\nwhile (dateLabels.length < DAYS) {\n  const weekday = cursor.getUTCDay();\n  if (weekday !== 0 && weekday !== 6) {\n    dateLabels.push(`${MONTHS[cursor.getUTCMonth()]} ${cursor.getUTCDate()}`);\n  }\n  cursor.setUTCDate(cursor.getUTCDate() + 1);\n}\n\nconst STOCKS = [\n  { symbol: \"AAPL\", start: 185, drift: 0.0006, vol: 0.016 },\n  { symbol: \"MSFT\", start: 370, drift: 0.0005, vol: 0.013 },\n  { symbol: \"GOOGL\", start: 140, drift: 0.0003, vol: 0.017 },\n  { symbol: \"SPY\", start: 475, drift: 0.0004, vol: 0.008 },\n];\n\nconst series = STOCKS.map(({ symbol, start, drift, vol }) => {\n  const prices = [start];\n  for (let day = 1; day < DAYS; day++) {\n    const shock = (nextRand() - 0.5) * 2 * vol;\n    prices.push(prices[day - 1] * (1 + drift + shock));\n  }\n  const firstPrice = prices[0];\n  return { symbol, rebased: prices.map((price) => (price / firstPrice) * 100) };\n});\n\n// Thin dashed line at the common rebase point, drawn with Chart.js's native\n// per-chart plugin hook (no external annotation plugin). Font matches the\n// axis tick font (14px, Chart.js default family) instead of a hardcoded value.\nconst baselinePlugin = {\n  id: \"baselineMarker\",\n  afterDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const y = scales.y.getPixelForValue(100);\n    ctx.save();\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1.5;\n    ctx.setLineDash([6, 4]);\n    ctx.beginPath();\n    ctx.moveTo(chartArea.left, y);\n    ctx.lineTo(chartArea.right, y);\n    ctx.stroke();\n\n    ctx.setLineDash([]);\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = `14px ${Chart.defaults.font.family}`;\n    ctx.textAlign = \"right\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(\"Start = 100\", chartArea.right - 6, y - 4);\n    ctx.restore();\n  },\n};\n\n// End-of-series callout: labels the year's best and worst performer directly\n// at their final data point, sharpening the takeaway beyond the legend alone.\nconst bestIdx = series.reduce((best, s, i) => (s.rebased.at(-1) > series[best].rebased.at(-1) ? i : best), 0);\nconst worstIdx = series.reduce((worst, s, i) => (s.rebased.at(-1) < series[worst].rebased.at(-1) ? i : worst), 0);\n\nconst performanceLabelsPlugin = {\n  id: \"performanceLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    ctx.save();\n    ctx.font = `600 14px ${Chart.defaults.font.family}`;\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    [bestIdx, worstIdx].forEach((i) => {\n      const finalValue = series[i].rebased.at(-1);\n      const change = finalValue - 100;\n      const sign = change >= 0 ? \"+\" : \"\";\n      ctx.fillStyle = t.palette[i];\n      ctx.fillText(\n        `${series[i].symbol} ${sign}${change.toFixed(1)}%`,\n        chartArea.right + 8,\n        scales.y.getPixelForValue(finalValue),\n      );\n    });\n    ctx.restore();\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  data: {\n    labels: dateLabels,\n    datasets: series.map((s, i) => ({\n      label: s.symbol,\n      data: s.rebased,\n      borderColor: t.palette[i],\n      backgroundColor: t.palette[i],\n      borderWidth: 3.5,\n      pointRadius: 0,\n      pointHoverRadius: 4,\n      tension: 0,\n    })),\n  },\n  plugins: [baselinePlugin, performanceLabelsPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    interaction: { intersect: false, mode: \"index\" },\n    layout: { padding: { top: 8, right: 130, bottom: 4, left: 4 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"line-stock-comparison · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"bold\" },\n        padding: { top: 4, bottom: 18 },\n      },\n      legend: {\n        labels: { color: t.ink, font: { size: 16 }, usePointStyle: true, pointStyle: \"line\", padding: 18 },\n      },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 14 }, maxTicksLimit: 12, maxRotation: 0 },\n        grid: { display: false },\n        title: { display: true, text: \"Date\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Rebased Price (Start = 100)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}