{"spec_id":"ridgeline-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// ridgeline-basic: Basic Ridgeline Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-07-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Monthly average daily temperature distributions for a temperate city.\nconst MONTHS = [\n  { name: \"Jan\", mean: -1, std: 4.2 },\n  { name: \"Feb\", mean: 1, std: 4.0 },\n  { name: \"Mar\", mean: 6, std: 4.3 },\n  { name: \"Apr\", mean: 12, std: 4.0 },\n  { name: \"May\", mean: 17, std: 3.6 },\n  { name: \"Jun\", mean: 21, std: 3.2 },\n  { name: \"Jul\", mean: 24, std: 3.0 },\n  { name: \"Aug\", mean: 23, std: 3.1 },\n  { name: \"Sep\", mean: 18, std: 3.5 },\n  { name: \"Oct\", mean: 12, std: 3.9 },\n  { name: \"Nov\", mean: 5, std: 4.1 },\n  { name: \"Dec\", mean: 0, std: 4.3 },\n];\nconst OBS_PER_GROUP = 130;\n\n// Fixed-seed LCG — the browser has no seeded RNG.\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\nconst randNormal = () => {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n};\n\nconst groupsRaw = MONTHS.map((m) => ({\n  name: m.name,\n  mean: m.mean,\n  samples: Array.from({ length: OBS_PER_GROUP }, () => m.mean + m.std * randNormal()),\n}));\n\n// --- Kernel density estimation (Gaussian kernel, Silverman bandwidth) -------\nconst kde = (samples, xGrid, bandwidth) => {\n  const norm = 1 / (samples.length * bandwidth * Math.sqrt(2 * Math.PI));\n  return xGrid.map((xi) => {\n    let sum = 0;\n    for (const s of samples) {\n      const u = (xi - s) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum * norm;\n  });\n};\n\nconst silvermanBandwidth = (samples) => {\n  const n = samples.length;\n  const mean = d3.mean(samples);\n  const variance = d3.sum(samples, (v) => (v - mean) ** 2) / (n - 1);\n  return 1.06 * Math.sqrt(variance) * n ** (-1 / 5);\n};\n\nconst allSamples = groupsRaw.flatMap((g) => g.samples);\nconst xMin = d3.min(allSamples) - 4;\nconst xMax = d3.max(allSamples) + 4;\nconst GRID_POINTS = 160;\nconst xGrid = d3.range(GRID_POINTS).map((i) => xMin + (i / (GRID_POINTS - 1)) * (xMax - xMin));\n\nconst groups = groupsRaw.map((g) => {\n  const bandwidth = silvermanBandwidth(g.samples);\n  const density = kde(g.samples, xGrid, bandwidth);\n  // Clip near-zero tails below 0.8% of this group's own peak so the Gaussian\n  // KDE's asymptotic tails taper to a clean point instead of a flat sliver.\n  const peak = d3.max(density);\n  const clipped = density.map((d) => (d < peak * 0.008 ? 0 : d));\n  return { name: g.name, mean: g.mean, density: clipped };\n});\nconst globalMaxDensity = d3.max(groups, (g) => d3.max(g.density));\n\n// --- Layout -------------------------------------------------------------------\nconst margin = { top: 100, right: 80, bottom: 90, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst RIDGE_HEIGHT = 120; // px height of the tallest peak (60% row overlap)\nconst ROW_STEP = 48;\nconst baselineY = (i) => RIDGE_HEIGHT + i * ROW_STEP;\nconst axisY = baselineY(groups.length - 1) + 40;\n\n// --- Scales ---------------------------------------------------------------\nconst x = d3.scaleLinear().domain([xMin, xMax]).range([0, iw]);\n// imprint_seq is reserved for genuinely continuous data, so key the gradient\n// to each group's mean temperature (a real continuous quantity) rather than\n// its ordinal position — coldest month anchors to brand green, hottest to blue,\n// making ridge color a redundant encoding of the same seasonal signal as the\n// x-position of each hump.\nconst color = d3\n  .scaleSequential(d3.interpolateRgbBasis(t.seq))\n  .domain(d3.extent(groups, (g) => g.mean));\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// --- Ridges (drawn back-to-front so lower ridges occlude the ones above) ----\nconst area = d3\n  .area()\n  .curve(d3.curveBasis)\n  .x((d) => x(d.x))\n  .y0((d) => d.baseline)\n  .y1((d) => d.baseline - (d.density / globalMaxDensity) * RIDGE_HEIGHT);\n\ngroups.forEach((grp, i) => {\n  const baseline = baselineY(i);\n  const points = xGrid.map((xi, j) => ({ x: xi, density: grp.density[j], baseline }));\n  g.append(\"path\")\n    .datum(points)\n    .attr(\"d\", area)\n    .attr(\"fill\", color(grp.mean))\n    .attr(\"fill-opacity\", 0.92)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 2);\n});\n\n// --- Group labels (y-axis shows group names, not numeric values) ------------\ng.selectAll(\".ridge-label\")\n  .data(groups)\n  .join(\"text\")\n  .attr(\"class\", \"ridge-label\")\n  .attr(\"x\", -14)\n  .attr(\"y\", (_, i) => baselineY(i))\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d.name);\n\n// --- X axis -------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${axisY})`).call(d3.axisBottom(x).ticks(7));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", axisY + 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Average Daily Temperature (°C)\");\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(\"ridgeline-basic · javascript · d3 · anyplot.ai\");\n"}