{"spec_id":"ridgeline-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// ridgeline-basic: Basic Ridgeline Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 88/100 | Created: 2026-07-25\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: monthly high-temperature distributions (deterministic LCG) ------\nconst MONTHS = [\n  { name: \"Jan\", mean: -3, std: 4.0 },\n  { name: \"Feb\", mean: -1, std: 4.0 },\n  { name: \"Mar\", mean: 6, std: 4.0 },\n  { name: \"Apr\", mean: 12, std: 4.0 },\n  { name: \"May\", mean: 18, std: 3.5 },\n  { name: \"Jun\", mean: 24, std: 3.0 },\n  { name: \"Jul\", mean: 27, std: 2.5 },\n  { name: \"Aug\", mean: 26, std: 2.5 },\n  { name: \"Sep\", mean: 21, std: 3.5 },\n  { name: \"Oct\", mean: 14, std: 4.0 },\n  { name: \"Nov\", mean: 6, std: 4.0 },\n  { name: \"Dec\", mean: -1, std: 4.0 },\n];\nconst SAMPLES_PER_MONTH = 150;\n\nlet lcgSeed = 42;\nfunction lcgUniform() {\n  lcgSeed = (lcgSeed * 1664525 + 1013904223) % 4294967296;\n  return lcgSeed / 4294967296;\n}\nfunction lcgGaussian(mean, std) {\n  const u1 = Math.max(lcgUniform(), 1e-9);\n  const u2 = lcgUniform();\n  const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z0 * std;\n}\n\nconst monthSamples = MONTHS.map((m) =>\n  Array.from({ length: SAMPLES_PER_MONTH }, () => lcgGaussian(m.mean, m.std)),\n);\n\n// --- Kernel density estimation over a shared temperature grid --------------\nconst X_MIN = -15;\nconst X_MAX = 35;\nconst GRID_POINTS = 120;\nconst BANDWIDTH = 1.8;\nconst xGrid = Array.from(\n  { length: GRID_POINTS },\n  (_, i) => X_MIN + (i * (X_MAX - X_MIN)) / (GRID_POINTS - 1),\n);\n\nfunction kde(samples, bandwidth) {\n  const norm = 1 / (samples.length * bandwidth * Math.sqrt(2 * Math.PI));\n  return xGrid.map((x) => {\n    let sum = 0;\n    for (const s of samples) {\n      const u = (x - s) / bandwidth;\n      sum += Math.exp(-0.5 * u * u);\n    }\n    return sum * norm;\n  });\n}\n\nconst monthDensities = monthSamples.map((samples) => kde(samples, BANDWIDTH));\n\n// Clip each ridge to where its density is non-negligible so flat\n// near-zero-density tails don't run across the full canvas width.\nfunction clipToSignificant(x, density, threshold) {\n  let lo = 0;\n  let hi = density.length - 1;\n  while (lo < hi && density[lo] < threshold) lo++;\n  while (hi > lo && density[hi] < threshold) hi--;\n  lo = Math.max(0, lo - 1);\n  hi = Math.min(density.length - 1, hi + 1);\n  return { x: x.slice(lo, hi + 1), density: density.slice(lo, hi + 1) };\n}\nconst monthCurves = monthDensities.map((density) =>\n  clipToSignificant(xGrid, density, Math.max(...density) * 0.02),\n);\n\n// --- Layout: stack ridges bottom (Jan) to top (Dec), ~55% overlap ----------\nconst SPACING = 40;\nconst HEIGHT_SCALE = 650;\n\n// --- Color: diverging imprint colormap keyed to mean temperature -----------\n// (domain convention: hot -> red, cold -> blue; see default-style-guide.md\n// \"Semantic exception\")\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction lerpRgb(hexA, hexB, f) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  return a.map((v, i) => Math.round(v + (b[i] - v) * f));\n}\nfunction divergingColor(mean, centerTemp, halfRange, alpha) {\n  const norm = Math.max(-1, Math.min(1, (mean - centerTemp) / halfRange));\n  const [hot, mid, cold] = t.div; // t.div = [red, midpoint, blue]\n  const target = norm >= 0 ? hot : cold;\n  const rgb = lerpRgb(mid, target, Math.abs(norm));\n  return `rgba(${rgb.join(\",\")},${alpha})`;\n}\nconst meanTemps = MONTHS.map((m) => m.mean);\nconst centerTemp = (Math.min(...meanTemps) + Math.max(...meanTemps)) / 2;\nconst halfRange = (Math.max(...meanTemps) - Math.min(...meanTemps)) / 2;\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart: ridges built from filled line datasets, one per month ---------\nconst numMonths = MONTHS.length;\nconst datasets = MONTHS.map((month, i) => {\n  const baseline = i * SPACING;\n  const borderColor = divergingColor(month.mean, centerTemp, halfRange, 1);\n  const fillColor = divergingColor(month.mean, centerTemp, halfRange, 0.45);\n  const { x: curveX, density: curveDensity } = monthCurves[i];\n  return {\n    label: month.name,\n    data: curveX.map((x, j) => ({\n      x,\n      y: baseline + curveDensity[j] * HEIGHT_SCALE,\n    })),\n    borderColor,\n    backgroundColor: fillColor,\n    fill: { target: { value: baseline } },\n    borderWidth: 2,\n    pointRadius: 0,\n    tension: 0.3,\n    // Lower months are drawn last so they sit in front of the ridge above,\n    // producing the mountain-range overlap.\n    order: numMonths - i,\n  };\n});\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"ridgeline-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: X_MIN,\n        max: X_MAX,\n        title: { display: true, text: \"Temperature (°C)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n      y: {\n        min: -20,\n        max: (numMonths - 1) * SPACING + 130,\n        afterBuildTicks: (scale) => {\n          scale.ticks = MONTHS.map((_, i) => ({ value: i * SPACING }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => MONTHS[value / SPACING]?.name ?? \"\",\n        },\n        title: { display: true, text: \"Month\", color: t.ink, font: { size: 16 } },\n        grid: { display: false },\n      },\n    },\n  },\n});\n"}