I have a temperature chart, with X axis = time, and Y axis is temperature.
Because I have data collected every minutes since several years, I have a lot of data, and I don't want to display all of them at once on the graph.
So I create a ChartJS line chart with the following characteristics :
This logic works fine, except the Y axis does not adapt the scale according to the data.
For example, yearly Y value oscillates from 7 to 33. The chart adapt the Y scale from 5 to 35 (which is fine). If I zoom to a day where a value is 10, without doing any panning, the scale Y adapt from 9 to 11. And zooming out from this point to the year will change the Y scale back to 5-35.
But if I zoom on a point, and I pan right/left/up/down (which is usefull to explore the graph), when I zoom out, the scale is not going back to the initial value. So it often happens that the line is not displayed because out of range of the Y axis scale.
Is there a way to force the Y scale to adapt itself to display all data, and not having point outside of the scale ?
I tried to update chart.scales.y.min after fetching new data from the server, but after the chart.update('none'), the value is set again to the previous value, and scale is not changed.
Here is a sample code, with simulated data.
const PI = Math.PI;
function daysIntoYear(date) {
return (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000;
}
function secondsToDhms(seconds) {
seconds = Number(seconds);
var d = Math.floor(seconds / (3600 * 24));
var h = Math.floor(seconds % (3600 * 24) / 3600);
var m = Math.floor(seconds % 3600 / 60);
var s = Math.floor(seconds % 60);
var dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : "";
var hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
var mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
var sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
return dDisplay + hDisplay + mDisplay + sDisplay;
}
function dataSimulation(from, to, grouping) {
const timeDiff = (new Date(to) - new Date(from)) / 1000;
console.log("fromDate=" + from + " toDate=" + to + " diff=" + secondsToDhms(timeDiff) + " group=" + secondsToDhms(grouping));
datamapped = [];
dataEvery = 60 * 20; // Data get every 20mn
min = 999;
max = -999;
i = 0;
sum = 0;
for (x = new Date(from).getTime(); x <= new Date(to).getTime(); x = x + 1000 * dataEvery) {
date = new Date(x);
H = date.getHours();
M = date.getMinutes();
month = date.getMonth();
day = date.getDate();
nday = daysIntoYear(date);
value = day + (H / 100) + (M / 10000); // simple simulation
value = 20 + (10 * Math.sin(nday * (PI / 180))) + 3 * Math.sin(H * (360 / 24) * (PI / 180)); // more complex
sum = sum + value;
if (value > max)
max = value;
if (value < min)
min = value;
if ((i * dataEvery) > grouping) {
datamapped.push({
x: new Date(x).toISOString(),
min: min,
max: max,
avg: sum / i
});
i = 0;
sum = 0;
min = 999;
max = -999;
}
i = i + 1;
}
return datamapped;
}
async function fetchData(from, to, group) {
/**
const response = await fetch(`data.php?from=${from}&to=${to}&sensor=OWM&grouptime=86400`);
const data = await response.json();
datamapped = data.map(item => ({
x: item[0],
min: item[1],
max: item[2],
avg: item[3]
}));
**/
datamapped = dataSimulation(from, to, group);
return datamapped;
}
var LASTUPDATETIME;
LASTUPDATETIME = new Date();
var LOCK;
LOCK = false;
async function updateData(chart) {
difftime = (new Date().getTime() - LASTUPDATETIME.getTime());
console.log("LOCK=" + LOCK + " difftime=" + difftime);
if (LOCK == true) {
if (difftime < 1000)
return;
}
LOCK = true;
//if ( difftime < 500)
//{ // debounce
// console.log("too soon");
// return;
//}
const xmin = chart.scales.x.min;
const xmax = chart.scales.x.max;
const fromDate = new Date(xmin).toISOString();
const toDate = new Date(xmax).toISOString();
const timeDiff = (xmax - xmin) / 1000;
group = 31 * 24 * 3600;
if (timeDiff < 1 * 24 * 3600) { // <1 days, display per every minute
group = 60;
} else if (timeDiff < 4 * 24 * 3600) { // <4 days, display per every hours
group = 3600;
} else if (timeDiff < 33 * 24 * 3600) { // <1.1month, display per 4xday
group = 4 * 3600;
} else if (timeDiff < 4 * 31 * 24 * 3600) { // <4month, display per day
group = 24 * 3600;
}
/**
response = await fetch(`data.php?fmt=json&from=${fromDate}&to=${toDate}&sensor=OWM&grouptime=${group}`);
data = await response.json();
datamapped = data.map(item => ({
x: item[0],
min: item[1],
max: item[2],
avg: item[3]
}));
**/
datamapped = dataSimulation(fromDate, toDate, group);
chart.data.datasets[0].data = datamapped;
chart.data.datasets[1].data = datamapped;
chart.data.datasets[2].data = datamapped;
chart.scales.y.min = -100; // as a test, the Y axis should be at -100, but not working
chart.update('none');
LASTUPDATETIME = new Date();
LOCK = false;
}
async function createChart(from, to, group) {
const data = await fetchData(from, to, group);
const ctx = document.getElementById('temperatureChart').getContext('2d');
const temperatureChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
data: data, // The three values are on the same data ? strange
parsing: {
yAxisKey: 'min'
},
fill: '+1',
borderWidth: 0
},
{
data: data, // this is strange to have the same data than the previous
parsing: {
yAxisKey: 'max'
},
borderWidth: 0
},
{
data: data,
parsing: {
yAxisKey: 'avg'
},
borderColor: 'green',
fill: false,
borderWidth: 1
}
]
},
options: {
responsive: true,
animation: false,
elements: {
point: {
radius: 1
}
},
scales: {
x: {
type: 'time',
time: {
tooltipFormat: 'yyyy-MM-dd HH:mm'
},
title: {
display: true,
text: 'Date/Time'
},
},
y: {
beginAtZero: false,
title: {
display: true,
text: 'Temperature (°C)'
},
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
zoom: {
pan: {
// pan options and/or events
enabled: true,
onPanComplete: function({
chart
}) {
updateData(chart);
}
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoomComplete: function({
chart
}) {
updateData(chart);
}
}
}
}
}
});
}
// Example usage
createChart('2024-01-01', '2024-12-31', 31 * 24 * 3600);
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Temperature Line Chart</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script>
</head>
<div style="width: 100%; margin: auto;">
<canvas id="temperatureChart"></canvas>
</div>
See the fiddle here : https://jsfiddle.net/1bnzsx5L/1/
If you want to set the min
value explicitly, you have to set the options
:
chart.options.scales.y.min = -100;
chart.update('none');
The call to chart.update
is reading the data and options and sets
the dynamic values like chart.scales.y.min
according to these. Thus,
values like chart.scales.y.min
are only useful for reading the state
of the chart objects (in this case 'y'
axis), but can't be used to
change that state.
If you want to leave it to the standard chart.js algorithm to find the nice
limits to the axis, you may try disabling y
panning (by setting
mode: 'x'
, as it's already set for zoom
). A slight vertical change
while panning will interfere with the way the limits are computed, and
might then be amplified by a subsequent zoom operation.
My testing with x-mode panning seemed to always produce good limits, but if
there are still cases when it's not working, please let me know. Here's
the snippet (I also set options.scales.y.ticks.includeBounds = true
):
const PI = Math.PI;
function daysIntoYear(date) {
return (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000;
}
function secondsToDhms(seconds) {
seconds = Number(seconds);
var d = Math.floor(seconds / (3600 * 24));
var h = Math.floor(seconds % (3600 * 24) / 3600);
var m = Math.floor(seconds % 3600 / 60);
var s = Math.floor(seconds % 60);
var dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : "";
var hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : "";
var mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : "";
var sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
return dDisplay + hDisplay + mDisplay + sDisplay;
}
function dataSimulation(from, to, grouping) {
const timeDiff = (new Date(to) - new Date(from)) / 1000;
//console.log("fromDate=" + from + " toDate=" + to + " diff=" + secondsToDhms(timeDiff) + " group=" + secondsToDhms(grouping));
datamapped = [];
dataEvery = 60 * 20; // Data get every 20mn
min = 999;
max = -999;
i = 0;
sum = 0;
for (x = new Date(from).getTime(); x <= new Date(to).getTime(); x = x + 1000 * dataEvery) {
date = new Date(x);
H = date.getHours();
M = date.getMinutes();
month = date.getMonth();
day = date.getDate();
nday = daysIntoYear(date);
value = day + (H / 100) + (M / 10000); // simple simulation
value = 20 + (10 * Math.sin(nday * (PI / 180))) + 3 * Math.sin(H * (360 / 24) * (PI / 180)); // more complex
sum = sum + value;
if (value > max)
max = value;
if (value < min)
min = value;
if ((i * dataEvery) > grouping) {
datamapped.push({
x: new Date(x).toISOString(),
min: min,
max: max,
avg: sum / i
});
i = 0;
sum = 0;
min = 999;
max = -999;
}
i = i + 1;
}
return datamapped;
}
async function fetchData(from, to, group) {
/**
const response = await fetch(`data.php?from=${from}&to=${to}&sensor=OWM&grouptime=86400`);
const data = await response.json();
datamapped = data.map(item => ({
x: item[0],
min: item[1],
max: item[2],
avg: item[3]
}));
**/
datamapped = dataSimulation(from, to, group);
return datamapped;
}
var LASTUPDATETIME;
LASTUPDATETIME = new Date();
var LOCK;
LOCK = false;
async function updateData(chart) {
difftime = (new Date().getTime() - LASTUPDATETIME.getTime());
//console.log("LOCK=" + LOCK + " difftime=" + difftime);
if (LOCK == true) {
if (difftime < 1000)
return;
}
LOCK = true;
//if ( difftime < 500)
//{ // debounce
// console.log("too soon");
// return;
//}
const xmin = chart.scales.x.min;
const xmax = chart.scales.x.max;
const fromDate = new Date(xmin).toISOString();
const toDate = new Date(xmax).toISOString();
const timeDiff = (xmax - xmin) / 1000;
group = 31 * 24 * 3600;
if (timeDiff < 1 * 24 * 3600) { // <1 days, display per every minute
group = 60;
} else if (timeDiff < 4 * 24 * 3600) { // <4 days, display per every hours
group = 3600;
} else if (timeDiff < 33 * 24 * 3600) { // <1.1month, display per 4xday
group = 4 * 3600;
} else if (timeDiff < 4 * 31 * 24 * 3600) { // <4month, display per day
group = 24 * 3600;
}
/**
response = await fetch(`data.php?fmt=json&from=${fromDate}&to=${toDate}&sensor=OWM&grouptime=${group}`);
data = await response.json();
datamapped = data.map(item => ({
x: item[0],
min: item[1],
max: item[2],
avg: item[3]
}));
**/
datamapped = dataSimulation(fromDate, toDate, group);
chart.data.datasets[0].data = datamapped;
chart.data.datasets[1].data = datamapped;
chart.data.datasets[2].data = datamapped;
const yDataMax = Math.max(...datamapped.map(({max}) => max)),
yDataMin = Math.min(...datamapped.map(({min}) => min));
//chart.scales.options.y.min = -100; // as a test, the Y axis should be at -100, but not working
chart.update('none'); // with 'none' it's synchronous, so we can get the results immediately after:
console.clear(); // to preserve snippet space
console.log(`after .update, data:, [${yDataMin}, ${yDataMax}], scale: [${chart.scales.y.min}, ${chart.scales.y.max}]`);
LASTUPDATETIME = new Date();
LOCK = false;
}
async function createChart(from, to, group) {
const data = await fetchData(from, to, group);
const ctx = document.getElementById('temperatureChart').getContext('2d');
const temperatureChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
data: data, // The three values are on the same data ? strange
parsing: {
yAxisKey: 'min'
},
fill: '+1',
borderWidth: 0
},
{
data: data, // this is strange to have the same data than the previous
parsing: {
yAxisKey: 'max'
},
borderWidth: 0
},
{
data: data,
parsing: {
yAxisKey: 'avg'
},
borderColor: 'green',
fill: false,
borderWidth: 1
}
]
},
options: {
responsive: true,
animation: false,
elements: {
point: {
radius: 1
}
},
scales: {
x: {
type: 'time',
time: {
tooltipFormat: 'yyyy-MM-dd HH:mm'
},
title: {
display: true,
text: 'Date/Time'
},
},
y: {
beginAtZero: false,
ticks: {
includeBounds: false
},
title: {
display: true,
text: 'Temperature (°C)'
},
}
},
plugins: {
legend: {
display: true,
position: 'top'
},
zoom: {
pan: {
// pan options and/or events
enabled: true,
mode: "x",
onPanComplete: function({
chart
}) {
updateData(chart);
}
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
onZoomComplete: function({
chart
}) {
updateData(chart);
}
}
}
}
}
});
}
// Example usage
createChart('2024-01-01', '2024-12-31', 31 * 24 * 3600);
<div style="width: 100%; margin: auto;">
<canvas id="temperatureChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script>