{"spec_id":"line-stock-comparison","library":"echarts","language":"javascript","code":"// anyplot.ai\n// line-stock-comparison: Stock Price Comparison Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny LCG — the browser has no seeded Math.random, so this stands in for it.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst symbols = [\"AAPL\", \"GOOGL\", \"MSFT\", \"SPY\"];\nconst dailyDrifts = [0.00055, 0.00035, 0.0004, 0.00025];\nconst dailyVolatilities = [0.016, 0.019, 0.014, 0.009];\n\nconst startDate = new Date(\"2024-01-02T00:00:00Z\");\nconst tradingDays = 252;\nconst dates = [];\nfor (let offset = 0, count = 0; count < tradingDays; offset++) {\n  const date = new Date(startDate.getTime() + offset * 86400000);\n  const weekday = date.getUTCDay();\n  if (weekday !== 0 && weekday !== 6) {\n    dates.push(date);\n    count++;\n  }\n}\n\n// Each series is its own random walk in log-returns, rebased so day 0 = 100.\nconst stockSeries = symbols.map((symbol, s) => {\n  let rebasedPrice = 100;\n  const points = dates.map((date, i) => {\n    if (i > 0) {\n      const dailyReturn = dailyDrifts[s] + (lcg() - 0.5) * 2 * dailyVolatilities[s];\n      rebasedPrice *= 1 + dailyReturn;\n    }\n    return [date.getTime(), rebasedPrice];\n  });\n  return { symbol, points };\n});\n\n// Tight y-axis bounds so the rebased band (clustered near 100) fills the\n// canvas instead of an echarts value axis forcing a 0 baseline.\nconst allValues = stockSeries.flatMap((s) => s.points.map(([, price]) => price));\nconst valueRange = Math.max(...allValues) - Math.min(...allValues);\nconst yPadding = valueRange * 0.12;\nconst yMin = Math.floor((Math.min(...allValues) - yPadding) / 5) * 5;\nconst yMax = Math.ceil((Math.max(...allValues) + yPadding) / 5) * 5;\n\n// Identify the winner/laggard so the chart can tell that story directly\n// instead of leaving it to be read off the legend.\nconst finalValues = stockSeries.map((s) => s.points[s.points.length - 1][1]);\nconst bestIndex = finalValues.indexOf(Math.max(...finalValues));\nconst worstIndex = finalValues.indexOf(Math.min(...finalValues));\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"line-stock-comparison · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    top: 66,\n    data: symbols,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n  tooltip: { trigger: \"axis\" },\n  grid: { left: 110, right: 130, top: 140, bottom: 80 },\n  xAxis: {\n    type: \"time\",\n    name: \"Date\",\n    nameLocation: \"middle\",\n    nameGap: 32,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    min: yMin,\n    max: yMax,\n    name: \"Rebased Price (Start = 100)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameRotate: 90,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: stockSeries.map((s, i) => {\n    const isEmphasis = i === bestIndex || i === worstIndex;\n    const pctChange = s.points[s.points.length - 1][1] - 100;\n    const sign = pctChange >= 0 ? \"+\" : \"\";\n    return {\n      name: s.symbol,\n      type: \"line\",\n      data: s.points,\n      showSymbol: false,\n      z: isEmphasis ? 3 : 2,\n      lineStyle: { width: isEmphasis ? 3.5 : 2.5, opacity: 0.88 },\n      endLabel: {\n        show: isEmphasis,\n        formatter: () => `${s.symbol} ${sign}${pctChange.toFixed(0)}%`,\n        color: t.palette[i % t.palette.length],\n        fontSize: 13,\n        fontWeight: 600,\n        distance: 10,\n      },\n      ...(i === 0\n        ? {\n            markLine: {\n              symbol: \"none\",\n              silent: true,\n              lineStyle: { color: t.ink, type: \"dashed\", width: 1.5 },\n              label: { show: false },\n              data: [{ yAxis: 100 }],\n            },\n          }\n        : {}),\n    };\n  }),\n});\n"}