在数据可视化项目中,将销售额、用户量等绝对数值与增长率、转化率等相对数值放在同一张图表上,是一种常见需求。例如,用柱状图展示每月的销售额,用折线图展示同比增长率。但如果两者共用一条Y轴,由于数值范围差异过大,增长率的曲线往往会被压成一条贴近横轴的直线,失去参考意义。Chart.js 提供了多轴支持,允许为不同数据集绑定不同的纵轴,从而让每个系列都能在合适的刻度范围内展示。本文以柱状图加折线图的组合为例,详细介绍多轴复合图表的配置方法,并重点解决轴标签管理、刻度对齐等易错点。

一、基础多轴配置:yAxisID与scales定义
在Chart.js 3.x及更高版本中,每个数据集(dataset)可以通过yAxisID属性关联到scales中定义的某个Y轴。默认情况下,所有数据集绑定到ID为y的轴。如果需要第二个Y轴,可以在scales对象中新增一个键(例如y1),然后让某个数据集的yAxisID指向它。
以下是一个最小化的双轴配置示例,柱状图使用左轴(y),折线图使用右轴(y1)。
const ctx = document.getElementById('myChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['一月', '二月', '三月', '四月', '五月', '六月'],
datasets: [
{
label: '销售额(万元)',
data: [320, 480, 290, 510, 460, 570],
backgroundColor: 'rgba(54, 162, 235, 0.6)',
yAxisID: 'y', // 绑定左轴
},
{
label: '同比增长率',
data: [12, 8, -5, 15, 10, 18],
type: 'line',
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
yAxisID: 'y1', // 绑定右轴
tension: 0.3,
}
]
},
options: {
scales: {
y: {
beginAtZero: true,
title: { display: true, text: '销售额(万元)' }
},
y1: {
position: 'right', // 将轴显示在右侧
grid: { drawOnChartArea: false }, // 避免右轴绘制重复的网格线
title: { display: true, text: '增长率(%)' }
}
}
}
});
在scales配置中,y是默认的左轴,y1是我们新增的右轴。position: 'right'将新轴显示在图表右侧,使其与左轴区分。注意grid.drawOnChartArea被设置为false,这样右轴的网格线就不会绘制在绘图区域上,避免与左轴网格线重叠造成视觉混乱。
如果不做任何额外处理,左右两个轴会根据各自的数据范围自动计算刻度,这会导致水平网格线无法对齐。例如左轴刻度为0、100、200、300,右轴刻度为0、5、10、15,水平网格线会出现在不同高度,影响阅读。针对这一问题,后续小节会探讨刻度对齐方案。
二、轴标签管理与ticks格式化
轴标签直接决定了用户能否快速理解数据含义。Chart.js 提供了多种方式来自定义刻度标签的显示格式,包括使用ticks.callback函数、设置ticks.precision、ticks.stepSize等。当柱状图和折线图分别代表金额和百分比时,我们可以给每个轴设置不同的标签格式。
以金额为例,如果原始数据以元为单位,而轴标签希望显示为“万元”且保留一位小数,可以在左轴的ticks中配置回调函数:
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value, index, ticks) {
// 假设原始数据单位为万元,直接显示即可
return value + ' 万';
}
},
title: { display: true, text: '销售额' }
},
y1: {
position: 'right',
grid: { drawOnChartArea: false },
ticks: {
callback: function(value) {
return value + '%';
}
},
title: { display: true, text: '增长率' }
}
}
}
在Chart.js 3及更高版本中,轴标题的配置从旧的scaleLabel变为了title对象。如果在网上查找资料时看到scaleLabel的写法,那多半是版本2的遗留代码,需要转换为当前版本。此外,ticks.callback会接收三个参数:当前刻度值、刻度索引、刻度数组。你可以根据实际业务需求返回字符串或数字,甚至可以返回null来隐藏某个刻度。
对于百分比轴,除了追加百分号,有时还希望让负数显示为红色。这可以通过在回调中返回带样式的数组来实现,但通常更推荐使用ticks.color函数动态设置颜色,例如:
ticks: {
color: (context) => {
const value = context.tick && context.tick.value;
return value < 0 ? 'red' : '#666';
},
callback: (value) => value + '%'
}
实际项目中,轴标签的格式化还可以结合Intl.NumberFormat实现千分位分隔、货币符号等。但要注意回调函数会在每次渲染时被调用,如果内部有复杂的计算,可以考虑使用插件或缓存结果来优化性能。
三、双轴刻度对齐与网格线优化
当左右两个轴的数据范围差异较大时,最常见的问题就是水平网格线无法对齐。比如左轴范围0到600,右轴范围-10到20,如果各自独立计算刻度,最终图上的网格线会错落不齐,使得读者难以将柱状图的高度与折线图的变化对应起来。解决这个问题的思路是让两个轴使用相同数量的刻度划分,或者成比例地划分。
Chart.js 本身没有提供直接对齐双轴刻度的配置项,但我们可以通过两个途径实现:手动设置min、max和stepSize,或者使用自定义插件在图表更新前调整轴的max和min。手动设置的问题是数据往往是动态的,预先计算合适的步长并不容易。更灵活的做法是使用afterDataLimits钩子,在Chart.js计算完数据边界之后调整轴的边界,然后再让Chart.js生成刻度。
下面是一个自定义插件的示例,它会让右轴的刻度数量与左轴保持一致,从而实现网格线对齐。插件通过afterDataLimits读取左轴的min和max,然后计算左轴需要的刻度间隔数量,再设置右轴的min和max为能容纳相同数量间隔的对称范围。
const alignScalesPlugin = {
id: 'alignScales',
afterDataLimits(chart) {
const leftAxis = chart.scales.y;
const rightAxis = chart.scales.y1;
if (!leftAxis || !rightAxis) return;
const leftMin = leftAxis.min;
const leftMax = leftAxis.max;
const leftStep = (leftMax - leftMin) / leftAxis.ticks.length;
// 右轴的刻度数量与左轴一致,因此设置相同的刻度间隔大小
// 这里需要结合右轴数据的实际范围,计算合适的 min 和 max
const rightDataMin = Math.min(...chart.data.datasets.find(d => d.yAxisID === 'y1').data);
const rightDataMax = Math.max(...chart.data.datasets.find(d => d.yAxisID === 'y1').data);
const tickCount = leftAxis.ticks.length;
// 计算一个能包含数据范围的区间,区间长度为 tickCount * leftStep 的整数倍?此处简化:使用左右步长比例
// 实际上更好的做法是让右轴的步长与左轴步长成比例,但为了对齐,通常直接设置相同数量的 ticks
const rightRange = rightDataMax - rightDataMin;
const rawStep = rightRange / (tickCount - 1);
// 对 rawStep 进行取整或向上取整为“好看”的数值,例如 5、10 等
const niceStep = Math.ceil(rawStep);
rightAxis.options.ticks.stepSize = niceStep;
// 设置 min 和 max 使得刻度数量与左轴一致
rightAxis.min = Math.floor(rightDataMin / niceStep) * niceStep;
rightAxis.max = rightAxis.min + niceStep * (tickCount - 1);
// 更新轴配置
rightAxis.options.min = rightAxis.min;
rightAxis.options.max = rightAxis.max;
}
};
// 注册插件
Chart.register(alignScalesPlugin);
上面的代码只是一个基础思路,实际项目中需要处理数据为空、负数范围、小数步长等情况。更稳妥的做法是结合Chart.js的afterBuildTicks钩子,在刻度生成后根据左轴刻度数量重新生成右轴刻度,并保持步长的美观性。需要注意的是,在Chart.js 3中,插件钩子的参数和内部API与2.x有较大差异,编写插件时应以官方文档为准。
除了刻度对齐,网格线的优化同样重要。默认情况下,左轴和右轴都会在绘图区域绘制网格线,导致线条数量翻倍。我们可以将右轴的grid.drawOnChartArea设置为false(如前文示例),只保留左轴网格线。同时可以设置grid.drawBorder为false来隐藏轴线边框,让图表更简洁。对于右轴,还可以使用grid.color设置虚线等,以区分不同轴对应的参考线。
四、常见问题与调试技巧
在配置多轴复合图表的过程中,有几个高频问题值得注意。首先是yAxisID拼写错误或轴ID不匹配,会导致数据集无法正确绑定,此时Chart.js通常会使用默认的左轴,并且不会报错,排查起来比较费时。建议在初始化图表后打印chart.scales对象,确认各个轴是否成功创建,以及数据集是否绑定到了预期的轴。
其次是版本差异。网上大量Chart.js教程仍然基于2.x版本,其中的scales.yAxes数组写法在3.x中已经废弃。如果你在复制示例代码时发现图表无法渲染,先检查当前使用的Chart.js版本,再选择对应的配置结构。官方文档的迁移指南详细列出了所有变更点,建议查阅。
另一个常见的误区是认为设置position: 'right'就自动完成了多轴配置。实际上,仅仅是scales.y1存在并不会被任何数据集使用,必须通过yAxisID显式关联。同理,如果希望折线图使用第二个X轴,则需要在scales中定义x1,并为数据集设置xAxisID。
调试时,可以利用Chart.js提供的chart.options动态修改配置,然后调用chart.update()来观察变化。例如在浏览器控制台中执行chart.options.scales.y1.grid.drawOnChartArea = true; chart.update();,可以快速验证网格线设置是否生效。此外,使用浏览器开发者工具监听图表容器的resize事件,也有助于发现响应式布局下的轴标签截断问题。