{"spec_id":"streamgraph-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// streamgraph-basic: Basic Stream Graph\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 87/100 | Created: 2026-08-05\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Monthly subscriber base (millions) for 5 streaming platforms over 3 years —\n// some rise, some fade, one is flat, so the stack shows genuine hand-off\n// between layers instead of everything drifting in the same direction.\nconst platforms = [\"Streamly\", \"Wavecast\", \"Orbit\", \"Nimbus\", \"Pulse\"];\nconst monthCount = 36;\nconst monthLabels = [];\nfor (let m = 0; m < monthCount; m++) {\n  const year = 2022 + Math.floor(m / 12);\n  const label = new Date(Date.UTC(year, m % 12, 1)).toLocaleString(\"en-US\", {\n    month: \"short\",\n  });\n  monthLabels.push(`${label} '${String(year).slice(2)}`);\n}\n\n// Small deterministic LCG so the wobble is reproducible without a real RNG.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\n\nconst platformParams = [\n  { base: 28, trend: 0.75, amp: 6, phase: 0.3 }, // Streamly — steady grower\n  { base: 20, trend: 1.05, amp: 5, phase: 1.6 }, // Wavecast — fast riser\n  { base: 34, trend: -0.55, amp: 7, phase: 2.4 }, // Orbit — fading incumbent\n  { base: 14, trend: 0.1, amp: 8, phase: 4.2 }, // Nimbus — seasonal, flat trend\n  { base: 10, trend: 0.35, amp: 3, phase: 5.4 }, // Pulse — small, slow climb\n];\n\nconst seriesData = platformParams.map(() => []);\nfor (let m = 0; m < monthCount; m++) {\n  platformParams.forEach((p, i) => {\n    const seasonal = p.amp * Math.sin(m / 4.5 + p.phase);\n    const noise = (rand() - 0.5) * 2.5;\n    const value = Math.max(2, p.base + p.trend * m + seasonal + noise);\n    seriesData[i].push(Math.round(value * 10) / 10);\n  });\n}\n\n// --- Streamgraph geometry ------------------------------------------------\n// Only the core Highcharts bundle is loaded (no streamgraph/arearange\n// module), and plain `stacking: 'normal'` cannot combine a negative offset\n// series with positive-valued series into one continuous stack — it keeps\n// positive and negative stacks separate. So the true symmetric envelope is\n// computed by hand: `baseline[m]` is -total(m)/2 and `cumTop[i][m]` is the\n// running top edge after stacking platforms 0..i on top of that baseline.\n// Each platform is then drawn as its own NON-stacked areaspline reaching\n// from a shared low threshold up to cumTop[i], largest cumulative first so\n// each later (smaller-cumulative) layer opaquely nests over it — only the\n// thin strip between two consecutive cumulative curves stays visible, and\n// that strip is exactly that platform's value. A final background-colored\n// mask layer then erases the region below the wiggling baseline so the\n// bottom edge moves with the total too, not just the top.\nconst baseline = [];\nconst cumTop = platforms.map(() => []);\nfor (let m = 0; m < monthCount; m++) {\n  let total = 0;\n  for (let i = 0; i < seriesData.length; i++) total += seriesData[i][m];\n  const base = -total / 2;\n  baseline.push(base);\n  let running = base;\n  for (let i = 0; i < seriesData.length; i++) {\n    running += seriesData[i][m];\n    cumTop[i].push(Math.round(running * 100) / 100);\n  }\n}\nconst lowThreshold = Math.min(...baseline) - 20;\n\n// zIndex (not array order) controls paint order here: the platform with the\n// largest cumulative (i = platforms.length - 1, the running total of all of\n// them) must paint behind everything else, and each smaller cumulative\n// paints progressively in front of it, ending with platform 0 frontmost.\nconst bandSeries = platforms.map((name, i) => ({\n  name,\n  color: t.palette[i],\n  data: cumTop[i].map((y, m) => ({ y, custom: { actual: seriesData[i][m] } })),\n  zIndex: platforms.length - i,\n}));\n\nconst maskSeries = {\n  name: \"__baseline_mask__\",\n  data: baseline,\n  color: t.pageBg,\n  lineWidth: 0,\n  fillOpacity: 1,\n  enableMouseTracking: false,\n  showInLegend: false,\n  zIndex: platforms.length + 1,\n};\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"areaspline\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"streamgraph-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Monthly subscriber base by streaming platform (millions)\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    categories: monthLabels,\n    tickInterval: 3,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: \"transparent\",\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    title: {\n      text: \"Month (2022–2024)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n  },\n  yAxis: {\n    visible: false,\n    startOnTick: false,\n    endOnTick: false,\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    shared: false,\n    formatter: function () {\n      return `<b>${this.x}</b><br/><span style=\"color:${this.color}\">●</span> ${this.series.name}: <b>${this.point.custom.actual}M</b> subscribers`;\n    },\n  },\n  plotOptions: {\n    series: {\n      animation: false,\n      marker: { enabled: false },\n      lineWidth: 1.5,\n      lineColor: t.pageBg,\n      threshold: lowThreshold,\n      fillOpacity: 1,\n    },\n  },\n  series: [...bandSeries, maskSeries],\n});\n"}