前段时间有个好玩的需求:把
人、货、场
的整个流程通过一个个节点有序排列出来。一开始觉得这很简单呀,用G6一条线搞定。接着,需要把不同领域拆分开来,那也简单,按照规则拆出几份出来就好了。但在做的过程中,发现不太对,每个领域在同一个时间节点的数据没有上下对齐,每个节点下罗列的几行内容,横向看起来也有错落,看起来不直观。接着,对整个数据结构进行一个算法调整,完美实现上下对齐,图文节点清晰。过程中踩了些许的坑,很上头。
以下是我们最终的产出效果:(部分信息做模糊处理)
本次先不聊算法调整,接下来一起聊聊如何正确姿势使用G6自定义节点让图文排列整齐,希望能够帮助大家绕开一些坑点。
首先,对于G6的一些基础概念,前面有同学已经总结过了,可以回顾一下~ 本文涉及:
- 自定义节点方法
- 节点间动画
目前G6官网教程里介绍了三种自定义节点: 原生
、类JSX语法
、React
。前文已讲过使用原生方式,本文则会介绍后面两种方式。在介绍这两种方式前,需要先说明两个核心概念,Shape
和keyShape
。
Shape 指 G6 中的图形、形状,它可以是圆形、矩形、路径等。它一般与 G6 中的节点、边、Combo 相关。G6 中的每一种节点/边/ Combo 由一个或多个 Shape 组成
每个节点/边/Combo都必须有一个唯一的关键图形 keyShape。keyShape
是在节点/边/ Combo 的 draw()
方法或 drawShape()
方法中返回的图形对象。
确定节点 / Combo 的包围盒(Bounding Box) —— bbox(x, y, width, height) ,从而计算相关边的连入点(与相关边的交点),如下示例:
从例子来看, 自定义节点的边连接点
受唯一的keyShape影响。
对图形Shape和关键图形keyShape有个印象后,接下来看下第一种自定义节点方式。
在 G6 3.7.0 及以后的版本中, 使用类似 JSX 的语法来定义节点,只需要在使用 G6.registerNode 自定义节点时,将第二个参数设置为字符串或一个Function,其返回值为
string
类型
看一个官网的例子:
G6.registerNode(
'custom-node',
(cfg) => `
<rect
name="test"
style={{
width: 100, height: 20, fill: '#1890ff', stroke: '#1890ff', radius: [6, 6, 0, 0]}}
keyshape="true"
>
<text
style={{ // 样式,Object结构
marginTop: 2,
marginLeft: 50,
textAlign: 'center',
fontWeight: 'bold',
fill: '#fff' }}
name="title"
>${cfg.label || cfg.id}</text>
<polygon
style={{
points:[[ 30, 30 ], [ 40, 20 ], [ 30, 50 ], [ 60, 100 ]],
fill: 'red'
}} />
<image
style={{ img: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 48, height: 48, marginTop: 100 }} />
</rect>`,
);
其基础语法和大家熟悉的 HTML 标记语言基本相同:
这里需要注意几点:
- keyshape是全小写
- 这种写法,默认所有布局都会按照正常的文档流,自上而下布局
- 为了相对定位,形状样式这里新增了
marginTop
和marginLeft
来定义左边和上边的间隔- 如果涉及到需要横向排列的元素,在上一个元素使用
next: inline
来实现下一个元素跟随在上个元素后方
虽然类JSX语法相比原始的api方法简便多了,但在调整纵向横向节点的形状样式时,还是有一些复杂,总觉得形状怎么不听使唤呢,抓狂。
接着,尝试了另一种自定义节点的方式:React
首先在安装完 G6 后,需要额外安装 @antv/g6-react-node
npm install @antv/g6-react-node
// yarn add @antv/g6-react-node
根据类jsx写法的例子,修改后:
import React from 'react';
import G6 from '@antv/g6';
import { Rect, Text, Image, Polygon, createNodeFromReact } from '@antv/g6-react-node';
const CustomNode = ({ cfg }) => {
const { label, id } = cfg
return (
<Rect keyShape style={{
width: 100, height: 20, fill: '#1890ff', stroke: '#1890ff', radius: [6, 6, 0, 0]
}} keyShape name="test">
<Text style={{
margin: [2, 0, 0, 50],
textAlign: 'center',
fontWeight: 'bold',
fill: '#fff'
}}
name="title">{label || id}</Text>
<Polygon style={{points:[[ 30, 30 ], [ 40, 20 ], [ 30, 50 ], [ 60, 100 ]], fill: 'red'}} />
<Image style={{ img: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', width: 48, height: 48, margin: [100, 0, 0, 0] }} />
</Rect>
)
}
G6.registerNode('custom-node', createNodeFromReact(CustomNode))
从代码来看,不难发现,定义了一个函数组件,返回了自定义节点。这看起来,对于熟悉react开发的同学来说,更容易上手了。
相比起来,还有两个优点:
但在做的过程中,依旧是踩坑了:
- 虽然文档说明支持marginTop、marginLeft, 但是实际使用
margin
才能生效keyshape
关键图形,必须是驼峰形式keyShape
- 如果发现某个形状的事件响应有问题,大概率是子形状的影响,可以在子形状设置
capture=false
这三种方式都做了尝试,虽然文档不全,但相对推荐最后一种react自定义节点。
除了节点设置形状样式外,还涉及到动画制作。
当我们使用上面的方式完成一幅图时,其基本功能已经实现,动画更多的作用是锦上添花。接下来主要说一下在本次需求中使用的边动画
。
先来看一下动画的核心——动画函数
原理是通过该函数的返回值,定义每一帧的样式。
shape.animate(
(ratio) => {
// 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
return {
// 返回需要变化的参数集
};
},
{
repeat: true, // 动画重复
duration: 3000, // 一次动画的时间长度
},
);
大致链接这个函数的原理,剩下的就是发动小脑筋去想一下如何通过这个函数去实现我们想要的效果,以下面这个效果为例:
可以分为简单的三步:
- 自定义一条边
- 额外创建一个小的圆形节点
- 在animate方法中,根据入参radio,不断修改小圆点的x,y坐标
G6.registerEdge(
'circle-running',
{
afterDraw(cfg, group) {
// 获得当前边的第一个图形,这里是边本身的 path
const shape = group.get('children')[0];
// 边 path 的起点位置
const startPoint = shape.getPoint(0);
// 添加 circle 图形,并使其初始位置在边的起点上
const circle = group.addShape('circle', {
attrs: {
x: startPoint.x,
y: startPoint.y,
r: 3, // 小圆的半径
},
});
// 对小圆点添加动画
circle.animate(
(ratio) => {
// 根据比例值,获得在边 path 上对应比例的位置。
const tmpPoint = shape.getPoint(ratio);
// 返回需要变化的参数集,这里返回了位置 x 和 y
return {
x: tmpPoint.x,
y: tmpPoint.y,
};
},
{
repeat: true,
duration: 3000,
},
);
},
},
'cubic',
); // 该自定义边继承内置三阶贝塞尔曲线 cubic
此例子中,动画是写在afterDraw
里,也就是在节点绘制完成后就开始动画了。那一整张图里,多条线,可能会同时运动,眼睛可能不会注意图里的重要信息,只顾着看动效了,这就没达到效果。
在某些时候,我们应该能够根据不同的场景,通过鼠标来控制动画的启动和停止。
比如需求需要:在鼠标悬浮在节点边的时候才触发当前节点到下一节点的动画效果,在鼠标离开节点边时停止动画。
这里我们用到了另一组方法:
setState
: 定义状态setItemState
: 设置状态
// lineDash array 虚线运动效果,通过修改该数组可以自定义虚线的效果
const lineDash = [4, 2, 1, 2];
G6.registerEdge(
'to-others',
{
// name:状态名称,可以给该元素定义多个状态名,根据传入的状态名不同做出不同的回应
// value:状态是否可用,为 true 时可用,否则不可用
// item:元素实例
setState(name, value, item) {
// 获取关键图形
const shape = item.get('keyShape')
// 只有当设置的状态名为'running-edge'时执行
if (name === 'running-edge') {
if (value) {
// 状态值为true,表示开始执行动画
let index = 0;
shape.animate(
() => {
index++;
if (index > 9) index = 0;
const res = {
lineDash,
lineDashOffset: -index,
};
return res;
},
{
repeat: true,
duration: 3000,
},
);
} else {
// 状态值为false,表示停止动画
// 停止动画
shape.stopAnimate();
// 动画停止后并不会自动将元素状态复原,需要手动将元素的属性值修改回初始值
shape.attr('lineDash', null);
}
}
},
},
'polyline'
)
graph.on('edge:mouseenter', (e) => {
const edges = e.item
// 给当前触发mouseenter事件的边元素,设置状态:running-edge为true
graph.setItemState(edges, 'running-edge', true)
})
graph.on('edge:mouseleave', (e) => {
const edges = e.item
// 给当前触发mouseleave事件的边元素,设置状态:running-edge为false
graph.setItemState(edges, 'running-edge', false)
})
通过这两步,就可以实现动画自由启动和停止了。当然,也可以尝试使用节点动画
,让图在某些操作时看起来更生动,其实现原理类似。
以上,大致是react自定义节点和动画的基本内容,不完全踩坑。
以前在可视化编辑图和图分析方向的尝试比较少,这次或多或少遇到了一些难点和坑点。虽然做图很累很上头,但看到成果能够大幅度提升后续的协作效率,还是挺值得的。社区中G6/X6等方案也比较成熟,后续会有类似的需求产出,会做更多的尝试,并在此基础上做迭代沉淀。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8