{"spec_id":"line-timeseries","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-timeseries: Time Series Line Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 60, bottom: 70, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: daily closing price random walk over one trading year -----------\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) % 2147483648;\n    return state / 2147483648;\n  };\n}\nconst random = lcg(42);\n\nconst startDate = new Date(2024, 0, 1);\nlet price = 148;\nconst data = Array.from({ length: 365 }, (_, i) => {\n  const date = new Date(startDate);\n  date.setDate(date.getDate() + i);\n  price = Math.max(price + (random() - 0.485) * 3.4, 20);\n  return { date, close: Math.round(price * 100) / 100 };\n});\n\n// --- 30-day rolling average via d3.cumsum prefix sums -----------------------\n// Smooths the daily noise into a trend line, giving the viewer a focal point\n// beyond the raw series (an expanding window for the first 29 days).\nconst WINDOW = 30;\nconst cum = d3.cumsum(data, (d) => d.close);\nconst rolling = data.map((d, i) => {\n  const lo = Math.max(0, i - WINDOW + 1);\n  const sum = cum[i] - (lo > 0 ? cum[lo - 1] : 0);\n  return { date: d.date, avg: sum / (i - lo + 1) };\n});\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleTime().domain(d3.extent(data, (d) => d.date)).range([0, iw]);\nconst y = d3.scaleLinear().domain(d3.extent(data, (d) => d.close)).nice().range([ih, 0]);\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// --- Grid (drawn first, behind the line) ------------------------------------\nconst gridY = g.append(\"g\").call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"));\ngridY.selectAll(\"line\").attr(\"stroke\", t.grid);\ngridY.select(\".domain\").remove();\n\nconst gridX = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(10).tickSize(-ih).tickFormat(\"\"));\ngridX.selectAll(\"line\").attr(\"stroke\", t.grid);\ngridX.select(\".domain\").remove();\n\n// --- Area: subtle fill under the line to emphasize trend magnitude ---------\nconst area = d3\n  .area()\n  .x((d) => x(d.date))\n  .y0(ih)\n  .y1((d) => y(d.close))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(data).attr(\"fill\", t.palette[0]).attr(\"fill-opacity\", 0.12).attr(\"d\", area);\n\n// --- Line: intelligent date axis handles the temporal formatting -----------\nconst line = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.close))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(data)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3)\n  .attr(\"stroke-linejoin\", \"round\")\n  .attr(\"stroke-linecap\", \"round\")\n  .attr(\"d\", line);\n\n// --- Rolling average overlay: highlights the year's trend reversals ---------\nconst avgLine = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.avg))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(rolling)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"6,4\")\n  .attr(\"stroke-linecap\", \"round\")\n  .attr(\"d\", avgLine);\n\n// --- Axes -------------------------------------------------------------------\n// d3's default time-scale tick format is already \"smart\": it switches\n// resolution (year / month / day / hour) to match the ticks it selects for\n// the current domain, so no custom multi-format function is needed here.\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(10));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickFormat((d) => `$${d}`));\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Y-axis label -------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2))\n  .attr(\"y\", 30)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Closing Price (USD)\");\n\n// --- X-axis label ---------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 22)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Date\");\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 42)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-timeseries · javascript · d3 · anyplot.ai\");\n\n// --- Legend: distinguishes the daily series from the smoothed trend --------\nconst legendItems = [\n  { label: \"Daily close\", stroke: t.palette[0], dash: null },\n  { label: \"30-day avg\", stroke: t.ink, dash: \"6,4\" },\n];\nconst legendG = svg.append(\"g\").attr(\"transform\", `translate(${width - margin.right - 130},64)`);\nlegendItems.forEach((item, i) => {\n  const row = legendG.append(\"g\").attr(\"transform\", `translate(0,${i * 20})`);\n  row\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 22)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", item.stroke)\n    .attr(\"stroke-width\", 3)\n    .attr(\"stroke-dasharray\", item.dash);\n  row.append(\"text\").attr(\"x\", 28).attr(\"y\", 4).attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\").text(item.label);\n});\n"}