Chart.js无法自动缩放以包含当前视图中没有点的数据系列
我在一个使用时间刻度的图表上显示数据,并且视图是固定的(即只查看在两个设定的日期时间之间的数据)。
我的问题是图表不会自动缩放,以显示在该范围内并没有值的系列。
示例:
当前的最小/最大时间是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 轴的 min 与 max 之间没有任何点,其对y 轴的 min 与 max 值将不会产生影响;不管它是第二个、第一个,甚至是唯一的数据集。同样,这样的数据集始终会被绘制,但如果它的线条位于图表区域之外,便不会可见。
Chart.js没有办法使用部分可见的 segments 来计算数值轴的最小/最大值,因此没有简单的选项让它们可见。
一种替代在x 领域内没有可见点的数据集中添加插值点的方法,是显式设置y 轴刻度的 max 或 suggestedMax 值(如有需要,还可以设置 min 或 suggestedMin)。
这可以自动完成,将 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>