{"spec_id":"funnel-meta-analysis","library":"muix","language":"javascript","code":"// anyplot.ai\n// funnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-10\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useDrawingArea, useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) -----------------------------------------\n// Meta-analysis of 18 RCTs: antihypertensive therapy vs placebo on major CV events\n// Log odds ratios (negative = protective effect) with standard errors\nconst studies = [\n  // Large trials — low SE, cluster tightly around pooled effect\n  { id: \"s01\", x: -0.42, y: 0.10 },\n  { id: \"s02\", x: -0.29, y: 0.12 },\n  { id: \"s03\", x: -0.38, y: 0.11 },\n  // Medium trials\n  { id: \"s04\", x: -0.55, y: 0.22 },\n  { id: \"s05\", x: -0.18, y: 0.20 },\n  { id: \"s06\", x: -0.47, y: 0.24 },\n  { id: \"s07\", x: -0.33, y: 0.19 },\n  { id: \"s08\", x: -0.61, y: 0.26 },\n  { id: \"s09\", x: -0.12, y: 0.23 },\n  // Small trials — wider scatter; asymmetry: fewer on the positive side\n  { id: \"s10\", x: -0.72, y: 0.38 },\n  { id: \"s11\", x: -0.25, y: 0.35 },\n  { id: \"s12\", x: -0.88, y: 0.42 },\n  { id: \"s13\", x: -0.50, y: 0.40 },\n  { id: \"s14\", x: -1.04, y: 0.46 },\n  // Very small trials\n  { id: \"s15\", x: -0.65, y: 0.58 },\n  { id: \"s16\", x: -1.20, y: 0.62 },\n  { id: \"s17\", x: -0.40, y: 0.55 },\n  { id: \"s18\", x: -0.80, y: 0.60 },\n];\n\n// Inverse-variance weighted pooled effect\nconst weights = studies.map((s) => 1 / (s.y * s.y));\nconst totalWeight = weights.reduce((a, b) => a + b, 0);\nconst pooledEffect = studies.reduce((sum, s, i) => sum + s.x * weights[i], 0) / totalWeight;\n\n// Axis extents — generous padding around data range\nconst maxSE = 0.72; // y-axis max (bottom)\nconst xMin = -1.55;\nconst xMax = 0.65;\n\n// Funnel cone: x = pooled ± 1.96 * SE at each SE level\n// At SE=0 → x = pooled (apex); at SE=maxSE → x = pooled ± 1.96*maxSE (base)\nconst funnelHalfWidth = 1.96 * maxSE;\n\n// --- Chart title (mandatory anyplot title format) ----------------------------\nconst TITLE = \"funnel-meta-analysis · javascript · muix · anyplot.ai\";\n\nfunction ChartTitle() {\n  const { left, top, width } = useDrawingArea();\n  return (\n    <text\n      x={left + width / 2}\n      y={top * 0.55}\n      textAnchor=\"middle\"\n      dominantBaseline=\"middle\"\n      fill={t.ink}\n      fontSize={22}\n      fontWeight=\"500\"\n      fontFamily=\"sans-serif\"\n    >\n      {TITLE}\n    </text>\n  );\n}\n\n// --- Funnel cone overlay (custom SVG using drawing-area + scales) ------------\nfunction FunnelCone() {\n  const { left, top, width, height } = useDrawingArea();\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  // SVG pixel coordinates for funnel apex (SE=0) and base (SE=maxSE)\n  const apexX = xScale(pooledEffect);\n  const apexY = yScale(0);\n  const baseY = yScale(maxSE);\n  const baseXLeft = xScale(pooledEffect - funnelHalfWidth);\n  const baseXRight = xScale(pooledEffect + funnelHalfWidth);\n\n  // Clamp to drawing area\n  const clampX = (x) => Math.max(left, Math.min(left + width, x));\n\n  const cLeft = clampX(baseXLeft);\n  const cRight = clampX(baseXRight);\n\n  const lineStyle = {\n    stroke: t.inkSoft,\n    strokeWidth: 2,\n    strokeDasharray: \"8 5\",\n    fill: \"none\",\n    opacity: 0.8,\n  };\n\n  return (\n    <g>\n      {/* Funnel left boundary: apex → lower-left */}\n      <line\n        x1={apexX}\n        y1={apexY}\n        x2={cLeft}\n        y2={baseY}\n        style={lineStyle}\n      />\n      {/* Funnel right boundary: apex → lower-right */}\n      <line\n        x1={apexX}\n        y1={apexY}\n        x2={cRight}\n        y2={baseY}\n        style={lineStyle}\n      />\n      {/* Shaded funnel region (subtle) */}\n      <polygon\n        points={`${apexX},${apexY} ${cLeft},${baseY} ${cRight},${baseY}`}\n        fill={t.palette[0]}\n        fillOpacity={0.04}\n      />\n    </g>\n  );\n}\n\n// --- Chart (default-exported component) -------------------------------------\nexport default function Chart() {\n  const w = window.ANYPLOT_SIZE.width;\n  const h = window.ANYPLOT_SIZE.height;\n\n  return (\n    <ScatterChart\n      width={w}\n      height={h}\n      skipAnimation\n      margin={{ top: 80, right: 60, bottom: 90, left: 90 }}\n      xAxis={[\n        {\n          id: \"xMain\",\n          scaleType: \"linear\",\n          min: xMin,\n          max: xMax,\n          label: \"Log Odds Ratio\",\n          tickNumber: 7,\n          tickLabelStyle: { fill: t.inkSoft, fontSize: 14 },\n          labelStyle: { fill: t.ink, fontSize: 16 },\n        },\n      ]}\n      yAxis={[\n        {\n          id: \"yMain\",\n          scaleType: \"linear\",\n          min: 0,\n          max: maxSE,\n          reverse: true,\n          label: \"Standard Error\",\n          tickNumber: 6,\n          tickLabelStyle: { fill: t.inkSoft, fontSize: 14 },\n          labelStyle: { fill: t.ink, fontSize: 16 },\n        },\n      ]}\n      series={[\n        {\n          type: \"scatter\",\n          data: studies,\n          color: t.palette[0],\n          markerSize: 9,\n          label: \"Individual study\",\n        },\n      ]}\n      grid={{ vertical: true, horizontal: true }}\n      slotProps={{\n        legend: { hidden: true },\n      }}\n    >\n      {/* Mandatory plot title */}\n      <ChartTitle />\n\n      {/* Funnel cone lines drawn in data-coordinate space */}\n      <FunnelCone />\n\n      {/* Pooled effect — solid vertical reference line */}\n      <ChartsReferenceLine\n        x={pooledEffect}\n        lineStyle={{\n          stroke: t.palette[0],\n          strokeWidth: 2.5,\n          strokeDasharray: \"0\",\n        }}\n        labelStyle={{ fill: t.ink, fontSize: 13 }}\n        label={`Pooled: ${pooledEffect.toFixed(2)}`}\n        labelAlign=\"start\"\n      />\n\n      {/* Null effect (log OR = 0) — dashed reference line */}\n      <ChartsReferenceLine\n        x={0}\n        lineStyle={{\n          stroke: t.inkSoft,\n          strokeWidth: 1.5,\n          strokeDasharray: \"6 4\",\n        }}\n        labelStyle={{ fill: t.inkSoft, fontSize: 12 }}\n        label=\"Null (OR=1)\"\n        labelAlign=\"end\"\n      />\n    </ScatterChart>\n  );\n}\n"}