How to draw dual tree flow based on G6? | JD Cloud technical team

1. Background

  • Business background: As the CRM system gradually increases the demand for fine-grained distribution of leads by each business line, the flow rules of each line will become more and more complicated. The lead flow rules of each line and even the entire CRM urgently need a tree-shaped visualization graph to express.

  • Technical background: Three solutions were considered before development, native canvas, fabric and G6, each with its own advantages and disadvantages

native canvas fabric G6
advantage Flexible, free, and very customizable Encapsulate the API of canvas, easy and flexible to use APIs such as complex trees and graphs are provided, and only need to be configured according to the document
shortcoming Complex and time-consuming to develop For complex and time-consuming construction of large trees, graphs, etc. Before development, you need to read the api documentation carefully, and it is slow to get started

From the comparison of the above tables, it can be seen that G6 has obvious advantages in building more complex trees and graphs, and has an active open source community, which provides a guarantee for subsequent maintenance and upgrades.

2. Basic knowledge

The following are a few core concepts about this dual-tree flow drawing, a brief introduction, if you are interested, it is recommended to read the official G6 API document

  • Graph

The graph is instantiated by new G6.Graph(config). The parameter config is the configuration item of the Object type graph, and most functions of the graph can be configured globally through this configuration item.

If it is a tree graph, it needs to be instantiated using the new G6.TreeGraph method

const graph = new G6.Graph({
  container: 'mountNode', // 指定图画布的容器 id,与第 9 行的容器对应
  // 画布宽高
  width: 800,
  height: 500,
});
// 读取数据
graph.data(data);
// 渲染图
graph.render();
  • Graph Elements - Nodes

The built-in nodes of G6 include circle, rect, ellipse, diamond, triangle, star, image, modelRect, donut (supported since v4.2.5). The default styles of these built-in nodes are shown in the figure below.

Built-in node configuration

const graph = new G6.Graph({
  container: 'mountNode',
  width: 800,
  height: 600,
  defaultNode: {
    type: 'circle', // 节点类型
    // ... 其他配置
  },
});

Custom node configuration

// 在节点配置中配置自定义节点名称以及尺寸
defaultNode: {
       type: 'card-node',
       size: [338, 70],
}
// 使用G6.registerNode自定义节点,在自定义节点中可以自定义各种形状的图形,包括text等
G6.registerNode('card-node', {
        draw: function drawShape(cfg, group) {
          const w = cfg.size[0];
          const h = cfg.size[1];
          group.addShape('rect', {
            attrs: {
              x: -w / 2 - 2,
              y: -(h - 30) / 2,
              width: 6, //200,
              height: h - 30, // 60
              fill: '#3c6ef0',
              radius: [0, 4, 4, 0]
            },
            name: 'mark-box',
            draggable: true,
          });
  • Graph Elements - Edges

G6 provides 9 built-in edges:

line: straight line, does not support control points;

polyline: polyline, supports multiple control points;

arc: arc line;

quadratic: second-order Bezier curve;

cubic: third-order Bezier curve;

cubic-vertical: a third-order Bezier curve in the vertical direction, regardless of the control points passed in by the user from the outside;

cubic-horizontal: a third-order Bezier curve in the horizontal direction, regardless of the control points passed in by the user from the outside;

loop: self-loop.

built-in edge configuration

const graph = new G6.Graph({
  container: 'mountNode',
  width: 800,
  height: 600,
  defaultEdge: {
    type: 'cubic',
    // 其他配置
  },
});

Custom Edge Configuration

在配置中引用自定义边
defaultEdge: {
    type: 'hvh',
    // 其他配置
  }
// 使用G6.registerEdge方法配置自定义边
G6.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],
        ],
      },
      // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
      name: 'path-shape',
    });
    return shape;
  },
});
  • Graph Layout - Treemap Layout

Overview of TreeGraph Layout Methods

CompactBox Layout: compact tree layout;

Dendrogram Layout: tree layout (leaf node layout aligned to the same layer);

Indented Layout: indented layout;

Mindmap Layout: mind map layout

const graph = new G6.TreeGraph({
  container: 'mountNode',
  modes: {
    default: [
      {
        // 定义展开/收缩行为
        type: 'collapse-expand',
      },
      'drag-canvas',
    ],
  },
  // 定义布局
  layout: {
    type: 'dendrogram', // 布局类型
    direction: 'LR', // 自左至右布局,可选的有 H / V / LR / RL / TB / BT
    nodeSep: 50, // 节点之间间距
    rankSep: 100, // 每个层级之间的间距
    excludeInvisibles: true, // 布局计算是否排除掉隐藏的节点,v4.8.8 起支持
  },
});
  • Interactions and Events

global event

As long as it occurs within the range on the canvas, it will be triggered, such as mousedown, mouseup, click, mouseenter, mouseleave, etc.

graph.on('click', (ev) => {
  const shape = ev.target;
  const item = ev.item;
  if (item) {
    const type = item.getType();
  }
});

canvas event

It is only triggered in the blank space of the canvas, such as canvas:mousedown, canvas:click, etc., with canvas:eventName as the event name.

graph.on('canvas:click', (ev) => {
  const shape = ev.target;
  const item = ev.item;
  if (item) {
    const type = item.getType();
  }
});

Events on nodes/edges

For example, node:mousedown, edge:click, combo:click, etc., use type:eventName as the event name.

graph.on('node:click', (ev) => {
  const node = ev.item; // 被点击的节点元素
  const shape = ev.target; // 被点击的图形,可根据该信息作出不同响应,以达到局部响应效果
  // ... do sth
});

graph.on('edge:click', (ev) => {
  const edge = ev.item; // 被点击的边元素
  const shape = ev.target; // 被点击的图形,可根据该信息作出不同响应,以达到局部响应效果
  // ... do sth
});

graph.on('combo:click', (ev) => {
  const combo = ev.item; // 被点击 combo 元素
  const shape = ev.target; // 被点击的图形,可根据该信息作出不同响应,以达到局部响应效果
  // ... do sth
});

3. Technical solution & implementation

  • data preparation

It is required that each node in the data must have an id, and the id must be of string type. The module field root indicates the root node, right indicates the child node, and left indicates the parent node. flowCountList represents the edge, from a certain node to a certain node

data: {
  id: '1',
  name: '根节点',
  flowInCount: 9999,
  flowOutCount: 9999,
  currentCount: 9999,
  module: 'root',
  children: [
    {
      id: '2',
      name: '右一节点',
      flowInCount: 9999,
      flowOutCount: 9999,
      currentCount: 9999,
      module: 'son',
    },
    {
      id: '3',
      name: '左一节点',
      flowInCount: 9999,
      flowOutCount: 9999,
      currentCount: 9999,
      module: 'father',
    }
  ]
}
flowCountList: [
    {
        fromPoolId: '1',
        toPoolId: '2',
        clueCount: 111
    },
    {
        fromPoolId: '1',
        toPoolId: '3',
        clueCount: 222
    }
]
  • Initialize the Minimap instance

If you need to add a Minimap, you can add a div#minimap to the outer div of the canvas, and put the minimap in it. The delegate in the following configuration indicates that the minimap only renders the frame of the elements in the canvas to reduce performance loss.

const miniMap = document.getElementById('minimap');
const minimap = new G6.Minimap({
    container: miniMap,
    type: 'delegate',
    size: [178, 138]
  });
  • Initialize the tree graph

The drag-canvas configured in modes indicates support for dragging the canvas, zoom-canvas indicates support for zooming in and out of the canvas, and tooltip indicates adding a tooltip prompt to the node.

The getSide method in the layout will judge whether the current node belongs to the parent node or the child node according to the data type. This method is only valid for the root node.

 this.graph = new G6.TreeGraph({
    container: 'clueCanvas',
    width:1000, // width和height可以根据自己画布大小进行赋值
    height:500,
    modes: {
      default: ['drag-canvas', 'zoom-canvas',{
        type: 'tooltip',
        formatText: function formatText(model) {
          return model.name;
        },
        shouldBegin: (e) => {
          const target = e.target;
          if (target.get('name') === 'title') return true;
          return false;
        },
      }],
    },
    defaultNode: {
      type: 'card-node',
      size: [338, 70],
    },
    defaultEdge: {
      type: 'custom-edge',
      style: {
        lineWidth: 4,
        lineAppendWidth: 8,
        stroke: '#BABEC7',
      }
    },
    layout: {
      type: 'mindmap',
      direction: 'H',
      getHeight: () => {return 70;}, //节点高度
      getWidth: () => {return 338;}, // 节点宽度
      getVGap: () => {return 8;}, // 节点之间的垂直间距
      getHGap: () => {return 100;}, // 节点之间的水平间距
      getSide: (d) => {
        if (d.data.module === 'father') {
          return 'left';
        }
        return 'right';
      },
    },
    plugins: [minimap]
  });
  this.graph.data(data);
  this.graph.render(); // 渲染
  this.graph.fitView(); // 全屏展示
  • custom node

The custom node is to define some graphic elements when the default node cannot meet our needs, which can be combined individually or in multiples, as shown in the following figure:

Due to limited space, the following code only shows the drawing of the outer border and the eye icon, and the drawing of other elements is basically similar. Among them, the setState callback method is used to monitor state changes. For example, in this example, monitor tree node expansion/collapse, eye icon switching, and node background color change logic.

G6.registerNode('card-node', {
        draw: function drawShape(cfg, group) {
          const w = cfg.size[0];
          const h = cfg.size[1];
          const shape = group.addShape('rect', {
            attrs: {
              x: -w / 2,
              y: -h / 2,
              width: w, //200,
              height: h, // 60
              radius: 8,
              fill: 'l(0) 0:rgba(197,213,255,1) 0.3:rgba(226,233,253,1) 1:rgba(255,255,255,1)',
              shadowOffsetX: -2,
              shadowOffsetY: 0,
              shadowColor: '#82A2F5',
              shadowBlur: 0,
              stroke: 'l(0) 0.1:rgba(138,169,249,1) 1:rgba(202,216,254,1)'
            },
            // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
            name: 'main-box',
            draggable: true,
          });
          cfg.children &&
            group.addShape('image', {
              attrs: {
                x: w / 2 - 35,
                y: -h / 2 + 10,
                cursor: 'pointer',
                img: flowEyeOpen,//cfg.collapsed ? flowEyeOpen : flowEyeClose,
                width: 16,
                height: 16
              },
              name: 'collapse-icon',
            });
          return shape;
        },
        setState(name, value, item) {
          if (name === 'collapsed') {
            // 替换眼睛图标
            const marker = item.get('group').find((ele) => ele.get('name') === 'collapse-icon');
            const icon = value ? flowEyeClose : flowEyeOpen;
            marker.attr('img', icon);
            // 替换卡片背景
            const mainBox = item.get('group').find((ele) => ele.get('name') === 'main-box');
            const fill = value ? '#fff' : 'l(0) 0:rgba(197,213,255,1) 0.3:rgba(226,233,253,1) 1:rgba(255,255,255,1)'
            const shadowOffsetX = value ? 0 : -2
            mainBox.attr('fill', fill)
            mainBox.attr('shadowOffsetX', shadowOffsetX)

          }
        },
      });
  • Node event listener

When the eye icon in the node is clicked, it will expand/collapse, update its state, and redraw. After the state is updated, the setState callback function above is triggered, and then the node style is changed according to the new expanded/collapsed state

this.graph.on('node:click', (e) => {
    // 点击眼睛图标展开子节点
    if (e.target.get('name') === 'collapse-icon') {
      e.item.getModel().collapsed = !e.item.getModel().collapsed;
      this.graph.setItemState(e.item, 'collapsed', e.item.getModel().collapsed);
      this.graph.layout();
    }
  });
  • custom side

The default style of the side uses the built-in side cubic-horizontal type. When the mouse moves over the side, a quantity data will appear as shown in the figure below:

When the state becomes active, set the transparency of the element on the edge to 1 to be visible, otherwise set to 0 to be invisible. Default is 0

G6.registerEdge(
        'custom-edge',
        {
          afterDraw(cfg, group) {
            const source = cfg.sourceNode._cfg.model.id
            const target = cfg.targetNode._cfg.model.id
            let current = self.flowCountList.find(item=>{
              return source === item.fromPoolId && target === item.toPoolId
            })
            // 如果未找到,在进行反向查一次
            if(!current) {
              current = self.flowCountList.find(item=>{
                return source === item.toPoolId && target === item.fromPoolId
              })
            }
            // 获取图形组中的第一个图形,在这里就是边的路径图形
            const shape = group.get('children')[0];
            // 获取路径图形的中点坐标
            const midPoint = shape.getPoint(0.5);
            // 在中点增加一个矩形,注意矩形的原点在其左上角
            group.addShape('rect', {
              attrs: {
                width: 92,
                height: 20,
                fill: '#BABEC7',
                stroke: '#868D9F',
                lineWidth: 1,
                radius: 10,
                opacity: 0,
                // x 和 y 分别减去 width / 2 与 height / 2,使矩形中心在 midPoint 上
                x: midPoint.x - 92/2,
                y: midPoint.y - 20/2,
              },
              name: 'edge-count', // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
            });
            group.addShape('text', {
              attrs: {
                textBaseline: 'top',
                x: midPoint.x - 92/2 + 20,
                y: midPoint.y - 12/2 + 1,
                lineHeight: 12,
                text: `流出 ${current ? current.clueCount : 0}`,
                fontSize: 12,
                fill: '#ffffff',
                opacity: 0,
              },
              name: 'edge-text',
            });
          },
          // // 响应状态变化
          setState(name, value, item) {
            if (name === 'active') {
              const edgeCount = item.get('group').find((ele) => ele.get('name') === 'edge-count');
              const edgeText = item.get('group').find((ele) => ele.get('name') === 'edge-text');
              edgeCount.attr('opacity', value ? 1 : 0)
              edgeText.attr('opacity', value ? 1 : 0)
            }
          },
        },
        'cubic-horizontal',
      );
  • side event monitoring

Monitor the mouseenter and mouseleave events of the edge, and update its activation status

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

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

This is the end of the introduction of double-number rendering logic. If you have a similar case, please refer to it.

4. Trample record

  • Indent tree - top aligned, left tree won't

The configuration is as follows indented means indented tree, dropCap is set to false means to close the drooping tree, only the right tree is displayed normally, if the double tree is rendered, the left tree will not be indented, which is a bug of g6 itself

layout: {
    type: 'indented',
    dropCap: false,
  },

  • When loading the tree structure for the first time, it is limited to expand to two levels and only need to add the attribute collapsed: true to the second level node. The data structure is as follows:
{
    id: '1',
    children: [{
        id: '1-1',
        children: [{
            id: '1-1-1',
            collapsed: true
        }]
    }]
}

  • A node has two local areas to add tooltip
const tooltip = new G6.Tooltip({
    className: 'g6-tooltip',
    offsetX: -5,
    offsetY: -165,
    getContent(e) {
      return '111'
    },
    shouldBegin(e){
      return true
    },
    itemTypes: ['node']
});
// 增加tooltip插件来实现一个节点多个局部区域增加tooltip
plugins: [tooltip,tooltip1]
  • Questions about compilation errors

Because my local ts version is too low, ts reported an error when checking the code of g6. The specific error message is as follows:

Cause Analysis:

The g6 code base uses the advanced syntax of ts, which causes the current low version ts to fail to check it

Solution:

(1). Add "skipLibCheck" in tsconfig.json: true to skip the ts check, but found that the test environment and the pre-release environment were compiled successfully, and the npm run build failed to compile when going online

(2). Upgrade the ts version and upgrade the currently used 3.5.3 to the latest version. It is found that although the above problems have been solved, there are errors reported elsewhere in the system. In order to reduce the risk of going online, this method was abandoned

(3). Downgrade the version of @antv/g6 from the latest version to 4.3.4. Finally, it is found that it is feasible, there is no problem in compiling, and the function is running normally. The poor experience is @antv/g6 about every version The upgrade did not mention this problem, so it needs to be tested according to the version, and it took a whole day to find this version. If anyone encounters this problem in the future, they can learn from it

5. Achievement display

  • global display

  • partial display

Author: JD Logistics Tian Leilei

Source: JD Cloud Developer Community

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/9697319