一. 前言
一直使用echarts做产业图谱, 奈何它依然不够完美: 1. 节点的自定义样式不够灵活;2. 节点的激活状态不够多样化;3.节点的多重组合类型缺失; 导致产业链图谱的多重节点和节点的多重组合效果很难实现;故而转战阿里的AntV/G6技术栈
二.先看最终实现效果

三.Antv/G6
我用的是最新的 v5.0.47版本, 这真是个小坑, 搜边全网没有几个v5版本的案例, 普遍都是v4版本, v4升级到v5变更又特别多,只能先把文档从头到尾熟读几遍, 这里只说重点:节点Node, 边Edge, 组合Combo, 状态State, 交互 Behavior, 这几个核心模块文档至少看三遍
四.实现思路
- 如何把一二三级的所有节点平均分布在画布上?
①把画布分成三等份; ②每一级产业链平均分布在各自等份中并且居中; 代码如下:
js
// 设置node坐标点
function setNodeXY(arr, elementId) {
arr.forEach((item, index) => {
const nodeHeight = 50 - 10 * (item.level-1);
const totalHeight = arr.length * nodeHeight;
let x = 200 + ((elementId.offsetWidth / 3) * 2)*(item.level-1);
let y = (elementId.offsetHeight - totalHeight) / 2;
item.xy = [x, y + index * nodeHeight];
});
}
- 如何把每一级的节点进行分组,方便同组区分?
①把树形结构数据按照层级拆成三级, level01List, level02List,level03List (G6的nodes: [...level01List,......level02List,......level03List])
②把每一级的数据根据parentId相同的进行分组, (G6的combos: [])
js
// 设置node的组合combos
function setCombos(arr) {
let result = Object.values(
arr.reduce((acc, item) => {
const key = item.parentId;
if (!acc[key]) {
acc[key] = {
id: `level${item.level}-${key}`,
data: { label: item.name },
};
}
return acc;
}, {})
);
return result;
}
- 现在有了节点(node), 有了组合(combo), 怎么把两者连接起来, 需要连线边(edge) ?
①首先要知道combo与combo之间是没有edge的, 只有node之间有
②如果所有节点都用边相连,会导致一个父级有四条边连向子级, 我想要相同的子级只连一条边到父级
③把边的数据处理成看着像组合与组合相连的样子,如上效果图中,本来有18条连线边, 处理成只有4条连线边 (G6的edges: [])
js
// 设置node的连线边edges
function setEdges(startArr, endArr) {
startArr.forEach((startItem, startIndex) => {
let tempArr = [];
endArr.forEach((endItem, endIndex) => {
if (startItem.id === endItem.parentId) {
tempArr.push(endObj);
}
});
if (tempArr && tempArr.length) {
let tempEdge = {
id: `${startItem.id}-${tempArr[Math.floor(tempArr.length / 2)].id}`,
source: startObj.id,
target:
tempArr &&
tempArr.length &&
tempArr[Math.floor(tempArr.length / 2)].id
};
edgesList.value.push(tempEdge);
}
});
}
五.交互效果: 鼠标移入节点时, 高亮当前节点, 高亮边, 高亮子级组合
- G6的交互效果需要配置behaviors, 只有移入node节点时才需要交互, 所以enable参数处理了edge和combo的移入,代码如下:
js
behaviors: [
{
type: "hover-activate",
key: "hover-activate-1",
enable: (e) => {
if (e.targetType === "node") {
return true;
}
return false;
},
}
]
- node节点的默认配置和激活高亮配置, 代码如下:
js
node: {
type: "html",
style: {
innerHTML: (d) => {
return `
<div class="g6-node-html">
<span>${d.data.name}</span>
</div>
`;
},
zIndex: 2,
},
state: {
active: {
innerHTML: (d) => {
return `
<div class="g6-node-html-active">
<span>${d.data.name}</span>
</div>
`;
},
}
}
}
- edge边的默认配置和激活高亮配置, 代码如下:
js
edge: {
type: "polyline",
style: {
stroke: "rgba(46, 53, 66, 1)",
controlPoints: (d) => [
[d.data.start.xy[0] , d.data.start.xy[1] + 20], // 起
[
d.data.start.xy[0] + d.data.middle * 20 + 100,
d.data.start.xy[1] + 20,
], // 拐
[
d.data.start.xy[0] + d.data.middle * 20 + 100,
d.data.end.xy[1],
], // 拐
[d.data.end.xy[0], d.data.end.xy[1]], // 终
],
radius: 10,
lineJoin: "round",
lineWidth: 10,
zIndex: 1,
},
state: {
active: {
stroke: (d) => {
return "linear-gradient(0deg, #6fd4df00 0%, #7DBDF2 50%,#7DBDF2 100%)";
},
lineWidth: 8,
},
}
}
- combo组合的默认配置和激活高亮配置,代码如下:
js
combo: {
type: "rect",
style: {
padding: 10,
radius: 6,
fill: "linear-gradient(90deg,rgba(75, 75, 103, 0.3) 0%, rgba(75, 75, 103, 0.08) 100%)",
fillOpacity: 1,
stroke: "rgba(255, 255, 255, 0.1)",
zIndex: 3,
},
state: {
active: {
stroke: "rgba(184, 243, 255, 1)",
},
}
}
六. 最最最麻烦的交互: 鼠标移入节点时, 连线的边有个小球在运动
怎么说呢, 文档没有详细api, 没有具体案例, 网上也没有教程, 怎么办? 又把文档啃了几遍,发现有个Shape图形,shape有几个模棱两可的说明, 尝试了几十次, 具体思路已经完全靠猜靠经验靠尝试, 实现了, 代码如下:
js
// 自定义边 路径 运动小球
let runMarkerEdge = null;
class RunMarkerEdge extends Polyline {
getMarkerStyle(attributes) {
if (attributes.stroke.includes("linear-gradient")) {
return {
...subStyleProps(attributes, "marker"),
size: 6,
fill: "#fff",
visibility: "visible",
offsetPath: this.shapeMap.key,
};
} else {
return {
...subStyleProps(attributes, "marker"),
size: 6,
fill: "#7DBDF2",
visibility: "hidden",
offsetPath: this.shapeMap.key,
};
}
}
getKeyPath(attributes) {
const [sourcePoint, targetPoint] = this.getEndpoints(attributes);
return [
["M", sourcePoint[0] + 15, sourcePoint[1]],
["L", targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], sourcePoint[1]],
["L", targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], targetPoint[1]],
["L", targetPoint[0] - 15, targetPoint[1]],
];
}
onCreate() {
runMarkerEdge = this.upsert(
"marker",
Circle,
this.getMarkerStyle(this.attributes),
this
);
runMarkerEdge.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {
duration: 2000,
iterations: Infinity,
});
}
onUpdate() {
runMarkerEdge = this.upsert(
"marker",
Circle,
this.getMarkerStyle(this.attributes),
this
);
runMarkerEdge.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {
duration: 2000,
iterations: Infinity,
});
}
}
register(ExtensionCategory.EDGE, "run-mark-edge", RunMarkerEdge);
七. 束语
作为一个有自尊心的开发。 当产品说,这个交互别人能实现,你能实现一下吗? 本能是拒绝的, 但不能说实现不了, 头发掉光也要实现它。