javascriptchart.jspanchartjs-plugin-zoom

Auto Y scale not working when zooming and panning with data update from server


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/


Solution

  • 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>