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