• 2

  • 491

揭秘G6图表自定义的真相

3星期前

序言

​ 前一段时间,我们风控部门提出了一个“关联图谱”的需求,实现一个平台上人员关联信息的展示,其目的是将用户间的多层级多维度的关联关系可视化、清晰化,从而让审核人员能更快更准确的发现各种违法违规行为,降低平台的遭受风险。

​ 整个需求除了要展示大量的关联数据之外,还要根据确切关联因子或关联uid让对应节点和边的样式发生的变化,鼠标悬浮展示数据等功能,并且有一些图形上的特殊要求。

以下为该需求的基础效果

​ 目前,在前端实现关系数据的可视化上,主要有echarts和G6两个主流的技术,相比于echarts,G6在图的编辑和分析上更胜一筹,它支持各种复杂的交互,且更加侧重于展示更多节点和边的性能,因此我选择了G6来实现“关联图谱”,但在探索过程中发现,单纯的使用内置的图形和交互依旧满足不了需求,但G6还提供了强大的自定义能力,通过这个功能我完美的实现了“关联图谱”的需求,而本篇文章就带大家揭秘一下如何使用G6的自定义功能。

需要了解的基础概念

要了解G6自定义功能,自然要从一些基础概念入手。

上图是G6 3.0的架构图,其底层其实是结合Canvas和SVG来设计的,第二层体现了G6 3.0提供的一些能力,包括图形扩展、状态管理等,第三层则是图的基本构成要素。针对第三层,我画了一幅Graph的层级关系图,根据这个包含关系,我们来逐级了解一些概念。

图表Graph

Graph 是 G6 图表的载体,所有的 G6 节点实例操作以及事件,行为监听都在 Graph 实例上进行,其下包括一个或多个图形分组group。

Graph 的生命周期主要有五个:初始化—>加载数据—>渲染—>更新—>销毁。五个生命周期对应五个方法:G6.Graph、data(data)、render()、changeData(data)、clear()

图形分组Group

图形分组 group 类似于 SVG中的标签:元素 g 是用来组合图形对象的容器。在 group 上添加变换(例如剪裁、旋转、放缩、平移等)会应用到其所有的子元素上。

在 G6 中,Graph 的一个实例中的所有节点属于同一个变量名为 nodeGroup 的 group,所有的边属于同一个变量名为 edgeGroup 的 group。节点 group 在视觉上的层级(zIndex)高于边 group,即所有节点会绘制在所有边的上层。

如下图所示,三个节点属于 nodeGroup ,两条边属于 edgeGroupnodeGroup 层级高于 edgeGroup ,三个节点绘制在三条边的上层。

元素item

元素ItemNodeEdgeGuide等图项的抽象类。

其中最常用的图项有两个,节点(node)和边(edge)。

节点的基本结构node:

边的基本结构edge:

形状shape

Shape 指 G6 中的图形、形状,它可以是圆形、矩形、路径等。G6 中的每一种节点或边由一个或多个 Shape 组成。内置节点的有 'circle', 'rect','ellipse',...;内置边的有 'line','polyline','cubic',...;你可以通过将几个内置shape结合,组成一个能够满足自己需要的图形;

下图就是一个由circle和text两个shape组合而成的图形。

要自定义节点/边时,需要了解shape的生命周期方法,对其方法进行有选择性的复写。

  1. draw(cfg, group): 绘制,提供了绘制的配置项(数据定义时透传过来)和图形容器(图形分组group) ;
  2. update(cfg, n): 更新,更新时的配置项(更新的字段和原始字段的合并)和节点对象;
  3. afterDraw(cfg, group): 绘制后的操作
  4. ...

下图展示了几个shape中的生命周期方法

关键图形keyShape

每一种节点和边都有一个唯一的关键图形 ----keyShape。keyShape 是在节点的 draw 方法中返回的图形对象,用于**确定节点的包围盒(Bounding Box)**从而计算相关边的连入点(与相关边的交点)。

注册自定义图形

在定制我们自己的图形之前我们需要G6实例中全局注册它们。

如下图所示,下面注册了一个节点,其名字叫做circle-image-node,使用defaultNodeConf来配置定义节点的各种方法的,且基于G6内置的image进行扩展。

在Graph的配置中配置shape后即可使用自定义的图形。

基础--扩展内置的shape

在我们得出G6中内置的图形不能满足需求的结论同时,我们也会发现,内置的图形往往只需稍加改变便可满足需求,这时就需要复写内置图形的几常用方法来扩展内置的shape。

自定义节点

需求: 在之前“关联图谱”的需求中,需要在原有关系图的基础之上,通过数据中给定一个字段(level),在该字段为4时给节点label加个一个红色框突出展示

我们仍以‘circle-image-node’为例,不过这次我们选择扩展circle。

circle节点定义

在复写shape的生命周期方法中,方法名为afterDraw,在draw方法之后执行,常用于扩展现有的节点和边

节点的源数据如下图

可以看到该节点是一个半径为50,lebal为‘Circle’的节点

afterDraw方法有两个参数,通过afterDraw的第一个参数cfg,我们可以拿到源数据中的所有数据,并附加上所有的样式配置。获取到数据之后可以通过group.addShape()方法来添加shape,其第一个参数是shape的名称,第二个参数为配置的属性

这里通过添加一个无填充色且stroke为红色的rect图形,并将text包起来,来实现效果。

可以看出坐标原点其实是在circle图案的对称中心。只需稍加定位一番便可实现功能。

最终的实现效果

连点成图

接下来我们增加节点,并通过edge将其相连。更改后的数据源如下图

效果如图所示

有人问你现在是一条边连接连个节点,如果我有两个边连接呢,是不是边和边会重叠呢?

我们添加一个反方向且名为edge2的边。

可以看到如果不做额外处理,连接两个节点的边确实会重叠。

这样就引出了下一部分要说的自定义边。

自定义边

G6 提供了一共 9 种内置边:

  • line:直线,不支持控制点;
  • polyline:折线,支持多个控制点;
  • arc:圆弧线;
  • quadratic:二阶贝塞尔曲线,controlPoints 不指定时,会默认线的一半处弯曲 ;
  • cubic:三阶贝塞尔曲线,controlPoints 不指定时,会默认线的 1/3, 2/3 处弯曲 ;
  • cubic-vertical:垂直方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
  • cubic-horizontal;水平方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
  • loop:自环。

当出现边与边重叠的情况时,如果使用的是带有控制点的内置边的话,就可以通过自定义边的控制点,达到节点间多条边不重叠的效果。

这次我们对拥有一个控制点的quadratic进行扩展,在源数据有两条相同方向的边的情况下,我们定义一个名叫quadratic-controllable-edge的边。

若要修改控制点的位置,需要复写shape的getControlPoints(cfg),该方法需要返回shape的控制点实例。

整个复写getControlPoints(cfg)的过程中,需要你通过Util.getControlPoint方法生成新的控制点,并将新控制点返回。

Util.getControlPoint方法主要有四个参数:

  1. startPoint 边的起始点
  2. endPoint 边的终点
  3. position 控制点在边上的位置
  4. offset 沿着startPoint, endPoint 的垂直向量(顺时针)方向,距离线的距离

通过打印边的getControlPoints方法的cfg参数可以看到,起点和终点的坐标都可以通过cfg获取到。

这里使用了0-9的随机值来生成一个level,并根据一定的计算规则生成控制点的offset,而position默认处于边的中间。运行一下看看结果。

Update方法

还有一个屡试不爽的招数便复写shape的update方法。

需求: 假如说我要求点击哪条边,哪条边可以变红。

第一步要做的就是设置点击事件监听:

G6的所有监听事件都设置在graph实例上,常用的事件有这几种: click,dblclick,mouseenter,mouseleave...

使用语法:

graph.on('click', cb);  
复制代码

当然,通过与前缀'node','edge','group', 'guide'进行自由组合,可以更快的获取到想要的目标。

可以通过获取ev下的item来获取到所点击的边对应的实例,随后调用update方法更改边的样式。

如果说我希望有多套可以配置且彼此隔离的样式,通过改变映射表就可以实现不同的样式搭配,这样就轮到我们复写边的update方法的时候了。

假如我规定,一个样式映射表,有多种模式,模式中为需要配置的样式组合,这样就仅仅通过改变边数据的其中一个属性便能多套样式间的切换。

这里分别设置两种模式,type1为蓝色宽度为2px,type2为红色宽度为4px

由于我们将样式抽象出来了,所以需要改变一下数据源

接下来就是复写update的时候了,我们索要做的就是将数据中抽象的属性对应为边实际的样式,并同步设置给边。

先通过cfg获取到源数据中的type,并确定具体的style,然后再通过attr方法将对应的样式设置到实例上,结果如下图。

初始状态ok之后,只需在点击时设置边的type属性,便可实现样式的抽象化配置,即使后续有样式改动,也可以通过配置映射表完美应对。

进阶--完全自定义图形

在经过了这么多的demo之后,想必你一定对自定义shape的几个常用的生命周期方法大概有了些许了解,可如果连扩展shape也无法满足要求呢?那就需要完全自定义一个图形出来了。

接下来我们从无到有自定义一个菱形:

注:如果不从任何现有的节点扩展新节点时,draw 方法是必须的

这里使用path来画出一个菱形,而在定义路径时使用的是svg的path路径的绘制方法。

常见的命令有5种分别为

  1. M 移动到(moveTo) x,y 开始点坐标
  2. Z 闭合路径(closepath) 将路径的开始和结束点用直线连接
  3. L 直线(lineTo) x,y 当前节点到指定(x,y)节点,直线连接
  4. H 水平直线 x 保持当前点的y坐标不变,x轴移动到x,形成水平线
  5. V 垂直直线 y 保持当前点的x坐标不变,y轴移动到y,形成垂直线

要想获取path,首先需要复写shape的getPath方法。

在获取到path之后,我们便可以在draw中定义shape了。

将原有的节点替换掉,可以看到一个崭新的菱形shape出现在了画布上。

完全自定义时,复写的draw方法一定要返回一个shape作为keyshape

相比之前的内置原型节点,这里没有将数据源中的label展示出来,这是因为内置的节点都有一个text,用来单独展示节点的label数据。所以我们只需在draw方法里添加一个text图形就行实现了。

自定义图形代码链接note.youdao.com/noteshare?i…

高阶--自定义交互

除了对图中的图形进行自定义之外,还可以对图的交互行为进行定制。其中有两个重要的概念:ModeBehavior

Mode与Behavior是G6提供的图事件定义与管理机制。其中Mode指当前图的事件模式,一个mode可能包含多个Behavior。通过在图上切换Mode,可以切换当前事件对应的行为。Behavior指G6中的复合交互,一般behavior包含一个或多个事件的监听与处理以及一系列对图中元素的操作。

设想这样一个例子,实现一个简单的图编辑器:可以在画布上添加节点并能拖动节点;可以在节点间添加边。

为了区分不同的场景,我们将整个功能分为三个模式,即添加节点addNode,添加边的addEdge,以及默认模式default

只需在全局配置中加入modes:

图中drag-node和click-select皆为内置行为,分别实现可拖拽节点和可选中功能,因此只需我们定义出click-add-node和click-add-edge行为即可。

注册一个行为的API:G6.registerBehavior,它有两个参数,一个为行为的名称,一个为复写的方法。

首先我们注册click-add-node的行为

其中getEvents为必实现的方法,它返回一个方法名的映射,如图中所示。由于这里只涉及点击画布时的监听事件,所以将onclick方法映射到canvas:click方法上。

这样在触发canvas:click时便会触发你所定义onclick方法。

并通过graph.addItem添加一个节点(这里id是根据增加的节点数量来标识的)。

第二部分是定义click-add-edge行为,分析一下需要监听以下几个事件

  1. node:click:若要在两点间添加一条边,首先要确定点的哪两个点,
  2. mousemove: 新增的线随着鼠标而移动
  3. edge:click:点击空白处,取消边

点击时需要生成一个边,并设置好边的source或target。

这里通过一个addingEdge状态代表此时为设置target还是source。并使用edge来暂存未完全设置时的边数据

在点击第一次后通过动态更新边的target属性,以达到边跟随鼠标移动的效果。

如果我们想取消生成一半的边呢?由于动态设置edge的target的结果,此时触发的是点击边的事件监听,所以,只需在此时去除刚刚添加的临时边实例,并清空临时的边数据,重置addingEdge状态。

在实现了行为之后,我们只差最后一步----切换模式。切换模式使用graph.setMode(modeName)进行切换。

这里通过一个下拉框选中不同的值来实现切换模式,通过模式来隔离多个行为的互相影响。

最终的效果如图:

自定义行为demo实现代码链接如下: note.youdao.com/noteshare?i…

补充说明

  1. 在力导向布局下,如果出现游离的节点(没有相连的边)且未设置在画布上的位置(节点的X,Y),则会出现节点的位置跨度过大的问题(画布本身很大时该情况尤甚),毕竟它“居无定所”。
  2. G6不支持自定义边tooltip的react写法,不过既然G6可以监听到鼠标事件,我们完全可以通过mouseenter和mouseleave事件来控制一个react组件的位置,来达成自定义tooltip的效果。
  3. 文中所述的使用level调节控制点的方法,最好对节点间边的重复做一下计数,并依据计数来设置level值。

总结

本篇文章主要讲解了G6自定义图形和自定义行为两大部分。

在自定义前,需要先通过registerNode、registerEdge、registerBehavior注册你的自定义内容,并在对应的Graph配置中使用。

自定义图形时,你需要对shape的几个生命周期方法进行复写,最常见的就是复写afterDraw方法,来实现对内置图形的扩展。

自定义交互时,你需要实现自己的事件监听方法,并通过getEvents方法的返回值,将监听方法与时机触发的事件进行映射。如果希望有多个行为相互隔离影响,就需要结合mode来使用,并通过graph.setMode切换mode。

尾声

​ 叨叨了这么半天,想必你也已经对G6自定义的功能有了些许的了解。其实除对图形和行为、模式的自定义功能外,还有许多值得去DIY的地方,像导引,组群,甚至动画都可以进行自定义。如果你需要实现一个关系数据可视化图谱,且拥有非常规的图形展示,或需要对图谱进行CRUD等复杂的操作时,那么本文做说的G6自定义功能一定能够助你快速完成开发。

本文参考:

官方文档:https://g6.antv.vision/zh/docs/manual/introduction

语雀文档:https://www.yuque.com/antv/g6

AntV 架构演进-G6 篇:https://www.yuque.com/antv/blog/bs243t

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

程序员

491

相关文章推荐

未登录头像

暂无评论