{"spec_id":"histogram-epidemic","library":"d3","language":"javascript","code":"// anyplot.ai\n// histogram-epidemic: Epidemic Curve (Epi Curve)\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: 130, right: 120, bottom: 80, 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 nextRandom() {\n  seed = (seed * 9301 + 49297) % 233280;\n  return seed / 233280;\n}\n\nconst START_DATE = new Date(Date.UTC(2024, 2, 1));\nconst NUM_DAYS = 60;\nconst LOCKDOWN_DAY = 17;\nconst PEAK_DAY = 15;\n\nconst days = d3.range(NUM_DAYS).map((i) => {\n  const date = new Date(START_DATE);\n  date.setUTCDate(date.getUTCDate() + i);\n\n  // Gamma-shaped epidemic curve: fast rise to a peak, slower decay after the\n  // lockdown intervention — typical point-source-then-controlled outbreak.\n  const shape =\n    i < PEAK_DAY\n      ? Math.pow(i / PEAK_DAY, 2.1)\n      : Math.pow(Math.max(0, 1 - (i - PEAK_DAY) / 50), 1.5);\n  const base = 72 * shape;\n  const noise = (nextRandom() - 0.5) * base * 0.3;\n  const total = Math.max(0, Math.round(base + noise));\n\n  const confirmedShare = 0.55 + nextRandom() * 0.1;\n  const probableShare = 0.25 + nextRandom() * 0.08;\n  const confirmed = Math.round(total * confirmedShare);\n  const probable = Math.round(total * probableShare);\n  const suspect = Math.max(0, total - confirmed - probable);\n\n  return { date, confirmed, probable, suspect, total: confirmed + probable + suspect };\n});\n\nlet runningTotal = 0;\nfor (const d of days) {\n  runningTotal += d.total;\n  d.cumulative = runningTotal;\n}\n\nconst STACK_KEYS = [\"confirmed\", \"probable\", \"suspect\"];\nconst STACK_LABELS = { confirmed: \"Confirmed\", probable: \"Probable\", suspect: \"Suspect\" };\nconst seriesColor = d3.scaleOrdinal().domain(STACK_KEYS).range(t.palette.slice(0, 3));\nconst stackedSeries = d3.stack().keys(STACK_KEYS)(days);\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  .scaleBand()\n  .domain(days.map((d) => d.date))\n  .range([0, iw])\n  .padding(0.15);\n\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(days, (d) => d.total)])\n  .nice()\n  .range([ih, 0]);\n\nconst yCumulative = d3\n  .scaleLinear()\n  .domain([0, d3.max(days, (d) => d.cumulative)])\n  .nice()\n  .range([ih, 0]);\n\n// --- Stacked bars -----------------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"g.series\")\n  .data(stackedSeries)\n  .join(\"g\")\n  .attr(\"class\", \"series\")\n  .attr(\"fill\", (series) => seriesColor(series.key))\n  .selectAll(\"rect\")\n  .data((series) => series)\n  .join(\"rect\")\n  .attr(\"x\", (d) => x(d.data.date))\n  .attr(\"y\", (d) => y(d[1]))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"height\", (d) => Math.max(0, y(d[0]) - y(d[1])));\n\n// --- Cumulative case line (secondary axis, neutral/baseline role) -----------\nconst cumulativeLine = d3\n  .line()\n  .x((d) => x(d.date) + x.bandwidth() / 2)\n  .y((d) => yCumulative(d.cumulative))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(days)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 3)\n  .attr(\"d\", cumulativeLine);\n\n// --- Intervention annotation (spec explicitly requests this) ----------------\nconst lockdownDate = days[LOCKDOWN_DAY].date;\nconst lockdownX = x(lockdownDate) + x.bandwidth() / 2;\n\ng.append(\"line\")\n  .attr(\"x1\", lockdownX)\n  .attr(\"x2\", lockdownX)\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.amber)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"7,5\");\n\ng.append(\"text\")\n  .attr(\"x\", lockdownX + 12)\n  .attr(\"y\", 22)\n  .attr(\"fill\", t.amber)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Lockdown announced\");\n\n// --- Axes ---------------------------------------------------------------\nconst tickDates = days.filter((d, i) => i % 7 === 0).map((d) => d.date);\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues(tickDates).tickFormat(d3.timeFormat(\"%b %d\")));\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\n\nconst yCumulativeAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(${iw},0)`)\n  .call(d3.axisRight(yCumulative).ticks(6));\n\nfor (const axis of [xAxis, yAxis, yCumulativeAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\"text\").attr(\"dy\", \"1em\");\n\ng.append(\"g\")\n  .selectAll(\"line.grid\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"class\", \"grid\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .lower();\n\n// --- Axis labels --------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Daily New Cases\");\n\ng.append(\"text\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", iw + 90)\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Cumulative Cases\");\n\n// --- Legend ---------------------------------------------------------------\nconst legendItems = [\n  ...STACK_KEYS.map((key) => ({ label: STACK_LABELS[key], color: seriesColor(key), type: \"swatch\" })),\n  { label: \"Cumulative cases\", color: t.ink, type: \"line\" },\n];\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top - 60})`);\nlet legendX = 0;\nfor (const item of legendItems) {\n  const entry = legend.append(\"g\").attr(\"transform\", `translate(${legendX},0)`);\n  if (item.type === \"swatch\") {\n    entry.append(\"rect\").attr(\"width\", 16).attr(\"height\", 16).attr(\"fill\", item.color);\n  } else {\n    entry\n      .append(\"line\")\n      .attr(\"x1\", 0)\n      .attr(\"x2\", 16)\n      .attr(\"y1\", 8)\n      .attr(\"y2\", 8)\n      .attr(\"stroke\", item.color)\n      .attr(\"stroke-width\", 3);\n  }\n  const label = entry\n    .append(\"text\")\n    .attr(\"x\", 24)\n    .attr(\"y\", 13)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n  legendX += 24 + label.node().getComputedTextLength() + 36;\n}\n\n// --- Title ------------------------------------------------------------------\nconst TITLE = \"COVID-19 Outbreak by Symptom Onset · histogram-epidemic · javascript · d3 · anyplot.ai\";\nconst TITLE_FONT_SIZE = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${TITLE_FONT_SIZE}px`)\n  .style(\"font-weight\", \"600\")\n  .text(TITLE);\n"}