自定义节点

G6 提供了一系列内置节点,包括 circlerectellipsediamondtrianglestarimagemodelRect。若内置节点无法满足需求,用户还可以通过 G6.registerNode('nodeName', options) 进行自定义节点,方便用户开发更加定制化的节点,包括含有复杂图形的节点、复杂交互的节点、带有动画的节点等。

在本章中我们会通过四个案例,从简单到复杂讲解节点的自定义。这四个案例是:1. 从无到有的定义节点:绘制图形;优化性能。2. 扩展现有的节点:附加图形;增加动画。3. 调整节点的锚点;**4. 调整节点的鼠标选中/悬浮样式。**样式变化响应;动画响应。

通过 图形 Shape 章节的学习,我们应该已经知道了自定义节点时需要满足以下两点:

  • 控制节点的生命周期;
  • 解析用户输入的数据,在图形上展示。

G6 中自定义节点的 API 如下:

  1. G6.registerNode(
  2. 'nodeName',
  3. {
  4. options: {
  5. style: {},
  6. stateStyles: {
  7. hover: {},
  8. selected: {},
  9. },
  10. },
  11. /**
  12. * 绘制节点/边,包含文本
  13. * @param {Object} cfg 节点的配置项
  14. * @param {G.Group} group 节点的容器
  15. * @return {G.Shape} 绘制的图形,通过node.get('keyShape') 可以获取到
  16. */
  17. draw(cfg, group) {},
  18. /**
  19. * 绘制后的附加操作,默认没有任何操作
  20. * @param {Object} cfg 节点的配置项
  21. * @param {G.Group} group 节点的容器
  22. */
  23. afterDraw(cfg, group) {},
  24. /**
  25. * 更新节点,包含文本
  26. * @override
  27. * @param {Object} cfg 节点的配置项
  28. * @param {Node} node 节点
  29. */
  30. update(cfg, node) {},
  31. /**
  32. * 更新节点后的操作,一般同 afterDraw 配合使用
  33. * @override
  34. * @param {Object} cfg 节点的配置项
  35. * @param {Node} node 节点
  36. */
  37. afterUpdate(cfg, node) {},
  38. /**
  39. * 设置节点的状态,主要是交互状态,业务状态请在 draw 方法中实现
  40. * 单图形的节点仅考虑 selected、active 状态,有其他状态需求的用户自己复写这个方法
  41. * @param {String} name 状态名称
  42. * @param {Object} value 状态值
  43. * @param {Node} node 节点
  44. */
  45. setState(name, value, node) {},
  46. /**
  47. * 获取控制点
  48. * @param {Object} cfg 节点、边的配置项
  49. * @return {Array|null} 控制点的数组,如果为 null,则没有控制点
  50. */
  51. getAnchorPoints(cfg) {},
  52. },
  53. extendNodeName,
  54. );

注意:

  • 如果不从任何现有的节点扩展新节点时,draw 方法是必须的;
  • update 方法可以不定义,数据更新时会走 draw 方法,所有图形清除重绘;
  • afterDrawafterUpdate 方法一般用于扩展已有的节点/和边,例如:在矩形上附加图片,线上增加动画等;
  • setState 方法一般也不需要复写,有全局的样式可以替换;
  • getAnchorPoints 方法仅在需要限制与边的连接点时才需要复写,也可以在数据中直接指定。

1. 从无到有定义节点

绘制图形

我们自己来实现一个菱形的节点,如下图所示。

G6 有内置的菱形节点 diamond。为了演示,这里实现了一个自定义的菱形,相当于复写了内置的 diamond。

img

  1. G6.registerNode('diamond', {
  2. draw(cfg, group) {
  3. // 如果 cfg 中定义了 style 需要同这里的属性进行融合
  4. const shape = group.addShape('path', {
  5. attrs: {
  6. path: this.getPath(cfg), // 根据配置获取路径
  7. stroke: cfg.color, // 颜色应用到边上,如果应用到填充,则使用 fill: cfg.color
  8. },
  9. });
  10. if (cfg.label) {
  11. // 如果有文本
  12. // 如果需要复杂的文本配置项,可以通过 labeCfg 传入
  13. // const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
  14. // style.text = cfg.label;
  15. group.addShape('text', {
  16. // attrs: style
  17. attrs: {
  18. x: 0, // 居中
  19. y: 0,
  20. textAlign: 'center',
  21. textBaseline: 'middle',
  22. text: cfg.label,
  23. fill: '#666',
  24. },
  25. });
  26. }
  27. return shape;
  28. },
  29. // 返回菱形的路径
  30. getPath(cfg) {
  31. const size = cfg.size || [40, 40]; // 如果没有 size 时的默认大小
  32. const width = size[0];
  33. const height = size[1];
  34. // / 1 \
  35. // 4 2
  36. // \ 3 /
  37. const path = [
  38. ['M', 0, 0 - height / 2], // 上部顶点
  39. ['L', width / 2, 0], // 右侧顶点
  40. ['L', 0, height / 2], // 下部顶点
  41. ['L', -width / 2, 0], // 左侧顶点
  42. ['Z'], // 封闭
  43. ];
  44. return path;
  45. },
  46. });

上面的代码自定义了一个菱形节点,然后我们使用下面的数据输入就会绘制出 diamond 这个节点。

  1. const data = {
  2. nodes: [
  3. { x: 50, y: 100, shape: 'diamond' }, // 最简单的
  4. { x: 150, y: 100, shape: 'diamond', size: [50, 100] }, // 添加宽高
  5. { x: 250, y: 100, color: 'red', shape: 'diamond' }, // 添加颜色
  6. { x: 350, y: 100, label: '菱形', shape: 'diamond' }, // 附加文本
  7. ],
  8. };
  9. const graph = new G6.Graph({
  10. container: 'mountNode',
  11. width: 500,
  12. height: 500,
  13. });
  14. graph.data(data);
  15. graph.render();

img

优化性能

当图中节点或边通过 graph.update(item, cfg) 重绘时,默认情况下会调用节点的 draw 方法进行重新绘制。在数据量大或节点上图形数量非常多(特别是文本多)的情况下,draw 方法中对所有图形、赋予样式将会非常消耗性能。

在自定义节点时,重写 update 方法,在更新时将会调用该方法替代 draw。我们可以在该方法中指定需要更新的图形,从而避免频繁调用 draw 、全量更新节点上的所有图形。当然,update 方法是可选的,如果没有性能优化的需求可以不重写该方法。

在实现 diamond 的过程中,重写 update 方法,找到需要更新的 shape 进行更新,从而优化性能。寻找需要更新的图形可以通过:

  • group.get('children')[0] 找到 关键图形 keyShape,也就是 draw 方法返回的 shape;
  • group.get('children')[1] 找到 label 图形。

下面代码仅更新了 diamond 的关键图形的路径和颜色。

  1. G6.registerNode('diamond', {
  2. draw(cfg, group) {
  3. // ... // 见前面代码
  4. },
  5. getPath(cfg) {
  6. // ... // 见前面代码
  7. },
  8. update(cfg, node) {
  9. const group = node.getContainer(); // 获取容器
  10. const shape = group.get('children')[0]; // 按照添加的顺序
  11. const style = {
  12. path: this.getPath(cfg),
  13. stroke: cfg.color,
  14. };
  15. shape.attr(style); // 更新属性
  16. // 更新文本的逻辑类似,但是需要考虑 cfg.label 是否存在的问题
  17. // 通过 label.attr() 更新文本属性即可
  18. },
  19. });

2. 扩展现有节点

扩展 Shape

G6 中已经内置了一些节点,如果用户仅仅想对现有节点进行调整,复用原有的代码,则可以基于现有的节点进行扩展。同样实现 diamond ,可以基于 circle、ellipse、rect 等内置节点的进行扩展。simple-shape 是这些内置节点图形的基类,也可以基于它进行扩展。

下面以基于 single-shape 为例进行扩展。drawupdatesetState 方法在 simple-shape 中都有实现,这里仅需要复写 getShapeStyle 方法即可。返回的对象中包含自定义图形的路径和其他样式。

  1. G6.registerNode(
  2. 'diamond',
  3. {
  4. shapeType: 'path', // group.addShape 时需要指定的类型
  5. getShapeStyle(cfg) {
  6. const size = this.getSize(cfg); // 转换成 [width, height] 的模式
  7. const color = cfg.color;
  8. const width = size[0];
  9. const height = size[1];
  10. // / 1 \
  11. // 4 2
  12. // \ 3 /
  13. const path = [
  14. ['M', 0, 0 - height / 2], // 上部顶点
  15. ['L', width / 2, 0], // 右侧顶点
  16. ['L', 0, height / 2], // 下部顶点
  17. ['L', -width / 2, 0], // 左侧顶点
  18. ['Z'], // 封闭
  19. ];
  20. const style = Util.mix(
  21. {},
  22. {
  23. path: path,
  24. stroke: color,
  25. },
  26. cfg.style,
  27. );
  28. return style;
  29. },
  30. },
  31. // 注意这里继承了 'single-shape'
  32. 'single-shape',
  33. );

添加动画

通过 afterDraw 同样可以实现扩展,下面我们来看一个节点的动画场景,如下图所示。img

上面的动画效果,可以通过以下方式实现:

  • 扩展内置的 rect,在 rect 中添加一个图形;
  • 反复执行新添加图形的旋转动画。
  1. // 自定义一个名为 inner-animate 的节点
  2. G6.registerNode(
  3. 'inner-animate',
  4. {
  5. afterDraw(cfg, group) {
  6. const size = cfg.size;
  7. const width = size[0] - 14;
  8. const height = size[1] - 14;
  9. // 添加图片
  10. const image = group.addShape('image', {
  11. attrs: {
  12. x: -width / 2,
  13. y: -height / 2,
  14. width: width,
  15. height: height,
  16. img: cfg.img,
  17. },
  18. });
  19. // 执行旋转动画
  20. image.animate(
  21. {
  22. onFrame(ratio) {
  23. const matrix = Util.mat3.create();
  24. const toMatrix = Util.transform(matrix, [
  25. ['r', ratio * Math.PI * 2],
  26. ]);
  27. return {
  28. matrix: toMatrix,
  29. };
  30. },
  31. repeat: true,
  32. },
  33. 3000,
  34. 'easeCubic',
  35. );
  36. },
  37. },
  38. // 继承了 rect 节点
  39. 'rect',
  40. );

更多关于动画的实现,请参考基础动画章节。

3. 调整锚点 anchorPoint

节点上的锚点 anchorPoint 作用是确定节点与边的相交的位置,看下面的场景: imgimg

(左)没有设置锚点时。(右)diamond 设置了锚点后。

有两种方式来调整节点上的锚点:

  • 在数据里面指定 anchorPoints

    1. **适用场景:**可以为不同节点配置不同的锚点,更定制化。
  • 自定义节点中通过 getAnchorPoints 方法指定锚点。

    1. **适用场景:**全局配置锚点,所有该自定义节点类型的节点都相同。

数据中指定锚点

  1. const data = {
  2. nodes: [
  3. {
  4. id: 'node1',
  5. x: 100,
  6. y: 100,
  7. anchorPoints: [
  8. [0, 0.5], // 左侧中间
  9. [1, 0.5], // 右侧中间
  10. ],
  11. },
  12. //... // 其他节点
  13. ],
  14. edges: [
  15. //... // 边
  16. ],
  17. };

自定义时指定锚点

  1. G6.registerNode(
  2. 'diamond',
  3. {
  4. //... // 其他方法
  5. getAnchorPoints() {
  6. return [
  7. [0, 0.5], // 左侧中间
  8. [1, 0.5], // 右侧中间
  9. ];
  10. },
  11. },
  12. 'rect',
  13. );

4. 调整状态样式

常见的交互都需要节点和边通过样式变化做出反馈,例如鼠标移动到节点上、点击选中节点/边、通过交互激活边上的交互等,都需要改变节点和边的样式,有两种方式来实现这种效果:

  • 在数据上添加标志字段,在自定义 shape 过程中根据约定进行渲染;
  • 将交互状态同原始数据和绘制节点的逻辑分开,仅更新节点。 我们推荐用户使用第二种方式来实现节点的状态调整,可以通过以下方式来实现:
  • 在 G6 中自定义节点/边时在 setState 方法中进行节点状态的设置;
  • 通过 graph.setItemState() 方法来设置状态。

基于 rect 扩展出一个 custom 图形,默认填充色为白色,当鼠标点击时变成红色,实现这一效果的示例代码如下:

  1. // 基于 rect 扩展出新的图形
  2. G6.registerNode(
  3. 'custom',
  4. {
  5. // 设置状态
  6. setState(name, value, item) {
  7. const group = item.getContainer();
  8. const shape = group.get('children')[0]; // 顺序根据 draw 时确定
  9. if (name === 'selected') {
  10. if (value) {
  11. shape.attr('fill', 'red');
  12. } else {
  13. shape.attr('fill', 'white');
  14. }
  15. }
  16. },
  17. },
  18. 'rect',
  19. );
  20. // 点击时选中,再点击时取消
  21. graph.on('node:click', ev => {
  22. const node = ev.item;
  23. graph.setItemState(node, 'selected', !node.hasState('selected')); // 切换选中
  24. });

G6 并未限定节点的状态,只要你在 setState 方法中进行处理你可以实现任何交互,如实现鼠标放到节点上后节点逐渐变大的效果。img

  1. G6.registerNode(
  2. 'custom',
  3. {
  4. // 设置状态
  5. setState(name, value, item) {
  6. const group = item.getContainer();
  7. const shape = group.get('children')[0]; // 顺序根据 draw 时确定
  8. if (name === 'running') {
  9. if (value) {
  10. shape.animate(
  11. {
  12. r: 20,
  13. repeat: true,
  14. },
  15. 1000,
  16. );
  17. } else {
  18. shape.stopAnimate();
  19. shape.attr('r', 10);
  20. }
  21. }
  22. },
  23. },
  24. 'circle',
  25. );
  26. // 鼠标移动到上面 running,移出结束
  27. graph.on('node:mouseenter', ev => {
  28. const node = ev.item;
  29. graph.setItemState(node, 'running', true);
  30. });
  31. graph.on('node:mouseleave', ev => {
  32. const node = ev.item;
  33. graph.setItemState(node, 'running', false);
  34. });