{"spec_id":"ice-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// ice-basic: Individual Conditional Expectation (ICE) Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 96/100 | Created: 2026-08-17\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 96, bottom: 110, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: synthetic Emax dose-response model, one ICE curve per patient ---\n// Simulates a black-box model's prediction of symptom reduction as drug\n// dosage varies, holding each patient's latent response profile fixed —\n// patients differ in metabolism rate (curve steepness) and effect ceiling.\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst nPatients = 90;\nconst doseGrid = d3.range(60).map((i) => (i / 59) * 400);\n\nconst patients = d3.range(nPatients).map((id) => {\n  const sensitivity = 0.4 + rand() * 1.2; // response rate — fast vs slow metabolizers\n  const maxEffect = 45 + rand() * 40; // ceiling symptom reduction (%)\n  const noise = rand() * 5; // small per-patient baseline jitter\n  const observedDose = 20 + rand() * 360; // this patient's actual prescribed dose\n  const curve = doseGrid.map(\n    (dose) => maxEffect * (1 - Math.exp((-sensitivity * dose) / 130)) + noise,\n  );\n  return { id, observedDose, sensitivity, curve };\n});\n\nconst pdpCurve = doseGrid.map((_, j) => d3.mean(patients, (p) => p.curve[j]));\n\n// --- Scales ------------------------------------------------------------\nconst x = d3.scaleLinear().domain(d3.extent(doseGrid)).range([0, iw]);\nconst yMax = d3.max(patients, (p) => d3.max(p.curve));\nconst y = d3.scaleLinear().domain([0, yMax]).nice().range([ih, 0]);\n\n// Continuous color encoding: ICE-line hue reveals sensitivity (metabolism\n// rate) as a second feature, exposing which patient subgroup drives the\n// steepest early response — an interaction effect hidden by the flat PDP.\nconst sensitivityExtent = d3.extent(patients, (p) => p.sensitivity);\nconst seqColor = d3\n  .scaleSequential(d3.interpolateRgbBasis(t.seq))\n  .domain(sensitivityExtent);\n\n// --- SVG mount -----------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Y gridlines (line charts use y-axis grid only) ------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((axisGroup) => axisGroup.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Axes ------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .ticks(8)\n      .tickFormat((d) => `${d}`),\n  );\nconst yAxis = g.append(\"g\").call(\n  d3\n    .axisLeft(y)\n    .ticks(6)\n    .tickFormat((d) => `${d}%`),\n);\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// --- Individual ICE curves — low alpha to reveal density and divergence ----\nconst lineGen = d3\n  .line()\n  .x((d, i) => x(doseGrid[i]))\n  .y((d) => y(d));\ng.selectAll(\".ice-line\")\n  .data(patients)\n  .join(\"path\")\n  .attr(\"class\", \"ice-line\")\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (p) => seqColor(p.sensitivity))\n  .attr(\"stroke-width\", 1.2)\n  .attr(\"stroke-opacity\", 0.3)\n  .attr(\"d\", (p) => lineGen(p.curve));\n\n// --- PDP average overlay — bold, opaque -------------------------------------\ng.append(\"path\")\n  .datum(pdpCurve)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 4)\n  .attr(\"d\", lineGen);\n\n// --- Divergence annotation — bracket calling out the spread at max dose ----\nconst lastIdx = doseGrid.length - 1;\nconst finalValues = patients.map((p) => p.curve[lastIdx]);\nconst [spreadMin, spreadMax] = d3.extent(finalValues);\nconst bracketX = iw + 14;\nconst bracket = g.append(\"g\");\nbracket\n  .append(\"line\")\n  .attr(\"x1\", bracketX)\n  .attr(\"x2\", bracketX)\n  .attr(\"y1\", y(spreadMin))\n  .attr(\"y2\", y(spreadMax))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5);\nfor (const v of [spreadMin, spreadMax]) {\n  bracket\n    .append(\"line\")\n    .attr(\"x1\", bracketX - 5)\n    .attr(\"x2\", bracketX + 5)\n    .attr(\"y1\", y(v))\n    .attr(\"y2\", y(v))\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1.5);\n}\nbracket\n  .append(\"text\")\n  .attr(\"x\", bracketX + 9)\n  .attr(\"y\", (y(spreadMin) + y(spreadMax)) / 2)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(`Δ${Math.round(spreadMax - spreadMin)}pp`);\n\n// --- Rug plot: distribution of observed dosages along the x-axis -----------\ng.selectAll(\".rug\")\n  .data(patients)\n  .join(\"line\")\n  .attr(\"class\", \"rug\")\n  .attr(\"x1\", (p) => x(p.observedDose))\n  .attr(\"x2\", (p) => x(p.observedDose))\n  .attr(\"y1\", ih)\n  .attr(\"y2\", ih - 12)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-opacity\", 0.5)\n  .attr(\"stroke-width\", 1);\n\n// --- Legend — placed in the empty low-dose/low-effect corner ---------------\nconst gradientId = \"ice-sensitivity-gradient\";\nsvg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"100%\")\n  .selectAll(\"stop\")\n  .data(d3.range(0, 1.001, 0.1))\n  .join(\"stop\")\n  .attr(\"offset\", (d) => `${d * 100}%`)\n  .attr(\"stop-color\", (d) =>\n    seqColor(\n      sensitivityExtent[0] + d * (sensitivityExtent[1] - sensitivityExtent[0]),\n    ),\n  );\n\nconst legend = g.append(\"g\").attr(\"transform\", \"translate(16, 14)\");\nconst legendItems = [\n  {\n    label: \"Individual patients (by sensitivity)\",\n    color: `url(#${gradientId})`,\n    opacity: 0.8,\n    width: 3,\n  },\n  {\n    label: \"Population average (PDP)\",\n    color: t.palette[1],\n    opacity: 1,\n    width: 4,\n  },\n];\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0, ${i * 32})`);\n  row\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 30)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", item.color)\n    .attr(\"stroke-width\", item.width)\n    .attr(\"stroke-opacity\", item.opacity);\n  row\n    .append(\"text\")\n    .attr(\"x\", 40)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n});\n\n// --- Axis labels -------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Drug Dosage (mg)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -90)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Symptom Reduction (%)\");\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(\"ice-basic · javascript · d3 · anyplot.ai\");\n"}