Meta-Analysis Funnel Plot for Publication Bias — MUI X Charts

A funnel plot used in meta-analysis to assess publication bias by plotting individual study effect sizes against their precision (typically standard error). Studies scatter around a summary effect line, with pseudo 95% confidence limits forming an inverted funnel shape. In the absence of bias, studies distribute symmetrically around the summary effect; asymmetry suggests publication bias or systematic heterogeneity. This is a standard tool in systematic reviews and Cochrane-style meta-analyses.

Meta-Analysis Funnel Plot for Publication Bias rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// funnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias
// Library: muix 7.29.1 | JavaScript 22.22.3
// Quality: 91/100 | Created: 2026-06-10
import { ScatterChart } from "@mui/x-charts/ScatterChart";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import { useDrawingArea, useXScale, useYScale } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) -----------------------------------------
// Meta-analysis of 18 RCTs: antihypertensive therapy vs placebo on major CV events
// Log odds ratios (negative = protective effect) with standard errors
const studies = [
  // Large trials — low SE, cluster tightly around pooled effect
  { id: "s01", x: -0.42, y: 0.10 },
  { id: "s02", x: -0.29, y: 0.12 },
  { id: "s03", x: -0.38, y: 0.11 },
  // Medium trials
  { id: "s04", x: -0.55, y: 0.22 },
  { id: "s05", x: -0.18, y: 0.20 },
  { id: "s06", x: -0.47, y: 0.24 },
  { id: "s07", x: -0.33, y: 0.19 },
  { id: "s08", x: -0.61, y: 0.26 },
  { id: "s09", x: -0.12, y: 0.23 },
  // Small trials — wider scatter; asymmetry: fewer on the positive side
  { id: "s10", x: -0.72, y: 0.38 },
  { id: "s11", x: -0.25, y: 0.35 },
  { id: "s12", x: -0.88, y: 0.42 },
  { id: "s13", x: -0.50, y: 0.40 },
  { id: "s14", x: -1.04, y: 0.46 },
  // Very small trials
  { id: "s15", x: -0.65, y: 0.58 },
  { id: "s16", x: -1.20, y: 0.62 },
  { id: "s17", x: -0.40, y: 0.55 },
  { id: "s18", x: -0.80, y: 0.60 },
];

// Inverse-variance weighted pooled effect
const weights = studies.map((s) => 1 / (s.y * s.y));
const totalWeight = weights.reduce((a, b) => a + b, 0);
const pooledEffect = studies.reduce((sum, s, i) => sum + s.x * weights[i], 0) / totalWeight;

// Axis extents — generous padding around data range
const maxSE = 0.72; // y-axis max (bottom)
const xMin = -1.55;
const xMax = 0.65;

// Funnel cone: x = pooled ± 1.96 * SE at each SE level
// At SE=0 → x = pooled (apex); at SE=maxSE → x = pooled ± 1.96*maxSE (base)
const funnelHalfWidth = 1.96 * maxSE;

// --- Chart title (mandatory anyplot title format) ----------------------------
const TITLE = "funnel-meta-analysis · javascript · muix · anyplot.ai";

function ChartTitle() {
  const { left, top, width } = useDrawingArea();
  return (
    <text
      x={left + width / 2}
      y={top * 0.55}
      textAnchor="middle"
      dominantBaseline="middle"
      fill={t.ink}
      fontSize={22}
      fontWeight="500"
      fontFamily="sans-serif"
    >
      {TITLE}
    </text>
  );
}

// --- Funnel cone overlay (custom SVG using drawing-area + scales) ------------
function FunnelCone() {
  const { left, top, width, height } = useDrawingArea();
  const xScale = useXScale();
  const yScale = useYScale();

  // SVG pixel coordinates for funnel apex (SE=0) and base (SE=maxSE)
  const apexX = xScale(pooledEffect);
  const apexY = yScale(0);
  const baseY = yScale(maxSE);
  const baseXLeft = xScale(pooledEffect - funnelHalfWidth);
  const baseXRight = xScale(pooledEffect + funnelHalfWidth);

  // Clamp to drawing area
  const clampX = (x) => Math.max(left, Math.min(left + width, x));

  const cLeft = clampX(baseXLeft);
  const cRight = clampX(baseXRight);

  const lineStyle = {
    stroke: t.inkSoft,
    strokeWidth: 2,
    strokeDasharray: "8 5",
    fill: "none",
    opacity: 0.8,
  };

  return (
    <g>
      {/* Funnel left boundary: apex → lower-left */}
      <line
        x1={apexX}
        y1={apexY}
        x2={cLeft}
        y2={baseY}
        style={lineStyle}
      />
      {/* Funnel right boundary: apex → lower-right */}
      <line
        x1={apexX}
        y1={apexY}
        x2={cRight}
        y2={baseY}
        style={lineStyle}
      />
      {/* Shaded funnel region (subtle) */}
      <polygon
        points={`${apexX},${apexY} ${cLeft},${baseY} ${cRight},${baseY}`}
        fill={t.palette[0]}
        fillOpacity={0.04}
      />
    </g>
  );
}

// --- Chart (default-exported component) -------------------------------------
export default function Chart() {
  const w = window.ANYPLOT_SIZE.width;
  const h = window.ANYPLOT_SIZE.height;

  return (
    <ScatterChart
      width={w}
      height={h}
      skipAnimation
      margin={{ top: 80, right: 60, bottom: 90, left: 90 }}
      xAxis={[
        {
          id: "xMain",
          scaleType: "linear",
          min: xMin,
          max: xMax,
          label: "Log Odds Ratio",
          tickNumber: 7,
          tickLabelStyle: { fill: t.inkSoft, fontSize: 14 },
          labelStyle: { fill: t.ink, fontSize: 16 },
        },
      ]}
      yAxis={[
        {
          id: "yMain",
          scaleType: "linear",
          min: 0,
          max: maxSE,
          reverse: true,
          label: "Standard Error",
          tickNumber: 6,
          tickLabelStyle: { fill: t.inkSoft, fontSize: 14 },
          labelStyle: { fill: t.ink, fontSize: 16 },
        },
      ]}
      series={[
        {
          type: "scatter",
          data: studies,
          color: t.palette[0],
          markerSize: 9,
          label: "Individual study",
        },
      ]}
      grid={{ vertical: true, horizontal: true }}
      slotProps={{
        legend: { hidden: true },
      }}
    >
      {/* Mandatory plot title */}
      <ChartTitle />

      {/* Funnel cone lines drawn in data-coordinate space */}
      <FunnelCone />

      {/* Pooled effect — solid vertical reference line */}
      <ChartsReferenceLine
        x={pooledEffect}
        lineStyle={{
          stroke: t.palette[0],
          strokeWidth: 2.5,
          strokeDasharray: "0",
        }}
        labelStyle={{ fill: t.ink, fontSize: 13 }}
        label={`Pooled: ${pooledEffect.toFixed(2)}`}
        labelAlign="start"
      />

      {/* Null effect (log OR = 0) — dashed reference line */}
      <ChartsReferenceLine
        x={0}
        lineStyle={{
          stroke: t.inkSoft,
          strokeWidth: 1.5,
          strokeDasharray: "6 4",
        }}
        labelStyle={{ fill: t.inkSoft, fontSize: 12 }}
        label="Null (OR=1)"
        labelAlign="end"
      />
    </ScatterChart>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/funnel-meta-analysis/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "funnel-meta-analysis",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/funnel-meta-analysis/javascript/muix",
  "hub": "https://anyplot.ai/funnel-meta-analysis",
  "code_json": "https://api.anyplot.ai/specs/funnel-meta-analysis/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/funnel-meta-analysis",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/funnel-meta-analysis/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/funnel-meta-analysis/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/funnel-meta-analysis/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/funnel-meta-analysis/javascript/muix/plot-dark.html",
  "quality_score": 91.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Meta-Analysis Funnel Plot for Publication Bias on anyplot.ai.

Other implementations