{"spec_id":"forest-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// forest-basic: Meta-Analysis Forest Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst theme = window.ANYPLOT_THEME || \"light\";\nconst inkMuted = theme === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Meta-analysis of 14 RCTs: antihypertensive therapy vs. placebo, risk ratio\n// for stroke incidence. Ordered chronologically by publication year.\nconst studies = [\n  { study: \"Chen 2005\", rr: 0.85, lo: 0.62, hi: 1.16, weight: 4.8 },\n  { study: \"Patel 2006\", rr: 0.71, lo: 0.55, hi: 0.92, weight: 7.1 },\n  { study: \"Nakamura 2007\", rr: 0.93, lo: 0.68, hi: 1.27, weight: 5.0 },\n  { study: \"Kowalski 2008\", rr: 0.66, lo: 0.48, hi: 0.91, weight: 6.3 },\n  { study: \"Silva 2009\", rr: 0.79, lo: 0.61, hi: 1.02, weight: 8.2 },\n  { study: \"Andersen 2010\", rr: 0.58, lo: 0.39, hi: 0.86, weight: 4.1 },\n  { study: \"Martins 2011\", rr: 0.88, lo: 0.7, hi: 1.11, weight: 9.5 },\n  { study: \"Okafor 2012\", rr: 0.62, lo: 0.44, hi: 0.87, weight: 5.7 },\n  { study: \"Lindqvist 2013\", rr: 0.95, lo: 0.73, hi: 1.24, weight: 6.9 },\n  { study: \"Dubois 2014\", rr: 0.7, lo: 0.52, hi: 0.94, weight: 7.8 },\n  { study: \"Yamamoto 2015\", rr: 0.81, lo: 0.64, hi: 1.03, weight: 10.1 },\n  { study: \"Novak 2016\", rr: 0.68, lo: 0.49, hi: 0.95, weight: 5.4 },\n  { study: \"Reyes 2017\", rr: 0.9, lo: 0.71, hi: 1.14, weight: 8.6 },\n  { study: \"Larsson 2019\", rr: 0.77, lo: 0.6, hi: 0.99, weight: 9.5 },\n];\nconst pooled = { study: \"Pooled Estimate\", rr: 0.78, lo: 0.71, hi: 0.86 };\nconst rows = [...studies.map((d) => d.study), pooled.study];\n\n// --- SVG mount ---------------------------------------------------------------\nconst margin = { top: 110, right: 70, bottom: 110, left: 230 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\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.scaleLog().domain([0.35, 1.5]).range([0, iw]);\nconst y = d3.scaleBand().domain(rows).range([0, ih]).padding(0.35);\nconst r = d3\n  .scaleSqrt()\n  .domain(d3.extent(studies, (d) => d.weight))\n  .range([7, 15]);\n\n// --- Null-effect reference line (RR = 1) --------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(1))\n  .attr(\"x2\", x(1))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,5\");\n\ng.append(\"text\")\n  .attr(\"x\", x(1))\n  .attr(\"y\", -16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", inkMuted)\n  .style(\"font-size\", \"13px\")\n  .text(\"RR = 1 (no effect)\");\n\n// --- Per-study whiskers + point estimates -------------------------------------\nconst capH = 7;\nfor (const d of studies) {\n  const yc = y(d.study) + y.bandwidth() / 2;\n\n  g.append(\"line\")\n    .attr(\"x1\", x(d.lo))\n    .attr(\"x2\", x(d.hi))\n    .attr(\"y1\", yc)\n    .attr(\"y2\", yc)\n    .attr(\"stroke\", t.palette[0])\n    .attr(\"stroke-width\", 2.5);\n\n  for (const xv of [d.lo, d.hi]) {\n    g.append(\"line\")\n      .attr(\"x1\", x(xv))\n      .attr(\"x2\", x(xv))\n      .attr(\"y1\", yc - capH)\n      .attr(\"y2\", yc + capH)\n      .attr(\"stroke\", t.palette[0])\n      .attr(\"stroke-width\", 2.5);\n  }\n\n  g.append(\"circle\")\n    .attr(\"cx\", x(d.rr))\n    .attr(\"cy\", yc)\n    .attr(\"r\", r(d.weight))\n    .attr(\"fill\", t.palette[0])\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.5);\n}\n\n// --- Pooled estimate diamond (width spans the CI, height fixed) --------------\nconst pyc = y(pooled.study) + y.bandwidth() / 2;\nconst halfH = (y.bandwidth() / 2) * 0.8;\ng.append(\"polygon\")\n  .attr(\n    \"points\",\n    [\n      [x(pooled.lo), pyc],\n      [x(pooled.rr), pyc - halfH],\n      [x(pooled.hi), pyc],\n      [x(pooled.rr), pyc + halfH],\n    ]\n      .map((p) => p.join(\",\"))\n      .join(\" \"),\n  )\n  .attr(\"fill\", t.ink);\n\n// --- Axes ----------------------------------------------------------------------\nconst xTicks = [0.4, 0.5, 0.75, 1, 1.5];\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues(xTicks).tickFormat(d3.format(\".2~f\")).tickSizeOuter(0));\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\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickSize(0));\nyAxis\n  .selectAll(\"text\")\n  .attr(\"fill\", (d) => (d === pooled.study ? t.ink : t.inkSoft))\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", (d) => (d === pooled.study ? \"600\" : \"400\"));\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Axis label -----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 34)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Risk Ratio (95% CI, log scale)\");\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(\"forest-basic · javascript · d3 · anyplot.ai\");\n"}