You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ChartJS缩放平移且服务器更新数据时Y轴自动缩放失效问题

问题描述

我用ChartJS实现了一个温度折线图,X轴为时间,Y轴为温度。由于多年来每分钟收集一次数据,数据量庞大,设计了以下特性:

  • 默认显示当前年度数据,按月聚合:图表包含最小值与最大值间的填充区域及月度平均值折线,共12个数据点(每月一个,含min/max/avg值)。
  • 平移图表时,根据新时间范围从服务器获取JSON数据更新图表。
  • 缩放图表时,根据新时间范围重新从服务器获取对应粒度的数据:
    • 时间范围≤1小时:返回无min/max聚合的原始数据
    • 1小时<范围≤1天:按小时聚合
    • 1天<范围≤1个月:按天聚合
    • 范围>1个月:按月聚合

图表逻辑运行正常,但Y轴自适应缩放存在异常:

  • 正常场景:年度数据Y值在7-33之间时,Y轴自动调整为5-35;直接缩放到某一天(Y值约10)时,Y轴调整为9-11;再缩放回年度时,Y轴能恢复5-35。
  • 异常场景:先缩放再平移,之后缩放回年度时,Y轴范围无法恢复初始值,导致折线因超出Y轴范围而无法显示。

尝试在从服务器获取新数据后更新chart.scales.y.min,但执行chart.update('none')后该值会被重置,Y轴范围未改变。

示例代码

JavaScript

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) {
  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;
  
  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;
  }
  
  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,
          parsing: {
            yAxisKey: 'min'
          },
          fill: '+1',
          borderWidth: 0
        },
        {
          data: data,
          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: {
            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

<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>
<body>
  <div style="width: 100%; margin: auto;">
    <canvas id="temperatureChart"></canvas>
  </div>
</body>
</html>
解决方案

这个问题的核心是缩放/平移操作后,Y轴的min/max被隐性锁定,导致后续更新时ChartJS无法自动重新计算自适应范围。可通过以下步骤修复:

1. 重置Y轴的手动锁定状态

不要直接修改chart.scales.y.min,而是修改图表配置中的Y轴参数,将min和max设为undefined,让ChartJS重新根据数据集计算自适应范围:

// 在updateData函数的数据集更新后添加
chart.options.scales.y.min = undefined;
chart.options.scales.y.max = undefined;
// 使用默认update模式,触发完整的轴范围计算
chart.update();

2. 优化防抖逻辑

原有的LOCK机制可能导致更新不及时,替换为更可靠的防抖处理:

let updateTimeout;
async function updateData(chart) {
  clearTimeout(updateTimeout);
  updateTimeout = setTimeout(async () => {
    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;
    
    // 原有聚合粒度判断逻辑...
    const datamapped = await dataSimulation(fromDate, toDate, group);
    
    chart.data.datasets.forEach(ds => ds.data = datamapped);
    // 重置Y轴范围
    chart.options.scales.y.min = undefined;
    chart.options.scales.y.max = undefined;
    // 触发完整更新
    chart.update();
  }, 300); // 300ms防抖,可按需调整
}

3. 限制缩放插件仅作用于X轴

确保缩放/平移操作仅影响X轴,避免干扰Y轴的自动计算:

zoom: {
  pan: {
    enabled: true,
    mode: 'x', // 仅允许X轴平移
    onPanComplete: ({ chart }) => updateData(chart)
  },
  zoom: {
    wheel: { enabled: true },
    pinch: { enabled: true },
    mode: 'x', // 仅允许X轴缩放
    onZoomComplete: ({ chart }) => updateData(chart)
  }
}

原理说明

  • 直接修改chart.scales.y.min是临时状态,update('none')会跳过轴范围重新计算,导致原有锁定的范围被保留。
  • 修改chart.options.scales.y.min/max为undefined,会告知ChartJS放弃手动锁定,重新基于当前数据集计算Y轴的最优范围。
  • 使用默认的chart.update()会触发完整的图表更新流程,包括轴范围的重新计算。

内容的提问来源于stack exchange,提问作者JayMore

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 13:28:10