{"spec_id":"line-stock-comparison","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-stock-comparison: Stock Price Comparison Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 120, right: 260, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic random walk, rebased to 100) -----------\nfunction lcg(seed) {\n  let s = seed;\n  return () => {\n    s = (s * 1664525 + 1013904223) % 4294967296;\n    return s / 4294967296;\n  };\n}\nconst rand = lcg(20260826);\n\nconst numDays = 252; // one trading year (weekdays only)\nconst startDate = new Date(2025, 0, 2);\nconst dates = [];\nfor (let d = new Date(startDate); dates.length < numDays; d.setDate(d.getDate() + 1)) {\n  const weekday = d.getDay();\n  if (weekday !== 0 && weekday !== 6) dates.push(new Date(d));\n}\n\n// Clean-energy sector comparison: three constituents vs. a sector ETF benchmark\nconst stocks = [\n  { symbol: \"FSLR\", name: \"First Solar\", startPrice: 210, drift: 0.0011, vol: 0.023 },\n  { symbol: \"ENPH\", name: \"Enphase Energy\", startPrice: 95, drift: -0.0009, vol: 0.03 },\n  { symbol: \"NEE\", name: \"NextEra Energy\", startPrice: 72, drift: 0.0003, vol: 0.011 },\n  { symbol: \"ICLN\", name: \"Clean Energy ETF\", startPrice: 14, drift: 0.0001, vol: 0.009 },\n];\n\nconst series = stocks.map((stock) => {\n  const prices = [stock.startPrice];\n  for (let i = 1; i < numDays; i++) {\n    const shock = (rand() - 0.5) * stock.vol * 2;\n    const next = prices[i - 1] * (1 + stock.drift + shock);\n    prices.push(Math.max(next, stock.startPrice * 0.15));\n  }\n  const first = prices[0];\n  return {\n    symbol: stock.symbol,\n    name: stock.name,\n    values: prices.map((price, i) => ({ date: dates[i], rebased: (price / first) * 100 })),\n  };\n});\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleTime().domain(d3.extent(dates)).range([0, iw]);\n\nconst allRebased = series.flatMap((s) => s.values.map((d) => d.rebased));\nconst yExtent = d3.extent(allRebased);\nconst yPad = (yExtent[1] - yExtent[0]) * 0.08;\nconst y = d3\n  .scaleLinear()\n  .domain([yExtent[0] - yPad, yExtent[1] + yPad])\n  .nice()\n  .range([ih, 0]);\n\nconst color = d3.scaleOrdinal().domain(series.map((s) => s.symbol)).range(t.palette);\n\nconst line = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.rebased))\n  .curve(d3.curveMonotoneX);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (y-axis only) -----------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\").ticks(6))\n  .call((sel) => sel.select(\".domain\").remove())\n  .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid));\n\n// --- Baseline reference at rebased = 100 -----------------------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(100))\n  .attr(\"y2\", y(100))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,5\");\n\ng.append(\"text\")\n  .attr(\"x\", iw)\n  .attr(\"y\", y(100) - 10)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"start = 100\");\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(d3.timeMonth.every(1)).tickFormat(d3.timeFormat(\"%b\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.select(\".domain\").remove();\n}\n\n// --- Lines ------------------------------------------------------------------\ng.selectAll(\".stock-line\")\n  .data(series)\n  .join(\"path\")\n  .attr(\"class\", \"stock-line\")\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (d) => color(d.symbol))\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-linejoin\", \"round\")\n  .attr(\"stroke-linecap\", \"round\")\n  .attr(\"d\", (d) => line(d.values));\n\n// --- Endpoint markers + inline symbol labels --------------------------------\nconst endpoints = g\n  .selectAll(\".endpoint\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"class\", \"endpoint\")\n  .attr(\"transform\", (d) => {\n    const last = d.values[d.values.length - 1];\n    return `translate(${x(last.date)},${y(last.rebased)})`;\n  });\n\nendpoints.append(\"circle\").attr(\"r\", 6).attr(\"fill\", (d) => color(d.symbol)).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2);\n\nendpoints\n  .append(\"text\")\n  .attr(\"x\", 12)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", (d) => color(d.symbol))\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d.symbol);\n\n// --- Peak annotation (computed extremum via d3.greatest) -------------------\nconst flatPoints = series.flatMap((s) => s.values.map((d) => ({ ...d, symbol: s.symbol })));\nconst peak = d3.greatest(flatPoints, (d) => d.rebased);\nconst peakX = x(peak.date);\nconst peakY = y(peak.rebased);\nconst peakLabelAbove = peakY > 60;\nconst peakLabelY = peakLabelAbove ? peakY - 30 : peakY + 40;\nconst peakLabelAnchor = peakX > iw * 0.8 ? \"end\" : \"start\";\nconst peakLabelX = peakX + (peakLabelAnchor === \"end\" ? -16 : 16);\n\ng.append(\"circle\")\n  .attr(\"cx\", peakX)\n  .attr(\"cy\", peakY)\n  .attr(\"r\", 7)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", color(peak.symbol))\n  .attr(\"stroke-width\", 2);\n\ng.append(\"line\")\n  .attr(\"x1\", peakX)\n  .attr(\"y1\", peakY)\n  .attr(\"x2\", peakLabelX)\n  .attr(\"y2\", peakLabelY + (peakLabelAbove ? 6 : -6))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\ng.append(\"text\")\n  .attr(\"x\", peakLabelX)\n  .attr(\"y\", peakLabelY)\n  .attr(\"text-anchor\", peakLabelAnchor)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(`${peak.symbol} peak: ${peak.rebased.toFixed(0)}`);\n\n// --- Axis labels --------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Trading Date (2025)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Rebased Price (Start = 100)\");\n\n// --- Legend -------------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 40},${margin.top + 20})`);\n\nconst legendRows = legend\n  .selectAll(\".legend-row\")\n  .data(series)\n  .join(\"g\")\n  .attr(\"class\", \"legend-row\")\n  .attr(\"transform\", (d, i) => `translate(0,${i * 32})`);\n\nlegendRows\n  .append(\"rect\")\n  .attr(\"width\", 16)\n  .attr(\"height\", 16)\n  .attr(\"rx\", 3)\n  .attr(\"fill\", (d) => color(d.symbol));\n\nlegendRows\n  .append(\"text\")\n  .attr(\"x\", 24)\n  .attr(\"y\", 8)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d.name);\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-stock-comparison · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 92)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"17px\")\n  .text(\"Clean-energy sector: relative performance vs. sector ETF benchmark\");\n"}