{"spec_id":"forest-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// forest-basic: Meta-Analysis Forest Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst MUTED = window.ANYPLOT_THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Meta-analysis of 11 randomized controlled trials comparing a new therapy\n// against placebo on treatment response (odds ratio > 1 favors the therapy).\nconst studies = [\n  { study: \"Anderson 2014\", effect: 1.35, ciLower: 0.82, ciUpper: 2.22, weight: 5.4 },\n  { study: \"Bianchi 2015\", effect: 1.62, ciLower: 1.05, ciUpper: 2.5, weight: 6.8 },\n  { study: \"Castillo 2016\", effect: 0.95, ciLower: 0.58, ciUpper: 1.56, weight: 5.1 },\n  { study: \"Dubois 2017\", effect: 1.88, ciLower: 1.2, ciUpper: 2.94, weight: 7.2 },\n  { study: \"Eriksson 2018\", effect: 1.41, ciLower: 1.02, ciUpper: 1.95, weight: 11.5 },\n  { study: \"Fujimoto 2018\", effect: 2.05, ciLower: 1.18, ciUpper: 3.56, weight: 4.6 },\n  { study: \"Gupta 2019\", effect: 1.22, ciLower: 0.88, ciUpper: 1.69, weight: 10.3 },\n  { study: \"Haddad 2020\", effect: 1.55, ciLower: 1.15, ciUpper: 2.09, weight: 12.7 },\n  { study: \"Ivanov 2021\", effect: 1.1, ciLower: 0.71, ciUpper: 1.7, weight: 6.9 },\n  { study: \"Jensen 2022\", effect: 1.78, ciLower: 1.28, ciUpper: 2.48, weight: 9.8 },\n  { study: \"Kowalski 2023\", effect: 1.48, ciLower: 1.18, ciUpper: 1.86, weight: 19.7 },\n];\nconst pooled = { study: \"Pooled Effect\", effect: 1.45, ciLower: 1.28, ciUpper: 1.65 };\nconst nullEffect = 1;\n\nconst labels = [...studies.map((s) => s.study), pooled.study];\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart --------------------------------------------------------------------\nconst title = \"forest-basic · javascript · chartjs · anyplot.ai\";\n\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels,\n    datasets: [\n      {\n        label: \"Null effect (OR = 1)\",\n        type: \"line\",\n        data: labels.map((label) => ({ x: nullEffect, y: label })),\n        borderColor: MUTED,\n        borderDash: [8, 5],\n        borderWidth: 2,\n        pointRadius: 0,\n        fill: false,\n        tension: 0,\n      },\n      {\n        label: \"95% confidence interval\",\n        data: [...studies.map((s) => [s.ciLower, s.ciUpper]), [pooled.ciLower, pooled.ciUpper]],\n        backgroundColor: MUTED,\n        barThickness: 4,\n        borderSkipped: false,\n      },\n      {\n        label: \"Study OR (marker size ∝ weight)\",\n        type: \"scatter\",\n        data: studies.map((s) => ({ x: s.effect, y: s.study })),\n        pointStyle: \"rect\",\n        pointRadius: studies.map((s) => 5 + s.weight * 0.75),\n        pointHoverRadius: studies.map((s) => 5 + s.weight * 0.75),\n        backgroundColor: t.palette[0],\n        borderWidth: 0,\n      },\n      {\n        label: \"Pooled OR\",\n        type: \"scatter\",\n        data: [{ x: pooled.effect, y: pooled.study }],\n        pointStyle: \"rectRot\",\n        pointRadius: 20,\n        pointHoverRadius: 20,\n        backgroundColor: t.ink,\n        borderWidth: 0,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 10, right: 40, bottom: 10, left: 10 } },\n    plugins: {\n      title: { display: true, text: title, color: t.ink, font: { size: 22 } },\n      subtitle: {\n        display: true,\n        text: \"Drug A vs. placebo · square markers sized by study weight · diamond = pooled estimate\",\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 12 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: { color: t.ink, font: { size: 14 }, boxHeight: 10, boxWidth: 10 },\n      },\n    },\n    scales: {\n      x: {\n        type: \"logarithmic\",\n        min: 0.4,\n        max: 4,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => {\n            const allowed = [0.4, 0.5, 1, 2, 4];\n            const rounded = Math.round(value * 100) / 100;\n            return allowed.includes(rounded) ? rounded : null;\n          },\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Odds Ratio (log scale)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n      },\n    },\n  },\n});\n"}