{"spec_id":"streamgraph-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// streamgraph-basic: Basic Stream Graph\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-08-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 190, bottom: 60, left: 30 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: monthly streaming hours by music genre, 2024-2025 (deterministic) --\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst months = d3.range(24).map((i) => new Date(2024, i, 1));\n\nconst genreConfig = [\n  { name: \"Pop\", base: 55, amp: 18, phase: 0.3, trend: 0 },\n  { name: \"Hip-Hop\", base: 42, amp: 14, phase: 1.8, trend: 0.6 },\n  { name: \"Electronic\", base: 30, amp: 16, phase: 2.6, trend: 0.5 },\n  { name: \"Rock\", base: 34, amp: 9, phase: 0.9, trend: -0.35 },\n  { name: \"Jazz\", base: 16, amp: 5, phase: 4.0, trend: 0.05 },\n  { name: \"Classical\", base: 11, amp: 3, phase: 5.2, trend: -0.05 },\n];\nconst genres = genreConfig.map((g) => g.name);\nconst freq = (2 * Math.PI) / 12;\n\nconst data = months.map((date, i) => {\n  const row = { date };\n  for (const g of genreConfig) {\n    const seasonal = Math.sin(i * freq + g.phase) * g.amp;\n    const noise = (rand() - 0.5) * 4;\n    row[g.name] = Math.max(2, g.base + seasonal + g.trend * i + noise);\n  }\n  return row;\n});\n\n// --- Stack (wiggle offset = symmetric streamgraph baseline) ------------------\nconst stack = d3\n  .stack()\n  .keys(genres)\n  .order(d3.stackOrderInsideOut)\n  .offset(d3.stackOffsetWiggle);\nconst series = stack(data);\n\nconst x = d3.scaleTime().domain(d3.extent(months)).range([0, iw]);\nconst yExtent = d3.extent(series.flatMap((s) => s.flatMap((d) => [d[0], d[1]])));\nconst y = d3.scaleLinear().domain(yExtent).range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(genres).range(t.palette.slice(0, genres.length));\n\nconst area = d3\n  .area()\n  .curve(d3.curveBasis)\n  .x((d) => x(d.data.date))\n  .y0((d) => y(d[0]))\n  .y1((d) => y(d[1]));\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// --- Streams --------------------------------------------------------------\ng.selectAll(\"path.layer\")\n  .data(series)\n  .join(\"path\")\n  .attr(\"class\", \"layer\")\n  .attr(\"fill\", (d) => color(d.key))\n  .attr(\"d\", area);\n\n// --- Time axis (no value axis — the wiggle baseline has no meaningful zero) --\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih + 16})`)\n  .call(d3.axisBottom(x).ticks(d3.timeMonth.every(3)).tickFormat(d3.timeFormat(\"%b %Y\")).tickSizeOuter(0));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Legend -----------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${width - margin.right + 34}, ${margin.top + 20})`);\ngenres.forEach((name, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0, ${i * 34})`);\n  row.append(\"rect\").attr(\"width\", 18).attr(\"height\", 18).attr(\"rx\", 3).attr(\"fill\", color(name));\n  row\n    .append(\"text\")\n    .attr(\"x\", 26)\n    .attr(\"y\", 14)\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"15px\")\n    .text(name);\n});\n\n// --- Title --------------------------------------------------------------------\nconst title = \"Music Genre Streaming · streamgraph-basic · javascript · d3 · anyplot.ai\";\nconst ratio = title.length > 67 ? 67 / title.length : 1.0;\nconst titleFontSize = Math.max(14, Math.round(22 * ratio));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}