前端 | 使用 ECharts 绘制关系图
0 需求
做的项目需要画一个关系图,主要需求如下:
- 需要展示6种对象之间的关系:数据机构 数据 合约 模型 计算机构 应用
- 支持突出显示6种对象中的某一种的所有对象
- 支持Top x子图功能。top x子图的定义:在6种对象中的每一种对象,取关系数最多的x个,将这至多6*x个对象绘制在一张图中
- 任务仅为原型展示,无后端,可以面向数据编程
众所周知 ECharts 是一个功能强大的 JS 图表库,这次也使用 echarts 进行图表绘制。
最终效果大致如下图;感兴趣的话可以在 Codepen 预览动画效果,也可以亲自尝试各种参数的修改。
1 使用 ECharts
使用echarts想必大家都比较熟悉了,官网上也有很详细的教程,这里就简单介绍一下。
charts 库支持的图表类型有二三十种,基本涵盖了能想象到和完全没想过的各种图表绘制。同时echarts也支持各种常用的引入方式:从源码构建、npm安装、CDN引入等。
引入 JS 库后在网页中使用也很容易:
<!-- 为 ECharts 准备一个具备大小的 DOM -->
<div id="model-kg-graph" style="height: 800px"></div>
var graph_chart = echarts.init(document.getElementById("model-kg-graph"));
let option = {
// 相应的配置项
}
graph_chart.setOption(option);
不管绘制什么图表,需要的代码都是如上几行,区别只在 option 配置项的不同。主要的配置项有这些:
let option = {
// 图例,因为一张图中可能存在多个图表(例如柱状图+折线图),因此参数是数组
// 数组中每一项对应一个系列,每项是一个包含类别名称数组的对象;
// categories: ["数据机构", "数据", "合约", "模型", "计算机构", "应用"],
legend: [
{
data: graph_data.categories,
},
],
// 图表的配置;如果数组有多项几个图表将绘制在一起
// 每种不同的图表都有大量专属的配置内容,以下是比较常见通用的几种
series: {[
type: "graph", // 指定绘制的是什么图
roam: true, // 使用鼠标滚轮缩放、点击移动
animation: false, // 是否开启动画;默认是开启
emphasis: {}, // 可以设置被选中的元素突出显示的特殊样式
]},
};
除此以外还有标题、坐标系(柱状图/折线图中很常用)等等常见配置,这次没用到就忽略了。使用时可以查阅echarts事无巨细的配置项文档。
2 关系图 Graph
本节中提到的所有配置项均是在 option.series 内,下文不再特殊说明
图的基本构成
说回关系图。关系图其实就是平时常说的图,基本构成是节点和边。在配置项中对应的就是:
// 也可以叫 nodes
data: [
{ name: "数据机构1", symbolSize: 3.0 },
],
// 也可以叫 edges
links: [
{ source: "数据1", target:"数据机构3" },
],
- data 中的每个对象对应一个节点,name 为唯一表示,不能重复,否则会报错
Cannot set property 'dataIndex' of undefined
,直接渲染失败;symbolSize 表示节点的大小,不设置会有默认值,根据连接数设置不同大小也是常见的做法。 - links 中的每个对象对应一条边,对应 data 的 name 属性;如果找不到
name == source || name == target
的节点,那这条边就不会渲染。
分类
需求中提到有6类对象需要分别表示,也就是节点对应不同的颜色。相应的配置项:
// categories: ["数据机构", "数据", "合约", "模型", "计算机构", "应用"],
categories: graph_data.categories.map((c) => ({ name: c })),
data: [
{ name: "数据机构1", category: 0, symbolSize: 3.0 },
],
- categories 虽然就是类别的数组,但数组的每项是一个对象,对象只有一个叫 name 的属性。
- 如果不熟悉map语句,简单来说这个值就是
[ {name: "aaa"}, {name: "bbb"}, ... ]
- 这里的名称需要和 legend (图例)里面的那个类别数组对应,否则会缺少对应的图例
- 如果不熟悉map语句,简单来说这个值就是
- 每个节点的数据里增加类别的索引,对应类别数组的下标。注意这里必须是一个 number,不能用字符串,否则整张图无法渲染(亲自踩过的抗)
图的布局
有了节点和边就可以构造出一张图了。echarts 提供了三种布局方式:
layout: "none" | "circular" | "force"
-
none:布局完全由每个节点中指定的 (x, y) 坐标决定;显然对于数据很多并且不确定的图来说并不现实
-
circular:环形布局;很有特点的布局方式,见下图
-
force:力引导布局;这种其实就是最常见的图的样子,可以根据参数自动渲染。文中使用的就是这种方式。
使用 force 布局之后还有一些细节设置可以选择:
force: {
initLayout: "circular",
repulsion: 1000,
layoutAnimation: false,
},
- initLayout:初始布局,之后会根据设置的力引导属性继续变化直到稳定。说实话指不指定好像样子区别也不大
- repulsion:斥力大小,简而言之数值越大节点之间距离越远,反之节点距离越近
- layoutAnimation:渲染动画,就是从初始位置直到稳定的动画过程。官方文档中的说法是”节点数据较多(>100)的时候不建议关闭,布局过程会造成浏览器假死。“但那个动画真的十分魔性,建议去文章开头的链接里亲自体验一下。比起看这个视觉污染动画我宁愿他假死。
强调和样式
图有一些可以自定义的样式配置,基本节点、边、标签等等的颜色形状位置都可以自定义。这里介绍几个我用到的:
focusNodeAdjacency: true,
legendHoverLink: true,
lineStyle: {
color: "source",
opacity: 0.2,
curveness: 0.3,
},
- focusNodeAdjacency:聚焦邻接节点,就是下图所示这种喜闻乐见的样式,很不戳。文档中说这个选项的默认值是true,但我用v4.9.0版本的库手动添加这一句之后才有效果,可能是v5中改了。
- legendHoverLink:文档的说法是鼠标悬停在图例上节点高亮,但实际上至少v5.0.2还并不是这个效果;可能是库的bug,可以期待一下之后会不会改进。
- lineStyle:顾名思义,线的样式:
- curveness :线的曲率,不设置将为直线
- opacity/color:透明度/颜色,可以用 source/target 指定为源/目标节点的颜色
其他配置项设置的是静态状态下图的样式,对于高亮状态的元素(例如鼠标悬停在节点/边上),还可以单独设置强调样式 emphasis。同样,基本所有元素的各种样式都能设置,以下是几个例子:
emphasis: {
itemStyle: {
shadowColor: "rgba(0, 0, 0, 0.4)",
shadowBlur: 15,
},
lineStyle: {
width: 3,
},
label: {
textBorderColor: "rgba(255, 255, 255, 0.8)",
textBorderWidth: 2,
},
},
3 突出显示指定节点
有一项需求是:突出显示某个类别的节点。(按理说鼠标悬停图例应该是这个效果,但他并不能用)
不过echarts提供了API,可以对图执行动作(action)。调用方式如下:
graph_chart.dispatchAction({
type: action,
seriesIndex: 0,
name: names,
});
- type:指定动作,这里要用到的是 "highlight"/"downplay",高亮/取消高亮
- seriesIndex/seriesName:用下标/名字指定操作的系列,数组指定多个
- dataIndex/name:用data[]中的下标/名字指定要操作的数据,数组指定多个
据此可以写出函数:
nodesAction(action, category) {
if (category !== "") {
// 取到指定图option中的data[]
let nodes = this.graph_chart.getOption().series[0].data;
// 根据category下标,获取到对应类别所有name的数组
let names = nodes
.filter((node) => node.category == category)
.map((node) => node.name);
// 对指定的name[]执行指定操作
this.graph_chart.dispatchAction({
type: action,
seriesIndex: 0,
name: names,
});
}
}
项目使用的Vue框架,下拉选择框绑定了graphFocus
属性,对这个数据添加一个侦听器:
watch: {
graphFocus(n, o) {
// n:新的值;o: 旧的值
this.nodesAction("highlight", n);
this.nodesAction("downplay", o);
},
},
就能实现切换选项的同时高亮对应的节点:
如果你使用的echarts版本在5.0.0以上,还可以通过以下配置实现类似于focusNodeAdjacency的效果:
emphasis: {
focus: "adjacency",
}
4 替换数据
最后一个需求是显示不同的子图。根据之前说的“如果两端节点不同时存在则边不会渲染”,我们只需要将data[]
替换成子图的节点,就可以实现对应的子图。
按理说top节点应该有后端直接返回,所以如何获取节点的过程这里省略了。有了top节点之后,使用setOption
API更新新的配置项:
switchSubgraph(value) {
let data = graph_data.nodes;
switch (value) {
case 1:
data = this.top1Sub;
break;
case 3:
data = this.top3Sub;
break;
case 5:
data = this.top5Sub;
break;
}
this.graph_chart.setOption({
series: { data: data, zoom: 1 }, // zoom=1 重置缩放
});
},
setOption
的默认更新方式是合并更新,意思是只更改传入的新option与原来不同的地方,其他部分保持不变;所以我们只需要将更换的data传进去,不需要复制整个原先的option。
如果不希望使用合并更新,可以手动传入第二个参数notMerge=true
,就会将整个option替换为传入的新选项
如果打开了动画,还可以自动根据更新前后的差异展示适当的动画。
- 但是动画这东西是个坑。用v4.9.0的时候,替换data后边的显示是乱的,需要手动缩放一下;换成v5.0.0之后,替换完data显示没问题,一缩放又坏了。设置了
animation: false
之后啥毛病都没了。 - 可能这功能还不够完善,对于常用的柱状图折线图表现比较好,关系图这种不那么常用又复杂的东西就有各种神秘bug。期望日后的版本中可以改善。
替换节点后,得到了期望的Top子图,并且之前的高亮功能也可以正常使用。
结语
以上就是我这次使用ECharts关系图的过程中遇到的问题以及最终的解决方式,希望也可以帮到你。如果有相关的问题、或是文章中存在疏漏,欢迎在评论区留言讨论!
PS:第一次用CodePen,真的够呛,研究这玩意的时间快和写文差不多长了。眼看可以就如何使用CodePen再写一篇(跑