{"spec_id":"forest-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// forest-basic: Meta-Analysis Forest Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n//# anyplot-orientation: landscape\n// anyplot.ai\n// forest-basic: Meta-Analysis Forest Plot\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-05\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Meta-analysis of a new antihypertensive drug vs. placebo: mean difference in\n// systolic blood pressure (mmHg) across 8 RCTs. Negative = larger SBP\n// reduction, favoring the drug. Weight = % contribution (inverse-variance).\nconst STUDIES = [\n  { name: \"Anderson et al. 2019\",  effect: -4.2, lower: -7.1,  upper: -1.3, weight: 9.8 },\n  { name: \"Chen et al. 2020\",      effect: -6.8, lower: -9.5,  upper: -4.1, weight: 12.4 },\n  { name: \"Dubois et al. 2020\",    effect: -3.1, lower: -6.9,  upper: 0.7,  weight: 7.2 },\n  { name: \"Fernandez et al. 2021\", effect: -8.5, lower: -11.2, upper: -5.8, weight: 13.1 },\n  { name: \"Garcia et al. 2021\",    effect: -5.0, lower: -8.8,  upper: -1.2, weight: 8.6 },\n  { name: \"Halvorsen et al. 2022\", effect: -7.3, lower: -10.0, upper: -4.6, weight: 12.9 },\n  { name: \"Ivanova et al. 2022\",   effect: -2.0, lower: -5.5,  upper: 1.5,  weight: 6.5 },\n  { name: \"Kowalski et al. 2023\",  effect: -6.1, lower: -8.7,  upper: -3.5, weight: 11.5 },\n];\n\n// Pooled estimate (random-effects), drawn as a diamond spanning its 95% CI.\nconst OVERALL = { name: \"Pooled Effect (Random Effects)\", effect: -5.6, lower: -6.9, upper: -4.3 };\n\nconst N = STUDIES.length;\nconst OVERALL_Y = -1.6;\n\n// Row 0 (top of chart) is the first study; higher y-domain values render\n// higher on screen, so row order is reversed against the y position.\nconst ROWS = STUDIES.map((s, i) => ({ ...s, y: N - i }));\n\nconst Y_TICKS = [...ROWS.map((r) => r.y), OVERALL_Y];\nconst Y_LABELS = new Map(\n  Y_TICKS.map((y) => [y, y === OVERALL_Y ? OVERALL.name : ROWS.find((r) => r.y === y).name])\n);\n\nconst WEIGHTS = STUDIES.map((s) => s.weight);\nconst MIN_WEIGHT = Math.min(...WEIGHTS);\nconst MAX_WEIGHT = Math.max(...WEIGHTS);\nconst markerHalf = (w) => 5 + ((w - MIN_WEIGHT) / (MAX_WEIGHT - MIN_WEIGHT)) * 6; // 5-11 px half-size\n\nconst X_MIN = Math.min(...STUDIES.map((s) => s.lower), OVERALL.lower) - 1.5;\nconst X_MAX = Math.max(...STUDIES.map((s) => s.upper), OVERALL.upper) + 1.5;\n\n// Rendered inside ChartContainer to access the D3 coordinate scales.\nfunction ForestMarks() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const drawingArea = useDrawingArea();\n\n  const dividerY = yScale((ROWS[ROWS.length - 1].y + OVERALL_Y) / 2) ?? 0;\n  const textX = drawingArea.left + drawingArea.width + 20;\n  const capHalf = 6;\n\n  return (\n    <g>\n      <line\n        x1={drawingArea.left}\n        x2={drawingArea.left + drawingArea.width}\n        y1={dividerY}\n        y2={dividerY}\n        stroke={t.grid}\n        strokeWidth={1}\n      />\n\n      {ROWS.map((r) => {\n        const cx = xScale(r.effect) ?? 0;\n        const cy = yScale(r.y) ?? 0;\n        const x1 = xScale(r.lower) ?? 0;\n        const x2 = xScale(r.upper) ?? 0;\n        const half = markerHalf(r.weight);\n        return (\n          <g key={r.name}>\n            <line x1={x1} x2={x2} y1={cy} y2={cy} stroke={t.palette[0]} strokeWidth={2} />\n            <line x1={x1} x2={x1} y1={cy - capHalf} y2={cy + capHalf} stroke={t.palette[0]} strokeWidth={2} />\n            <line x1={x2} x2={x2} y1={cy - capHalf} y2={cy + capHalf} stroke={t.palette[0]} strokeWidth={2} />\n            <rect x={cx - half} y={cy - half} width={half * 2} height={half * 2} fill={t.palette[0]} />\n            <text x={textX} y={cy + 5} fontSize={14} fill={t.inkSoft} fontFamily=\"sans-serif\">\n              {`${r.effect.toFixed(1)} [${r.lower.toFixed(1)}, ${r.upper.toFixed(1)}]  ·  ${r.weight.toFixed(1)}%`}\n            </text>\n          </g>\n        );\n      })}\n\n      {(() => {\n        const cy = yScale(OVERALL_Y) ?? 0;\n        const xL = xScale(OVERALL.lower) ?? 0;\n        const xC = xScale(OVERALL.effect) ?? 0;\n        const xU = xScale(OVERALL.upper) ?? 0;\n        const halfH = 13;\n        return (\n          <g>\n            <polygon\n              points={`${xL},${cy} ${xC},${cy - halfH} ${xU},${cy} ${xC},${cy + halfH}`}\n              fill={t.ink}\n              stroke={t.pageBg}\n              strokeWidth={1}\n            />\n            <text x={textX} y={cy + 5} fontSize={14} fontWeight={700} fill={t.ink} fontFamily=\"sans-serif\">\n              {`${OVERALL.effect.toFixed(1)} [${OVERALL.lower.toFixed(1)}, ${OVERALL.upper.toFixed(1)}]`}\n            </text>\n          </g>\n        );\n      })()}\n    </g>\n  );\n}\n\nconst TITLE = \"Systolic BP Reduction · forest-basic · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_SIZE = Math.round(22 * Math.min(1, 67 / TITLE.length));\nconst SUBTITLE = \"Mean difference vs. placebo (mmHg) · squares sized by study weight · diamond = pooled effect\";\nconst TITLE_HEIGHT = 64;\nconst SUBTITLE_HEIGHT = 40;\nconst MARGIN = { top: 20, right: 250, bottom: 76, left: 240 };\n\nexport default function Chart() {\n  const chartWidth = window.ANYPLOT_SIZE.width;\n  const chartHeight = window.ANYPLOT_SIZE.height - TITLE_HEIGHT - SUBTITLE_HEIGHT;\n\n  return (\n    <Box\n      sx={{\n        width: chartWidth,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Box sx={{ height: TITLE_HEIGHT, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", flexShrink: 0 }}>\n        <Typography sx={{ color: t.ink, fontSize: TITLE_FONT_SIZE, fontWeight: 500 }}>{TITLE}</Typography>\n      </Box>\n      <Box sx={{ height: SUBTITLE_HEIGHT, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", flexShrink: 0 }}>\n        <Typography sx={{ color: t.inkSoft, fontSize: 15 }}>{SUBTITLE}</Typography>\n      </Box>\n\n      <ChartContainer\n        skipAnimation\n        width={chartWidth}\n        height={chartHeight}\n        margin={MARGIN}\n        series={[]}\n        xAxis={[{\n          scaleType: \"linear\",\n          min: X_MIN,\n          max: X_MAX,\n          label: \"Mean Difference in Systolic Blood Pressure (mmHg)\",\n          labelStyle: { fontSize: 15, fill: t.ink },\n          tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n        }]}\n        yAxis={[{\n          scaleType: \"linear\",\n          min: OVERALL_Y - 1.2,\n          max: N + 0.8,\n          tickInterval: Y_TICKS,\n          valueFormatter: (v) => Y_LABELS.get(v) ?? \"\",\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          tickSize: 4,\n        }]}\n        sx={{ \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeOpacity: 0.5 } }}\n      >\n        <ChartsGrid vertical />\n        <ChartsReferenceLine\n          x={0}\n          label=\"No difference\"\n          labelAlign=\"end\"\n          labelStyle={{ fontSize: 13, fill: t.inkSoft }}\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 4\", strokeWidth: 1.5 }}\n        />\n        <ForestMarks />\n        <ChartsXAxis />\n        <ChartsYAxis disableLine />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}