Chart.js 高级主题 #
动画效果 #
动画基础 #
Chart.js 内置了强大的动画系统,可以为图表添加流畅的过渡效果:
javascript
const options = {
animation: {
// 动画持续时间(毫秒)
duration: 1000,
// 缓动函数
easing: 'easeInOutQuart',
// 延迟函数
delay: (context) => {
return context.dataIndex * 100;
},
// 是否循环
loop: false
}
};
缓动函数详解 #
javascript
// 可用的缓动函数
const easingTypes = {
// 线性
linear: 'linear',
// 二次方
easeInQuad: 'easeInQuad',
easeOutQuad: 'easeOutQuad',
easeInOutQuad: 'easeInOutQuad',
// 三次方
easeInCubic: 'easeInCubic',
easeOutCubic: 'easeOutCubic',
easeInOutCubic: 'easeInOutCubic',
// 四次方
easeInQuart: 'easeInQuart',
easeOutQuart: 'easeOutQuart',
easeInOutQuart: 'easeInOutQuart',
// 五次方
easeInQuint: 'easeInQuint',
easeOutQuint: 'easeOutQuint',
easeInOutQuint: 'easeInOutQuint',
// 正弦
easeInSine: 'easeInSine',
easeOutSine: 'easeOutSine',
easeInOutSine: 'easeInOutSine',
// 指数
easeInExpo: 'easeInExpo',
easeOutExpo: 'easeOutExpo',
easeInOutExpo: 'easeInOutExpo',
// 圆形
easeInCirc: 'easeInCirc',
easeOutCirc: 'easeOutCirc',
easeInOutCirc: 'easeInOutCirc',
// 弹性
easeInElastic: 'easeInElastic',
easeOutElastic: 'easeOutElastic',
easeInOutElastic: 'easeInOutElastic',
// 回弹
easeInBack: 'easeInBack',
easeOutBack: 'easeOutBack',
easeInOutBack: 'easeInOutBack',
// 弹跳
easeInBounce: 'easeInBounce',
easeOutBounce: 'easeOutBounce',
easeInOutBounce: 'easeInOutBounce'
};
属性级动画 #
javascript
const options = {
animation: {
// 数字属性动画
numbers: {
properties: ['x', 'y', 'borderWidth', 'radius'],
duration: 1000,
easing: 'easeInOutQuart'
},
// 颜色属性动画
colors: {
properties: ['color', 'borderColor', 'backgroundColor'],
duration: 1000,
easing: 'easeInOutQuart',
from: 'transparent'
},
// 显示动画
show: {
colors: {
from: 'transparent'
},
visible: {
type: 'show',
duration: 500
}
},
// 隐藏动画
hide: {
colors: {
to: 'transparent'
},
visible: {
type: 'hide',
duration: 500
}
}
}
};
动画回调 #
javascript
const options = {
animation: {
// 动画开始
onStart: (animation) => {
console.log('动画开始');
},
// 动画进行中
onProgress: (animation) => {
const progress = animation.currentStep / animation.numSteps;
console.log(`动画进度: ${(progress * 100).toFixed(0)}%`);
},
// 动画完成
onComplete: (animation) => {
console.log('动画完成');
}
}
};
自定义动画 #
javascript
// 使用自定义属性动画
const options = {
animation: {
// 自定义动画属性
customProperty: {
properties: ['customValue'],
duration: 1000,
easing: 'easeInOutQuart'
}
}
};
// 在插件中使用
const customPlugin = {
id: 'customAnimation',
beforeDraw: (chart) => {
const { ctx, chartArea } = chart;
const { customValue = 0 } = chart.options.animation;
// 使用自定义动画值
ctx.globalAlpha = customValue;
}
};
响应式设计 #
基本响应式配置 #
javascript
const options = {
// 启用响应式
responsive: true,
// 保持宽高比
maintainAspectRatio: true,
// 宽高比
aspectRatio: 2,
// 调整大小延迟
resizeDelay: 0,
// 使用 ResizeObserver
resizeObserver: true
};
容器配置 #
html
<!-- 固定高度容器 -->
<div style="height: 400px;">
<canvas id="myChart"></canvas>
</div>
<!-- 响应式容器 -->
<div style="width: 100%; height: 50vh;">
<canvas id="myChart"></canvas>
</div>
断点配置 #
javascript
function getChartOptions() {
const width = window.innerWidth;
const baseOptions = {
responsive: true,
maintainAspectRatio: false
};
// 根据屏幕宽度调整配置
if (width < 576) {
// 手机端
return {
...baseOptions,
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12,
font: { size: 10 }
}
},
title: {
font: { size: 14 }
}
},
scales: {
x: {
ticks: {
font: { size: 10 },
maxRotation: 45
}
}
}
};
} else if (width < 992) {
// 平板端
return {
...baseOptions,
plugins: {
legend: {
position: 'top'
}
}
};
} else {
// 桌面端
return {
...baseOptions,
plugins: {
legend: {
position: 'right'
}
}
};
}
}
响应式事件处理 #
javascript
let myChart;
function initChart() {
const ctx = document.getElementById('myChart');
myChart = new Chart(ctx, {
type: 'bar',
data: chartData,
options: getChartOptions()
});
}
// 监听窗口大小变化
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
// 更新图表配置
myChart.options = getChartOptions();
myChart.update();
}, 250);
});
自适应标签 #
javascript
const options = {
scales: {
x: {
ticks: {
// 根据可用空间自动调整标签
autoSkip: true,
autoSkipPadding: 20,
maxRotation: 45,
minRotation: 0,
// 自定义标签显示
callback: function(value, index, ticks) {
const label = this.getLabelForValue(value);
const width = this.width;
// 根据宽度截断标签
if (width < 400) {
return label.length > 5 ? label.substring(0, 5) + '...' : label;
}
return label;
}
}
}
}
};
性能优化 #
数据优化 #
javascript
// 1. 限制数据点数量
function limitDataPoints(data, maxPoints = 100) {
if (data.length <= maxPoints) return data;
const step = Math.ceil(data.length / maxPoints);
return data.filter((_, index) => index % step === 0);
}
// 2. 数据采样
function sampleData(data, sampleSize = 50) {
if (data.length <= sampleSize) return data;
const result = [];
const step = data.length / sampleSize;
for (let i = 0; i < sampleSize; i++) {
const index = Math.floor(i * step);
result.push(data[index]);
}
return result;
}
// 3. 数据聚合
function aggregateData(data, groupSize = 10) {
const result = [];
for (let i = 0; i < data.length; i += groupSize) {
const group = data.slice(i, i + groupSize);
const avg = group.reduce((sum, val) => sum + val, 0) / group.length;
result.push(avg);
}
return result;
}
渲染优化 #
javascript
const options = {
// 禁用动画(提升性能)
animation: {
duration: 0
},
// 减少重绘
responsive: true,
resizeDelay: 100,
// 优化提示框
plugins: {
tooltip: {
// 减少提示框计算
mode: 'index',
intersect: false
}
},
// 优化交互
interaction: {
mode: 'index',
intersect: false
}
};
更新优化 #
javascript
// 批量更新
function updateChartBatch(chart, newData) {
// 禁用动画
chart.options.animation = false;
// 批量更新数据
chart.data.labels = newData.labels;
chart.data.datasets.forEach((dataset, i) => {
dataset.data = newData.datasets[i].data;
});
// 单次更新
chart.update('none');
}
// 增量更新
function updateChartIncremental(chart, newPoint) {
// 添加新数据点
chart.data.labels.push(newPoint.label);
chart.data.datasets[0].data.push(newPoint.value);
// 移除旧数据点(保持固定数量)
if (chart.data.labels.length > 100) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
// 无动画更新
chart.update('none');
}
Canvas 优化 #
javascript
// 设置设备像素比
const options = {
devicePixelRatio: window.devicePixelRatio || 1
};
// 优化 Canvas 渲染
const ctx = document.getElementById('myChart').getContext('2d');
// 禁用图像平滑(提升性能)
ctx.imageSmoothingEnabled = false;
// 使用离屏 Canvas
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');
内存管理 #
javascript
// 正确销毁图表
let chartInstance = null;
function createChart(data) {
// 销毁旧图表
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
// 创建新图表
const ctx = document.getElementById('myChart');
chartInstance = new Chart(ctx, {
type: 'line',
data: data
});
}
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
if (chartInstance) {
chartInstance.destroy();
}
});
数据处理 #
数据格式化 #
javascript
// 数字格式化
const options = {
scales: {
y: {
ticks: {
callback: (value) => {
// 千分位
return value.toLocaleString();
}
}
}
},
plugins: {
tooltip: {
callbacks: {
label: (context) => {
const value = context.parsed.y;
// 格式化为货币
return '¥' + value.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
}
}
}
};
数据转换 #
javascript
// CSV 转 Chart.js 数据
function csvToChartData(csv, delimiter = ',') {
const lines = csv.trim().split('\n');
const headers = lines[0].split(delimiter);
const labels = [];
const datasets = headers.slice(1).map(header => ({
label: header.trim(),
data: []
}));
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(delimiter);
labels.push(values[0].trim());
datasets.forEach((dataset, j) => {
dataset.data.push(parseFloat(values[j + 1]));
});
}
return { labels, datasets };
}
// JSON 转 Chart.js 数据
function jsonToChartData(json, labelKey, valueKey) {
return {
labels: json.map(item => item[labelKey]),
datasets: [{
data: json.map(item => item[valueKey])
}]
};
}
数据验证 #
javascript
function validateChartData(data) {
// 检查必需字段
if (!data.labels || !Array.isArray(data.labels)) {
throw new Error('labels 必须是数组');
}
if (!data.datasets || !Array.isArray(data.datasets)) {
throw new Error('datasets 必须是数组');
}
// 检查数据长度
data.datasets.forEach((dataset, index) => {
if (!dataset.data || !Array.isArray(dataset.data)) {
throw new Error(`datasets[${index}].data 必须是数组`);
}
if (dataset.data.length !== data.labels.length) {
console.warn(`datasets[${index}].data 长度与 labels 不匹配`);
}
});
return true;
}
数据过滤 #
javascript
// 过滤空值
function filterNullData(data) {
return data.filter(value => value !== null && value !== undefined);
}
// 过滤异常值
function filterOutliers(data, threshold = 3) {
const mean = data.reduce((sum, val) => sum + val, 0) / data.length;
const std = Math.sqrt(
data.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / data.length
);
return data.filter(value => {
const zScore = Math.abs((value - mean) / std);
return zScore < threshold;
});
}
导出功能 #
导出为图片 #
javascript
// 导出为 PNG
function exportAsPNG(chart, filename = 'chart.png') {
const link = document.createElement('a');
link.download = filename;
link.href = chart.toBase64Image();
link.click();
}
// 导出为高质量图片
function exportAsHighQuality(chart, scale = 2, filename = 'chart.png') {
const canvas = chart.canvas;
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = canvas.width * scale;
tempCanvas.height = canvas.height * scale;
tempCtx.scale(scale, scale);
tempCtx.drawImage(canvas, 0, 0);
const link = document.createElement('a');
link.download = filename;
link.href = tempCanvas.toDataURL('image/png', 1.0);
link.click();
}
导出为 PDF #
javascript
// 需要安装 jsPDF
// npm install jspdf
import jsPDF from 'jspdf';
function exportAsPDF(chart, filename = 'chart.pdf') {
const canvas = chart.canvas;
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: canvas.width > canvas.height ? 'landscape' : 'portrait',
unit: 'px',
format: [canvas.width, canvas.height]
});
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
pdf.save(filename);
}
导出数据 #
javascript
// 导出为 CSV
function exportDataAsCSV(chart, filename = 'chart-data.csv') {
const labels = chart.data.labels;
const datasets = chart.data.datasets;
// 构建表头
let csv = 'Label,' + datasets.map(d => d.label).join(',') + '\n';
// 构建数据行
for (let i = 0; i < labels.length; i++) {
csv += labels[i];
datasets.forEach(dataset => {
csv += ',' + dataset.data[i];
});
csv += '\n';
}
// 下载文件
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
// 导出为 JSON
function exportDataAsJSON(chart, filename = 'chart-data.json') {
const data = {
labels: chart.data.labels,
datasets: chart.data.datasets.map(dataset => ({
label: dataset.label,
data: dataset.data
}))
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
实时数据 #
WebSocket 实时更新 #
javascript
const ctx = document.getElementById('myChart');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '实时数据',
data: [],
borderColor: 'rgb(75, 192, 192)',
tension: 0.4
}]
},
options: {
animation: {
duration: 0
}
}
});
// WebSocket 连接
const ws = new WebSocket('wss://example.com/realtime');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 添加新数据
chart.data.labels.push(data.time);
chart.data.datasets[0].data.push(data.value);
// 保持最多 50 个数据点
if (chart.data.labels.length > 50) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none');
};
定时轮询 #
javascript
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
return data;
}
// 定时更新
setInterval(async () => {
const data = await fetchData();
chart.data.datasets[0].data = data.values;
chart.update('none');
}, 5000);
最佳实践 #
1. 按需加载 #
javascript
// 只导入需要的组件
import {
Chart,
BarController,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
} from 'chart.js';
Chart.register(
BarController,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
2. 配置复用 #
javascript
// 基础配置
const baseConfig = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
}
}
};
// 创建图表时复用
const chart1 = new Chart(ctx1, {
...baseConfig,
type: 'bar',
data: data1
});
const chart2 = new Chart(ctx2, {
...baseConfig,
type: 'line',
data: data2
});
3. 错误处理 #
javascript
function createChartSafe(ctx, config) {
try {
return new Chart(ctx, config);
} catch (error) {
console.error('创建图表失败:', error);
return null;
}
}
下一步 #
现在你已经掌握了高级主题,接下来学习 交互功能,了解如何添加丰富的交互效果!
最后更新:2026-03-28