{"spec_id":"bland-altman-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// bland-altman-basic: Bland-Altman Agreement Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-08-11\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 190, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic LCG + Box-Muller) -----------------------\nfunction lcg(seed) {\n  let state = seed;\n  return function () {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Systolic blood pressure (mmHg) from a reference sphygmomanometer vs. a\n// test cuff, with a small proportional bias plus measurement noise.\nconst nPairs = 100;\nconst reference = [];\nconst testCuff = [];\nfor (let i = 0; i < nPairs; i++) {\n  const systolic = 120 + 15 * randNormal();\n  const bias = 3;\n  const proportionalError = 0.02 * (systolic - 120);\n  const noise = 6 * randNormal();\n  reference.push(systolic);\n  testCuff.push(systolic + bias + proportionalError + noise);\n}\n\nconst pairs = reference.map((v, i) => ({\n  mean: (v + testCuff[i]) / 2,\n  diff: v - testCuff[i],\n}));\nconst diffs = pairs.map((d) => d.diff);\nconst meanDiff = d3.mean(diffs);\nconst sdDiff = d3.deviation(diffs);\nconst upperLoA = meanDiff + 1.96 * sdDiff;\nconst lowerLoA = meanDiff - 1.96 * sdDiff;\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(d3.extent(pairs, (d) => d.mean)).nice().range([0, iw]);\nconst yMin = Math.min(d3.min(diffs), lowerLoA);\nconst yMax = Math.max(d3.max(diffs), upperLoA);\nconst yPad = (yMax - yMin) * 0.12;\nconst y = d3.scaleLinear().domain([yMin - yPad, yMax + yPad]).nice().range([ih, 0]);\n\n// --- Agreement-zone band (behind gridlines/points, frames ±1.96 SD) --------\ng.append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", y(upperLoA))\n  .attr(\"width\", iw)\n  .attr(\"height\", y(lowerLoA) - y(upperLoA))\n  .attr(\"fill\", t.ink)\n  .attr(\"fill-opacity\", 0.05);\n\n// --- Gridlines (both axes, subtle) -----------------------------------------\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickSize(-ih).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.selectAll(\".domain\").remove();\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));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Limits-of-agreement reference lines ------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(meanDiff))\n  .attr(\"y2\", y(meanDiff))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2.5);\n\nfor (const loa of [upperLoA, lowerLoA]) {\n  g.append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", iw)\n    .attr(\"y1\", y(loa))\n    .attr(\"y2\", y(loa))\n    .attr(\"stroke\", t.ink)\n    .attr(\"stroke-width\", 2)\n    .attr(\"stroke-dasharray\", \"9,6\")\n    .attr(\"opacity\", 0.7);\n}\n\n// --- Annotations (mean bias + limits of agreement) --------------------------\nconst annotations = [\n  { y: meanDiff, label: `Bias: ${meanDiff.toFixed(1)} mmHg` },\n  { y: upperLoA, label: `+1.96 SD: ${upperLoA.toFixed(1)}` },\n  { y: lowerLoA, label: `−1.96 SD: ${lowerLoA.toFixed(1)}` },\n];\ng.selectAll(\".loa-label\")\n  .data(annotations)\n  .join(\"text\")\n  .attr(\"x\", iw + 12)\n  .attr(\"y\", (d) => y(d.y))\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d.label);\n\n// --- Points (out-of-limit observations flagged in amber) -------------------\nconst outOfLimits = (d) => d.diff > upperLoA || d.diff < lowerLoA;\ng.selectAll(\"circle\")\n  .data(pairs)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.mean))\n  .attr(\"cy\", (d) => y(d.diff))\n  .attr(\"r\", (d) => (outOfLimits(d) ? 9 : 7))\n  .attr(\"fill\", (d) => (outOfLimits(d) ? t.amber : t.palette[0]))\n  .attr(\"fill-opacity\", (d) => (outOfLimits(d) ? 0.85 : 0.55))\n  .attr(\"stroke\", (d) => (outOfLimits(d) ? t.ink : t.pageBg))\n  .attr(\"stroke-width\", (d) => (outOfLimits(d) ? 1.5 : 1));\n\n// --- Axis labels --------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 28)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Mean of Two Methods (mmHg)\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${34},${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Difference: Reference − Test Cuff (mmHg)\");\n\n// --- Title ----------------------------------------------------------------\nconst title = \"Blood Pressure Cuff Comparison · bland-altman-basic · javascript · d3 · anyplot.ai\";\nconst titleFontSize = title.length > 67 ? Math.round(22 * (67 / title.length)) : 22;\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\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"}