Chart.js 交互功能 #
交互概述 #
text
┌─────────────────────────────────────────────────────────────┐
│ Chart.js 交互功能 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 提示框 │ │ 图例 │ │ 缩放 │ │
│ │ Tooltip │ │ Legend │ │ Zoom │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 平移 │ │ 拖拽 │ │ 点击 │ │
│ │ Pan │ │ Drag │ │ Click │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
提示框(Tooltip) #
基本配置 #
javascript
const options = {
plugins: {
tooltip: {
// 是否启用
enabled: true,
// 模式:'point', 'nearest', 'index', 'dataset', 'x', 'y'
mode: 'index',
// 是否必须相交
intersect: false,
// 背景色
backgroundColor: 'rgba(0, 0, 0, 0.8)',
// 标题颜色
titleColor: '#fff',
// 正文颜色
bodyColor: '#fff',
// 内边距
padding: 10,
// 圆角
cornerRadius: 4
}
}
};
提示框位置 #
javascript
const options = {
plugins: {
tooltip: {
// 位置模式:'average', 'nearest'
position: 'average',
// 对齐方式
xAlign: 'center', // 'left', 'center', 'right'
yAlign: 'center', // 'top', 'center', 'bottom'
// 偏移量
xOffset: 10,
yOffset: 10,
// 自定义位置函数
position: function(elements, eventPosition) {
const tooltip = this;
return {
x: eventPosition.x,
y: eventPosition.y
};
}
}
}
};
自定义内容 #
javascript
const options = {
plugins: {
tooltip: {
callbacks: {
// 标题
title: function(context) {
return '📊 ' + context[0].label;
},
// 标签
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toLocaleString();
}
return label;
},
// 标签颜色
labelColor: function(context) {
return {
borderColor: context.dataset.borderColor,
backgroundColor: context.dataset.backgroundColor,
borderWidth: 2,
borderRadius: 2
};
},
// 标签点样式
labelPointStyle: function(context) {
return {
pointStyle: 'rectRounded',
rotation: 0
};
},
// 页脚
footer: function(context) {
const total = context.reduce((sum, item) => sum + item.parsed.y, 0);
return '总计: ' + total.toLocaleString();
},
// 之前/之后回调
beforeTitle: function(context) { return ''; },
afterTitle: function(context) { return ''; },
beforeBody: function(context) { return ''; },
afterBody: function(context) { return ''; },
beforeLabel: function(context) { return ''; },
afterLabel: function(context) { return ''; },
beforeFooter: function(context) { return ''; },
afterFooter: function(context) { return ''; }
}
}
}
};
外部提示框 #
javascript
// 创建自定义提示框元素
const tooltipEl = document.getElementById('chartjs-tooltip');
const options = {
plugins: {
tooltip: {
enabled: false,
external: function(context) {
const tooltipModel = context.tooltip;
// 隐藏提示框
if (tooltipModel.opacity === 0) {
tooltipEl.style.opacity = '0';
return;
}
// 设置提示框内容
if (tooltipModel.body) {
const titleLines = tooltipModel.title || [];
const bodyLines = tooltipModel.body.map(b => b.lines);
let innerHtml = '<thead>';
titleLines.forEach(title => {
innerHtml += '<tr><th>' + title + '</th></tr>';
});
innerHtml += '</thead><tbody>';
bodyLines.forEach((body, i) => {
const colors = tooltipModel.labelColors[i];
const style = 'background:' + colors.backgroundColor;
innerHtml += '<tr><td><span style="' + style + '"></span>' + body + '</td></tr>';
});
innerHtml += '</tbody>';
const tableRoot = tooltipEl.querySelector('table');
tableRoot.innerHTML = innerHtml;
}
// 定位提示框
const position = context.chart.canvas.getBoundingClientRect();
tooltipEl.style.opacity = '1';
tooltipEl.style.position = 'absolute';
tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px';
tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px';
tooltipEl.style.fontFamily = tooltipModel._bodyFontFamily;
tooltipEl.style.fontSize = tooltipModel.bodyFontSize + 'px';
tooltipEl.style.fontStyle = tooltipModel._bodyFontStyle;
tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px';
tooltipEl.style.pointerEvents = 'none';
}
}
}
};
提示框样式 #
javascript
const options = {
plugins: {
tooltip: {
// 字体配置
titleFont: {
family: 'Arial',
size: 14,
style: 'bold',
weight: 'bold'
},
bodyFont: {
family: 'Arial',
size: 12
},
footerFont: {
family: 'Arial',
size: 12,
weight: 'bold'
},
// 对齐
textAlign: 'left',
bodyAlign: 'left',
titleAlign: 'left',
footerAlign: 'left',
// 间距
titleMarginBottom: 6,
bodySpacing: 4,
footerMarginTop: 6,
// 显示颜色框
displayColors: true,
boxWidth: 12,
boxHeight: 12,
boxPadding: 3,
usePointStyle: false,
// 边框
borderColor: 'rgba(0, 0, 0, 0)',
borderWidth: 0,
// 阴影
shadowOffsetX: 0,
shadowOffsetY: 2,
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
}
};
图例交互 #
图例配置 #
javascript
const options = {
plugins: {
legend: {
// 是否显示
display: true,
// 位置
position: 'top',
// 对齐
align: 'center',
// 是否可点击切换
onClick: function(e, legendItem, legend) {
const index = legendItem.datasetIndex;
const ci = legend.chart;
const meta = ci.getDatasetMeta(index);
// 切换隐藏/显示
meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null;
ci.update();
},
// 悬停事件
onHover: function(e, legendItem, legend) {
e.native.target.style.cursor = 'pointer';
},
// 离开事件
onLeave: function(e, legendItem, legend) {
e.native.target.style.cursor = 'default';
}
}
}
};
自定义图例 #
javascript
const options = {
plugins: {
legend: {
display: false // 禁用默认图例
}
}
};
// 创建自定义图例
function createCustomLegend(chart) {
const legendContainer = document.getElementById('legend-container');
legendContainer.innerHTML = '';
chart.data.datasets.forEach((dataset, i) => {
const item = document.createElement('div');
item.className = 'legend-item';
item.innerHTML = `
<span class="legend-color" style="background-color: ${dataset.backgroundColor}"></span>
<span class="legend-label">${dataset.label}</span>
`;
item.onclick = () => {
const meta = chart.getDatasetMeta(i);
meta.hidden = !meta.hidden;
chart.update();
item.classList.toggle('hidden', meta.hidden);
};
legendContainer.appendChild(item);
});
}
HTML 图例 #
javascript
const options = {
plugins: {
htmlLegend: {
containerID: 'legend-container'
}
}
};
// HTML 图例插件
const htmlLegendPlugin = {
id: 'htmlLegend',
afterUpdate(chart, args, options) {
const ul = document.getElementById(options.containerID);
ul.innerHTML = '';
chart.data.datasets.forEach((dataset, i) => {
const li = document.createElement('li');
li.onclick = () => {
const meta = chart.getDatasetMeta(i);
meta.hidden = !meta.hidden;
chart.update();
};
const colorBox = document.createElement('span');
colorBox.style.backgroundColor = dataset.backgroundColor;
colorBox.style.borderColor = dataset.borderColor;
const text = document.createTextNode(dataset.label);
li.appendChild(colorBox);
li.appendChild(text);
ul.appendChild(li);
});
}
};
Chart.register(htmlLegendPlugin);
缩放和平移 #
安装插件 #
bash
npm install chartjs-plugin-zoom
基本缩放配置 #
javascript
import zoomPlugin from 'chartjs-plugin-zoom';
Chart.register(zoomPlugin);
const options = {
plugins: {
zoom: {
// 缩放配置
zoom: {
// 滚轮缩放
wheel: {
enabled: true,
speed: 0.1
},
// 捏合缩放(触摸设备)
pinch: {
enabled: true
},
// 拖拽缩放
drag: {
enabled: true,
backgroundColor: 'rgba(0, 0, 0, 0.1)',
borderColor: 'rgba(0, 0, 0, 0.3)',
borderWidth: 1
},
// 缩放模式:'x', 'y', 'xy'
mode: 'xy'
},
// 平移配置
pan: {
enabled: true,
mode: 'xy',
// 按住修饰键平移
modifierKey: 'shift'
},
// 缩放限制
limits: {
x: {
min: 'original',
max: 'original',
minRange: 1
},
y: {
min: 'original',
max: 'original'
}
}
}
}
};
缩放控制 #
javascript
// 重置缩放
chart.resetZoom();
// 缩放到指定范围
chart.zoomScale('x', { min: 0, max: 100 }, 'default');
// 平移图表
chart.pan({ x: 100, y: 0 }, 'default');
// 获取当前缩放状态
const zoomState = chart.getZoomLevel();
console.log('缩放级别:', zoomState);
缩放事件 #
javascript
const options = {
plugins: {
zoom: {
zoom: {
onZoomStart: function({ chart, event, point }) {
console.log('缩放开始');
return true; // 返回 false 取消缩放
},
onZoom: function({ chart }) {
console.log('缩放中');
},
onZoomComplete: function({ chart }) {
console.log('缩放完成');
}
},
pan: {
onPanStart: function({ chart, event, point }) {
console.log('平移开始');
return true;
},
onPan: function({ chart }) {
console.log('平移中');
},
onPanComplete: function({ chart }) {
console.log('平移完成');
}
}
}
}
};
拖拽交互 #
拖拽数据点 #
javascript
// 拖拽插件
const dragPlugin = {
id: 'drag',
beforeInit: function(chart) {
chart.dragging = false;
chart.dragPoint = null;
},
beforeEvent: function(chart, args) {
const event = args.event;
if (event.type === 'mousedown') {
const points = chart.getElementsAtEventForMode(event, 'point', { intersect: true }, false);
if (points.length) {
chart.dragging = true;
chart.dragPoint = points[0];
}
}
if (event.type === 'mousemove' && chart.dragging) {
const yScale = chart.scales.y;
const newValue = yScale.getValueForPixel(event.y);
const datasetIndex = chart.dragPoint.datasetIndex;
const index = chart.dragPoint.index;
chart.data.datasets[datasetIndex].data[index] = newValue;
chart.update('none');
}
if (event.type === 'mouseup') {
chart.dragging = false;
chart.dragPoint = null;
}
}
};
Chart.register(dragPlugin);
点击交互 #
点击事件 #
javascript
const options = {
onClick: function(event, elements, chart) {
if (elements.length > 0) {
const element = elements[0];
const datasetIndex = element.datasetIndex;
const index = element.index;
const label = chart.data.labels[index];
const value = chart.data.datasets[datasetIndex].data[index];
console.log(`点击了: ${label} = ${value}`);
// 高亮选中的数据点
highlightPoint(chart, datasetIndex, index);
}
}
};
function highlightPoint(chart, datasetIndex, index) {
// 重置所有点
chart.data.datasets.forEach((dataset, i) => {
dataset.backgroundColor = dataset.backgroundColor.map ?
dataset.backgroundColor.map((_, j) => i === datasetIndex && j === index ? 'red' : 'blue') :
'blue';
});
chart.update();
}
双击事件 #
javascript
let lastClickTime = 0;
const doubleClickDelay = 300;
const options = {
onClick: function(event, elements, chart) {
const currentTime = new Date().getTime();
if (currentTime - lastClickTime < doubleClickDelay) {
// 双击事件
if (elements.length > 0) {
const element = elements[0];
console.log('双击:', element);
onDoubleClick(chart, element);
}
} else {
// 单击事件
setTimeout(() => {
if (new Date().getTime() - lastClickTime >= doubleClickDelay) {
if (elements.length > 0) {
console.log('单击:', elements[0]);
}
}
}, doubleClickDelay);
}
lastClickTime = currentTime;
}
};
function onDoubleClick(chart, element) {
// 放大显示详情
const modal = document.getElementById('detailModal');
modal.style.display = 'block';
modal.querySelector('.detail-content').innerHTML = `
<h3>${chart.data.labels[element.index]}</h3>
<p>值: ${chart.data.datasets[element.datasetIndex].data[element.index]}</p>
`;
}
悬停交互 #
悬停效果 #
javascript
const options = {
onHover: function(event, elements, chart) {
// 改变鼠标样式
event.native.target.style.cursor = elements.length ? 'pointer' : 'default';
if (elements.length > 0) {
const element = elements[0];
const datasetIndex = element.datasetIndex;
const index = element.index;
// 高亮悬停的数据点
highlightHoverPoint(chart, datasetIndex, index);
} else {
resetHighlight(chart);
}
},
// 元素悬停样式
hover: {
animationDuration: 200,
mode: 'nearest',
intersect: true
}
};
function highlightHoverPoint(chart, datasetIndex, index) {
chart.data.datasets.forEach((dataset, i) => {
if (i === datasetIndex) {
dataset.pointRadius = dataset.data.map((_, j) => j === index ? 8 : 3);
dataset.pointBackgroundColor = dataset.data.map((_, j) =>
j === index ? 'red' : 'blue'
);
}
});
chart.update('none');
}
function resetHighlight(chart) {
chart.data.datasets.forEach(dataset => {
dataset.pointRadius = 3;
dataset.pointBackgroundColor = 'blue';
});
chart.update('none');
}
选择交互 #
多选功能 #
javascript
const selectedPoints = new Set();
const options = {
onClick: function(event, elements, chart) {
if (elements.length > 0) {
const element = elements[0];
const key = `${element.datasetIndex}-${element.index}`;
if (event.native.ctrlKey || event.native.metaKey) {
// Ctrl/Cmd + 点击:切换选择
if (selectedPoints.has(key)) {
selectedPoints.delete(key);
} else {
selectedPoints.add(key);
}
} else {
// 普通点击:单选
selectedPoints.clear();
selectedPoints.add(key);
}
updateSelection(chart);
}
}
};
function updateSelection(chart) {
chart.data.datasets.forEach((dataset, datasetIndex) => {
dataset.pointRadius = dataset.data.map((_, index) => {
const key = `${datasetIndex}-${index}`;
return selectedPoints.has(key) ? 8 : 3;
});
dataset.pointBackgroundColor = dataset.data.map((_, index) => {
const key = `${datasetIndex}-${index}`;
return selectedPoints.has(key) ? 'red' : 'blue';
});
});
chart.update('none');
}
交互最佳实践 #
1. 性能优化 #
javascript
const options = {
interaction: {
mode: 'index',
intersect: false
},
plugins: {
tooltip: {
// 减少提示框更新频率
animation: {
duration: 0
}
}
},
// 减少悬停计算
hover: {
animationDuration: 0
}
};
2. 可访问性 #
javascript
const options = {
// 键盘导航
onHover: (event, elements) => {
if (elements.length) {
elements[0].element.options.backgroundColor = 'red';
}
},
plugins: {
tooltip: {
// 屏幕阅读器支持
enabled: true
}
}
};
3. 移动端适配 #
javascript
const options = {
interaction: {
// 触摸设备优化
mode: 'nearest',
intersect: true
},
plugins: {
tooltip: {
// 触摸设备提示框
position: 'nearest',
yAlign: 'bottom'
},
legend: {
// 移动端图例位置
position: window.innerWidth < 768 ? 'bottom' : 'top'
}
}
};
下一步 #
现在你已经掌握了交互功能,接下来学习 插件开发,了解如何开发自定义插件扩展 Chart.js 功能!
最后更新:2026-03-28