{"spec_id":"forest-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// forest-basic: Meta-Analysis Forest Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic): meta-analysis of RCTs, treatment vs control on 30-day readmission\nconst studies = [\n  { name: \"Chen et al. (2015)\", effect: 0.72, lower: 0.55, upper: 0.94, weight: 9.5 },\n  { name: \"Alvarez et al. (2016)\", effect: 0.68, lower: 0.48, upper: 0.97, weight: 7.2 },\n  { name: \"Kowalski et al. (2017)\", effect: 0.85, lower: 0.62, upper: 1.16, weight: 8.8 },\n  { name: \"Osei et al. (2018)\", effect: 0.61, lower: 0.45, upper: 0.83, weight: 11.3 },\n  { name: \"Tanaka et al. (2019)\", effect: 0.79, lower: 0.58, upper: 1.08, weight: 9.9 },\n  { name: \"Nguyen et al. (2020)\", effect: 0.55, lower: 0.38, upper: 0.8, weight: 6.4 },\n  { name: \"Ferreira et al. (2021)\", effect: 0.7, lower: 0.52, upper: 0.94, weight: 10.1 },\n  { name: \"Whitfield et al. (2022)\", effect: 0.64, lower: 0.49, upper: 0.84, weight: 13.8 },\n];\nconst pooled = { name: \"Pooled Estimate\", effect: 0.68, lower: 0.59, upper: 0.78, weight: 100 };\n\n// Category axis places index 0 at the bottom; the pooled diamond anchors the\n// bottom row, studies stack upward in chronological order (oldest on top).\nconst rows = [pooled, ...studies.slice().reverse()];\nconst categories = rows.map((row) => row.name);\nconst maxWeight = Math.max(...studies.map((row) => row.weight));\n\nconst seriesData = rows.map((row, index) => [\n  row.effect,\n  index,\n  row.lower,\n  row.upper,\n  row.weight,\n  row === pooled ? 1 : 0,\n]);\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Render: per-row whisker + point-estimate circle, pooled row a diamond\nfunction renderItem(params, api) {\n  const yIndex = api.value(1);\n  const isPooled = api.value(5) === 1;\n  const lowerPt = api.coord([api.value(2), yIndex]);\n  const upperPt = api.coord([api.value(3), yIndex]);\n  const pointPt = api.coord([api.value(0), yIndex]);\n  const color = isPooled ? t.ink : t.palette[0];\n\n  if (isPooled) {\n    const halfHeight = 22;\n    return {\n      type: \"polygon\",\n      shape: {\n        points: [\n          [pointPt[0], pointPt[1] - halfHeight],\n          [upperPt[0], pointPt[1]],\n          [pointPt[0], pointPt[1] + halfHeight],\n          [lowerPt[0], pointPt[1]],\n        ],\n      },\n      style: { fill: color, stroke: color },\n    };\n  }\n\n  const radius = 8 + (api.value(4) / maxWeight) * 10;\n  const capHalf = 7;\n  return {\n    type: \"group\",\n    children: [\n      {\n        type: \"line\",\n        shape: { x1: lowerPt[0], y1: lowerPt[1], x2: upperPt[0], y2: upperPt[1] },\n        style: { stroke: color, lineWidth: 2.5 },\n      },\n      {\n        type: \"line\",\n        shape: { x1: lowerPt[0], y1: lowerPt[1] - capHalf, x2: lowerPt[0], y2: lowerPt[1] + capHalf },\n        style: { stroke: color, lineWidth: 2.5 },\n      },\n      {\n        type: \"line\",\n        shape: { x1: upperPt[0], y1: upperPt[1] - capHalf, x2: upperPt[0], y2: upperPt[1] + capHalf },\n        style: { stroke: color, lineWidth: 2.5 },\n      },\n      {\n        type: \"circle\",\n        shape: { cx: pointPt[0], cy: pointPt[1], r: radius },\n        style: { fill: color },\n      },\n    ],\n  };\n}\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"forest-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  grid: { left: 280, right: 110, top: 90, bottom: 90 },\n  xAxis: {\n    type: \"value\",\n    min: 0.3,\n    max: 1.5,\n    name: \"Odds Ratio (95% CI) — Treatment vs Control\",\n    nameLocation: \"middle\",\n    nameGap: 42,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"category\",\n    data: categories,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  series: [\n    {\n      type: \"custom\",\n      encode: { x: 0, y: 1 },\n      data: seriesData,\n      renderItem: renderItem,\n      markLine: {\n        symbol: \"none\",\n        silent: true,\n        label: { show: false },\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        data: [{ xAxis: 1 }],\n      },\n      z: 3,\n    },\n  ],\n});\n"}