{"spec_id":"line-annotated-events","library":"echarts","language":"javascript","code":"// anyplot.ai\n// line-annotated-events: Annotated Line Plot with Event Markers\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Daily active users over one year, with product-launch events boosting growth.\nlet lcgState = 20260905;\nfunction nextRandom() {\n  lcgState = (lcgState * 1103515245 + 12345) % 2147483648;\n  return lcgState / 2147483648;\n}\n\nconst START_DATE = new Date(\"2024-01-02T00:00:00Z\");\nconst NUM_DAYS = 252; // one trading year, business days only\nconst dates = [];\nfor (let i = 0, d = 0; d < NUM_DAYS; i++) {\n  const day = new Date(START_DATE.getTime() + i * 86400000);\n  const weekday = day.getUTCDay();\n  if (weekday !== 0 && weekday !== 6) {\n    dates.push(day);\n    d++;\n  }\n}\n\nconst events = [\n  { dayIndex: 35, label: \"v2.0 Launch\" },\n  { dayIndex: 90, label: \"Referral Program\", steep: true },\n  { dayIndex: 140, label: \"Mobile App Release\" },\n  { dayIndex: 175, label: \"Premium Tier\", steep: true },\n  { dayIndex: 225, label: \"Holiday Campaign\" },\n];\nconst eventDayIndices = new Set(events.map((e) => e.dayIndex));\n\nlet dau = 12000;\nconst series = [];\nfor (let i = 0; i < dates.length; i++) {\n  const trend = 18 + i * 0.15;\n  const noise = (nextRandom() - 0.5) * 220;\n  const boost = eventDayIndices.has(i) ? 900 + nextRandom() * 400 : 0;\n  dau = Math.max(8000, dau + trend + noise + boost);\n  series.push([dates[i].getTime(), Math.round(dau)]);\n}\n\n// Offset labels left of their marker and alternate above/below so they never\n// sit on top of the vertical dashed event line or the rising data line.\n// A label offset backward in time (negative x) lands over the pre-event\n// trace, which sits at roughly the same height as a \"below\" placement right\n// after a steep step — so steep events shift the label forward in time\n// instead, over the flatter post-event trace, to get real clearance.\nconst eventPoints = events.map((e, idx) => {\n  const magnitude = e.steep ? 40 : 34;\n  const below = idx % 2 !== 0;\n  return {\n    coord: [dates[e.dayIndex].getTime(), series[e.dayIndex][1]],\n    label: e.label,\n    offset: [e.steep && below ? 16 : -16, below ? magnitude : -magnitude],\n    align: e.steep && below ? \"left\" : \"right\",\n  };\n});\n\n// Shade a 10-business-day post-event window so viewers can see how quickly\n// each launch/campaign moved the trend, not just where it started.\nconst postEventWindows = events.map((e) => {\n  const endIdx = Math.min(dates.length - 1, e.dayIndex + 10);\n  return [{ xAxis: dates[e.dayIndex].getTime() }, { xAxis: dates[endIdx].getTime() }];\n});\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"line-annotated-events · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  tooltip: {\n    trigger: \"axis\",\n    valueFormatter: (v) => `${v.toLocaleString()} DAU`,\n  },\n  grid: { left: 100, right: 60, top: 110, bottom: 90 },\n  xAxis: {\n    type: \"time\",\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Daily Active Users (thousands)\",\n    nameLocation: \"middle\",\n    nameGap: 70,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: (v) => (v / 1000).toFixed(0) + \"k\" },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Daily Active Users\",\n      type: \"line\",\n      data: series,\n      showSymbol: false,\n      lineStyle: { width: 3, color: t.palette[0] },\n      itemStyle: { color: t.palette[0] },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        label: { show: false },\n        lineStyle: { type: \"dashed\", width: 1.5, color: t.palette[1] },\n        data: events.map((e) => ({ xAxis: dates[e.dayIndex].getTime() })),\n      },\n      markArea: {\n        silent: true,\n        itemStyle: { color: t.palette[1], opacity: 0.06 },\n        data: postEventWindows,\n      },\n      markPoint: {\n        symbol: \"circle\",\n        symbolSize: 12,\n        itemStyle: { color: t.palette[1] },\n        label: {\n          show: true,\n          formatter: (p) => p.data.label,\n          color: t.ink,\n          fontSize: 14,\n          fontWeight: 500,\n        },\n        data: eventPoints.map((p) => ({\n          coord: p.coord,\n          label: {\n            position: p.offset,\n            align: p.align,\n            verticalAlign: \"middle\",\n            formatter: () => p.label,\n          },\n        })),\n      },\n    },\n  ],\n});\n"}