Chart.js 插件开发 #

插件概述 #

Chart.js 提供了强大的插件系统,允许你在图表渲染过程的各个阶段注入自定义逻辑:

text
┌─────────────────────────────────────────────────────────────┐
│                    Chart.js 插件系统                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   插件生命周期                        │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  beforeInit → afterInit                             │   │
│  │  beforeUpdate → afterUpdate                         │   │
│  │  beforeRender → afterRender                         │   │
│  │  beforeDraw → afterDraw                             │   │
│  │  beforeDestroy → afterDestroy                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   插件类型                            │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  内置插件:Title, Legend, Tooltip, Filler            │   │
│  │  第三方插件:Zoom, Datalabels, Annotation            │   │
│  │  自定义插件:根据需求开发                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

插件基础结构 #

基本插件格式 #

javascript
const myPlugin = {
  // 插件 ID(必需)
  id: 'myPlugin',
  
  // 默认配置
  defaults: {
    option1: 'default value',
    option2: true
  },
  
  // 初始化之前
  beforeInit: function(chart, args, options) {
    console.log('图表初始化之前');
  },
  
  // 初始化之后
  afterInit: function(chart, args, options) {
    console.log('图表初始化之后');
  },
  
  // 更新之前
  beforeUpdate: function(chart, args, options) {
    console.log('图表更新之前');
  },
  
  // 更新之后
  afterUpdate: function(chart, args, options) {
    console.log('图表更新之后');
  },
  
  // 绘制之前
  beforeDraw: function(chart, args, options) {
    console.log('图表绘制之前');
  },
  
  // 绘制之后
  afterDraw: function(chart, args, options) {
    console.log('图表绘制之后');
  },
  
  // 销毁之前
  beforeDestroy: function(chart, args, options) {
    console.log('图表销毁之前');
  },
  
  // 销毁之后
  afterDestroy: function(chart, args, options) {
    console.log('图表销毁之后');
  }
};

// 注册插件
Chart.register(myPlugin);

插件配置 #

javascript
// 全局注册
Chart.register(myPlugin);

// 在图表中使用
new Chart(ctx, {
  type: 'bar',
  data: data,
  options: {
    plugins: {
      myPlugin: {
        option1: 'custom value',
        option2: false
      }
    }
  }
});

// 局部注册(只对当前图表生效)
new Chart(ctx, {
  type: 'bar',
  data: data,
  options: {
    plugins: {
      myPlugin: {
        enabled: true
      }
    }
  },
  plugins: [myPlugin]
});

插件生命周期 #

完整生命周期钩子 #

javascript
const lifecyclePlugin = {
  id: 'lifecycle',
  
  // 实例化阶段
  beforeInit: function(chart, args, options) {
    console.log('1. beforeInit - 图表实例化之前');
  },
  afterInit: function(chart, args, options) {
    console.log('2. afterInit - 图表实例化之后');
  },
  
  // 更新阶段
  beforeUpdate: function(chart, args, options) {
    console.log('3. beforeUpdate - 数据更新之前');
  },
  afterUpdate: function(chart, args, options) {
    console.log('4. afterUpdate - 数据更新之后');
  },
  
  // 布局阶段
  beforeLayout: function(chart, args, options) {
    console.log('5. beforeLayout - 布局计算之前');
  },
  afterLayout: function(chart, args, options) {
    console.log('6. afterLayout - 布局计算之后');
  },
  
  // 数据处理阶段
  beforeDatasetsUpdate: function(chart, args, options) {
    console.log('7. beforeDatasetsUpdate - 数据集更新之前');
  },
  afterDatasetsUpdate: function(chart, args, options) {
    console.log('8. afterDatasetsUpdate - 数据集更新之后');
  },
  
  // 渲染阶段
  beforeRender: function(chart, args, options) {
    console.log('9. beforeRender - 渲染之前');
  },
  afterRender: function(chart, args, options) {
    console.log('10. afterRender - 渲染之后');
  },
  
  // 绘制阶段
  beforeDraw: function(chart, args, options) {
    console.log('11. beforeDraw - 绘制之前');
  },
  afterDraw: function(chart, args, options) {
    console.log('12. afterDraw - 绘制之后');
  },
  
  // 销毁阶段
  beforeDestroy: function(chart, args, options) {
    console.log('13. beforeDestroy - 销毁之前');
  },
  afterDestroy: function(chart, args, options) {
    console.log('14. afterDestroy - 销毁之后');
  }
};

钩子函数参数 #

javascript
const myPlugin = {
  id: 'myPlugin',
  
  beforeDraw: function(chart, args, options) {
    // chart: 图表实例
    // args: 事件参数(可选)
    // options: 插件配置选项
    
    // 访问图表数据
    const data = chart.data;
    const datasets = chart.data.datasets;
    
    // 访问图表区域
    const { chartArea, width, height } = chart;
    const { top, bottom, left, right } = chartArea;
    
    // 访问 Canvas 上下文
    const ctx = chart.ctx;
    
    // 访问配置选项
    const pluginOptions = chart.options.plugins.myPlugin;
    
    // 返回 false 可以阻止后续操作
    if (someCondition) {
      return false;
    }
  }
};

实用插件示例 #

水印插件 #

javascript
const watermarkPlugin = {
  id: 'watermark',
  
  defaults: {
    text: 'Watermark',
    font: '16px Arial',
    color: 'rgba(0, 0, 0, 0.1)',
    position: 'bottom-right'
  },
  
  afterDraw: function(chart, args, options) {
    const ctx = chart.ctx;
    const { width, height, chartArea } = chart;
    
    ctx.save();
    ctx.font = options.font;
    ctx.fillStyle = options.color;
    ctx.textAlign = 'right';
    ctx.textBaseline = 'bottom';
    
    let x, y;
    
    switch (options.position) {
      case 'top-left':
        x = chartArea.left + 10;
        y = chartArea.top + 20;
        break;
      case 'top-right':
        x = chartArea.right - 10;
        y = chartArea.top + 20;
        break;
      case 'bottom-left':
        x = chartArea.left + 10;
        y = chartArea.bottom - 10;
        break;
      case 'bottom-right':
      default:
        x = chartArea.right - 10;
        y = chartArea.bottom - 10;
    }
    
    ctx.fillText(options.text, x, y);
    ctx.restore();
  }
};

Chart.register(watermarkPlugin);

背景插件 #

javascript
const backgroundPlugin = {
  id: 'background',
  
  defaults: {
    color: 'transparent'
  },
  
  beforeDraw: function(chart, args, options) {
    const { ctx, chartArea, width, height } = chart;
    
    ctx.save();
    ctx.fillStyle = options.color;
    ctx.fillRect(0, 0, width, height);
    ctx.restore();
  }
};

// 使用
new Chart(ctx, {
  type: 'line',
  data: data,
  options: {
    plugins: {
      background: {
        color: '#f5f5f5'
      }
    }
  }
});

空数据提示插件 #

javascript
const emptyDataPlugin = {
  id: 'emptyData',
  
  defaults: {
    text: '暂无数据',
    font: '16px Arial',
    color: '#999',
    backgroundColor: 'transparent'
  },
  
  afterDraw: function(chart, args, options) {
    const { data } = chart;
    const hasData = data.datasets.some(dataset => dataset.data.length > 0);
    
    if (!hasData) {
      const { ctx, width, height } = chart;
      
      ctx.save();
      ctx.font = options.font;
      ctx.fillStyle = options.color;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      
      ctx.fillText(options.text, width / 2, height / 2);
      ctx.restore();
    }
  }
};

Chart.register(emptyDataPlugin);

平均线插件 #

javascript
const averageLinePlugin = {
  id: 'averageLine',
  
  defaults: {
    color: 'rgba(255, 99, 132, 0.5)',
    width: 2,
    dash: [5, 5],
    text: true,
    textColor: 'rgba(255, 99, 132, 1)',
    textFont: '12px Arial'
  },
  
  afterDraw: function(chart, args, options) {
    const dataset = chart.data.datasets[0];
    if (!dataset || !dataset.data.length) return;
    
    const data = dataset.data;
    const sum = data.reduce((a, b) => a + b, 0);
    const avg = sum / data.length;
    
    const yScale = chart.scales.y;
    const yPos = yScale.getPixelForValue(avg);
    
    const { ctx, chartArea } = chart;
    
    ctx.save();
    ctx.beginPath();
    ctx.strokeStyle = options.color;
    ctx.lineWidth = options.width;
    ctx.setLineDash(options.dash);
    
    ctx.moveTo(chartArea.left, yPos);
    ctx.lineTo(chartArea.right, yPos);
    ctx.stroke();
    
    if (options.text) {
      ctx.font = options.textFont;
      ctx.fillStyle = options.textColor;
      ctx.textAlign = 'left';
      ctx.textBaseline = 'bottom';
      ctx.fillText(`平均: ${avg.toFixed(2)}`, chartArea.left + 5, yPos - 5);
    }
    
    ctx.restore();
  }
};

Chart.register(averageLinePlugin);

数据标签插件 #

javascript
const datalabelsPlugin = {
  id: 'datalabels',
  
  defaults: {
    color: '#333',
    font: '12px Arial',
    formatter: (value) => value,
    anchor: 'end',
    align: 'top',
    offset: 4
  },
  
  afterDatasetsDraw: function(chart, args, options) {
    const { ctx } = chart;
    
    chart.data.datasets.forEach((dataset, datasetIndex) => {
      const meta = chart.getDatasetMeta(datasetIndex);
      
      meta.data.forEach((element, index) => {
        const value = dataset.data[index];
        const position = element.getProps(['x', 'y']);
        
        ctx.save();
        ctx.font = options.font;
        ctx.fillStyle = options.color;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';
        
        const label = options.formatter(value, { chart, datasetIndex, index });
        
        let x = position.x;
        let y = position.y;
        
        if (options.anchor === 'end') {
          y -= options.offset;
        } else if (options.anchor === 'start') {
          y += options.offset;
        }
        
        ctx.fillText(label, x, y);
        ctx.restore();
      });
    });
  }
};

Chart.register(datalabelsPlugin);

十字准线插件 #

javascript
const crosshairPlugin = {
  id: 'crosshair',
  
  defaults: {
    line: {
      color: 'rgba(0, 0, 0, 0.3)',
      width: 1,
      dash: [3, 3]
    },
    sync: {
      enabled: false,
      group: 1
    }
  },
  
  afterDraw: function(chart, args, options) {
    if (!chart.crosshair) return;
    
    const { ctx, chartArea } = chart;
    const { x, y } = chart.crosshair;
    
    ctx.save();
    ctx.strokeStyle = options.line.color;
    ctx.lineWidth = options.line.width;
    ctx.setLineDash(options.line.dash);
    
    // 垂直线
    ctx.beginPath();
    ctx.moveTo(x, chartArea.top);
    ctx.lineTo(x, chartArea.bottom);
    ctx.stroke();
    
    // 水平线
    ctx.beginPath();
    ctx.moveTo(chartArea.left, y);
    ctx.lineTo(chartArea.right, y);
    ctx.stroke();
    
    ctx.restore();
  },
  
  beforeEvent: function(chart, args, options) {
    const event = args.event;
    
    if (event.type === 'mousemove') {
      chart.crosshair = {
        x: event.x,
        y: event.y
      };
      chart.draw();
    } else if (event.type === 'mouseout') {
      chart.crosshair = null;
      chart.draw();
    }
  }
};

Chart.register(crosshairPlugin);

第三方插件 #

chartjs-plugin-datalabels #

bash
npm install chartjs-plugin-datalabels
javascript
import ChartDataLabels from 'chartjs-plugin-datalabels';

Chart.register(ChartDataLabels);

new Chart(ctx, {
  type: 'bar',
  data: data,
  options: {
    plugins: {
      datalabels: {
        color: '#333',
        anchor: 'end',
        align: 'top',
        offset: 4,
        formatter: (value, context) => {
          return value.toLocaleString();
        },
        font: {
          weight: 'bold'
        }
      }
    }
  }
});

chartjs-plugin-annotation #

bash
npm install chartjs-plugin-annotation
javascript
import annotationPlugin from 'chartjs-plugin-annotation';

Chart.register(annotationPlugin);

new Chart(ctx, {
  type: 'line',
  data: data,
  options: {
    plugins: {
      annotation: {
        annotations: {
          line1: {
            type: 'line',
            yMin: 60,
            yMax: 60,
            borderColor: 'rgb(255, 99, 132)',
            borderWidth: 2,
            borderDash: [6, 6],
            label: {
              content: '目标线',
              enabled: true,
              position: 'end'
            }
          },
          box1: {
            type: 'box',
            yMin: 40,
            yMax: 80,
            backgroundColor: 'rgba(255, 99, 132, 0.1)',
            borderColor: 'rgba(255, 99, 132, 0.3)',
            borderWidth: 1
          }
        }
      }
    }
  }
});

chartjs-plugin-zoom #

bash
npm install chartjs-plugin-zoom
javascript
import zoomPlugin from 'chartjs-plugin-zoom';

Chart.register(zoomPlugin);

new Chart(ctx, {
  type: 'line',
  data: data,
  options: {
    plugins: {
      zoom: {
        zoom: {
          wheel: {
            enabled: true
          },
          pinch: {
            enabled: true
          },
          mode: 'xy'
        },
        pan: {
          enabled: true,
          mode: 'xy'
        }
      }
    }
  }
});

插件开发最佳实践 #

1. 使用默认配置 #

javascript
const myPlugin = {
  id: 'myPlugin',
  
  defaults: {
    enabled: true,
    color: '#333',
    font: {
      size: 12,
      family: 'Arial'
    }
  },
  
  afterDraw: function(chart, args, options) {
    // 合并默认配置和用户配置
    const config = { ...this.defaults, ...options };
    
    if (!config.enabled) return;
    
    // 使用配置
    chart.ctx.fillStyle = config.color;
    chart.ctx.font = `${config.font.size}px ${config.font.family}`;
  }
};

2. 性能优化 #

javascript
const myPlugin = {
  id: 'myPlugin',
  
  afterDraw: function(chart, args, options) {
    // 缓存计算结果
    if (!chart._myPluginCache) {
      chart._myPluginCache = this.calculateSomething(chart);
    }
    
    // 使用缓存
    const cached = chart._myPluginCache;
    
    // 绘制...
  },
  
  beforeUpdate: function(chart, args, options) {
    // 清除缓存
    delete chart._myPluginCache;
  }
};

3. 错误处理 #

javascript
const myPlugin = {
  id: 'myPlugin',
  
  afterDraw: function(chart, args, options) {
    try {
      // 插件逻辑
      this.doSomething(chart);
    } catch (error) {
      console.error('Plugin error:', error);
      // 不影响图表正常渲染
    }
  }
};

4. 清理资源 #

javascript
const myPlugin = {
  id: 'myPlugin',
  
  afterInit: function(chart, args, options) {
    // 创建资源
    chart._myPlugin = {
      canvas: document.createElement('canvas'),
      listener: this.handleEvent.bind(this)
    };
    
    chart.canvas.addEventListener('click', chart._myPlugin.listener);
  },
  
  beforeDestroy: function(chart, args, options) {
    // 清理资源
    if (chart._myPlugin) {
      chart.canvas.removeEventListener('click', chart._myPlugin.listener);
      delete chart._myPlugin;
    }
  }
};

5. TypeScript 支持 #

typescript
import { Chart, Plugin } from 'chart.js';

interface MyPluginOptions {
  enabled: boolean;
  color: string;
}

const myPlugin: Plugin<MyPluginOptions> = {
  id: 'myPlugin',
  
  defaults: {
    enabled: true,
    color: '#333'
  },
  
  afterDraw: (chart, args, options) => {
    if (!options.enabled) return;
    
    chart.ctx.fillStyle = options.color;
    // ...
  }
};

插件调试 #

调试钩子 #

javascript
const debugPlugin = {
  id: 'debug',
  
  beforeInit: (chart) => console.log('beforeInit'),
  afterInit: (chart) => console.log('afterInit'),
  beforeUpdate: (chart) => console.log('beforeUpdate'),
  afterUpdate: (chart) => console.log('afterUpdate'),
  beforeDraw: (chart) => console.log('beforeDraw'),
  afterDraw: (chart) => console.log('afterDraw')
};

Chart.register(debugPlugin);

检查插件状态 #

javascript
// 检查插件是否已注册
console.log(Chart.plugins.get('myPlugin'));

// 获取所有已注册插件
console.log(Chart.plugins.getAll());

// 取消注册插件
Chart.unregister(myPlugin);

总结 #

Chart.js 的插件系统提供了强大的扩展能力,通过合理使用插件可以:

  1. 添加自定义绘制内容
  2. 修改图表行为
  3. 增强交互功能
  4. 实现复杂的可视化需求

掌握插件开发是成为 Chart.js 专家的关键一步!


恭喜你完成了 Chart.js 完全指南的学习!现在你已经掌握了从基础到高级的所有 Chart.js 技能,可以开始在实际项目中应用这些知识了。

最后更新:2026-03-28