I’m using Chart.js to graph two datasets on the same bar chart for a time-based comparison (x-axis). However, I’m encountering an issue where the height of one graph squashes or expands to fit the height of the other, making it difficult to compare them effectively.
For example, the purple graph below increases by 26x, while the blue graph increases by only 5x. Due to the scaling, it’s hard to assess the impact of these changes, as the purple graph is squashed into the height of the blue graph.
I think starting the purple graph at the same level as the blue graph at the first data point might help, but I’m concerned that the data will still be squashed to fit the blue graph’s height.
Are there specific Chart.js settings or configurations I can adjust to improve the comparability of these datasets?
My code for chart scales is as follows:
y: {
beginAtZero: true,
grid: {
display: false,
drawBorder: false,
drawOnChartArea: false,
},
ticks: {
display: true,
},
},
},
y1: {
beginAtZero: true,
position: 'right',
grid: {
display: false,
drawBorder: false,
drawOnChartArea: false,
},
ticks: {
display: true,
},
},
}
Current graph
Expected graph
There isn't a chart.js option that would arrange the values such that the first bars of the
two datasets have equal height, but it can be arranged by computing the value of axes
max
dynamically:
function computeMax(which, data){
// computes max for scale `which` (0 -> 'y', 1 -> 'y1') such that the first
// bars of the two datasets have the same height
const other = 1 - which,
dataValues = data.datasets.map(dataset=>dataset.data),
dataMax = dataValues.map(data => Math.max(...data)),
firstToMax = dataValues.map((data, i) => data[0] / dataMax[i])
if(firstToMax[which] > firstToMax[other]){
return firstToMax[which]/firstToMax[other] * dataMax[which];
}
else{
return dataMax[which];
}
}
the function computeMax
being called for the scriptable
max
property of each axis, like this:
options: {
........... other options
scales: {
y: {
.......... other y axis options
ticks: {
display: true,
includeBounds: false
},
max: ({chart}) => computeMax(0, chart.data)
},
y1: {
.......... other y1 axis options
ticks: {
display: true,
includeBounds: false
},
max: ({chart}) => computeMax(1, chart.data)
}
}
}
Here's a snippet that includes that piece of code:
function computeMax(which, data){
// computes max for scale `which` (0 -> 'y', 1 -> 'y1') such that the first
// bars of the two datasets have the same height
const other = 1 - which,
dataValues = data.datasets.map(dataset=>dataset.data),
dataMax = dataValues.map(data => Math.max(...data)),
firstToMax = dataValues.map((data, i) => data[0] / dataMax[i])
if(firstToMax[which] > firstToMax[other]){
return firstToMax[which]/firstToMax[other] * dataMax[which];
}
else{
return dataMax[which];
}
}
const config = {
type: "bar",
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [
{
label: 2023,
data: [20, 65, 80, 102, 55, 56, 65, 59, 80, 81, 56, 55],
},
{
label: 2024,
data: [50, 59, 80, 81, 56, 55, 65, 108, 80, 81, 56, 55].map(x => x * 10),
yAxisID: 'y1'
},
]
},
options: {
maintainAspectRatio: false,
scales: {
y: {
grid: {
display: false,
drawBorder: false,
drawOnChartArea: false
},
ticks: {
display: true,
includeBounds: false
},
max: ({chart}) => computeMax(0, chart.data)
},
y1: {
beginAtZero: true,
position: 'right',
grid: {
display: false,
drawBorder: false,
drawOnChartArea: false,
},
ticks: {
display: true,
includeBounds: false
},
max: ({chart}) => computeMax(1, chart.data)
},
}
}
};
new Chart('myChart', config);
<div style="height: 300px">
<canvas id="myChart">
</canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
Since the axes had beginAtZero
set true
it was clear that all the values are
positive. A generalization with multiple datasets and including negative values
can be found in this fiddle.