{"spec_id":"histogram-epidemic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// histogram-epidemic: Epidemic Curve (Epi Curve)\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Simulated foodborine-illness outbreak, daily symptom-onset counts over 60 days.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst DAYS = 60;\nconst LOCKDOWN_DAY = 24; // intervention: public health advisory issued\n\nconst dates = [];\nconst confirmed = [];\nconst probable = [];\nconst suspect = [];\n\nfor (let day = 0; day < DAYS; day++) {\n  const d = new Date(2026, 2, 1 + day); // Mar 1 2026 start\n  dates.push(`${d.getMonth() + 1}/${d.getDate()}`);\n\n  // Point-source outbreak: sharp rise, peak, decay; intervention accelerates the decline.\n  const peakDistance = day - 14;\n  const preIntervention = 42 * Math.exp(-(peakDistance * peakDistance) / 60);\n  const decayBoost = day > LOCKDOWN_DAY ? Math.exp(-(day - LOCKDOWN_DAY) / 6) : 1;\n  const base = Math.max(0, preIntervention * decayBoost);\n\n  // Taper the post-intervention noise to zero so the resolved outbreak's tail\n  // reads as genuinely over, instead of a sporadic near-zero background rate.\n  const noiseScale = day > LOCKDOWN_DAY ? Math.max(0, 1 - (day - LOCKDOWN_DAY) / 25) : 1;\n  const total = Math.round(base + (lcg() - 0.5) * 6 * noiseScale);\n  const c = Math.max(0, Math.round(total * (0.55 + lcg() * 0.1)));\n  const p = Math.max(0, Math.round(total * (0.25 + lcg() * 0.1)));\n  const s = Math.max(0, total - c - p);\n\n  confirmed.push(c);\n  probable.push(p);\n  suspect.push(Math.max(0, s));\n}\n\nconst cumulative = [];\nlet running = 0;\nfor (let day = 0; day < DAYS; day++) {\n  running += confirmed[day] + probable[day] + suspect[day];\n  cumulative.push(running);\n}\n// Headroom on the secondary axis keeps the intervention markLine label clear\n// of the cumulative line's own inflection point near the top of the chart.\nconst cumulativeMax = Math.ceil((cumulative[cumulative.length - 1] * 1.2) / 50) * 50;\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: [t.palette[0], t.palette[1], t.palette[2]],\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"histogram-epidemic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    top: 50,\n    data: [\"Confirmed\", \"Probable\", \"Suspect\", \"Cumulative cases\"],\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  grid: { left: 90, right: 100, top: 110, bottom: 90 },\n  xAxis: {\n    type: \"category\",\n    data: dates,\n    name: \"Symptom onset date\",\n    nameLocation: \"middle\",\n    nameGap: 46,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, interval: 4, rotate: 0 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: [\n    {\n      type: \"value\",\n      name: \"New cases\",\n      nameLocation: \"middle\",\n      nameGap: 60,\n      nameTextStyle: { color: t.ink, fontSize: 16 },\n      axisLabel: { color: t.inkSoft, fontSize: 14 },\n      axisLine: { show: false },\n      axisTick: { show: false },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      type: \"value\",\n      name: \"Cumulative cases\",\n      nameLocation: \"middle\",\n      nameGap: 70,\n      max: cumulativeMax,\n      nameTextStyle: { color: t.ink, fontSize: 16 },\n      axisLabel: { color: t.inkSoft, fontSize: 14 },\n      axisLine: { show: false },\n      axisTick: { show: false },\n      splitLine: { show: false },\n    },\n  ],\n  series: [\n    {\n      name: \"Confirmed\",\n      type: \"bar\",\n      stack: \"cases\",\n      barCategoryGap: \"10%\",\n      data: confirmed,\n      itemStyle: { color: t.palette[0] },\n    },\n    {\n      name: \"Probable\",\n      type: \"bar\",\n      stack: \"cases\",\n      data: probable,\n      itemStyle: { color: t.palette[1] },\n    },\n    {\n      name: \"Suspect\",\n      type: \"bar\",\n      stack: \"cases\",\n      data: suspect,\n      itemStyle: { color: t.palette[2] },\n      markLine: {\n        symbol: \"none\",\n        silent: true,\n        label: {\n          formatter: \"Advisory issued\",\n          color: t.inkSoft,\n          fontSize: 13,\n          position: \"insideEndTop\",\n          distance: 6,\n        },\n        lineStyle: { color: t.amber, type: \"dashed\", width: 2 },\n        data: [{ xAxis: LOCKDOWN_DAY }],\n      },\n    },\n    {\n      name: \"Cumulative cases\",\n      type: \"line\",\n      yAxisIndex: 1,\n      data: cumulative,\n      symbol: \"none\",\n      lineStyle: { color: t.ink, width: 2.5, type: \"solid\" },\n      itemStyle: { color: t.ink },\n    },\n  ],\n});\n"}