Highcharts散点图Tooltip悬停关闭及内部图表重复渲染问题
问题排查:Highcharts散点图Tooltip嵌套图表的两个异常问题
问题现象
- 悬停Tooltip中上部区域时,Tooltip意外关闭
- 悬停Tooltip内的图表时,图表持续重复渲染
原因分析
1. Tooltip意外关闭
- 同时开启
split: true和shared: true会拆分Tooltip结构,鼠标移至非数据点关联区域时触发关闭逻辑 stickyTracking: false导致鼠标离开散点后,Tooltip无法保持激活状态- Tooltip内部的图表容器未被识别为Tooltip的一部分,鼠标移至图表区域时被判定为离开Tooltip范围
2. 图表重复渲染
- 监听
Tooltip.refresh事件,该事件会在鼠标移动(包括在Tooltip内部移动)时频繁触发,导致每次都调用renderChart重新请求数据并渲染 - 未缓存请求数据,也未销毁旧图表,既造成重复请求,也引发内存泄漏
解决方案
针对Tooltip关闭问题
- 移除
split: true和shared: true配置,单个散点不需要共享Tooltip - 保留
stickOnContact: true,确保鼠标接触Tooltip时保持显示 - 给Tooltip容器添加样式,让内部图表区域可以正常响应鼠标事件,同时避免事件穿透关闭Tooltip
针对重复渲染问题
- 缓存已请求的数据,避免重复fetch
- 仅当hover的散点发生变化时,才重新渲染内部图表
- 渲染新图表前销毁旧图表,清理资源
修改后的完整代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://code.highcharts.com/stock/highstock.js"></script> <script src="https://code.highcharts.com/stock/modules/accessibility.js"></script> <style> /* 确保Tooltip内部图表可响应鼠标,同时避免关闭Tooltip */ .highcharts-tooltip .hc-tooltip-container { pointer-events: auto; } .highcharts-tooltip { pointer-events: none; } </style> </head> <body> <div id="scatter_container" style="height: 90vh;"></div> <script> // 缓存请求的数据 let cachedStockData = null; // 记录当前hover的点ID,避免重复渲染 let currentHoverPointId = null; // 保存内部图表实例,用于销毁 let innerChartInstance = null; async function getStockData() { if (cachedStockData) return cachedStockData; const data = await fetch( 'https://demo-live-data.highcharts.com/aapl-c.json' ).then(response => response.json()); cachedStockData = data; return data; } async function renderChart(point) { // 点未变化时不重复渲染 if (point.id === currentHoverPointId) return; currentHoverPointId = point.id; // 销毁旧图表 if (innerChartInstance) { innerChartInstance.destroy(); } const data = await getStockData(); // 创建新图表 innerChartInstance = Highcharts.stockChart('hc-tooltip', { rangeSelector: { selected: 1 }, title: { text: 'AAPL Stock Price' }, series: [{ name: 'AAPL', data: data, tooltip: { valueDecimals: 2 } }] }); } Highcharts.addEvent( Highcharts.Tooltip, 'refresh', function () { const hoverPoint = this.chart.hoverPoint; if (hoverPoint) { renderChart(hoverPoint); } } ); const series = [{ name: 'Basketball', id: 'basketball', marker: { symbol: 'circle', width: 32, height: 32 } }]; async function getData() { const response = await fetch('https://api.jsonbin.io/v3/qs/65b8d298dc746540189dacb7'); return response.json(); } getData().then(data => { const getData = sportName => { const temp = []; data.record.forEach(elm => { temp.push([elm.height, elm.weight]); }); return temp; }; series.forEach(s => { s.data = getData(s.id); }); Highcharts.chart('scatter_container', { chart: { type: 'scatter', zoomType: 'xy' }, title: { text: 'Olympics athletes by height and weight', align: 'left' }, subtitle: { text:'', align: 'left' }, xAxis: { title: { text: 'Height' }, labels: { format: '{value} m' }, startOnTick: true, endOnTick: true, showLastLabel: true }, yAxis: { title: { text: 'Weight' }, labels: { format: '{value} kg' } }, legend: { enabled: true }, plotOptions: { scatter: { marker: { radius: 2.5, symbol: 'circle', states: { hover: { enabled: true, lineColor: 'rgb(100,100,100)' } } }, states: { hover: { marker: { enabled: false } } }, jitter: { x: 0.005 } } }, credits:{ enabled:false }, tooltip: { stickOnContact: true, useHTML: true, headerFormat: '', pointFormat: '<div id="hc-tooltip" class="hc-tooltip-container" style="width:320px;height:45vh;"></div>' }, series }); }); </script> </body> </html>
内容的提问来源于stack exchange,提问作者Steve James
相关产品推荐
相关产品推荐

