{"spec_id":"funnel-meta-analysis","library":"echarts","language":"javascript","code":"// anyplot.ai\n// funnel-meta-analysis: Meta-Analysis Funnel Plot for Publication Bias\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-10\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data -------------------------------------------------------------------\n// 18 RCTs comparing drug vs. placebo: log odds ratios and standard errors\nconst studyData = [\n  [-0.41, 0.08], [-0.52, 0.09], [-0.44, 0.12], [-0.55, 0.13],\n  [-0.39, 0.15], [-0.28, 0.16], [-0.48, 0.18], [-0.62, 0.20],\n  [-0.35, 0.22], [-0.58, 0.25], [-0.33, 0.28], [-0.71, 0.30],\n  [-0.19, 0.31], [-0.27, 0.35], [-0.12, 0.38], [-0.65, 0.40],\n  [-0.22, 0.43], [-0.50, 0.45],\n];\n\nconst summaryEffect = -0.43;\nconst maxSE = 0.52;\nconst boundLeft  = summaryEffect - 1.96 * maxSE;  // ≈ −1.449\nconst boundRight = summaryEffect + 1.96 * maxSE;  // ≈  0.589\n\n// Precision weights for symbol sizing (inverse variance)\nconst weights = studyData.map(([, se]) => 1 / (se * se));\nconst wMin = Math.min(...weights);\nconst wMax = Math.max(...weights);\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n\n  title: {\n    text: \"funnel-meta-analysis · javascript · echarts · anyplot.ai\",\n    subtext: \"Drug vs. Placebo RCTs — 18 studies\",\n    left: \"center\",\n    top: 18,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 },\n  },\n\n  legend: {\n    data: [\n      { name: \"Studies\",               icon: \"circle\" },\n      { name: \"Summary effect (−0.43)\", icon: \"line\"  },\n      { name: \"95% CI bounds\",          icon: \"line\"  },\n      { name: \"Null effect (0)\",         icon: \"line\"  },\n    ],\n    bottom: 20,\n    left: \"center\",\n    itemWidth: 22,\n    itemHeight: 14,\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n  },\n\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    textStyle: { color: t.ink, fontSize: 13 },\n    formatter: params => {\n      if (params.seriesName !== \"Studies\") return \"\";\n      const [es, se] = params.data;\n      return `Study ${params.dataIndex + 1}<br/>LOR: ${es.toFixed(3)}<br/>SE: ${se.toFixed(3)}`;\n    },\n  },\n\n  grid: { left: 100, right: 70, top: 115, bottom: 110 },\n\n  xAxis: {\n    type: \"value\",\n    name: \"Log Odds Ratio\",\n    nameLocation: \"center\",\n    nameGap: 45,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    min: -1.7,\n    max: 0.8,\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine:  { lineStyle: { color: t.inkSoft } },\n    axisTick:  { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n\n  yAxis: {\n    type: \"value\",\n    name: \"Standard Error\",\n    nameLocation: \"center\",\n    nameGap: 60,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    inverse: true,\n    min: 0,\n    max: 0.55,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 13,\n      formatter: val => val.toFixed(2),\n    },\n    axisLine:  { lineStyle: { color: t.inkSoft } },\n    axisTick:  { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n\n  series: [\n    {\n      name: \"Studies\",\n      type: \"scatter\",\n      data: studyData,\n      symbolSize: data => {\n        const w = 1 / (data[1] * data[1]);\n        return 8 + ((w - wMin) / (wMax - wMin)) * 16;\n      },\n      itemStyle: {\n        color: t.palette[0],\n        opacity: 0.85,\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n      },\n      markLine: {\n        silent: true,\n        symbol: [\"none\", \"none\"],\n        label: { show: false },\n        data: [\n          // Null effect reference (LOR = 0)\n          [\n            { coord: [0, 0],             lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 } },\n            { coord: [0, maxSE] },\n          ],\n          // Summary effect (LOR = −0.43)\n          [\n            { coord: [summaryEffect, 0],     lineStyle: { color: t.ink, type: \"solid\", width: 2.5 } },\n            { coord: [summaryEffect, maxSE] },\n          ],\n          // Left funnel boundary (pseudo 95% CI)\n          [\n            { coord: [summaryEffect, 0],  lineStyle: { color: t.palette[2], type: \"dashed\", width: 2, opacity: 0.8 } },\n            { coord: [boundLeft,  maxSE] },\n          ],\n          // Right funnel boundary (pseudo 95% CI)\n          [\n            { coord: [summaryEffect, 0],  lineStyle: { color: t.palette[2], type: \"dashed\", width: 2, opacity: 0.8 } },\n            { coord: [boundRight, maxSE] },\n          ],\n        ],\n      },\n    },\n    // Dummy series used only for legend icons\n    {\n      name: \"Summary effect (−0.43)\",\n      type: \"line\",\n      data: [],\n      lineStyle: { color: t.ink, type: \"solid\", width: 2.5 },\n      symbolSize: 0,\n      legendHoverLink: false,\n    },\n    {\n      name: \"95% CI bounds\",\n      type: \"line\",\n      data: [],\n      lineStyle: { color: t.palette[2], type: \"dashed\", width: 2 },\n      symbolSize: 0,\n      legendHoverLink: false,\n    },\n    {\n      name: \"Null effect (0)\",\n      type: \"line\",\n      data: [],\n      lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n      symbolSize: 0,\n      legendHoverLink: false,\n    },\n  ],\n});\n"}