{"spec_id":"funnel-meta-analysis","library":"d3","language":"javascript","code":"// anyplot.ai\n// funnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-10\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 210, bottom: 90, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (drug vs placebo RCT meta-analysis, 20 studies, deterministic) ---\nconst summaryEffect = -0.29;\nconst nullEffect = 0;\nconst maxSE = 0.50;\n\nconst studies = [\n  { se: 0.06, logOR: -0.35 },\n  { se: 0.08, logOR: -0.28 },\n  { se: 0.09, logOR: -0.32 },\n  { se: 0.11, logOR: -0.24 },\n  { se: 0.13, logOR: -0.38 },\n  { se: 0.15, logOR: -0.20 },\n  { se: 0.16, logOR: -0.31 },\n  { se: 0.18, logOR: -0.44 },\n  { se: 0.19, logOR: -0.15 },\n  { se: 0.21, logOR: -0.52 },\n  { se: 0.23, logOR: -0.18 },\n  { se: 0.24, logOR: -0.08 },\n  { se: 0.26, logOR: -0.35 },\n  { se: 0.28, logOR: -0.62 },\n  { se: 0.29, logOR:  0.05 },\n  { se: 0.31, logOR: -0.55 },\n  { se: 0.34, logOR: -0.10 },\n  { se: 0.37, logOR: -0.78 },\n  { se: 0.40, logOR:  0.10 },\n  { se: 0.44, logOR: -0.68 },\n];\n\n// --- SVG mount ---\nconst svg = d3.select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---\nconst funnelHalfWidth = 1.96 * maxSE;\nconst x = d3.scaleLinear()\n  .domain([summaryEffect - funnelHalfWidth - 0.12, summaryEffect + funnelHalfWidth + 0.12])\n  .range([0, iw])\n  .nice();\nconst y = d3.scaleLinear()\n  .domain([0, maxSE])\n  .range([0, ih]);\n\n// --- Gridlines ---\ng.selectAll(\".hgrid\")\n  .data(y.ticks(5))\n  .join(\"line\").attr(\"class\", \"hgrid\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", d => y(d)).attr(\"y2\", d => y(d))\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n\ng.selectAll(\".vgrid\")\n  .data(x.ticks(6))\n  .join(\"line\").attr(\"class\", \"vgrid\")\n  .attr(\"x1\", d => x(d)).attr(\"x2\", d => x(d))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n\n// --- Funnel pseudo-95%-CI region ---\nconst apexX = x(summaryEffect);\nconst apexY = y(0);\nconst baseY = y(maxSE);\nconst baseLeftX = x(summaryEffect - funnelHalfWidth);\nconst baseRightX = x(summaryEffect + funnelHalfWidth);\n\ng.append(\"polygon\")\n  .attr(\"points\", `${apexX},${apexY} ${baseLeftX},${baseY} ${baseRightX},${baseY}`)\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"fill-opacity\", 0.08);\n\ng.append(\"line\")\n  .attr(\"x1\", apexX).attr(\"y1\", apexY)\n  .attr(\"x2\", baseLeftX).attr(\"y2\", baseY)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"10,6\");\n\ng.append(\"line\")\n  .attr(\"x1\", apexX).attr(\"y1\", apexY)\n  .attr(\"x2\", baseRightX).attr(\"y2\", baseY)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"10,6\");\n\n// --- Null effect vertical reference line ---\ng.append(\"line\")\n  .attr(\"x1\", x(nullEffect)).attr(\"x2\", x(nullEffect))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"5,5\");\n\n// --- Pooled effect vertical line ---\ng.append(\"line\")\n  .attr(\"x1\", x(summaryEffect)).attr(\"x2\", x(summaryEffect))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.palette[2]).attr(\"stroke-width\", 2.5);\n\n// --- Study points ---\ng.selectAll(\".study\")\n  .data(studies)\n  .join(\"circle\").attr(\"class\", \"study\")\n  .attr(\"cx\", d => x(d.logOR))\n  .attr(\"cy\", d => y(d.se))\n  .attr(\"r\", 8)\n  .attr(\"fill\", t.palette[0]).attr(\"fill-opacity\", 0.85)\n  .attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 1.5);\n\n// --- X axis ---\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickSize(6).tickPadding(8));\n\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\nxAxis.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\nxAxis.selectAll(\".tick text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n\n// --- Y axis ---\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(y).ticks(5).tickSize(6).tickPadding(8));\n\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\nyAxis.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\nyAxis.selectAll(\".tick text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n\n// --- Axis labels ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Log Odds Ratio\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(22,${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Standard Error (SE)\");\n\n// --- Legend ---\nconst legendItems = [\n  { y: 0,  symbol: \"circle\", color: t.palette[0], dash: null,   sw: 2.5, label: \"Study\" },\n  { y: 34, symbol: \"line\",   color: t.palette[2], dash: null,   sw: 2.5, label: \"Pooled effect\" },\n  { y: 68, symbol: \"line\",   color: t.inkSoft,    dash: \"10,6\", sw: 2,   label: \"95% pseudo-CI\" },\n  { y: 102, symbol: \"line\",  color: t.inkSoft,    dash: \"5,5\",  sw: 1.5, label: \"No effect\" },\n];\n\nconst legend = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 30},${margin.top + 20})`);\n\nlegendItems.forEach(item => {\n  if (item.symbol === \"circle\") {\n    legend.append(\"circle\")\n      .attr(\"cx\", 8).attr(\"cy\", item.y + 2)\n      .attr(\"r\", 8)\n      .attr(\"fill\", item.color).attr(\"fill-opacity\", 0.85)\n      .attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 1.5);\n  } else {\n    const ln = legend.append(\"line\")\n      .attr(\"x1\", 0).attr(\"x2\", 18)\n      .attr(\"y1\", item.y + 2).attr(\"y2\", item.y + 2)\n      .attr(\"stroke\", item.color)\n      .attr(\"stroke-width\", item.sw);\n    if (item.dash) ln.attr(\"stroke-dasharray\", item.dash);\n  }\n  legend.append(\"text\")\n    .attr(\"x\", 26).attr(\"y\", item.y + 7)\n    .attr(\"fill\", t.ink).style(\"font-size\", \"14px\")\n    .text(item.label);\n});\n\n// --- Title ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"funnel-meta-analysis · javascript · d3 · anyplot.ai\");\n"}