如何实现点击悬停中的hover template使其持久显示并可隐藏?
思路分析
你提到的通过添加trace实现持久hover的方向是正确的,但直接为每个点击点新建trace会导致性能问题,需要优化。另外还有更轻量的annotations方案可以选择,两种方案各有优劣:
方案一:复用单个Trace实现持久Hover
核心思路是维护一个专门用于显示持久hover的Trace,所有需要固定显示hover的点都添加到这个Trace中,避免创建大量Trace导致卡顿。
实现代码示例
// 假设原数据为data,布局为layout // 初始化时添加一个空的持久hover Trace const persistedTrace = { x: [], y: [], customdata: [], hovertemplate: data[0].hovertemplate, // 和原Trace的hover模板保持一致 mode: 'markers', marker: { size: 0 }, // 隐藏标记本身,只保留hover提示 name: 'Persisted Hover' }; data.push(persistedTrace); const fig = Plotly.newPlot('myDiv', data, layout); // 存储已持久化的点标识(避免重复添加) const persistedPoints = new Set(); // 监听点击事件 fig.on('plotly_click', function(clickData) { const point = clickData.points[0]; const pointKey = `${point.curveNumber}-${point.pointIndex}`; if (persistedPoints.has(pointKey)) { // 移除已持久化的点 const targetIndex = persistedTrace.x.indexOf(point.x); if (targetIndex !== -1) { persistedTrace.x.splice(targetIndex, 1); persistedTrace.y.splice(targetIndex, 1); persistedTrace.customdata.splice(targetIndex, 1); } persistedPoints.delete(pointKey); } else { // 添加需要持久化的点 persistedTrace.x.push(point.x); persistedTrace.y.push(point.y); persistedTrace.customdata.push(point.customdata); persistedPoints.add(pointKey); } // 更新图表的持久hover Trace Plotly.update('myDiv', [persistedTrace], {}, [data.length - 1]); });
原理说明
这个方案通过复用单个Trace来承载所有需要持久显示hover的点,每次点击只更新该Trace的数据,不会产生大量Trace占用资源。由于该Trace的hover模板和原数据一致,鼠标悬停在这些点上时会自动显示对应的hover内容,同时不影响其他点的正常hover交互。
方案二:用Annotations模拟持久Hover框
如果觉得Trace方案仍有性能顾虑,可以用Annotations模拟原生hover框的样式,点击时创建/删除对应Annotation,无需额外Trace。
实现代码示例
const fig = Plotly.newPlot('myDiv', data, layout); // 存储已创建的持久化Annotation的标识 const persistedAnnotations = new Map(); // 监听点击事件 fig.on('plotly_click', function(clickData) { const point = clickData.points[0]; const pointKey = `${point.curveNumber}-${point.pointIndex}`; if (persistedAnnotations.has(pointKey)) { // 删除已存在的Annotation const annoId = persistedAnnotations.get(pointKey); fig.layout.annotations = fig.layout.annotations.filter(anno => anno.id !== annoId); persistedAnnotations.delete(pointKey); Plotly.relayout('myDiv', 'annotations', fig.layout.annotations); return; } // 生成hover内容(替换模板中的占位符) let hoverContent = point.fullData.hovertemplate; hoverContent = hoverContent.replace('%{x}', point.x) .replace('%{y}', point.y) .replace('%{customdata}', point.customdata || ''); // 可根据实际需求替换更多占位符,比如%{text}等 // 创建模拟原生hover样式的Annotation const newAnnotation = { id: pointKey, x: point.x, y: point.y, xref: 'x', yref: 'y', text: hoverContent, showarrow: true, arrowhead: 1, ax: 20, ay: -30, bgcolor: '#ffffff', bordercolor: '#444444', borderwidth: 1, font: { family: 'Arial', size: 12, color: '#000000' }, opacity: 0.9 }; // 添加Annotation并更新图表 fig.layout.annotations = [...(fig.layout.annotations || []), newAnnotation]; persistedAnnotations.set(pointKey, pointKey); Plotly.relayout('myDiv', 'annotations', fig.layout.annotations); });
原理说明
该方案直接创建和原生hover框样式一致的Annotation,点击标记时添加Annotation,再次点击则删除。这种方式不需要额外Trace,性能开销极小,适合数据量较大的场景,唯一需要注意的是要根据自己的hover模板调整占位符的替换逻辑,保证显示内容和原生hover一致。
关于你提到的官方示例的说明
你参考的官方示例核心逻辑是:监听hover事件记录当前悬停的点,点击时将该点的数据复制到一个新的高亮Trace中,通过这个Trace来保留hover提示。但它的问题是每次点击都会新建一个Trace,当点击次数多了之后,大量Trace会导致浏览器卡顿。优化的关键就是复用单个Trace,而不是每次新建,这样就能解决性能问题。
内容的提问来源于stack exchange,提问作者WilliamMayor

