{"spec_id":"range-interval","library":"d3","language":"javascript","code":"// anyplot.ai\n// range-interval: Range Interval Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-02\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: monthly temperature range for a temperate city (deg C) ----------\n// Range widths vary (4-11 deg) to show volatile shoulder-season swings (Mar/Apr)\n// against calmer winter and mid-summer stretches.\nconst months = [\n  { label: \"Jan\", min: -2, max: 2, mean: 0.3 },\n  { label: \"Feb\", min: -1, max: 5, mean: 2.4 },\n  { label: \"Mar\", min: 2, max: 12, mean: 7.6 },\n  { label: \"Apr\", min: 6, max: 17, mean: 12.3 },\n  { label: \"May\", min: 11, max: 20, mean: 16.2 },\n  { label: \"Jun\", min: 15, max: 23, mean: 19.4 },\n  { label: \"Jul\", min: 18, max: 26, mean: 22.3 },\n  { label: \"Aug\", min: 18, max: 25, mean: 21.8 },\n  { label: \"Sep\", min: 13, max: 20, mean: 16.6 },\n  { label: \"Oct\", min: 7, max: 14, mean: 10.9 },\n  { label: \"Nov\", min: 2, max: 8, mean: 5.3 },\n  { label: \"Dec\", min: -1, max: 3, mean: 1.1 },\n];\nconst coldest = months.reduce((a, b) => (b.min < a.min ? b : a));\nconst warmest = months.reduce((a, b) => (b.max > a.max ? b : a));\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.scaleBand().domain(months.map((d) => d.label)).range([0, iw]).padding(0.35);\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min(months, (d) => d.min), d3.max(months, (d) => d.max)])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y 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// --- Mean trend curve (smoothed d3-shape overlay linking monthly means) --------\nconst meanLine = d3\n  .line()\n  .x((d) => x(d.label) + x.bandwidth() / 2)\n  .y((d) => y(d.mean))\n  .curve(d3.curveCatmullRom.alpha(0.5));\n\ng.append(\"path\")\n  .datum(months)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"2,5\")\n  .attr(\"stroke-opacity\", 0.3)\n  .attr(\"d\", meanLine);\n\n// --- Range bars -----------------------------------------------------------------\ng.selectAll(\"rect.range\")\n  .data(months)\n  .join(\"rect\")\n  .attr(\"class\", \"range\")\n  .attr(\"x\", (d) => x(d.label))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"y\", (d) => y(d.max))\n  .attr(\"height\", (d) => y(d.min) - y(d.max))\n  .attr(\"rx\", 4)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.55)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 1.5);\n\n// --- Endpoint markers (min / max emphasis) --------------------------------------\nfor (const key of [\"min\", \"max\"]) {\n  g.selectAll(`circle.${key}`)\n    .data(months)\n    .join(\"circle\")\n    .attr(\"class\", key)\n    .attr(\"cx\", (d) => x(d.label) + x.bandwidth() / 2)\n    .attr(\"cy\", (d) => y(d[key]))\n    .attr(\"r\", 7)\n    .attr(\"fill\", t.palette[0])\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 2.5);\n}\n\n// --- Extreme-month emphasis (coldest & warmest, sharpens the seasonal story) ----\nfor (const [d, key, label] of [\n  [coldest, \"min\", \"Coldest\"],\n  [warmest, \"max\", \"Warmest\"],\n]) {\n  const cx = x(d.label) + x.bandwidth() / 2;\n  const cy = y(d[key]);\n  g.append(\"circle\")\n    .attr(\"cx\", cx)\n    .attr(\"cy\", cy)\n    .attr(\"r\", 11)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.palette[0])\n    .attr(\"stroke-width\", 2)\n    .attr(\"stroke-opacity\", 0.6);\n  g.append(\"text\")\n    .attr(\"x\", cx)\n    .attr(\"y\", cy - 20)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .style(\"font-weight\", \"600\")\n    .text(label);\n}\n\n// --- Midpoint reference tick -----------------------------------------------------\ng.selectAll(\"line.mean\")\n  .data(months)\n  .join(\"line\")\n  .attr(\"class\", \"mean\")\n  .attr(\"x1\", (d) => x(d.label) + x.bandwidth() * 0.18)\n  .attr(\"x2\", (d) => x(d.label) + x.bandwidth() * 0.82)\n  .attr(\"y1\", (d) => y(d.mean))\n  .attr(\"y2\", (d) => y(d.mean))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-opacity\", 0.55)\n  .attr(\"stroke-width\", 2);\n\n// --- Axes ------------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickFormat((d) => `${d}°`));\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.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 + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\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\", \"16px\")\n  .text(\"Temperature (°C)\");\n\n// --- Title -------------------------------------------------------------------------\nconst title = \"Monthly Temperature Range · range-interval · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));\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"}