Open
Description
MinutePhysics called us out (by name!) for not having native support for range charts: https://www.youtube.com/watch?v=5zBg9pH_6bE
However, to be clear, it is possible to build this chart type in Plotly using the base
attribute for bar charts and doing a bit of data preparation.
Here's a live demo of a custom range chart and our built-ins: https://codepen.io/ndrezn/pen/XJrWboB
Code snippet
/**
* Creates a range bar chart using Plotly.js.
*
* @param {Object} data - The dataset, with keys as categories (e.g., cities) and values as arrays of measurements.
* @param {Array} labels - The labels for each measurement (e.g., temperature metrics).
* @param {string} containerId - The ID of the HTML container to render the chart.
* @param {string} title - The title of the chart.
*/
function rangeChart(data, labels, containerId, title) {
const categories = Object.keys(data); // Extract categories (e.g., cities)
// Generate bar traces for each label
const barTraces = labels.map((label, index) => {
const bases = categories.map((category) =>
index === 0 ? data[category][index] : data[category][index - 1]
); // base, i.e. the previous value
const heights = categories.map(
(category) => data[category][index] - bases[categories.indexOf(category)]
); // Height relative to the base, i.e. the current value
return {
x: categories,
y: heights, // Bar heights
base: bases, // Starting points for each bar
name: label,
type: "bar"
};
});
// Layout for the chart
const layout = {
title: title,
barmode: "overlay", // Overlapping bars to show ranges
xaxis: { title: "Category" },
yaxis: { title: "Value" }
};
// Render the chart
Plotly.newPlot(containerId, barTraces, layout);
}
const ranges = {
Ontario: [-9, 3, 8, 13, 27],
England: [3, 8, 12, 16, 24],
Kentucky: [-3, 8, 14, 20, 30]
};
const labels = [
"Winter mean low",
"Annual mean low",
"Annual mean",
"Annual mean high",
"Summer mean high"
];
rangeChart(
ranges,
labels,
"rangePlot",
"Temperature Ranges in Three Londons (range)"
);
That being said, having this chart-type built in would be great. The mental gymnastics are a bit tricky to get a true range chart working correctly.
Here are the built-in ways to achieve a similar chart:
Code snippet
// Data for the three Londons
const ranges = {
Ontario: [-9, 3, 8, 13, 27],
England: [3, 8, 12, 16, 24],
Kentucky: [-3, 8, 14, 20, 30]
};
const labels = [
"Winter mean low",
"Annual mean low",
"Annual mean",
"Annual mean high",
"Summer mean high"
];
const cities = Object.keys(ranges);
// Scatter plot traces
const scatterTraces = labels.map((label, index) => ({
x: cities,
y: cities.map((city) => ranges[city][index]),
mode: "markers",
name: label,
type: "scatter"
}));
const scatterLayout = {
title: "Temperature Ranges in Three Londons (scatter)",
xaxis: { title: "City" },
yaxis: { title: "Temperature" }
};
Plotly.newPlot("scatterPlot", scatterTraces, scatterLayout);
// Bar plot traces
const barTraces = labels.map((label, index) => ({
x: cities,
y: cities.map((city) => ranges[city][index]),
name: label,
type: "bar"
}));
const barLayout = {
title: "Temperature Ranges in Three Londons (overlay)",
barmode: "overlay",
xaxis: { title: "City" },
yaxis: { title: "Temperature" }
};
Plotly.newPlot("barPlot", barTraces, barLayout);
// Bar plot traces
const stackedBarTraces = labels.map((label, index) => ({
x: cities,
y: cities.map((city) => ranges[city][index]),
name: label,
type: "bar"
}));
const stackedBarLayout = {
title: "Temperature Ranges in Three Londons (stacked)",
barmode: "stack",
xaxis: { title: "City" },
yaxis: { title: "Temperature" }
};
Plotly.newPlot("stackedBarPlot", stackedBarTraces, stackedBarLayout);
However, these aren't quite right.
- The scatter plot is most legible, but doesn't depict a range
- The overlaid bar shows the right data, but because the
base
of all positive points is0
, they actually overlap on top of each other. We want the base to he previous, and for England, we want the base to be3
, not0
. - The stacked bar chart looks right at first glance, but it's actually summing all the temperatures (and of course, the
base
for England is at0
, not3
. And the negative values stack a bit weirdly.