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