使用AntV G6制作可‘追寻’上下游的关系图 | 🏆 技术专题第三期征文

2,536 阅读3分钟
 AntV G6是蚂蚁金服出品的图可视化引擎。

官网地址 g6.antv.vision/zh

本文将基于React和AntV G6实现一个demo:鼠标经过节点时,可高亮其上下游的关系图。

准备工作

1、使用create-react-app脚手架创建React项目

$ create-react-app antv-demo

2、安装@antv/g6,本demo使用@antv/g6**@3.4.8**

$ npm i @antv/g6@3.4.8

3、使用

import G6 from '@antv/g6';

Start!GOGOGO

1、数据应由请求返回,简单起见,我们造一个recordLists

    /**     * 节点的唯一标识为ID,即数据中的upID(上游ID)和_selfID(本节点ID)     */    this.recordLists = [        {            'upID': '111',            'superior': '董事长',            'superiorName': 'Bob',            '_selfID': '222',            '_self': '高级顾问',            '_selfName': 'Alice'        },        {            'upID': '111',            'superior': '董事长',            'superiorName': 'Bob',            '_selfID': '333',            '_self': '董事长助理',            '_selfName': 'Mary'        },        {            'upID': '111',            'superior': '董事长',            'superiorName': 'Bob',            '_selfID': '444',            '_self': '总经理',            '_selfName': 'Henry'        },        {            'upID': '222',            'superior': '高级顾问',            'superiorName': 'upTenant222',            '_selfID': '555',            '_self': '高级顾问助理',            '_selfName': 'William'        },        //......    ]

G6需要的数据data包括nodesedges:

const data = {            nodes: [],            edges: []        };

orderLists的内容解析到data中:

2、自定义节点

节点中需要显示职位及姓名,所以我们进行节点的自定义,继承基类‘single-shape’

`fittingString`方法处理当文字长度超过节点宽度时,超过文本框的内容使用`‘...’`
        G6.registerNode('relationNode', {            drawShape: function drawShape(cfg, group) {                const strokeColor = '#CDCDCD';                const calcStrLen = str => {                    let len = 0;                    for (let i = 0; i < str.length; i++) {                        if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {                            len++;                        } else {                            len += 2;                        }                    }                    return len;                };                const fittingString = (str, maxWidth, fontSize) => {                    const fontWidth = fontSize * 1.3; // 字号+边距                    maxWidth = maxWidth * 2; // 需要根据自己项目调整                    const width = calcStrLen(str) * fontWidth;                    const ellipsis = '…';                    if (width > maxWidth) {                        const actualLen = Math.floor((maxWidth - 10) / fontWidth);                        const result = str.substring(0, actualLen) + ellipsis;                        return result;                    }                    return str;                };                const rect = group.addShape('rect', {                    attrs: {                        x: -100 + 5,                        y: -25,                        width: 200,                        height: 60,                        radius: 3,                        stroke: strokeColor,                        fill: `l (0) 0:${strokeColor} ` + 0.015 + `:${strokeColor} ` + 0.015 + ':#fff',                        fillOpacity: 1,                        lineWidth: 1                    }                });                group.addShape('text', {                    attrs: {                        x: -95 + 10,                        y: 3,                        fill: '#333',                        text: fittingString(cfg.superiorName, 185, 14),                        fontSize: 14,                        fontWeight: 510,                        isName: true                    }                })                group.addShape('text', {                    attrs: {                        x: -95 + 10,                        y: 25,                        fill: '#999',                        text: fittingString(cfg.superior, 185, 12),                        fontSize: 12,                        fontWeight: 510,                        isPosition: true                    }                })                return rect;            }        }, 'single-shape');

3、图初始化

        const graph = new G6.Graph({            //挂载节点            container: 'mountNode',            width: this.props.width || window.innerWidth,            height: this.props.height || window.innerHeight,            layout: {                type: 'dagre',                ranksep: 40,                nodesep: 80,                controlPoints: true            },            modes: {                default: [                    'drag-canvas',//可拖拽                    'zoom-canvas'//可缩放                ]            },            defaultNode: {                //使用自定义节点                type: 'relationNode',                labelCfg: {                    style: {                        fill: '#666',                        fontSize: 14,                        fontWeight: 'bold'                    }                }            },            defaultEdge: {                type: 'polyline',                style: {                    radius: 20,                    endArrow: {                        path: 'M 0,0 L 8,4 A 5,5,0,0,1,8,-4 Z',                        fill: '#ddd'                    },                },            },        });

4、节点事件绑定

本demo使用的事件:

mouseenter:鼠标移入元素范围内触发,追寻上游和下游节点,并highlight,其余节点置灰;

mousemove:鼠标在元素内部移到时不断触发,若经过的区域为‘职级’或‘姓名’,则弹出tooltip;

mouseout:鼠标移出目标元素后触发,移除tooltip;

mouseleave:鼠标移出元素范围时触发,回到所有节点原始状态。

        graph.on('node:mouseenter', ev => {            const item = ev.item;            const edgeItems = ev.item.getInEdges() || [];            const sonEdgeItems = ev.item.getOutEdges() || [];            findParents(edgeItems, item, item);//追寻上游节点            findSons(sonEdgeItems, item, item);//追寻下游节点            graph.setItemState(item, 'highlight', true);            graph.update(item, {                style: {                    stroke: '#FD9839',                    fill: 'l (0) 0:#FD9839 ' + 0.015 + ':#FD9839 ' + 0.015 + ':#fff',                    cursor: 'pointer',                }            })            changeOthers();//其余节点置灰        });        graph.on('node:mousemove', (evt) => {            const { item, target, x, y } = evt;            const {                attrs: { isName, isPosition },            } = target;            const model = item.getModel();            const { superiorName, _selfName, superior, _self, id } = model;            if (isName || isPosition) {                const postion = graph.getClientByPoint(x, y);                createTooltip(postion, isName ? superiorName || _selfName : superior || _self, id);            } else {                removeTooltip(id);            }        });        graph.on('node:mouseout', (evt) => {            const { item, target } = evt;            const {                attrs: { isName, isPosition },            } = target;            const model = item.getModel();            const { id } = model;            if (isName || isPosition) {                removeTooltip(id);            }        });        graph.on('node:mouseleave', ev => {            const item = ev.item;            clearStates();            graph.setItemState(item, 'highlight', false);            graph.update(item, {                style: {                    stroke: '#CDCDCD',                    fill: 'l (0) 0:#CDCDCD ' + 0.015 + ':#CDCDCD ' + 0.015 + ':#fff',                    cursor: 'default',                    fillOpacity: 1                }            })        })

**demo完整示例 **https://github.com/Toxic12138/antv-demo

🏆 技术专题第三期 | 数据可视化的那些事......