{"spec_id":"forest-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// forest-basic: Meta-Analysis Forest Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Meta-analysis of randomized controlled trials: drug vs. placebo, risk ratio\n// for the primary outcome. Null effect for a ratio measure is RR = 1.\nconst studies = [\n  { label: \"Chen 2015\", effect: 0.72, ciLower: 0.55, ciUpper: 0.94, weight: 12 },\n  { label: \"Kumar 2016\", effect: 0.85, ciLower: 0.68, ciUpper: 1.08, weight: 15 },\n  { label: \"Silva 2017\", effect: 0.63, ciLower: 0.44, ciUpper: 0.9, weight: 9 },\n  { label: \"Fischer 2019\", effect: 0.91, ciLower: 0.75, ciUpper: 1.12, weight: 18 },\n  { label: \"Tanaka 2020\", effect: 0.77, ciLower: 0.6, ciUpper: 0.99, weight: 14 },\n  { label: \"Okafor 2021\", effect: 0.68, ciLower: 0.5, ciUpper: 0.93, weight: 11 },\n  { label: \"Rossi 2022\", effect: 0.8, ciLower: 0.64, ciUpper: 1.0, weight: 16 },\n];\nconst pooledEstimate = { label: \"Pooled Estimate\", effect: 0.78, ciLower: 0.7, ciUpper: 0.87 };\nconst nullEffect = 1;\n\nconst weights = studies.map((s) => s.weight);\nconst minWeight = Math.min(...weights);\nconst maxWeight = Math.max(...weights);\nconst markerRadius = (weight) => 6 + ((weight - minWeight) / (maxWeight - minWeight)) * 10;\n// CI whisker thickness echoes the same weight scale as the marker radius, so a\n// heavy study (e.g. Fischer 2019) reads as \"more precise\" via both encodings.\nconst whiskerLineWidth = (weight) => 1.5 + ((weight - minWeight) / (maxWeight - minWeight)) * 2.5;\n\nconst rows = [...studies.map((s) => ({ ...s, pooled: false })), { ...pooledEstimate, pooled: true }];\nconst pooledIndex = rows.length - 1;\n\n// --- Chart -------------------------------------------------------------------\n// Each whisker series draws the CI line plus a short perpendicular end-cap at\n// both bounds (classic forest-plot styling), using null points to break the\n// path into three disconnected segments: low cap, main line, high cap.\nconst capHalfWidth = 0.12;\nconst whiskerSeries = rows.map((row, index) => ({\n  type: \"line\",\n  data: [\n    [index - capHalfWidth, row.ciLower],\n    [index + capHalfWidth, row.ciLower],\n    [index, null],\n    [index, row.ciLower],\n    [index, row.ciUpper],\n    [index, null],\n    [index - capHalfWidth, row.ciUpper],\n    [index + capHalfWidth, row.ciUpper],\n  ],\n  color: Highcharts.color(row.pooled ? t.ink : t.palette[0])\n    .setOpacity(0.55)\n    .get(),\n  lineWidth: row.pooled ? 3 : whiskerLineWidth(row.weight),\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: false,\n  animation: false,\n}));\n\nconst ciLabelFormatter = function () {\n  return `${this.y.toFixed(2)} (${this.point.low.toFixed(2)}–${this.point.high.toFixed(2)})`;\n};\n\nconst studySeries = {\n  name: \"Study estimate\",\n  type: \"scatter\",\n  color: t.palette[0],\n  data: studies.map((s, index) => ({\n    x: index,\n    y: s.effect,\n    low: s.ciLower,\n    high: s.ciUpper,\n    marker: { radius: markerRadius(s.weight) },\n  })),\n  marker: { symbol: \"circle\", lineWidth: 1, lineColor: t.pageBg },\n  dataLabels: {\n    enabled: true,\n    formatter: ciLabelFormatter,\n    style: { color: t.inkSoft, fontSize: \"12px\", fontWeight: \"400\", textOutline: \"none\" },\n    verticalAlign: \"bottom\",\n    y: -12,\n  },\n  zIndex: 5,\n  animation: false,\n};\n\nconst pooledSeries = {\n  name: \"Pooled estimate\",\n  type: \"scatter\",\n  color: t.ink,\n  data: [\n    {\n      x: pooledIndex,\n      y: pooledEstimate.effect,\n      low: pooledEstimate.ciLower,\n      high: pooledEstimate.ciUpper,\n      marker: { radius: 14 },\n    },\n  ],\n  marker: { symbol: \"diamond\", lineWidth: 1, lineColor: t.pageBg },\n  dataLabels: {\n    enabled: true,\n    formatter: ciLabelFormatter,\n    style: { color: t.ink, fontSize: \"13px\", fontWeight: \"600\", textOutline: \"none\" },\n    verticalAlign: \"bottom\",\n    y: -14,\n  },\n  zIndex: 6,\n  animation: false,\n};\n\nHighcharts.chart(\"container\", {\n  chart: {\n    inverted: true,\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"forest-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"25px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    categories: rows.map((row) => row.label),\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    min: 0.4,\n    max: 1.2,\n    tickInterval: 0.1,\n    title: { text: \"Risk Ratio\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotLines: [\n      {\n        value: nullEffect,\n        color: t.inkSoft,\n        dashStyle: \"Dash\",\n        width: 2,\n        zIndex: 4,\n        label: {\n          text: \"No effect (RR = 1)\",\n          rotation: 0,\n          align: \"right\",\n          verticalAlign: \"top\",\n          x: -8,\n          y: 20,\n          style: { color: t.inkSoft, fontSize: \"13px\" },\n        },\n      },\n    ],\n  },\n  legend: {\n    enabled: true,\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: [...whiskerSeries, studySeries, pooledSeries],\n});\n"}