{"spec_id":"band-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// band-basic: Basic Band Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 70, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: 60-day streamflow forecast with a widening 90% prediction band --\n// Deterministic LCG so the \"uncertainty\" wiggle is reproducible without Math.random().\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\n\nconst days = 60;\nconst data = Array.from({ length: days }, (_, i) => {\n  const day = i + 1;\n  const seasonal = 8 * Math.sin((2 * Math.PI * day) / 45);\n  const trend = 0.15 * day;\n  const wiggle = (lcg() - 0.5) * 3;\n  const yCenter = 42 + trend + seasonal + wiggle;\n  // Forecast uncertainty grows the further out the prediction reaches.\n  const spread = 3 + 0.35 * day;\n  return { day, yCenter, yLower: yCenter - spread, yUpper: yCenter + spread };\n});\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// --- Scales --------------------------------------------------------------------\nconst x = d3\n  .scaleLinear()\n  .domain(d3.extent(data, (d) => d.day))\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min(data, (d) => d.yLower) - 3, d3.max(data, (d) => d.yUpper) + 3])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y-axis only) ---------------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Band (semi-transparent prediction interval, soft vertical gradient) -------\n// A single-hue gradient (still built only from t.palette[0]) that fades from the\n// band's outer edges toward its vertical middle, echoing the center trend line.\nconst bandGradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"bandGradient\")\n  .attr(\"x1\", \"0\")\n  .attr(\"x2\", \"0\")\n  .attr(\"y1\", \"0\")\n  .attr(\"y2\", \"1\");\nbandGradient.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.16);\nbandGradient.append(\"stop\").attr(\"offset\", \"50%\").attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.36);\nbandGradient.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.16);\n\nconst area = d3\n  .area()\n  .x((d) => x(d.day))\n  .y0((d) => y(d.yLower))\n  .y1((d) => y(d.yUpper))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(data).attr(\"d\", area).attr(\"fill\", \"url(#bandGradient)\").attr(\"stroke\", \"none\");\n\n// --- Center trend line -----------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.day))\n  .y((d) => y(d.yCenter))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(data)\n  .attr(\"d\", line)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-linejoin\", \"round\")\n  .attr(\"stroke-linecap\", \"round\");\n\n// --- Insight annotation: the forecast band widens sharply with lead time -------\nconst first = data[0];\nconst last = data[data.length - 1];\nconst widenFactor = (last.yUpper - last.yLower) / (first.yUpper - first.yLower);\ng.append(\"line\")\n  .attr(\"x1\", x(last.day))\n  .attr(\"x2\", x(last.day))\n  .attr(\"y1\", y(last.yLower))\n  .attr(\"y2\", y(last.yUpper))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"4,3\");\ng.append(\"text\")\n  .attr(\"x\", x(last.day) - 14)\n  .attr(\"y\", y(last.yUpper) - 12)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-style\", \"italic\")\n  .text(`Spread widens ~${widenFactor.toFixed(1)}× from day 1 to day ${last.day}`);\n\n// --- Axes -------------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(10).tickFormat((d) => `Day ${d}`));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(8).tickFormat((d) => `${d.toFixed(0)} m³/s`));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels --------------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Forecast Day\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"River Streamflow (m³/s)\");\n\n// --- Interactive hover: guide line + day/lower/center/upper readout ------------\n// Genuine D3 interactivity for the HTML detail view; hidden by default so the\n// static PNG (no synthetic mouse events) is unaffected.\nconst bisectDay = d3.bisector((d) => d.day).left;\nconst hoverGroup = g.append(\"g\").style(\"opacity\", 0);\nconst hoverLine = hoverGroup\n  .append(\"line\")\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1)\n  .attr(\"stroke-dasharray\", \"3,3\");\nconst hoverDotLower = hoverGroup.append(\"circle\").attr(\"r\", 4).attr(\"fill\", t.pageBg).attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 2);\nconst hoverDotCenter = hoverGroup.append(\"circle\").attr(\"r\", 5).attr(\"fill\", t.palette[0]);\nconst hoverDotUpper = hoverGroup.append(\"circle\").attr(\"r\", 4).attr(\"fill\", t.pageBg).attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 2);\nconst hoverText = hoverGroup.append(\"text\").attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"600\");\n\ng.append(\"rect\")\n  .attr(\"width\", iw)\n  .attr(\"height\", ih)\n  .attr(\"fill\", \"none\")\n  .attr(\"pointer-events\", \"all\")\n  .on(\"mousemove\", function (event) {\n    const [mx] = d3.pointer(event, this);\n    const day = x.invert(mx);\n    let i = bisectDay(data, day, 1);\n    i = Math.min(Math.max(i, 1), data.length - 1);\n    const d0 = data[i - 1];\n    const d1 = data[i];\n    const d = day - d0.day > d1.day - day ? d1 : d0;\n    const px = x(d.day);\n    const labelOnRight = px < iw - 210;\n    const lx = labelOnRight ? px + 14 : px - 14;\n\n    hoverLine.attr(\"x1\", px).attr(\"x2\", px);\n    hoverDotLower.attr(\"cx\", px).attr(\"cy\", y(d.yLower));\n    hoverDotCenter.attr(\"cx\", px).attr(\"cy\", y(d.yCenter));\n    hoverDotUpper.attr(\"cx\", px).attr(\"cy\", y(d.yUpper));\n\n    hoverText.selectAll(\"tspan\").remove();\n    hoverText.attr(\"text-anchor\", labelOnRight ? \"start\" : \"end\");\n    [`Day ${d.day}`, `Upper ${d.yUpper.toFixed(1)} m³/s`, `Center ${d.yCenter.toFixed(1)} m³/s`, `Lower ${d.yLower.toFixed(1)} m³/s`].forEach(\n      (lineText, idx) => {\n        hoverText\n          .append(\"tspan\")\n          .attr(\"x\", lx)\n          .attr(\"y\", 24 + idx * 18)\n          .text(lineText);\n      }\n    );\n\n    hoverGroup.style(\"opacity\", 1);\n  })\n  .on(\"mouseleave\", () => hoverGroup.style(\"opacity\", 0));\n\n// --- Title ------------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"24px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Streamflow Forecast · band-basic · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Shaded band shows the 90% prediction interval around the forecast mean\");\n"}