A forest plot sums up the studies on one question: each row shows a study’s result as a square, sized by how much it counts, with a line for its range of uncertainty. The diamond at the bottom is all the studies combined.
A forest plot is how meta-analyses show their evidence: each study’s estimate and interval in its own row, the weight of each by the size of its square, and the pooled result as a diamond. Readers see at once whether the studies agree and which ones carry the result.
Built with amCharts 5, a JavaScript library. The code includes your theme and renderer (canvas or SVG), but not the changes you make with the controls.
npm install @amcharts/amcharts5. See Getting started.
The same chart as a JSON config. Chart Maker reads and writes this format, the library's JSON parser turns it into a chart, and the <am5-chart> element puts it on a page with no code."type" is the class to create and "settings" what you would pass to its new() in code; "properties" fill in its parts, lists such as series and axes or templates such as columns.template; "refs" holds objects used in more than one place ("#series-0" stands for the one named series-0); a string starting with "@" takes a value from another object, such as "@series.get('fill')".
A complete page. Save it as an .html file and open it in a browser.
Paste this into an AI coding assistant to build the chart from scratch. More in amCharts & AI.
// Create root element
// https://www.amcharts.com/docs/v5/getting-started/#Root_element
var root = am5.Root.new("chartdiv");
// Set themes
// https://www.amcharts.com/docs/v5/concepts/themes/
root.setThemes([
am5themes_Animated.new(root),
am5themes_Responsive.new(root)
]);
// Create chart
// https://www.amcharts.com/docs/v5/charts/xy-chart/
var chart = root.container.children.push(am5xy.XYChart.new(root, {
panX: false, // no dragging the plot
panY: false,
wheelX: "panX", // a horizontal wheel or trackpad swipe pans...
wheelY: "zoomX", // ...and the vertical wheel zooms in on the values
layout: root.verticalLayout
}));
// Data
var data = [{
category: "Smith et al. 1991",
measure: 1.3,
bulletSize: 25,
high: 3.4,
low: 1.0
}, {
category: "Jones et al. 1993",
measure: 2.1,
bulletSize: 15,
high: 2.6,
low: 0.5
}, {
category: "Smith et al. 1999",
measure: 1.8,
bulletSize: 10,
high: 3.2,
low: 0.9
}, {
category: "Ng et al. 2004",
measure: 2.3,
bulletSize: 30,
high: 2.7,
low: 1.9
}, {
category: "Chu et al. 2009",
measure: 2.1,
bulletSize: 35,
high: 2.5,
low: 1.8
}, {
category: "Summary measure",
measure: 2.2,
bulletSize: 55,
high: 2.4,
low: 1.9,
// The summary is a hollow diamond: a square turned 45 degrees, filled with the background color
bulletSettings: {
rotation: 45,
fill: root.interfaceColors.get("background")
},
textSettings: {
text: "{valueX}"
}
}];
// Create axes
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
var yRenderer = am5xy.AxisRendererY.new(root, {
inversed: true, // the first study at the top
minorGridEnabled: true // a skipped study still gets a faint grid line
});
var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
categoryField: "category",
renderer: yRenderer
}));
yAxis.data.setAll(data);
// a second category axis on the right, its labels showing each study's measure and interval
var yRenderer2 = am5xy.AxisRendererY.new(root, {
opposite: true, // on the right side...
inversed: true // ...in the same top-down order
})
var yAxis2 = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
categoryField: "category",
renderer: yRenderer2
}));
yRenderer2.grid.template.setAll({
forceHidden: true // no grid lines from this axis
})
yRenderer2.labels.template.setAll({
populateText: true // fills in the {placeholders} from each study's data
})
yRenderer2.labels.template.adapters.add("text", function(text, target) {
return "[bold]{measure}[/] ({low}-{high})"; // the measure in bold, then the interval
})
yAxis2.data.setAll(data);
var xAxis = chart.xAxes.push(am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererX.new(root, {
strokeOpacity: 0.1 // a faint line along the value axis
})
}));
// Add series
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
// Column series: a thin bar from the low to the high end of each confidence interval
var series = chart.series.push(am5xy.ColumnSeries.new(root, {
xAxis: xAxis,
yAxis: yAxis,
valueXField: "high",
openValueXField: "low",
categoryYField: "category"
}));
series.columns.template.setAll({
height: 1, // 1px tall...
strokeWidth: 1 // ...plus a 1px outline: a thin line
});
series.data.setAll(data);
// Line series with no line, for the markers: each square is sized by the study's weight
var series2 = chart.series.push(am5xy.LineSeries.new(root, {
xAxis: xAxis,
yAxis: yAxis,
valueXField: "measure",
categoryYField: "category",
valueField: "bulletSize", // the value the heat rules size the squares by
// works out the lowest and highest bulletSize, which the heat rules below scale between
calculateAggregates: true,
fill: series.get("fill"), // in the same color as the bars
tooltip: am5.Tooltip.new(root, {
labelText: "[bold]{valueX}[/] ({low}-{high})" // the measure in bold, then the interval
})
}));
series2.strokes.template.setAll({
forceHidden: true
});
var rectangleTemplate = am5.Template.new({
stroke: series.get("fill"), // an outline...
fill: series.get("fill"), // ...and a fill in the bars' color
centerY: am5.p50, // centered on the point
centerX: am5.p50,
strokeWidth: 2, // a 2px outline, which shows on the hollow diamond
templateField: "bulletSettings" // the summary's diamond look, from bulletSettings in its data
});
series2.bullets.push(function() {
return am5.Bullet.new(root, {
sprite: am5.Rectangle.new(root, {}, rectangleTemplate)
});
});
// squares from 10 to 40px on a side, scaled by each study's bulletSize
series2.set("heatRules", [{
target: rectangleTemplate,
key: "width",
min: 10,
max: 40,
dataField: "value"
}, {
target: rectangleTemplate,
key: "height",
min: 10,
max: 40,
dataField: "value"
}]);
// a value label, with text only where the data has textSettings: in the summary's diamond
series2.bullets.push(function() {
return am5.Bullet.new(root, {
sprite: am5.Label.new(root, {
centerX: am5.p50, // centered on the point
centerY: am5.p50,
templateField: "textSettings",
populateText: true // fills in {valueX} from the data
})
});
});
series2.data.setAll(data);
// Create a summary line
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/axis-ranges/
var rangeDataItem = xAxis.makeDataItem({
value: 2.2 // at the summary measure
});
var range = xAxis.createAxisRange(rangeDataItem);
// A dashed line at the summary value, in the theme's alternative background color (dark on light, light on dark), so it shows on light and dark backgrounds
rangeDataItem.get("grid").setAll({
stroke: root.interfaceColors.get("alternativeBackground"),
strokeOpacity: 0.3,
strokeDasharray: [3, 3]
});
// Add cursor
// https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {
xAxis: xAxis
}));
cursor.lineY.set("visible", false); // no horizontal cursor line...
cursor.lineX.set("visible", false); // ...and no vertical one: the cursor just shows the tooltip
// Make stuff animate on load
// https://www.amcharts.com/docs/v5/concepts/animations/
series.appear();
series2.appear();
chart.appear(1000, 100);