Chart.js无法自动缩放以包含当前视图中没有点的数据系列

前端开发 2026-07-11

我在一个使用时间刻度的图表上显示数据,并且视图是固定的(即只查看在两个设定的日期时间之间的数据)。

我的问题是图表不会自动缩放,以显示在该范围内并没有值的系列。

示例:

当前的最小/最大时间是09:00-10:00,我的系列在08:45有值,随后在10:15也有值。由于未将其纳入自动缩放范围,图表并不总是显示这条线(当数值接近其他系列时有时会显示)。

我在 https://codepen.io/ADringer/pen/ogzYoJY 创建了一个示例。有一个第二组数据本应跨越到80的值,但基于可见的第一组数据,刻度尺只显示到60。

目前的临时解决办法是在人最小时间点之前重复数值,但我在想Chart.js是否有更好的办法来解决这个问题,而不是靠我自己来处理。

var ctx = document.getElementById("myChart");
var myChart = new Chart(ctx, {
  type: "line",
  data: {
    datasets: [
      {
        label: "first",
        data: [
          {
            x: "2021-11-06 23:39:30",
            y: 50
          },
          {
            x: "2021-11-07 01:00:28",
            y: 60
          },
          {
            x: "2021-11-07 23:00:28",
            y: 20
          }
        ]
      },
       {
        label: "second",
        data: [
          {
            x: "2021-11-06 23:39:30",
            y: 80
          },

          {
            x: "2021-11-08 09:00:28",
            y: 80
          }
        ]
      }
    ]
  },
  options: {
    scales: {
      x: {
        type: "time",
        min: "2021-11-07T00:00:00",
        max: "2021-11-08T00:00:00"
      }
    }
  }
});

在上面的例子中,添加:

{
            x: "2021-11-07 00:00:00",
            y: 80
          }

即重复该值,从而强制将其显示。

解决方案

你在问题中提到原因了,但让我再说清楚一些:唯一的问题是y 轴的自动计算最小值/最大值仅基于在x 轴定义域内可见的数据点。一个数据集如果在x 轴的 minmax 之间没有任何点,其对y 轴的 minmax 值将不会产生影响;不管它是第二个、第一个,甚至是唯一的数据集。同样,这样的数据集始终会被绘制,但如果它的线条位于图表区域之外,便不会可见。

Chart.js没有办法使用部分可见的 segments 来计算数值轴的最小/最大值,因此没有简单的选项让它们可见。

一种替代在x 领域内没有可见点的数据集中添加插值点的方法,是显式设置y 轴刻度的 maxsuggestedMax 值(如有需要,还可以设置 minsuggestedMin)。

这可以自动完成,将 suggestedMax 设置为一个函数(它是一个 scriptable 选项),例如:

options: {
   scales: {
      // .... x scale
      y:{
         // ... other options
         suggestedMax:function({chart}){
            if(chart){
               if(chart.$setYMax){ // use cached value 
                  return chart.$setYMax;
               }
               const xMin = chart.scales.x.min ?? null;
               if(xMin !== null){
                  const xMax = chart.scales.x.max;

                  let yMax = chart.scales.y.max;
                  const visibleMetas = chart.getSortedVisibleDatasetMetas();
                  for(const meta of visibleMetas){
                     for(let i = 0; i < meta._parsed.length - 1; i++){
                        const p0 = meta._parsed[i],
                           p1 = meta._parsed[i + 1];
                        if(p0.x < xMin && p1.x > xMin){
                           yMax = Math.max(yMax, p0.y + (xMin - p0.x) / (p1.x - p0.x) * (p1.y - p0.y));
                        }
                        if(p0.x < xMax && p1.x > xMax){
                           yMax = Math.max(yMax, p0.y + (xMax - p0.x)/(p1.x - p0.x)*(p1.y-p0.y));
                        }
                     }
                  }
                  if(yMax > chart.scales.y.max){
                     chart.$setYMax = yMax;
                     return yMax;
                  }
               }
            }
         }
      }
   }
}

下面是一个使用OP的数据的片段(为使问题更复杂略作修改)以及这个 suggestedMax 的示例:

const chart = new Chart("myChart", {
   type: "line",
   data: {
      datasets: [
         {
            label: "first",
            data: [
               {
                  x: "2021-11-06 23:39:30",
                  y: 50
               },
               {
                  x: "2021-11-07 01:00:28",
                  y: 60
               },
               {
                  x: "2021-11-07 23:00:28",
                  y: 20
               }
            ]
         },
         {
            label: "second",
            data: [
               {
                  x: "2021-11-06 13:39:30",
                  y: 90
               },

               {
                  x: "2021-11-08 09:00:28",
                  y: 70
               }
            ]
         }

      ]
   },
   options: {
      scales: {
         x: {
            type: "time",
            min: "2021-11-07T00:00:00",
            max: "2021-11-08T00:00:00"
         },
         y:{
            suggestedMax:function({chart}){
               if(chart){
                  if(chart.$setYMax){ // use cached value
                     return chart.$setYMax;
                  }
                  const xMin = chart.scales.x.min ?? null;
                  if(xMin !== null){
                     const xMax = chart.scales.x.max;

                     let yMax = chart.scales.y.max;
                     const visibleMetas = chart.getSortedVisibleDatasetMetas();
                     for(const meta of visibleMetas){
                        for(let i = 0; i < meta._parsed.length - 1; i++){
                           const p0 = meta._parsed[i],
                              p1 = meta._parsed[i + 1];
                           if(p0.x < xMin && p1.x > xMin){
                              yMax = Math.max(yMax, p0.y + (xMin - p0.x) / (p1.x - p0.x) * (p1.y - p0.y));
                           }
                           if(p0.x < xMax && p1.x > xMax){
                              yMax = Math.max(yMax, p0.y + (xMax - p0.x) / (p1.x - p0.x) * (p1.y-p0.y));
                           }
                        }
                     }
                     if(yMax > chart.scales.y.max){
                        chart.$setYMax = yMax;
                        return yMax;
                     }
                  }
               }
            }
         }
      }
   }
});
if((chart.$setYMax ?? null) === null){
   chart.update();
}
<canvas id="myChart" style="max-width:600px; max-height: 400px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>

这在较新版本的Chart.js中有效;而OP的旧版本3.5.1,可能需要在计算出 max 值后再延迟执行 chart.update 调用。

更新说明:正如评论中所揭示的,在某些情况下(例如在codepen中)当图表仅渲染一次时,最新版本的Chart.js也需要 update 调用,以触发可能的再次更新。为了适应这些情况,我在代码的最后三行加入了 if((chart.$setYMax ?? null) === null) chart.update(),在未计算出 suggestedMax 时就会更新图表。或者,可以使用更简单的 new Chart(.....).update() 模式,可能会导致不必要的更新,但用户通常不会在图表包含极大量数据时察觉。


同样的思路也可以作为一个插件来实现,具有模块化的优势;以下代码实现了这样一个插件,但范围比上述解法更大:它同时计算最小值和最大值,也会考虑内部点,并且应该适用于堆叠的线条。它仍然基于线性插值,因此不考虑与样条或贝塞尔插值的微小差异:

const _addStacks = _stacks => Object.entries(_stacks).filter(([x])=>parseFloat(x).toString()===x).reduce((s, [_, x]) => s+x, 0);
const _getX = p => p._stacks?.x ? _addStacks(p._stacks.x) : p.x;
const _getY = p => p._stacks?.y ? _addStacks(p._stacks.y) : p.y;
const pluginSegmentMinMax = {
   afterDataLimits(chart){
      if(chart){
         let cachedFound = false;
         if(chart.$setYMax){
            chart.scales.y.max = chart.$setYMax;
            cachedFound = true;
         }
         if(chart.$setYMin){
            chart.scales.y.min = chart.$setYMin;
            cachedFound = true;
         }
         if(cachedFound){
            return;
         }

         const xMin = chart.scales.x.min ?? null;
         if(xMin !== null){
            const xMax = chart.scales.x.max;
            let yMax = -1/0,
               yMin = 1/0;
            const visibleMetas = chart.getSortedVisibleDatasetMetas();
            for(const meta of visibleMetas){
               for(let i = 0; i < meta._parsed.length - 1; i++){
                  const p0 = meta._parsed[i],
                     p1 = meta._parsed[i + 1];
                  const x0 = _getX(p0), x1 = _getX(p1),
                     y0 = _getY(p0), y1 = _getY(p1);
                  if(x0 >= xMin && x0 <= xMax){
                     yMin = Math.min(yMin, y0);
                     yMax = Math.max(yMax, y0);
                  }
                  if(i === meta._parsed.length - 2 && x1 >= xMin && x1 <= xMax){
                     yMin = Math.min(yMin, y1);
                     yMax = Math.max(yMax, y1);
                  }
                  if(x0 < xMin && x1 > xMin){
                     let yNew = y0 + (xMin - x0) / (x1 - x0) * (y1 - y0);
                     yMin = Math.min(yMin, yNew);
                     yMax = Math.max(yMax, yNew);
                  }
                  if(p0.x < xMax && p1.x > xMax){
                     let yNew = y0 + (xMax - x0) / (x1 - x0) * (y1 - y0);
                     yMin = Math.min(yMin, yNew);
                     yMax = Math.max(yMax, yNew);
                  }
               }
            }
            if(yMax > -1/0){
               chart.$setYMax = yMax;
               chart.scales.y._dataLimitsCached = false;
               chart.scales.y.max = yMax;
            }
            if(yMin < 1/0){
               chart.$setYMin = yMin;
               chart.scales.y._dataLimitsCached = false;
               chart.scales.y.min = yMin;
            }
         }
      }
    }
};

var ctx = document.getElementById("myChart");
new Chart(ctx, {
    type: "line",
    data: {
      datasets: [
         {
            label: "first",
            data: [
               {
                  x: "2021-11-06 23:39:30",
                  y: 50
               },
               {
                  x: "2021-11-07 01:00:28",
                  y: 60
               },
               {
                  x: "2021-11-07 23:00:28",
                  y: 20
               }
            ]
         },
         {
            label: "second",
            data: [
               {
                  x: "2021-11-06 13:39:30",
                  y: 90
               },

               {
                  x: "2021-11-08 09:00:28",
                  y: 70
               }
            ]
         }
      ]
    },
    options: {
      scales: {
         x: {
            type: "time",
            min: "2021-11-07T00:00:00",
            max: "2021-11-08T00:00:00"
         }
      }
    },
    plugins: [pluginSegmentMinMax]
});
<canvas id="myChart" style="max-width:600px; max-height: 400px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章