{"spec_id":"timeseries-forecast-uncertainty","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// timeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG — no seeded RNG in the browser) ----\nlet seed = 42;\nfunction nextRandom() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst HIST_MONTHS = 36; // 3 years of monthly history\nconst FORECAST_MONTHS = 12; // 1-year-ahead forecast\nconst FORECAST_START_INDEX = HIST_MONTHS - 1; // forecast overlaps the last actual point\nconst TOTAL_MONTHS = HIST_MONTHS + FORECAST_MONTHS;\nconst START_YEAR = 2023;\nconst START_MONTH = 8; // September, 0-indexed\n\nconst timestamps = [];\nfor (let i = 0; i < TOTAL_MONTHS; i++) {\n  timestamps.push(Date.UTC(START_YEAR, START_MONTH + i, 1));\n}\n\nconst BASELINE = 110; // thousand sessions\nconst GROWTH_PER_MONTH = 1.6;\nconst SEASONAL_AMPLITUDE = 18;\n\nconst actualSeries = [];\nconst forecastSeries = [];\nconst base80 = [];\nconst band80 = [];\nconst base95 = [];\nconst band95 = [];\n\nfor (let i = 0; i < TOTAL_MONTHS; i++) {\n  const trend = BASELINE + GROWTH_PER_MONTH * i;\n  const seasonal = SEASONAL_AMPLITUDE * Math.sin((2 * Math.PI * (i % 12)) / 12 - Math.PI / 2);\n  const expected = trend + seasonal;\n  const ts = timestamps[i];\n\n  if (i <= FORECAST_START_INDEX) {\n    const noise = (nextRandom() - 0.5) * 14;\n    actualSeries.push([ts, Math.round((expected + noise) * 10) / 10]);\n  } else {\n    actualSeries.push([ts, null]);\n  }\n\n  if (i >= FORECAST_START_INDEX) {\n    const horizon = i - FORECAST_START_INDEX;\n    const point = Math.round(expected * 10) / 10;\n    forecastSeries.push([ts, point]);\n\n    const spread80 = 6 + 2.4 * Math.sqrt(horizon + 1);\n    const spread95 = 10 + 4.1 * Math.sqrt(horizon + 1);\n    base80.push([ts, Math.round((point - spread80) * 10) / 10]);\n    band80.push([ts, Math.round(spread80 * 2 * 10) / 10]);\n    base95.push([ts, Math.round((point - spread95) * 10) / 10]);\n    band95.push([ts, Math.round(spread95 * 2 * 10) / 10]);\n  } else {\n    forecastSeries.push([ts, null]);\n    base80.push([ts, null]);\n    band80.push([ts, null]);\n    base95.push([ts, null]);\n    band95.push([ts, null]);\n  }\n}\n\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst actualColor = t.palette[0]; // brand green — always first series\nconst forecastColor = t.palette[1]; // canonical position 2\nconst band80Color = hexToRgba(forecastColor, 0.38); // 80% CI — darker, tighter band\nconst band95Color = hexToRgba(forecastColor, 0.2); // 95% CI — lighter, wider band\n\n// --- Chart -------------------------------------------------------------------\nconst TITLE = \"timeseries-forecast-uncertainty · javascript · highcharts · anyplot.ai\";\nconst TITLE_FONTSIZE = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: TITLE,\n    style: { color: t.ink, fontSize: `${TITLE_FONTSIZE}px`, fontWeight: \"600\" },\n  },\n  xAxis: {\n    type: \"datetime\",\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotLines: [\n      {\n        value: timestamps[FORECAST_START_INDEX],\n        color: t.inkSoft,\n        width: 2,\n        dashStyle: \"Dash\",\n        zIndex: 5,\n        label: {\n          text: \"Forecast start\",\n          rotation: 0,\n          y: 20,\n          style: { color: t.inkSoft, fontSize: \"13px\" },\n        },\n      },\n    ],\n  },\n  yAxis: {\n    reversedStacks: false, // keep the invisible base series at the bottom of the stack\n    title: {\n      text: \"Website sessions (thousands)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  tooltip: {\n    shared: true,\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    style: { color: t.ink, fontSize: \"13px\" },\n    valueSuffix: \"k\",\n    xDateFormat: \"%b %Y\",\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false },\n    line: { marker: { enabled: false } },\n    area: { stacking: \"normal\", marker: { enabled: false }, animation: false },\n  },\n  series: [\n    {\n      name: \"95% confidence\",\n      type: \"area\",\n      data: base95,\n      stack: \"ci95\",\n      color: \"transparent\",\n      lineWidth: 0,\n      fillOpacity: 0,\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"95% confidence\",\n      type: \"area\",\n      data: band95,\n      stack: \"ci95\",\n      color: band95Color,\n      lineWidth: 0,\n      enableMouseTracking: false,\n      legendIndex: 3,\n    },\n    {\n      name: \"80% confidence\",\n      type: \"area\",\n      data: base80,\n      stack: \"ci80\",\n      color: \"transparent\",\n      lineWidth: 0,\n      fillOpacity: 0,\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"80% confidence\",\n      type: \"area\",\n      data: band80,\n      stack: \"ci80\",\n      color: band80Color,\n      lineWidth: 0,\n      enableMouseTracking: false,\n      legendIndex: 2,\n    },\n    {\n      name: \"Historical\",\n      type: \"line\",\n      data: actualSeries,\n      color: actualColor,\n      lineWidth: 3,\n      legendIndex: 0,\n    },\n    {\n      name: \"Forecast\",\n      type: \"line\",\n      data: forecastSeries,\n      color: forecastColor,\n      lineWidth: 2.5,\n      dashStyle: \"Dash\",\n      legendIndex: 1,\n    },\n  ],\n});\n"}