图的元素特指图上的节点 Node 和边 Edge 。在上一章节中,我们已经将 Tutorial 案例的图绘制了出来,但是各个元素及其 label 在视觉上很简陋。本文通过将上一章节中简陋的元素美化成如下效果,介绍元素的属性、配置方法。
图的元素特指图上的节点 Node 和边 Edge 。G6 内置了一系列 内置的节点 和 内置的边,供用户自由选择。G6 不同的内置节点或不同的内置边主要区别在于元素的 图形 Shape,例如,节点可以是圆形、矩形、图片等。
不论是节点还是边,它们的属性分为两种:
例如,G6 设定 hover 或 click 节点,造成节点状态的改变,只能自动改变节点的样式属性(如 fill、stroke 等),其他属性(如 type 等)不能被改变。如果需要改变其他属性,要通过 graph.updateItem 手动配置。样式属性是一个名为 style 的对象, style 字段与其他属性并行。
以节点元素为例,其属性的数据结构如下:
{
id: 'node0', // 元素的 id
type: 'circle', // 元素的图形
size: 40, // 元素的大小
label: 'node0' // 标签文字
labelCfg: { // 标签配置属性
positions: 'center',// 标签的属性,标签在元素中的位置
style: { // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
fontSize: 12 // 标签的样式属性,文字字体大小
// ... // 标签的其他样式属性
}
}
// ..., // 其他属性
style: { // 包裹样式属性的字段 style 与其他属性在数据结构上并行
fill: '#000', // 样式属性,元素的填充色
stroke: '#888', // 样式属性,元素的描边色
// ... // 其他样式属性
}
}
边元素的属性数据结构与节点元素相似,只是其他属性中多了 source 和 target 字段,代表起始和终止节点的 id。
细化在图 1 中 Tutorial 案例 的视觉需求,我们需要完成:
在 G6 中,根据不同的场景需求,有 7 种配置元素属性的方式。这里,我们简单介绍其中的两种:
适用场景:所有节点统一的属性配置,所有边统一的属性配置。
使用方式:使用图的两个配置项:
⚠️ 注意: 由于是统一的配置,不能根据数据中的属性(如 class、weight)等值的不同进行个性化设置,因此只能满足 R1、R2、R3、R4 需求。达到如下效果:
通过如下方式在实例化图时 defaultNode 和 defaultEdge ,可以完成上图效果:
const graph = new G6.Graph({
// ... // 图的其他配置
// 节点在默认状态下的样式配置(style)和其他配置
defaultNode: {
size: 30, // 节点大小
// ... // 节点的其他配置
// 节点样式配置
style: {
fill: 'steelblue', // 节点填充色
stroke: '#666', // 节点描边色
lineWidth: 1, // 节点描边粗细
},
// 节点上的标签文本配置
labelCfg: {
// 节点上的标签文本样式配置
style: {
fill: '#fff', // 节点标签文字颜色
},
},
},
// 边在默认状态下的样式配置(style)和其他配置
defaultEdge: {
// ... // 边的其他配置
// 边样式配置
style: {
opacity: 0.6, // 边透明度
stroke: 'grey', // 边描边颜色
},
// 边上的标签文本配置
labelCfg: {
autoRotate: true, // 边上的标签文本根据边的方向旋转
},
},
});
适用场景:不同节点/边可以有不同的个性化配置。
因此,这种配置方式可以满足 R5、R6 需求。
使用方式:可以直接将配置写入数据文件;也可以在读入数据后,通过遍历的方式写入配置。这里展示读入数据后,通过遍历的方式写入配置。下面代码展示了如何遍历数据进行属性的配置:
const nodes = remoteData.nodes;
nodes.forEach(node => {
if (!node.style) {
node.style = {};
}
switch (
node.class // 根据节点数据中的 class 属性配置图形
) {
case 'c0': {
node.type = 'circle'; // class = 'c0' 时节点图形为 circle
break;
}
case 'c1': {
node.type = 'rect'; // class = 'c1' 时节点图形为 rect
node.size = [35, 20]; // class = 'c1' 时节点大小
break;
}
case 'c2': {
node.type = 'ellipse'; // class = 'c2' 时节点图形为 ellipse
node.size = [35, 20]; // class = 'c2' 时节点大小
break;
}
}
});
graph.data(remoteData);
可以看到,图中有一些节点被渲染成了矩形,还有一些被渲染成了椭圆形。除了设置 type 属性之外,我们还覆盖了上文全局配置的节点的 size 属性,在矩形和椭圆的情况下,size 是一个数组;而在默认圆形的情况下,G6 将仍然读取全局配置的 size 属性为数字 30。也就是说,动态配置属性让我们既可以根据数据的不同配置不同的属性值,也可以有能力覆盖全局静态的属性值。
进一步地,我们尝试根据数据的比重不同,配置不一样边的粗细:
// const nodes = ...
// 遍历边数据
const edges = remoteData.edges;
edges.forEach(edge => {
if (!edge.style) {
edge.style = {};
}
edge.style.lineWidth = edge.weight; // 边的粗细映射边数据中的 weight 属性数值
});
graph.data(remoteData);
结果如下:
如图所示,边的粗细已经按照数据的比重成功渲染了出来,但是边原有的样式(透明度、颜色)却丢失了。这是因为我们提到过动态配置属性会覆盖全局配置属性,这里配置了 style.lineWidth,导致覆盖了全局的 style 对象。解决办法是将被覆盖的边的样式都移到动态配置里面来:
const graph = new G6.Graph({
// ...
defaultEdge: {
// 去掉全局配置的 style
labelCfg: {
// 边上的标签文本配置
autoRotate: true, // 边上的标签文本根据边的方向旋转
},
},
});
// 遍历点数据
// const nodes = ...
// nodes.forEach ...
// 遍历边数据
const edges = remoteData.edges;
edges.forEach(edge => {
if (!edge.style) {
edge.style = {};
}
edge.style.lineWidth = edge.weight; // 边的粗细映射边数据中的 weight 属性数值
// 移到此处
edge.style.opacity = 0.6;
edge.style.stroke = 'grey';
});
graph.data(remoteData);
graph.render();
至此,完整代码如下:
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Tutorial Demotitle>
head>
<body>
<div id="mountNode">div>
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-3.1.0/build/g6.js">script>
<script>
const graph = new G6.Graph({
container: 'mountNode',
width: 800,
height: 600,
fitView: true,
fitViewPadding: [20, 40, 50, 20],
defaultNode: {
size: 30,
labelCfg: {
style: {
fill: '#fff',
},
},
},
defaultEdge: {
labelCfg: {
autoRotate: true,
},
},
});
const main = async () => {
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/6cae02ab-4c29-44b2-b1fd-4005688febcb.json',
);
const remoteData = await response.json();
const nodes = remoteData.nodes;
const edges = remoteData.edges;
nodes.forEach(node => {
if (!node.style) {
node.style = {};
}
node.style.lineWidth = 1;
node.style.stroke = '#666';
node.style.fill = 'steelblue';
switch (node.class) {
case 'c0': {
node.type = 'circle';
break;
}
case 'c1': {
node.type = 'rect';
node.size = [35, 20];
break;
}
case 'c2': {
node.type = 'ellipse';
node.size = [35, 20];
break;
}
}
});
edges.forEach(edge => {
if (!edge.style) {
edge.style = {};
}
edge.style.lineWidth = edge.weight;
edge.style.opacity = 0.6;
edge.style.stroke = 'grey';
});
graph.data(remoteData);
graph.render();
};
main();
script>
body>
html>