自定义边

阅读时间约 5 分钟

F6 除了提供丰富的 内置边  外,还提供了自定义边的机制,方便用户开发更加定制化的边,包括含有复杂图形的边、复杂交互的边、带有动画的边等。

用户可以通过 F6.registerEdge(typeName: string, edgeDefinition: object, extendedTypeName?: string) 注册一个新的边类型,其中:

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

需要注意的是,自定义边/节点时,若给定了 extendedTypeName,如 drawupdatesetState 等必要的函数若不在 edgeDefinition 中进行复写,将会继承 extendedTypeName 中的相关定义。常见问题:

  • Q:边/节点更新时,没有按照在 edgeDefinition 中自定义实现的 drawdrawShape 逻辑更新。例如,有些图形没有被更新,增加了没有在 drawdrawShape 方法中定义的图形等。
  • A:由于继承了 extendedTypeName,且在 edgeDefinition 中没有复写 update 方法,导致边/节点更新时执行了 extendedTypeName 中的 update 方法,从而与自定义的 drawdrawShape 有出入。可以通过复写 update 方法为 undefined 解决。当 update 方法为 undefined 时,边/节点的更新将会执行 drawdrawShape 进行重绘。

在本章中我们会通过四个案例,从简单到复杂讲解边的自定义:
1. 从无到有的定义边;
2. 扩展现有边;
3. 增加额外图形;
4. 边的交互样式;
5. 自定义带箭头的边。

1. 从无到有定义边

我们来实现垂直的折线:

img img img

(左)直线边。(中)默认的折线边。(右)调整了节点的锚点(连入点)后的折线边。

自定义边

F6.registerEdge('hvh', {
  draw(cfg, group) {
    const startPoint = cfg.startPoint;
    const endPoint = cfg.endPoint;
    const shape = group.addShape('path', {
      attrs: {
        stroke: '#333',
        path: [
          ['M', startPoint.x, startPoint.y],
          ['L', endPoint.x / 3 + (2 / 3) * startPoint.x, startPoint.y], // 三分之一处
          ['L', endPoint.x / 3 + (2 / 3) * startPoint.x, endPoint.y], // 三分之二处
          ['L', endPoint.x, endPoint.y],
        ],
      },
      // must be assigned in F6 3.3 and later versions. it can be any value you want
      name: 'path-shape',
    });
    return shape;
  },
});
  • 上面自定义边中的 startPointendPoint 分别是是边两端与起始节点和结束节点的交点;
  • 可以通过修改节点的锚点(边连入点)来改变 startPointendPoint 的位置。

在数据中修改 anchorPoints

通过以下的数据,使用自定义的 hvh 边,就可以实现上图最右边的效果。

const data = {
  nodes: [
    {
      id: 'node1',
      x: 100,
      y: 200,
      anchorPoints: [
        [0, 0.5],
        [1, 0.5],
      ],
    },
    {
      id: 'node2',
      x: 200,
      y: 100,
      anchorPoints: [
        [0, 0.5],
        [1, 0.5],
      ],
    },
    {
      id: 'node3',
      x: 200,
      y: 300,
      anchorPoints: [
        [0, 0.5],
        [1, 0.5],
      ],
    },
  ],
  edges: [
    {
      id: 'edge1',
      target: 'node2',
      source: 'node1',
      type: 'hvh',
    },
    {
      id: 'edge2',
      target: 'node3',
      source: 'node1',
      type: 'hvh',
    },
  ],
};

2. 扩展现有边

通过 afterDraw 接口给现有的曲线增加动画。

img
F6.registerEdge(
  'line-growth',
  {
    afterDraw(cfg, group) {
      const shape = group.get('children')[0];
      const length = shape.getTotalLength();
      shape.animate(
        (ratio) => {
          const startLen = ratio * length;
          const cfg = {
            lineDash: [startLen, length - startLen],
          };
          return cfg;
        },
        {
          repeat: true,
          duration: 2000,
        },
      );
    },
  },
  'cubic',
);

3. 增加额外图形

通过实现 afterDraw 增加额外图形,为找到边的主要图形 path 上的某个点,可以使用 shape.getPoint(ratio) 获得。

img
F6.registerEdge(
  'mid-point-edge',
  {
    afterDraw(cfg, group) {
      // 获取图形组中的第一个图形,在这里就是边的路径图形
      const shape = group.get('children')[0];
      // 获取路径图形的中点坐标
      const midPoint = shape.getPoint(0.5);
      // 在中点增加一个矩形,注意矩形的原点在其左上角
      group.addShape('rect', {
        attrs: {
          width: 10,
          height: 10,
          fill: '#f00',
          // x 和 y 分别减去 width / 2 与 height / 2,使矩形中心在 midPoint 上
          x: midPoint.x - 5,
          y: midPoint.y - 5,
        },
      });
    },
    update: undefined,
  },
  'cubic',
);

4. 边的交互样式

以点击选中边为示例,实现如下需求:

  • 点击边时边变粗,再点击变成细;
  • 单击变成红色,双击变成 '#333'

效果如下图所示。

img
⚠️注意:

边过细时点击很难被击中,可以设置  lineAppendWidth  来提升击中范围。

// 基于 line 扩展出新的边
F6.registerEdge(
  'custom-edge',
  {
    // 响应状态变化
    setState(name, value, item) {
      const group = item.getContainer();
      const shape = group.get('children')[0]; // 顺序根据 draw 时确定
      if (name === 'active') {
        if (value) {
          shape.attr('stroke', 'red');
        } else {
          shape.attr('stroke', '#333');
        }
      }
      if (name === 'selected') {
        if (value) {
          shape.attr('lineWidth', 3);
        } else {
          shape.attr('lineWidth', 2);
        }
      }
    },
  },
  'line',
);

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

graph.on('edge:tap', (ev) => {
  const edge = ev.item;
  graph.setItemState(edge, 'active', true);
});

graph.on('edge:dbltap', (ev) => {
  const edge = ev.item;
  graph.setItemState(edge, 'active', false);
});

5. 自定义箭头

F6 为内置边、自定义边提供了默认箭头和内置箭头。很多时候,F6 提供的箭头并不能满足业务上的需求,这个时候,就需要我们自定义箭头。

img

(左)F6 内置箭头。(右)自定义边带有自定义箭头。

⚠️ 注意: F6 3.4.1 后的自定义箭头坐标系有所变化。如下图所示,左图为 F6 3.4.1 之前版本的演示,右图为 F6 3.4.1 及之后版本的演示。箭头由指向 x 轴负方向更正为指向 x 轴正方向。同时,偏移量 d 的方向也发生响应变化。不变的是,自定义箭头本身坐标系的原点都与相应边 / path 的端点重合,且自定义箭头的斜率与相应边 / path 端点处的微分斜率相同。
img

F6 中有三种途径在边上配置自定义箭头:

  • 配置自定义箭头到边的全局配置中;
  • 在数据中为单条边配置;
  • 在自定义边中配置。

方法 1: 全局配置

const graph = new Graph({
  // ... 图的其他配置项
  defaultEdge: {
    style: {
      endArrow: {
        // 自定义箭头指向(0, 0),尾部朝向 x 轴正方向的 path
        path: 'M 0,0 L 20,10 L 20,-10 Z',
        // 箭头的偏移量,负值代表向 x 轴正方向移动
        // d: -10,
        fill: '#333',
        stroke: '#666',
        opacity: 0.8,
        // ...
      },
    },
  },
});

方法 2: 在数据中配置

const data = {
  nodes: [
    { id: 'node1' },
    { id: 'node2' },
    // ... 其他节点
  ],
  edges: [
    {
      source: 'node1',
      target: 'node2',
      style: {
        endArrow: {
          // 自定义箭头指向(0, 0),尾部朝向 x 轴正方向的 path
          path: 'M 0,0 L 20,10 L 20,-10 Z',
          // 箭头的偏移量,负值代表向 x 轴正方向移动
          // d: -10,
          fill: '#333',
          stroke: '#666',
          opacity: 0.8,
          // ...
        },
      },
    },
    //... 其他边
  ],
};

方法 3: 自定义边中配置

// 使用方法二:自定义边,并带有自定义箭头
F6.registerEdge('line-arrow', {
  draw(cfg, group) {
    const { startPoint, endPoint } = cfg;
    const keyShape = group.addShape('path', {
      attrs: {
        path: [
          ['M', startPoint.x, startPoint.y],
          ['L', endPoint.x, endPoint.y],
        ],
        stroke: 'steelblue',
        lineWidth: 3,
        startArrow: {
          // 自定义箭头指向(0, 0),尾部朝向 x 轴正方向的 path
          path: 'M 0,0 L 20,10 L 20,-10 Z',
          // 箭头的偏移量,负值代表向 x 轴正方向移动
          // d: -10,
          fill: '#333',
          stroke: '#666',
          opacity: 0.8,
          // ...
        },
        endArrow: {
          // 自定义箭头指向(0, 0),尾部朝向 x 轴正方向的 path
          path: 'M 0,0 L 20,10 L 20,-10 Z',
          // 箭头的偏移量,负值代表向 x 轴正方向移动
          // d: -10,
          fill: '#333',
          stroke: '#666',
          opacity: 0.8,
          // ...
        },
      },
      // must be assigned in F6 3.3 and later versions. it can be any value you want
      name: 'path-shape',
    });
    return keyShape;
  },
});