{"spec_id":"funnel-meta-analysis","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// funnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-10\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// 15 RCTs comparing drug vs placebo — log odds ratios and standard errors\nconst studies = [\n  { logOR: 0.52, se: 0.18 },\n  { logOR: 0.41, se: 0.22 },\n  { logOR: 0.28, se: 0.31 },\n  { logOR: 0.35, se: 0.14 },\n  { logOR: 0.18, se: 0.25 },\n  { logOR: 0.60, se: 0.38 },\n  { logOR: 0.45, se: 0.12 },\n  { logOR: 0.22, se: 0.28 },\n  { logOR: -0.05, se: 0.42 },\n  { logOR: 0.38, se: 0.16 },\n  { logOR: 0.55, se: 0.34 },\n  { logOR: 0.30, se: 0.20 },\n  { logOR: 0.48, se: 0.45 },\n  { logOR: 0.15, se: 0.37 },\n  { logOR: 0.62, se: 0.11 },\n];\n\n// Inverse-variance weighted pooled effect\nconst weights = studies.map(s => 1 / (s.se * s.se));\nconst totalWeight = weights.reduce((a, b) => a + b, 0);\nconst summaryEffect = studies.reduce((sum, s, i) => sum + weights[i] * s.logOR, 0) / totalWeight;\n\nconst maxSE = Math.max(...studies.map(s => s.se)) * 1.15;\nconst axisMax = Math.ceil(maxSE * 10) / 10;  // round up to avoid duplicate boundary tick at 0.5\n\n// Funnel boundaries: apex at (summaryEffect, 0), base at SE=maxSE\nconst funnelLeft  = [{ x: summaryEffect, y: 0 }, { x: summaryEffect - 1.96 * maxSE, y: maxSE }];\nconst funnelRight = [{ x: summaryEffect, y: 0 }, { x: summaryEffect + 1.96 * maxSE, y: maxSE }];\n\n// Mount canvas\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      // Funnel left boundary — in legend as \"95% CI\", fills toward right boundary\n      {\n        type: \"line\",\n        label: \"95% Confidence Interval\",\n        data: funnelLeft,\n        borderColor: t.palette[1],\n        backgroundColor: t.palette[1] + \"26\",\n        borderWidth: 2,\n        borderDash: [8, 5],\n        pointRadius: 0,\n        tension: 0,\n        fill: \"+1\",\n      },\n      // Funnel right boundary — hidden from legend, fill target for left boundary\n      {\n        type: \"line\",\n        label: \"_funnel_right\",\n        data: funnelRight,\n        borderColor: t.palette[1],\n        backgroundColor: \"transparent\",\n        borderWidth: 2,\n        borderDash: [8, 5],\n        pointRadius: 0,\n        tension: 0,\n        fill: false,\n      },\n      // Pooled effect vertical line\n      {\n        type: \"line\",\n        label: `Pooled effect (${summaryEffect.toFixed(2)})`,\n        data: [{ x: summaryEffect, y: 0 }, { x: summaryEffect, y: maxSE }],\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        borderWidth: 2.5,\n        pointRadius: 0,\n        tension: 0,\n      },\n      // Null effect reference line\n      {\n        type: \"line\",\n        label: \"Null effect (0)\",\n        data: [{ x: 0, y: 0 }, { x: 0, y: maxSE }],\n        borderColor: t.inkSoft,\n        backgroundColor: \"transparent\",\n        borderWidth: 1.5,\n        borderDash: [4, 4],\n        pointRadius: 0,\n        tension: 0,\n      },\n      // Individual study points — Imprint palette position 1 as first series\n      {\n        type: \"scatter\",\n        label: \"Studies\",\n        data: studies.map(s => ({ x: s.logOR, y: s.se })),\n        backgroundColor: t.palette[0] + \"CC\",\n        borderColor: t.pageBg,\n        borderWidth: 2,\n        pointRadius: 9,\n        pointHoverRadius: 11,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 10, right: 40, bottom: 10, left: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"funnel-meta-analysis · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"600\" },\n        padding: { top: 16, bottom: 20 },\n      },\n      legend: {\n        display: true,\n        position: \"bottom\",\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          padding: 24,\n          usePointStyle: true,\n          filter: item => !item.text.startsWith(\"_\"),\n        },\n      },\n    },\n    scales: {\n      x: {\n        title: {\n          display: true,\n          text: \"Log Odds Ratio\",\n          color: t.ink,\n          font: { size: 16, weight: \"500\" },\n          padding: { top: 10 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n      y: {\n        reverse: true,\n        min: 0,\n        max: axisMax,\n        title: {\n          display: true,\n          text: \"Standard Error\",\n          color: t.ink,\n          font: { size: 16, weight: \"500\" },\n        },\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}