G6(七)进阶:自定义节点

G6 提供了一系列内置节点,包括 circle、rect、diamond、triangle、star、image、modelRect。若内置节点无法满足需求,还可以通过 G6.registerNode(typeName: string, nodeDefinition: object, extendedNodeType?: string) 进行自定义节点,方便用户开发更加定制化的节点,包括含有复杂图形的节点、复杂交互的节点、带有动画的节点等。其参数:

  • typeName:该新节点类型名称;
  • extendedNodeType:被继承的节点类型,可以是内置节点类型名,也可以是其他自定义节点的类型名。extendedNodeType 未指定时代表不继承其他类型的节点;
  • nodeDefinition:该新节点类型的定义,其中必要函数详见 自定义机制 API。当有 extendedNodeType 时,没被复写的函数将会继承 extendedNodeType 的定义。

需要注意的是,自定义节点/边时,若给定了 extendedNodeType,如 draw,update,setState 等必要的函数若不在 nodeDefinition 中进行复写,将会继承 extendedNodeType 中的相关定义。

自定义节点时需要满足以下两点:

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

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

js 复制代码
G6.registerNode(
  'nodeName',
  {
    options: {
      style: {},
      stateStyles: {
        hover: {},
        selected: {},
      },
    },
    /**
     * 绘制节点,包含文本
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中图形对象的容器
     * @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。
     * 关于 keyShape 可参考文档 核心概念-节点/边/Combo-图形 Shape 与 keyShape
     */
    draw(cfg, group) {},
    /**
     * 绘制后的附加操作,默认没有任何操作
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中图形对象的容器
     */
    afterDraw(cfg, group) {},
    /**
     * 更新节点,包含文本
     * @override
     * @param  {Object} cfg 节点的配置项
     * @param  {Node} node 节点
     */
    update(cfg, node) {},
    /**
     * 更新节点后的操作,一般同 afterDraw 配合使用
     * @override
     * @param  {Object} cfg 节点的配置项
     * @param  {Node} node 节点
     */
    afterUpdate(cfg, node) {},
    /**
     * 响应节点的状态变化。
     * 在需要使用动画来响应状态变化时需要被复写,其他样式的响应参见下文提及的 [配置状态样式] 文档
     * @param  {String} name 状态名称
     * @param  {Object} value 状态值
     * @param  {Node} node 节点
     */
    setState(name, value, node) {},
    /**
     * 获取锚点(相关边的连入点)
     * @param  {Object} cfg 节点的配置项
     * @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有控制点
     */
    getAnchorPoints(cfg) {},
  },
  // 继承内置节点类型的名字,例如基类 'single-node',或 'circle', 'rect' 等
  // 当不指定该参数则代表不继承任何内置节点类型
  extendedNodeType,
);

注意:

  • 如果不从任何现有的节点或从 'single-node' 扩展新节点时,draw 方法是必须的;

  • 节点内部所有图形使用相对于节点自身的坐标系 ,即 (0, 0) 是该节点的中心。而节点的坐标是

  • 相对于画布的,由该节点 group 上的矩阵控制,自定义节点中不需要用户感知。若在自定义节点内增加 rect 图形,要注意让它的 x 与 y 各减去其长与宽的一半。

  • update 方法可以不定义:

    • 当 update 未定义:若指定了 registerNode 的第三个参数 extendedNodeType(即代表继承指定的内置节点类型),则节点更新时将执行被继承的内置节点类型的 update 逻辑;若未指定 registerNode 的第三个参数,则节点更新时会执行 draw 方法,所有图形清除重绘;

    • 当定义了 update 方法,则不论是否指定 registerNode 的第三个参数,在节点更新时都会执行复写的 update 函数逻辑。

  • afterDraw,afterUpdate 方法一般用于扩展已有的节点,例如:在矩形节点上附加图片,圆节点增加动画等;

  • setState 只有在需要使用动画的方式来响应状态变化时需要复写,一般的样式响应状态变化可以通过 配置状态样式 实现;

  • getAnchorPoints 方法仅在需要限制与边的连接点时才需要复写,也可以在数据中直接指定。

现在,我们开始自定义节点。

定义节点

绘制图形

实现一个菱形的节点,如下图所示。

自定义节点中所有通过 addShape 增加的图形的坐标都是相对于节点自身的子坐标系 ,即 (0, 0) 是该节点的中心。如 'text' 图形的 x 和 y 均为 0,代表该图形相对于该节点居中;'path' 图形 path 属性中的坐标也是以 (0, 0) 为原点计算的。换句话说,在自定义节点时不需要感知相对于画布的节点坐标,节点坐标由该节点所在 group 的矩阵控制。

js 复制代码
G6.registerNode('diamond', {
  draw(cfg, group) {
    // 如果 cfg 中定义了 style 需要同这里的属性进行融合
    const keyShape = group.addShape('path', {
      attrs: {
        path: this.getPath(cfg), // 根据配置获取路径
        stroke: cfg.color, // 颜色应用到描边上,如果应用到填充,则使用 fill: cfg.color
      },
      // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
      name: 'path-shape',
      // 设置 draggable 以允许响应鼠标的图拽事件
      draggable: true,
    });
    if (cfg.label) {
      // 如果有文本
      // 如果需要复杂的文本配置项,可以通过 labeCfg 传入
      // const style = (cfg.labelCfg && cfg.labelCfg.style) || {};
      // style.text = cfg.label;
      const label = group.addShape('text', {
        // attrs: style
        attrs: {
          x: 0, // 居中
          y: 0,
          textAlign: 'center',
          textBaseline: 'middle',
          text: cfg.label,
          fill: '#666',
        },
        // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
        name: 'text-shape',
        // 设置 draggable 以允许响应鼠标的图拽事件
        draggable: true,
      });
    }
    return keyShape;
  },
  // 返回菱形的路径
  getPath(cfg) {
    const size = cfg.size || [40, 40]; // 如果没有 size 时的默认大小
    const width = size[0];
    const height = size[1];
    //  / 1 \
    // 4     2
    //  \ 3 /
    const path = [
      ['M', 0, 0 - height / 2], // 上部顶点
      ['L', width / 2, 0], // 右侧顶点
      ['L', 0, height / 2], // 下部顶点
      ['L', -width / 2, 0], // 左侧顶点
      ['Z'], // 封闭
    ];
    return path;
  },
});

上面的代码自定义了一个菱形节点。值得注意的是,G6需要用户为自定义节点中的图形设置 name 和 draggable。其中,name 值必须在同元素类型内唯一。draggable 为 true 是表示允许该图形响应鼠标的拖拽事件,只有 draggable: true 时,图上的交互行为 'drag-node' 才能在该图形上生效。若上面代码仅在 keyShape 上设置了 draggable: true,而 label 图形上没有设置,则鼠标拖拽只能在 keyShape 上响应。

现在,我们使用下面的数据输入就会绘制出 diamond 这个节点。

js 复制代码
const data = {
  nodes: [
    { id: 'node1', x: 50, y: 100, type: 'diamond' }, // 最简单的
    { id: 'node2', x: 150, y: 100, type: 'diamond', size: [50, 100] }, // 添加宽高
    { id: 'node3', x: 250, y: 100, color: 'red', type: 'diamond' }, // 添加颜色
    { id: 'node4', x: 350, y: 100, label: '菱形', type: 'diamond' }, // 附加文本
  ],
};
const graph = new G6.Graph({
  container: 'mountNode',
  width: 500,
  height: 500,
});
graph.data(data);
graph.render();

优化性能

当图中节点或边通过 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 的关键图形的路径和颜色。

js 复制代码
G6.registerNode('diamond', {
  draw(cfg, group) {
    // ... // 见前面代码
  },
  getPath(cfg) {
    // ... // 见前面代码
  },
  update(cfg, node) {
    const group = node.getContainer(); // 获取容器
    const shape = group.get('children')[0]; // 按照添加的顺序
    const style = {
      path: this.getPath(cfg),
      stroke: cfg.color,
    };
    shape.attr(style); // 更新属性
    // 更新文本的逻辑类似,但是需要考虑 cfg.label 是否存在的问题
    // 通过 label.attr() 更新文本属性即可
  },
});

扩展现有节点

扩展 Shape

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

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

js 复制代码
G6.registerNode(
  'diamond',
  {
    draw(cfg, group) {
      const size = this.getSize(cfg); // 转换成 [width, height] 的模式
      const color = cfg.color;
      const width = size[0];
      const height = size[1];
      //  / 1 \
      // 4     2
      //  \ 3 /
      const path = [
        ['M', 0, 0 - height / 2], // 上部顶点
        ['L', width / 2, 0], // 右侧顶点
        ['L', 0, height / 2], // 下部顶点
        ['L', -width / 2, 0], // 左侧顶点
        ['Z'], // 封闭
      ];
      const style = G6.Util.mix(
        {},
        {
          path: path,
          stroke: color,
        },
        cfg.style,
      );
      // 增加一个 path 图形作为 keyShape
      const keyShape = group.addShape('path', {
        attrs: {
          ...style,
        },
        draggable: true,
        name: 'diamond-keyShape', // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
      });
      // 返回 keyShape
      return keyShape;
    },
  },
  // 注意这里继承了 'single-node'
  'single-node',
);

调整锚点 anchorPoint

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

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

  • 在数据里面指定 anchorPoints。 适用场景: 可以为不同节点配置不同的锚点,更定制化。

  • 自定义节点中通过 getAnchorPoints 方法指定锚点。 适用场景: 全局配置锚点,所有该自定义节点类型的节点都相同。

数据中指定锚点

js 复制代码
const data = {
  nodes: [
    {
      id: 'node1',
      x: 100,
      y: 100,
      anchorPoints: [
        [0, 0.5], // 左侧中间
        [1, 0.5], // 右侧中间
      ],
    },
    //...       // 其他节点
  ],
  edges: [
    //... // 边
  ],
};

自定义时指定锚点

js 复制代码
G6.registerNode(
  'diamond',
  {
    //... // 其他方法
    getAnchorPoints() {
      return [
        [0, 0.5], // 左侧中间
        [1, 0.5], // 右侧中间
      ];
    },
  },
  'rect',
);

调整状态样式

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

  1. 在数据上添加标志字段,在自定义 shape 过程中根据约定进行渲染;
  2. 将交互状态同原始数据和绘制节点的逻辑分开,仅更新节点。

推荐使用第二种方式来实现节点的状态调整,可以通过以下方式来实现:

  • 在 G6 中自定义节点/边时在 setState 方法中进行节点状态变化的响应;
  • 通过 graph.setItemState() 方法来设置状态。

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

js 复制代码
// 基于 rect 扩展出新的图形
G6.registerNode(
  'custom',
  {
    // 响应状态变化
    setState(name, value, item) {
      const group = item.getContainer();
      const shape = group.get('children')[0]; // 顺序根据 draw 时确定
      if (name === 'selected') {
        if (value) {
          shape.attr('fill', 'red');
        } else {
          shape.attr('fill', 'white');
        }
      }
    },
  },
  'rect',
);

// 点击时选中,再点击时取消
graph.on('node:click', (ev) => {
  const node = ev.item;
  graph.setItemState(node, 'selected', !node.hasState('selected')); // 切换选中
});

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

js 复制代码
G6.registerNode(
  'custom',
  {
    // 响应状态变化
    setState(name, value, item) {
      const group = item.getContainer();
      const shape = group.get('children')[0]; // 顺序根据 draw 时确定
      if (name === 'running') {
        if (value) {
          shape.animate(
            {
              r: 20,
            },
            {
              repeat: true,
              duration: 1000,
            },
          );
        } else {
          shape.stopAnimate();
          shape.attr('r', 10);
        }
      }
    },
  },
  'circle',
);

// 鼠标移动到上面 running,移出结束
graph.on('node:mouseenter', (ev) => {
  const node = ev.item;
  graph.setItemState(node, 'running', true);
});

graph.on('node:mouseleave', (ev) => {
  const node = ev.item;
  graph.setItemState(node, 'running', false);
});
相关推荐
wayhome在哪1 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任1 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任2 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任4 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼4 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲5 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户2519162427117 天前
Canvas之画图板
前端·javascript·canvas
FogLetter10 天前
玩转Canvas:从静态图像到动态动画的奇妙之旅
前端·canvas
用户25191624271111 天前
Canvas之贪吃蛇
前端·javascript·canvas
用户25191624271111 天前
Canvas之粒子烟花
前端·javascript·canvas