如何实现带中间分组节点的Highcharts桑基图(航班数据场景)
实现Highcharts桑基图的完整三级数据流交互
原始航班数据
[ {"from":"NY", "to":"Oslo", "company": "Norwegian", "flights":3}, {"from":"NY", "to":"Paris", "company": "Norwegian", "flights":2}, {"from":"Miami", "to":"Berlin", "company": "Norwegian", "flights":1}, {"from":"Miami", "to":"London", "company": "SAS", "flights":1} ]
需求说明
需要实现Highcharts桑基图/冲积图,节点固定布局为:
- 左侧:出发地(
from) - 中间:航空公司(
company) - 右侧:目的地(
to)
要求图表能识别from=>company=>to的完整数据流,在鼠标悬停等交互中正确关联所有相关节点和连线(比如悬停NY时,需同时高亮Norwegian、Oslo、Paris以及对应的所有连线)。
当前实现的问题
通过伪造出发地=>航空公司、航空公司=>目的地的映射关系模拟外观后,交互存在缺陷:
- 悬停出发地(如NY)时,仅能关联到中间的航空公司(Norwegian),无法直接关联到最终目的地(Oslo、Paris),因为出发地与目的地之间无直接数据连线,仅通过中间节点间接连接。
解决方案
要实现完整的三级数据流交互,需从数据处理和图表配置两方面入手:
1. 构建带完整链路标识的数据结构
桑基图基于两两节点连线,因此转换数据时需为每条连线添加完整路径标识,同时收集所有唯一节点:
const rawData = [/* 原始航班数据 */]; const nodes = new Set(); const links = []; rawData.forEach(item => { const fullRoute = `${item.from}->${item.company}->${item.to}`; // 添加出发地到航空公司的连线,携带完整路径 links.push({ from: item.from, to: item.company, value: item.flights, fullRoute }); // 添加航空公司到目的地的连线,携带完整路径 links.push({ from: item.company, to: item.to, value: item.flights, fullRoute }); // 收集所有节点 nodes.add(item.from); nodes.add(item.company); nodes.add(item.to); }); // 转换为Highcharts所需的节点格式 const chartNodes = Array.from(nodes).map(name => ({ id: name, name }));
2. 自定义交互逻辑实现跨层级高亮
利用Highcharts的事件钩子,结合fullRoute字段实现跨层级的节点和连线高亮,同时自定义提示框内容:
Highcharts.chart('container', { series: [{ type: 'sankey', nodes: chartNodes, data: links, events: { mouseOver: function(e) { const targetRoute = e.point?.fullRoute || e.target.options.fullRoute; const chart = this.chart; // 重置所有元素状态 chart.series[0].points.forEach(point => point.setState('')); chart.series[0].nodes.forEach(node => node.setState('')); // 高亮同一路径的所有连线和节点 chart.series[0].points.forEach(point => { if (point.fullRoute === targetRoute) { point.setState('hover'); // 高亮对应节点 chart.series[0].nodes.find(n => n.id === point.from).setState('hover'); chart.series[0].nodes.find(n => n.id === point.to).setState('hover'); } }); }, mouseOut: function() { // 恢复所有元素默认状态 this.chart.series[0].points.forEach(point => point.setState('')); this.chart.series[0].nodes.forEach(node => node.setState('')); } }, tooltip: { formatter: function() { const routeParts = this.point.fullRoute.split('->'); return `<b>${routeParts[0]} → ${routeParts[1]} → ${routeParts[2]}</b><br/>航班数: ${this.point.value}`; } } }] });
3. 固定节点层级(可选)
如果需要确保节点严格按出发地→航空公司→目的地的顺序排列,可在构建节点时指定列位置:
const chartNodes = Array.from(nodes).map(name => { let column = 0; if (rawData.some(item => item.company === name)) column = 1; if (rawData.some(item => item.to === name)) column = 2; return ({ id: name, name, column }); });
配置完成后,悬停任意节点或连线时,同一条完整路径的所有元素都会被高亮,提示框也会显示完整的航线信息。
内容的提问来源于stack exchange,提问作者siggemannen
相关产品推荐
相关产品推荐

