{"spec_id":"sparkline-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// sparkline-basic: Basic Sparkline\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A tiny fixed-seed LCG — the browser has no seeded RNG.\nconst lcg = (seed) => {\n  let s = seed;\n  return () => {\n    s = (s * 1103515245 + 12345) & 0x7fffffff;\n    return s / 0x7fffffff;\n  };\n};\n\nconst buildSeries = (start, drift, volatility, seed, n = 40) => {\n  const rng = lcg(seed);\n  const values = [start];\n  for (let i = 1; i < n; i += 1) {\n    const step = drift + (rng() - 0.5) * volatility * 2;\n    values.push(Math.max(1, values[i - 1] + step));\n  }\n  return values;\n};\n\nconst stocks = [\n  {\n    name: \"Nova Robotics\",\n    ticker: \"NVRO\",\n    values: buildSeries(82, 0.55, 2.4, 11),\n  },\n  {\n    name: \"Bluewave Foods\",\n    ticker: \"BLUW\",\n    values: buildSeries(54, -0.35, 1.6, 23),\n  },\n  {\n    name: \"Crestline Energy\",\n    ticker: \"CRST\",\n    values: buildSeries(130, 0.9, 3.1, 37),\n  },\n  {\n    name: \"Halcyon Health\",\n    ticker: \"HLCN\",\n    values: buildSeries(38, -0.5, 1.1, 53),\n  },\n];\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 40)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"sparkline-basic · javascript · d3 · anyplot.ai\");\n\n// --- Layout: one table-style row per stock, sparkline embedded inline -------\nconst marginTop = 90;\nconst rowHeight = 190;\nconst labelX = 60;\nconst sparkX0 = 420;\nconst sparkX1 = width - 60;\nconst sparkHeight = 140;\n\n// Pre-compute per-stock change so the standout mover can be highlighted.\nconst changes = stocks.map((stock) => {\n  const values = stock.values;\n  const first = values[0];\n  const last = values[values.length - 1];\n  return ((last - first) / first) * 100;\n});\nconst standoutIndex = changes.reduce(\n  (best, c, i) => (Math.abs(c) > Math.abs(changes[best]) ? i : best),\n  0,\n);\n\n// Idiomatic D3 data-join: one <g class=\"row\"> per stock, data-bound.\nconst rows = svg.selectAll(\"g.row\").data(stocks).join(\"g\").attr(\"class\", \"row\");\n\nrows.each(function (stock, i) {\n  const row = d3.select(this);\n  const y0 = marginTop + i * rowHeight;\n  const values = stock.values;\n  const n = values.length;\n  const first = values[0];\n  const last = values[n - 1];\n  const change = changes[i];\n  const up = change >= 0;\n  const color = up ? t.palette[0] : t.palette[4];\n  const arrow = up ? \"▲\" : \"▼\";\n  const isStandout = i === standoutIndex;\n\n  // Subtle full-row tint calling out the largest absolute mover.\n  if (isStandout) {\n    row\n      .append(\"rect\")\n      .attr(\"x\", 0)\n      .attr(\"y\", y0)\n      .attr(\"width\", sparkX1)\n      .attr(\"height\", rowHeight)\n      .attr(\"fill\", color)\n      .attr(\"opacity\", 0.05);\n  }\n\n  // Divider between rows (table feel — not an axis of the sparkline itself)\n  if (i > 0) {\n    row\n      .append(\"line\")\n      .attr(\"x1\", labelX)\n      .attr(\"x2\", sparkX1)\n      .attr(\"y1\", y0)\n      .attr(\"y2\", y0)\n      .attr(\"stroke\", t.grid)\n      .attr(\"stroke-width\", 1);\n  }\n\n  // Label block — the standout mover gets a bolder company name.\n  row\n    .append(\"text\")\n    .attr(\"x\", labelX)\n    .attr(\"y\", y0 + 58)\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"20px\")\n    .style(\"font-weight\", isStandout ? \"700\" : \"600\")\n    .text(stock.name);\n\n  row\n    .append(\"text\")\n    .attr(\"x\", labelX)\n    .attr(\"y\", y0 + 82)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .style(\"letter-spacing\", \"0.05em\")\n    .text(stock.ticker);\n\n  const priceLabel = row\n    .append(\"text\")\n    .attr(\"x\", labelX)\n    .attr(\"y\", y0 + 120)\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"22px\")\n    .style(\"font-weight\", \"700\");\n  priceLabel.append(\"tspan\").text(`$${last.toFixed(2)}`);\n  priceLabel\n    .append(\"tspan\")\n    .attr(\"dx\", \"12\")\n    .attr(\"fill\", color)\n    .style(\"font-size\", \"16px\")\n    .style(\"font-weight\", \"600\")\n    .text(`${arrow} ${Math.abs(change).toFixed(1)}%`);\n\n  // Sparkline — pure line, no axes/ticks/gridlines\n  const sparkY0 = y0 + (rowHeight - sparkHeight) / 2;\n  const sparkY1 = sparkY0 + sparkHeight;\n  const x = d3\n    .scaleLinear()\n    .domain([0, n - 1])\n    .range([sparkX0, sparkX1]);\n  const [vMin, vMax] = d3.extent(values);\n  const pad = (vMax - vMin) * 0.1 || 1;\n  const y = d3\n    .scaleLinear()\n    .domain([vMin - pad, vMax + pad])\n    .range([sparkY1, sparkY0]);\n\n  const area = d3\n    .area()\n    .x((d, i2) => x(i2))\n    .y0(sparkY1)\n    .y1((d) => y(d))\n    .curve(d3.curveMonotoneX);\n  const line = d3\n    .line()\n    .x((d, i2) => x(i2))\n    .y((d) => y(d))\n    .curve(d3.curveMonotoneX);\n\n  row\n    .append(\"path\")\n    .datum(values)\n    .attr(\"d\", area)\n    .attr(\"fill\", color)\n    .attr(\"opacity\", 0.12);\n  row\n    .append(\"path\")\n    .datum(values)\n    .attr(\"d\", line)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", color)\n    .attr(\"stroke-width\", 2.5)\n    .attr(\"stroke-linejoin\", \"round\")\n    .attr(\"stroke-linecap\", \"round\");\n\n  // First point — subtle hollow reference marker\n  row\n    .append(\"circle\")\n    .attr(\"cx\", x(0))\n    .attr(\"cy\", y(first))\n    .attr(\"r\", 3.5)\n    .attr(\"fill\", t.pageBg)\n    .attr(\"stroke\", color)\n    .attr(\"stroke-width\", 1.5);\n\n  // Last point — filled marker, the current value\n  row\n    .append(\"circle\")\n    .attr(\"cx\", x(n - 1))\n    .attr(\"cy\", y(last))\n    .attr(\"r\", 5)\n    .attr(\"fill\", color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.5);\n\n  // Min/max markers — small faded dots, skipped where they'd sit on top of\n  // the first/last markers already drawn above.\n  const maxIdx = values.indexOf(vMax);\n  const minIdx = values.indexOf(vMin);\n  for (const idx of new Set([minIdx, maxIdx])) {\n    if (idx === 0 || idx === n - 1) continue;\n    row\n      .append(\"circle\")\n      .attr(\"cx\", x(idx))\n      .attr(\"cy\", y(values[idx]))\n      .attr(\"r\", 2.5)\n      .attr(\"fill\", color)\n      .attr(\"opacity\", 0.55);\n  }\n});\n"}