{"spec_id":"line-confidence","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-confidence: Line Plot with Confidence Interval\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 60, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: monthly demand forecast with a 90% prediction interval ----------\n// Small fixed-seed LCG so the \"noise\" around the forecast is reproducible.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst months = 24;\nconst data = [];\nfor (let i = 0; i < months; i += 1) {\n  const trend = 480 + 9.5 * i;\n  const seasonal = 40 * Math.sin((2 * Math.PI * i) / 12);\n  const forecast = trend + seasonal;\n  const spread = 25 + 2.2 * i + 30 * (lcg() - 0.5);\n  data.push({\n    month: i,\n    forecast,\n    lower: forecast - Math.abs(spread),\n    upper: forecast + Math.abs(spread),\n  });\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([0, months - 1])\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min(data, (d) => d.lower) - 20, d3.max(data, (d) => d.upper) + 20])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y-axis only) --------------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Confidence band -----------------------------------------------------------\n// Gradient fill (subtle → stronger opacity left-to-right) makes the growing\n// forecast uncertainty a first-class visual cue, not just an implicit shape.\nconst bandGradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"band-gradient\")\n  .attr(\"gradientUnits\", \"userSpaceOnUse\")\n  .attr(\"x1\", x(0))\n  .attr(\"x2\", x(months - 1))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", 0);\nbandGradient.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.12);\nbandGradient.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.38);\n\nconst band = d3\n  .area()\n  .x((d) => x(d.month))\n  .y0((d) => y(d.lower))\n  .y1((d) => y(d.upper))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(data).attr(\"d\", band).attr(\"fill\", \"url(#band-gradient)\").attr(\"stroke\", \"none\");\n\n// --- Central forecast line -----------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.month))\n  .y((d) => y(d.forecast))\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\", 4)\n  .attr(\"stroke-linejoin\", \"round\")\n  .attr(\"stroke-linecap\", \"round\");\n\n// --- Axes ------------------------------------------------------------------\nconst monthLabels = [\"Jan\", \"Apr\", \"Jul\", \"Oct\"];\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .tickValues(d3.range(0, months, 3))\n      .tickFormat((d) => `${monthLabels[(d / 3) % 4]} ${d < 12 ? \"'25\" : \"'26\"}`),\n  );\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat((d) => d3.format(\",\")(d)));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\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 + 66)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Month\");\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\", \"18px\")\n  .text(\"Units Sold (forecast)\");\n\n// --- Annotation: call out the widening uncertainty ----------------------------\nconst annotationPoint = data[months - 5];\nconst annoX = x(annotationPoint.month);\nconst annoTopY = y(annotationPoint.upper);\nconst labelY = annoTopY - 46;\n\ng.append(\"line\")\n  .attr(\"x1\", annoX)\n  .attr(\"y1\", labelY + 14)\n  .attr(\"x2\", annoX)\n  .attr(\"y2\", annoTopY - 6)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"3,3\");\n\ng.append(\"text\")\n  .attr(\"x\", annoX)\n  .attr(\"y\", labelY)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Interval widens as the forecast horizon grows\");\n\n// --- Legend ------------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${margin.left + 10},${margin.top - 22})`);\n\nlegend.append(\"line\").attr(\"x1\", 0).attr(\"x2\", 36).attr(\"y1\", 0).attr(\"y2\", 0).attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 4);\nlegend\n  .append(\"text\")\n  .attr(\"x\", 46)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Forecast\");\n\nlegend\n  .append(\"rect\")\n  .attr(\"x\", 180)\n  .attr(\"y\", -9)\n  .attr(\"width\", 36)\n  .attr(\"height\", 18)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.25)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 1);\nlegend\n  .append(\"text\")\n  .attr(\"x\", 226)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"90% Prediction Interval\");\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"40px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-confidence · javascript · d3 · anyplot.ai\");\n"}