{"spec_id":"line-annotated-events","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-annotated-events: Annotated Line Plot with Event Markers\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 50, bottom: 70, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fixed-seed LCG — the browser has no seeded Math.random().\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst NUM_DAYS = 220;\nconst START_DATE = new Date(2024, 0, 8);\nconst EVENTS = [\n  { day: 18, label: \"v3.2 launch\" },\n  { day: 52, label: \"Mobile app release\" },\n  { day: 90, label: \"Referral program\" },\n  { day: 128, label: \"Pricing change\" },\n  { day: 160, label: \"API partnership\" },\n  { day: 198, label: \"Enterprise tier\" },\n];\n\nlet dailyActiveUsers = 24; // thousands\nconst series = [];\nfor (let day = 0; day < NUM_DAYS; day++) {\n  const date = new Date(START_DATE);\n  date.setDate(date.getDate() + day);\n  const seasonal = 0.95 * Math.sin((day / 7) * Math.PI * 2);\n  const noise = (rand() - 0.5) * 1.1;\n  const launchBoost = EVENTS.some((e) => day >= e.day && day < e.day + 4) ? 0.55 : 0;\n  dailyActiveUsers += 0.045 + launchBoost + noise * 0.3;\n  series.push({ date, value: Math.max(dailyActiveUsers + seasonal, 1) });\n}\n\nconst eventPoints = EVENTS.map((e) => {\n  const date = new Date(START_DATE);\n  date.setDate(date.getDate() + e.day);\n  return { date, label: e.label, value: series[e.day].value };\n});\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// --- Scales ------------------------------------------------------------------\nconst x = d3\n  .scaleTime()\n  .domain(d3.extent(series, (d) => d.date))\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(series, (d) => d.value) * 1.08])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y-axis only, per style guide) --------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Axes ----------------------------------------------------------------------\n// Multi-scale date format: spell out the year only when a tick crosses into a\n// new year, otherwise show the month abbreviation.\nconst formatMonth = d3.timeFormat(\"%b\");\nconst formatMonthYear = d3.timeFormat(\"%b %Y\");\nconst monthTickFormat = (date) => (d3.timeYear(date) < date ? formatMonth(date) : formatMonthYear(date));\n\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(d3.timeMonth.every(1)).tickFormat(monthTickFormat));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat((d) => `${d}k`));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Y-axis label --------------------------------------------------------------\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\", \"16px\")\n  .text(\"Daily Active Users (thousands)\");\n\n// --- Area fill: subtle wash under the trend line for extra polish -----------\nconst area = d3\n  .area()\n  .x((d) => x(d.date))\n  .y0(ih)\n  .y1((d) => y(d.value))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\").datum(series).attr(\"fill\", t.palette[0]).attr(\"fill-opacity\", 0.12).attr(\"d\", area);\n\n// --- Event markers: dashed reference lines + alternating labels -------------\n// Built via D3's idiomatic selection.data(...).join(...) pattern rather than\n// a manual forEach + append loop.\nconst eventColor = t.palette[1];\nconst labelRowY = [78, 104];\nconst eventGroup = svg.append(\"g\");\n\neventGroup\n  .selectAll(\"line.event-line\")\n  .data(eventPoints)\n  .join(\"line\")\n  .attr(\"class\", \"event-line\")\n  .attr(\"x1\", (d) => margin.left + x(d.date))\n  .attr(\"x2\", (d) => margin.left + x(d.date))\n  .attr(\"y1\", margin.top - 18)\n  .attr(\"y2\", margin.top + ih)\n  .attr(\"stroke\", eventColor)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"6,5\");\n\neventGroup\n  .selectAll(\"circle.event-marker\")\n  .data(eventPoints)\n  .join(\"circle\")\n  .attr(\"class\", \"event-marker\")\n  .attr(\"cx\", (d) => margin.left + x(d.date))\n  .attr(\"cy\", (d) => margin.top + y(d.value))\n  .attr(\"r\", 7)\n  .attr(\"fill\", eventColor)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\neventGroup\n  .selectAll(\"text.event-label\")\n  .data(eventPoints)\n  .join(\"text\")\n  .attr(\"class\", \"event-label\")\n  .attr(\"x\", (d) => margin.left + x(d.date))\n  .attr(\"y\", (d, i) => labelRowY[i % 2])\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d.label);\n\n// --- Line ------------------------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.value))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\")\n  .datum(series)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3)\n  .attr(\"d\", line);\n\n// --- Legend ------------------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${height - 34})`);\nconst legendItems = [\n  { label: \"Daily active users\", color: t.palette[0], dashed: false },\n  { label: \"Product launch events\", color: eventColor, dashed: true },\n];\nlet lx = 0;\nlegendItems.forEach((item) => {\n  legend\n    .append(\"line\")\n    .attr(\"x1\", lx)\n    .attr(\"x2\", lx + 34)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", item.color)\n    .attr(\"stroke-width\", 3)\n    .attr(\"stroke-dasharray\", item.dashed ? \"6,5\" : null);\n  legend\n    .append(\"text\")\n    .attr(\"x\", lx + 42)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n  lx += 42 + item.label.length * 8 + 40;\n});\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(\"line-annotated-events · javascript · d3 · anyplot.ai\");\n"}