ChartJS禁用nearest模式下,如何高效获取鼠标悬停线段信息?
问题:Chart.js折线图线段悬停性能优化
我需要在鼠标悬停折线图的**线段(连接两点的线条)**而非数据点时,获取该线段的相关信息。Chart.js原生仅支持数据点的悬停功能,没有线段悬停的内置方法。由于图表用于展示地理位置,空白区域有重要意义,因此不能使用nearest交互模式。
我自己实现了一套自定义方案,但鼠标移动时会触发大量计算,导致性能急剧下降,现寻求更优实现方式。
当前实现代码
// CONFIG interaction: { mode: 'dataset', // 'nearest', 'point', 'index', 'x', 'y', 'dataset' // intersect: false; // axis: 'xy' // includeInvisible: false; }, events: ['mousemove', 'click', 'mouseleave'], onHover: (event: any, activeElements: any, chart: any) => { this.mousePosition = { x: event.native.clientX, y: event.native.clientY }; const canvasPosition = getRelativePosition(event, chart); const dataX = chart.scales.x.getValueForPixel(canvasPosition.x); const dataY = chart.scales.y.getValueForPixel(canvasPosition.y); let lineTouchingMouseOrUndefined: any; let hoverData: any; lineTouchingMouseOrUndefined = this.getLineIfTouchingMouse(chart, event.x, event.y); hoverData = { y: this.yLabelsMap[dataY], x: new Date(dataX), convoy: lineTouchingMouseOrUndefined?.convoy }; this.hoverEmitter.emit(hoverData) if (lineTouchingMouseOrUndefined.changedPoints) { this.hadUpdates = true; } if (this.hadUpdates) { clearTimeout(this.updateTimeout); this.updateTimeout = setTimeout(() => { chart.update(); this.hadUpdates = false; }, this.updateThreshold) } } //HELPER FUNCTIONS getLineIfTouchingMouse(chart: any, mouseX: any, mouseY: any) { let selectedLine: any; let convoyIndex; let indexPoint; let indexNextPoint; let removedSelectionFromOldPoint; let removedSelectionFromOldNextPoint; for (let i = 0; i < chart.data.datasets.length - 1; ++i) { let convoyPath = chart.data.datasets[i].data; for (let j = 1; j < convoyPath.length - 1; ++j) { let point = convoyPath[j - 1]; let nextPoint = convoyPath[j]; let convoy = convoyPath[j - 1].allData; if (this.previousSelectedPoint?.id == point.id) { removedSelectionFromOldPoint = true } if (this.previousSelectedNextPoint?.id == nextPoint.id) { removedSelectionFromOldNextPoint = true; } point.selected = false; nextPoint.selected = false; const closestY1 = this.extractKmFromY(this.getClosestYPoint(this.extractKmFromY(point.y), point.segmentId)) const closestY2 = this.extractKmFromY(this.getClosestYPoint(this.extractKmFromY(nextPoint.y), nextPoint.segmentId)) const y1labelIndex = this.yLabelsMap.findIndex(f => f.value == closestY1) const y2labelIndex = this.yLabelsMap.findIndex(f => f.value == closestY2) let line = { x1: chart.scales.x.getPixelForValue(point.x), x2: chart.scales.x.getPixelForValue(nextPoint.x), y1: chart.scales.y.getPixelForValue(y1labelIndex), y2: chart.scales.y.getPixelForValue(y2labelIndex), point, nextPoint } if (this.checkLineContainsMousePoint(mouseX, mouseY, line)) { convoyIndex = i indexPoint = j - 1; indexNextPoint = j; selectedLine = { line, convoy }; } } } if (convoyIndex && indexPoint && indexNextPoint) { // when there was a hovered line if (this.previousSelectedPoint?.id == chart.data.datasets[convoyIndex].data[indexPoint].id) { removedSelectionFromOldPoint = false; } if (this.previousSelectedNextPoint?.id == chart.data.datasets[convoyIndex].data[indexNextPoint].id) { removedSelectionFromOldNextPoint = false; } if (removedSelectionFromOldPoint || removedSelectionFromOldNextPoint) { selectedLine.changedPoints = true } else { selectedLine.changedPoints = false } chart.data.datasets[convoyIndex].data[indexPoint].selected = true; chart.data.datasets[convoyIndex].data[indexNextPoint].selected = true; this.previousSelectedPoint = chart.data.datasets[convoyIndex].data[indexPoint] this.previousSelectedNextPoint = chart.data.datasets[convoyIndex].data[indexNextPoint] } else { selectedLine = { changedPoints: (this.previousSelectedNextPoint || this.previousSelectedPoint) ? true : false } this.previousSelectedNextPoint = null; this.previousSelectedPoint = null; } return selectedLine; } checkLineContainsMousePoint(mouseX: any, mouseY: any, line: any) { if (mouseX < line.x1 || mouseX > line.x2) { return false; } var linepoint = this.linepointNearestMouse(line, mouseX, mouseY); var dx = mouseX - linepoint.x; var dy = mouseY - linepoint.y; var distance = Math.abs(Math.sqrt(dx * dx + dy * dy)); if (distance < 5) { return true; } else { return false; } } linepointNearestMouse(line: any, x: any, y: any) { var dx = line.x2 - line.x1; var dy = line.y2 - line.y1; var t = ((x - line.x1) * dx + (y - line.y1) * dy) / (dx * dx + dy * dy); var lineX = this.lerp(line.x1, line.x2, t); var lineY = this.lerp(line.y1, line.y2, t); return ({ x: lineX, y: lineY }); } private lerp = function (a: any, b: any, x: any) { return (a + x * (b - a)); };
优化方案
1. 减少不必要的循环与计算
- 提前终止循环:找到匹配的线段后立即跳出嵌套循环,无需遍历所有线段。可在
checkLineContainsMousePoint返回true后,直接return结果,避免后续无意义的遍历。 - 缓存像素坐标:在图表初始化或数据更新时,提前计算所有数据点的像素坐标(
x、y)并存储在点对象中,避免每次鼠标移动时重复调用getPixelForValue。
2. 优化碰撞检测逻辑
- 简化距离计算:去掉开平方运算(
Math.sqrt),直接比较距离的平方与阈值的平方(5*5=25),平方运算比开平方快很多,且不影响大小判断:// 替换原距离计算部分 const dxSquared = (mouseX - linepoint.x) ** 2; const dySquared = (mouseY - linepoint.y) ** 2; return dxSquared + dySquared < 25; - 快速过滤无关线段:除了X范围判断,增加Y范围过滤——如果鼠标Y坐标不在线段Y的最小/最大值范围内,直接跳过该线段,减少后续计算。
3. 优化数据查找效率
- 缓存Y标签索引映射:将
yLabelsMap转换为键值对对象,替换findIndex调用,对象查找时间复杂度为O(1),远快于数组遍历的O(n):// 初始化时创建映射 this.yLabelIndexMap = this.yLabelsMap.reduce((map, item, index) => { map[item.value] = index; return map; }, {}); // 使用时直接取值 const y1labelIndex = this.yLabelIndexMap[closestY1]; const y2labelIndex = this.yLabelIndexMap[closestY2];
4. 减少图表更新与事件开销
- 精准控制图表更新:仅当选中的线段发生变化时才标记需要更新,避免无意义的
chart.update()调用。 - 节流
mousemove事件:使用节流函数限制mousemove事件的触发间隔(比如每50ms处理一次),避免短时间内触发过多计算。 - 防抖事件发射:将
hoverEmitter.emit与防抖逻辑结合,减少事件发射频率,降低不必要的组件通信开销。
5. 预计算线段数据
- 利用
beforeRender钩子:在图表渲染前提前计算所有线段的像素坐标、Y索引等数据,存储在数据集的自定义属性中,避免在onHover中重复计算。
内容的提问来源于stack exchange,提问作者Matheus Kieling
相关产品推荐
相关产品推荐

