{"spec_id":"density-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// density-basic: Basic Density Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 70, bottom: 100, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: marathon finish times (minutes), mixture of competitive and\n// recreational runners so the density curve shows a subtle bimodal shape ---\nfunction lcg(seed) {\n  let state = seed % 2147483647;\n  if (state <= 0) state += 2147483646;\n  return function () {\n    state = (state * 16807) % 2147483647;\n    return (state - 1) / 2147483646;\n  };\n}\nconst rand = lcg(42);\nfunction randomNormal(mean, sd) {\n  const u1 = rand();\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * sd;\n}\n\nconst finishTimes = [];\nfor (let i = 0; i < 600; i++) {\n  const isCompetitive = rand() < 0.3;\n  const time = isCompetitive ? randomNormal(195, 15) : randomNormal(258, 32);\n  finishTimes.push(Math.max(120, time));\n}\n\n// --- Kernel density estimation (Gaussian kernel, Silverman bandwidth) ------\nconst meanTime = d3.mean(finishTimes);\nconst stdTime = d3.deviation(finishTimes);\nconst bandwidth = 1.06 * stdTime * Math.pow(finishTimes.length, -1 / 5);\n\nfunction kernelGaussian(bw) {\n  return (v) => Math.exp(-0.5 * (v / bw) ** 2) / (bw * Math.sqrt(2 * Math.PI));\n}\nfunction kde(kernel, sample, xValues) {\n  return xValues.map((x) => [x, d3.mean(sample, (v) => kernel(x - v))]);\n}\n\nconst domainMin = d3.min(finishTimes) - 3 * bandwidth;\nconst domainMax = d3.max(finishTimes) + 3 * bandwidth;\nconst grid = d3.range(domainMin, domainMax, (domainMax - domainMin) / 400);\nconst density = kde(kernelGaussian(bandwidth), finishTimes, grid);\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.scaleLinear().domain([domainMin, domainMax]).range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(density, (d) => d[1]) * 1.15])\n  .range([ih, 0]);\n\n// --- Y grid (subtle, y-axis only) --------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).ticks(5).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid).attr(\"stroke-opacity\", 0.6));\n\n// --- Area + line --------------------------------------------------------------\n// Vertical fill gradient (denser near the curve, fading toward the baseline)\n// for more visual depth than a flat fill-opacity.\nconst gradientId = \"density-fill-gradient\";\nsvg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0%\")\n  .attr(\"y1\", \"0%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y2\", \"100%\")\n  .call((grad) => {\n    grad.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.45);\n    grad.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.palette[0]).attr(\"stop-opacity\", 0.05);\n  });\n\nconst area = d3\n  .area()\n  .x((d) => x(d[0]))\n  .y0(ih)\n  .y1((d) => y(d[1]));\n\nconst line = d3\n  .line()\n  .x((d) => x(d[0]))\n  .y((d) => y(d[1]));\n\ng.append(\"path\").datum(density).attr(\"d\", area).attr(\"fill\", `url(#${gradientId})`);\ng.append(\"path\")\n  .datum(density)\n  .attr(\"d\", line)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3);\n\n// --- Rug plot: individual observations along the baseline --------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(finishTimes)\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", ih)\n  .attr(\"y2\", ih - 12)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 1.4)\n  .attr(\"stroke-opacity\", 0.5);\n\n// --- Peak annotations: call out the two-population (bimodal) shape ----------\nconst maxDensity = d3.max(density, (d) => d[1]);\nconst localMaxima = [];\nfor (let i = 1; i < density.length - 1; i++) {\n  const dCur = density[i][1];\n  if (dCur > density[i - 1][1] && dCur > density[i + 1][1] && dCur > maxDensity * 0.3) {\n    localMaxima.push(density[i]);\n  }\n}\nconst peaks = localMaxima\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 2)\n  .sort((a, b) => a[0] - b[0]);\nconst peakLabels = [\"Competitive finishers\", \"Recreational finishers\"];\n\nconst peakGroup = g.append(\"g\");\npeaks.forEach(([px, py], i) => {\n  const cx = x(px);\n  const cy = y(py);\n  peakGroup\n    .append(\"circle\")\n    .attr(\"cx\", cx)\n    .attr(\"cy\", cy)\n    .attr(\"r\", 5)\n    .attr(\"fill\", t.amber)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 2);\n  if (peakLabels[i]) {\n    peakGroup\n      .append(\"text\")\n      .attr(\"x\", cx)\n      .attr(\"y\", cy - 16)\n      .attr(\"text-anchor\", \"middle\")\n      .attr(\"fill\", t.inkSoft)\n      .style(\"font-size\", \"13px\")\n      .style(\"font-weight\", \"600\")\n      .text(peakLabels[i]);\n  }\n});\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(\".3f\")));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.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(\"Marathon Finish Time (minutes)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Density\");\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\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"density-basic · javascript · d3 · anyplot.ai\");\n"}