{"spec_id":"contour-density","library":"d3","language":"javascript","code":"// anyplot.ai\n// contour-density: Density Contour Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-04\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data ---------------------------------------------------------------\n// Geyser eruption duration vs. waiting time to the next eruption — a classic\n// bimodal bivariate pattern (short/quick eruptions vs. long/slow ones) that\n// makes multi-level density contours meaningful. Deterministic LCG + Box-Muller\n// stand in for a seeded RNG (the browser has none).\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction gaussian() {\n  const u1 = Math.max(lcg(), 1e-9);\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst points = [];\nconst n = 320;\nfor (let i = 0; i < n; i++) {\n  const isShortBurst = lcg() < 0.62;\n  const duration = isShortBurst ? 1.9 + gaussian() * 0.28 : 4.35 + gaussian() * 0.38;\n  const wait = isShortBurst\n    ? 54 + duration * 3.2 + gaussian() * 4.5\n    : 76 + duration * 2.4 + gaussian() * 5;\n  points.push({ duration, wait });\n}\n\n// --- SVG mount ------------------------------------------------------------\nconst margin = { top: 100, right: 220, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\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 durationExtent = d3.extent(points, (d) => d.duration);\nconst waitExtent = d3.extent(points, (d) => d.wait);\nconst durationPad = (durationExtent[1] - durationExtent[0]) * 0.12;\nconst waitPad = (waitExtent[1] - waitExtent[0]) * 0.12;\n\nconst x = d3\n  .scaleLinear()\n  .domain([durationExtent[0] - durationPad, durationExtent[1] + durationPad])\n  .range([0, iw])\n  .nice();\nconst y = d3\n  .scaleLinear()\n  .domain([waitExtent[0] - waitPad, waitExtent[1] + waitPad])\n  .range([ih, 0])\n  .nice();\n\n// --- Background grid (subtle, sits behind the contour fill) ---------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .selectAll(\"line.x-grid\")\n  .data(x.ticks(7))\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-opacity\", 0.4);\n\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .selectAll(\"line.y-grid\")\n  .data(y.ticks(7))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-opacity\", 0.4);\n\n// --- Density contours ---------------------------------------------------\nconst densityData = d3\n  .contourDensity()\n  .x((d) => x(d.duration))\n  .y((d) => y(d.wait))\n  .size([iw, ih])\n  .bandwidth(32)\n  .thresholds(16)(points);\n\nconst densityExtent = d3.extent(densityData, (d) => d.value);\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain(densityExtent);\n\ng.append(\"g\")\n  .selectAll(\"path\")\n  .data(densityData)\n  .join(\"path\")\n  .attr(\"d\", d3.geoPath())\n  .attr(\"fill\", (d) => color(d.value))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.5)\n  .attr(\"stroke-opacity\", 0.6);\n\n// --- Scatter overlay (context for the underlying point cloud) --------------\ng.append(\"g\")\n  .selectAll(\"circle\")\n  .data(points)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.duration))\n  .attr(\"cy\", (d) => y(d.wait))\n  .attr(\"r\", 2.75)\n  .attr(\"fill\", t.ink)\n  .attr(\"fill-opacity\", 0.35)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.6)\n  .attr(\"stroke-opacity\", 0.5);\n\n// --- Axes ---------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(7));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(7));\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\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\", \"18px\")\n  .text(\"Eruption Duration (minutes)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -72)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Waiting Time to Next Eruption (minutes)\");\n\n// --- Title ----------------------------------------------------------------\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\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"contour-density · javascript · d3 · anyplot.ai\");\n\n// --- Density legend (vertical gradient) ------------------------------------\nconst legendWidth = 22;\nconst legendHeight = ih * 0.6;\nconst legendX = margin.left + iw + 60;\nconst legendY = margin.top + (ih - legendHeight) / 2;\n\nconst gradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"density-gradient\")\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"y2\", \"0%\");\n\nconst gradientStops = 10;\nfor (let i = 0; i <= gradientStops; i++) {\n  const fraction = i / gradientStops;\n  gradient\n    .append(\"stop\")\n    .attr(\"offset\", `${fraction * 100}%`)\n    .attr(\"stop-color\", color(densityExtent[0] + (densityExtent[1] - densityExtent[0]) * fraction));\n}\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY)\n  .attr(\"width\", legendWidth)\n  .attr(\"height\", legendHeight)\n  .attr(\"fill\", \"url(#density-gradient)\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendWidth / 2)\n  .attr(\"y\", legendY - 16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"High\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendWidth / 2)\n  .attr(\"y\", legendY + legendHeight + 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Low\");\n\nsvg\n  .append(\"text\")\n  .attr(\n    \"transform\",\n    `translate(${legendX + legendWidth + 26}, ${legendY + legendHeight / 2}) rotate(90)`\n  )\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Point density\");\n"}