使用zrender(canvas) + d3绘制关系图谱

在这个dome中d3只用于位置计算和缩放,在关系图谱中,大量节点的情况下,对比与svg,canvas有着性能优势,因为它不需要频繁操作dom节点,不需要创建深层次的dom结构。
使用原生的canvas api比较复杂,很多第三方库提供了更加方便操作canvas的库,这里选用的是zrender。
canvas的一些比较复杂的东西比如事件,层级在zrender中也可以很好地实现。

zrender

api文档:https://ecomfe.github.io/zrender-doc/public/api.html
仓库地址:https://github.com/ecomfe/zrender
demo:https://github.com/ecomfe/zrender/tree/master/test
由于目前最新版本对一些功能的支持和文档不一样(图形文字),所以当前demo采用4.3.2版本。

核心api

初始化

选择html中对应存在的dom作为容器,返回一个实例对象,用于创建图形,这里要记得给#main设置宽高

const zr = zrender.init(document.querySelector('#main'));

创建一个圆形

<body>
    <div id="main" style="width:100vw;height:100vh"></div>
    <script src="../lib/zrender.v4.3.2.js"></script>
    <script>
        const zr = new zrender.init(document.querySelector('#main'));
        console.log(zr);
        const circle = new zrender.Circle({
            shape: {
                // 圆中心点x坐标
                cx: 100,
                // 圆中心点y坐标
                cy: 100,
                // 圆半径
                r: 100
            },
            style: {
                // 设置圆样式
                fill: 'blue',
                stroke: 'black',

                // 设置圆内文本样式
                text: '圆内文本',
                textFill: 'white',
                fontSize: 20,
                // 缩放文本大小跟随
                transformText: true
            }
        })
        zr.add(circle)
    </script>
</body>

image.png

创建一条直线

    const line = new zrender.Line({
            shape: {
                // 起始x坐标
                x1: 100,
                // 起始y坐标
                y1: 300,
                // 结束x坐标
                x2: 100,
                // 结束y坐标
                y2: 500
            },
            style: {
                // 线条颜色
                stroke: 'blue',
                // 线条粗细
                lineWidth: 2,

                text: '直线中间的文本',
                // 文字的包围矩形背景色
                textBackgroundColor: '#fff',
            }
        })
    zr.add(line);

image.png

绘制三角形

  const polygon = new zrender.Polygon({
        shape: {
            // 填充图形的坐标
            points: [[x3, y3], [x4, y4], [x2, y2]],
        },
        style: {
            fill: '填充颜色',
            opacity
        },
        z
  })

创建带箭头的直线

箭头的位置需要一些三角函数的运算,下面代码为封装好的绘制函数,offset为0的时候尖端是个矩形,可以调整为负数实现更好的效果,当前这个dome中,由于箭头指向圆变,所以要偏移圆半径的距离。

    const zr = new zrender.init(document.querySelector('#main'));
    /*
        x1,y1:起点坐标
        x2,y2:终点坐标
        offset:箭头距离终点偏移量
        l:箭头大小控制参数
    */
    function drawArrow(zr, x1, y1, x2, y2, offset = 0, l = 10) {
        const line = new zrender.Line({
            shape: {
                x1,
                y1,
                x2,
                y2
            },
            style: {
                // 线条颜色
                stroke: 'blue',
                // 线条粗细
                lineWidth: 2,

                text: '直线中间的文本',
                // 文字的包围矩形背景色
                textBackgroundColor: '#fff',
            }
        })

        const a = Math.atan2((y2 - y1), (x2 - x1));
        let tx = x2;
        let ty = y2
        // 偏移长度
        let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
        x2 = x1 + (totalLength - offset) * Math.cos(a)
        y2 = y1 + (totalLength - offset) * Math.sin(a)
        // D点x轴坐标
        const x3 = x2 - l * Math.cos(a + 30 * Math.PI / 180);
        // D点y轴坐标
        const y3 = y2 - l * Math.sin(a + 30 * Math.PI / 180);
        // C点x轴坐标
        const x4 = x2 - l * Math.cos(a - 30 * Math.PI / 180);
        // C点y轴坐标
        const y4 = y2 - l * Math.sin(a - 30 * Math.PI / 180);
        const polygon = new zrender.Polygon({
            shape: {
                // 填充图形的坐标
                points: [[x3, y3], [x4, y4], [x2, y2]],
            },
            style: {
                fill: '填充颜色',
                opacity: 1
            },
            z: 1
        })
        zr.add(line);
        zr.add(polygon)
    }
    drawArrow(zr, 200, 200, 300, 300);

image.png

d3.js

这是一个强大的可视化库,感兴趣更多细节可以查看文档或者我之前的文章,这个demo中主要使用了d3的Force布局。

完整代码

要注意版本号,zrenderd3都是v4版本,下面代码中对于画箭头的部分为之前探索的方案,在一些常见不适用(箭头消失),可以利用上面画直线箭头的方法进行修改。

这部分的难点是曲线箭头,需要调整贝塞尔曲线,利用三角函数计算出最合适的控制线,如有需要留言,我会出一篇文章记录。

拖动、点击显示关联等细节,可以下下面的完整demo中查看。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>d3关系图谱</title>
    <style>
        html,
        body,
        #main {
            width: 100%;
            height: 100%;
            margin: 0;
        }
    </style>
</head>

<body>
    <div id="main"></div>
    <script src="./lib/zrender.v4.3.2.js"></script>
    <script src="./lib/d3.v5.16.0.js"></script>
    <script>
        // 处理关系数组(分组)
        function dealRelations(links) {
            let linkGroup = {};  //用来分组,将两点之间的连线进行归类
            let linkMap = {};  //对连接线的计数
            for (let i = 0; i < links.length; i++) {
                let key = links[i].source.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
                if (!Object.prototype.hasOwnProperty.call(linkMap, key)) {
                    linkMap[key] = 0;
                }

                linkMap[key] += 1;
                if (!Object.prototype.hasOwnProperty.call(linkGroup, key)) {
                    linkGroup[key] = [];
                }
                linkGroup[key].push(links[i]);
            }
            //为每一条连接线分配size属性,同时对每一组连接线进行编号
            for (let i = 0; i < links.length; i++) {
                let key = links[i].source.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
                links[i].size = linkMap[key];
                //同一组的关系进行编号  
                let group = linkGroup[key];
                let keyPair = key.split(':');
                let type = 'noself';
                if (keyPair[0] == keyPair[1]) {  //指向两个不同实体还是同一个实体
                    type = 'self';
                }
                setLinkNumber(group, type); //给关系编号
            }
        }

        // 设置linknum
        function setLinkNumber(group, type) {
            if (group.length == 0) return;
            //对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
            let linksA = [], linksB = [];
            for (let i = 0; i < group.length; i++) {
                let link = group[i];
                if (link.source.name < link.target.name) {
                    link.direction = 1;
                    linksA.push(link);
                } else {
                    link.direction = -1;
                    linksB.push(link);
                }
            }
            //确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
            //特殊情况:当关系都是连接到同一个实体时,不平分
            let maxLinkNumber = 1;
            if (type == 'self') {
                maxLinkNumber = group.length;
            } else {
                maxLinkNumber = group.length % 2 == 0 ? group.length / 2 : (group.length + 1) / 2;
            }
            //如果两个方向的关系数量一样多,直接分别设置编号即可
            if (linksA.length == linksB.length) {
                let startLinkNumber = 1;
                for (let i = 0; i < linksA.length; i++) {
                    linksA[i].linknum = startLinkNumber++;
                }
                startLinkNumber = 1;
                for (let i = 0; i < linksB.length; i++) {
                    linksB[i].linknum = startLinkNumber++;
                }
            } else {
                //当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,
                // 然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
                //如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
                let biggerLinks, smallerLinks;
                if (linksA.length > linksB.length) {
                    biggerLinks = linksA;
                    smallerLinks = linksB;
                } else {
                    biggerLinks = linksB;
                    smallerLinks = linksA;
                }

                let startLinkNumber = maxLinkNumber;
                // 对短links进行逆序编号
                for (let i = 0; i < smallerLinks.length; i++) {
                    smallerLinks[i].linknum = startLinkNumber--;
                }
                let tmpNumber = startLinkNumber;

                startLinkNumber = 1;
                let p = 0;
                while (startLinkNumber <= maxLinkNumber) {
                    biggerLinks[p++].linknum = startLinkNumber++;
                }
                //开始负编号
                startLinkNumber = 0 - tmpNumber;
                for (let i = p; i < biggerLinks.length; i++) {
                    biggerLinks[i].linknum = startLinkNumber++;
                }
            }
        }
        class RelationShip {
            constructor(config) {
                this.drawConfig = {
                    company_circle_r: 35,
                    company_circle_c: '#76aae8',
                    company_circle_stroke: '#4089e6',
                    man_circle_r: 25,
                    man_circle_c: '#ed716d',
                    man_circle_stroke: '#ed716d',
                    circleTextSize: 10,
                    circleLineHeight: 14,

                    strokeColor: '#d2d2d2',
                    relationTextColor: '#8e8e8e',
                    relationTextSize: 10
                }
                this.data = config.data;
                this.zr = zrender.init(config.rootDom);
                this.zoomIdentity = d3.zoomIdentity;
                this.transform = null;
                this.simulation = null;
                this.isLockStyle = false;
                this.groupMap = new zrender.Group({ draggable: false, });
                this.zr.add(this.groupMap);

                // 点击事件处理
                this.handleEventListeners();
                // 处理力运动
                this.handleSimulation();
                // 处理分组
                dealRelations(this.data.links);
                setTimeout(() => {
                    this.initDrag();
                }, 100)
            }
            // 事件监听
            handleEventListeners() {
                this.zr.on('click', (e) => {
                    const { links } = this.data;
                    const { x, y } = this.getPos(e);
                    const node = this.simulation.find(x, y);
                    if (!this.isContain(node.circle, x, y)) {
                        this.resetHighlight(true);
                        this.isLockStyle = false;
                    } else {
                        this.isLockStyle = true;
                        links.forEach(link => {
                            link.source.circle.attr({ style: { opacity: .1 } })
                            link.target.circle.attr({ style: { opacity: .1 } })
                            link.opacity = .01;
                        })

                        links.forEach(link => {
                            if (link.source === node || link.target === node) {
                                link.source.circle.attr({ style: { opacity: 1 } })
                                link.target.circle.attr({ style: { opacity: 1 } })
                                link.opacity = 1;
                                const { company_circle_c, man_circle_c } = this.drawConfig;
                                link.straightLine?.line?.attr({
                                    style: {
                                        stroke: this.getArrowColor(link),
                                        textFill: this.getArrowColor(link),
                                    }
                                })
                                if (link.curve) {
                                    link.curve.bc.attr({
                                        style: {
                                            stroke: this.getArrowColor(link),
                                            textFill: this.getArrowColor(link),
                                        }
                                    })
                                }
                            }
                        })
                        e.event.stopPropagation();
                    }
                })
                this.zr.on('mousemove', e => {
                    if (this.isLockStyle) return;
                    const { x, y } = this.getPos(e);
                    const node = this.simulation.find(x, y);
                    if (this.isContain(node.circle, x, y)) {
                        this.isDynamicHoverHandle = true;
                        this.data.links.forEach(link => {
                            link.straightLine?.line?.attr({
                                style: {
                                    stroke: this.drawConfig.strokeColor,
                                    textFill: this.drawConfig.relationTextColor,
                                }
                            })
                            if (link.curve) {
                                link.curve.bc.attr({
                                    stroke: this.drawConfig.strokeColor,
                                    textFill: this.drawConfig.relationTextColor,
                                })
                            }
                            if (link.source === node || link.target === node) {
                                link.source.circle.attr({ style: { opacity: 1 } })
                                link.target.circle.attr({ style: { opacity: 1 } })
                                link.opacity = 1;
                                const { company_circle_c, man_circle_c } = this.drawConfig;
                                link.straightLine?.line?.attr({
                                    style: {
                                        stroke: this.getArrowColor(link),
                                        textFill: this.getArrowColor(link),
                                    }
                                })
                                if (link.curve) {
                                    link.curve.bc.attr('style', {
                                        stroke: this.getArrowColor(link),
                                        textFill: this.getArrowColor(link),
                                    })
                                }
                            }
                        })
                    } else {
                        this.resetHighlight();
                    }
                })

            }

            // 拖动
            initDrag() {
                const canvas = document.querySelector('canvas')
                d3.select(canvas)
                    .call(this.tick.bind(this))

                const dragFunc = d3.drag().container(canvas)
                    .subject(this.subject_from_event.bind(this))
                    .on("start", this.drag_started.bind(this))
                    .on("drag", this.dragged.bind(this))
                    .on("end", this.drag_ended.bind(this))
                dragFunc(d3.select('#main'))
                d3.select('#main')
                    .call(
                        d3.zoom()
                            .scaleExtent([0.2, 5])
                            .on("zoom",
                                (e) => {
                                    const transform = d3.event.transform;
                                    this.zoomIdentity = transform;
                                    const height = document.documentElement.clientHeight;
                                    const width = document.documentElement.clientWidth;
                                    this.groupMap.attr({
                                        scale: [transform.k, transform.k],
                                        position: [transform.x, transform.y]
                                    })
                                }
                            )
                    )
                d3.select('#main').on("dblclick.zoom", null)
            }

            // d3力学
            handleSimulation() {
                const { nodes, links } = this.data;
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                this.formatLink(links, nodes);
                const simulation = d3
                    .forceSimulation(nodes)
                    .force("link", d3.forceLink(links).distance(200))
                    .force("charge", d3.forceManyBody().strength(-200).distanceMax(100))
                    .force("center", d3.forceCenter(width / 2, height / 2))
                    .on('tick', this.tick.bind(this))
                    .on("end", function () {
                        nodes.forEach((node) => {
                            node.fx = node.x;
                            node.fy = node.y;
                        });
                    }).alphaDecay(0.03)
                    .tick(300);
                this.simulation = simulation;
            }

            // 格式化连接关系数据
            formatLink(links, nodes) {
                links.forEach((link, i) => {
                    link.index = i;
                    const src = nodes.find((node) => node.id == link.src);
                    const dst = nodes.find((node) => node.id == link.dst);
                    link["source"] = src;
                    link["target"] = dst;
                });
            }

            // 绘制三角箭头
            _drawArrow(ctx, x1, y1, x2, y2, offset, l, color) {
                const a = Math.atan2((y2 - y1), (x2 - x1));
                let tx = x2;
                let ty = y2

                // 偏移长度
                let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
                x2 = x1 + (totalLength - offset) * Math.cos(a)
                y2 = y1 + (totalLength - offset) * Math.sin(a)
                // D点x轴坐标
                const x3 = x2 - l * Math.cos(a + 30 * Math.PI / 180);
                // D点y轴坐标
                const y3 = y2 - l * Math.sin(a + 30 * Math.PI / 180);
                // C点x轴坐标
                const x4 = x2 - l * Math.cos(a - 30 * Math.PI / 180);
                // C点y轴坐标
                const y4 = y2 - l * Math.sin(a - 30 * Math.PI / 180);
                ctx.lineTo(x3, y3);
                ctx.lineTo(x4, y4);
                ctx.lineTo(x2, y2);
            }
            // 绘制曲线箭头
            drawCurvedArrow({
                x1,
                y1,
                x2,
                y2,
                reverse = false,
                strokeColor = 'black',
                fillColor = 'red',
                deg = 10,
                offset = 0,
                text = '',
                opacity = 1,
                level = -1
            }, curve) {
                const zr = this.groupMap;
                // function _drawArrow(ctx, x1, y1, x2, y2, offset, l, color) {
                //     const a = Math.atan2((y2 - y1), (x2 - x1));
                //     let tx = x2;
                //     let ty = y2

                //     // 偏移长度
                //     let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
                //     x2 = x1 + (totalLength - offset) * Math.cos(a)
                //     y2 = y1 + (totalLength - offset) * Math.sin(a)
                //     // D点x轴坐标
                //     const x3 = x2 - l * Math.cos(a + 30 * Math.PI / 180);
                //     // D点y轴坐标
                //     const y3 = y2 - l * Math.sin(a + 30 * Math.PI / 180);
                //     // C点x轴坐标
                //     const x4 = x2 - l * Math.cos(a - 30 * Math.PI / 180);
                //     // C点y轴坐标
                //     const y4 = y2 - l * Math.sin(a - 30 * Math.PI / 180);
                //     ctx.lineTo(x3, y3);
                //     ctx.lineTo(x4, y4);
                //     ctx.lineTo(x2, y2);
                // }

                const a = Math.atan2((y2 - y1), (x2 - x1));
                // 偏移长度
                let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
                x2 = x1 + (totalLength - offset) * Math.cos(a)
                y2 = y1 + (totalLength - offset) * Math.sin(a)

                if (totalLength > 200 && totalLength < 800) {
                    deg -= (totalLength - 200) / 80
                } else if (totalLength >= 800) {
                    deg = 3;
                }

                const degBAF = a / (Math.PI / 180);
                const LAD = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / 2;
                const LAC = LAD / Math.cos(deg * Math.PI / 180);
                let degCAE = 90 - deg - degBAF;
                let CE = LAC * Math.sin(degCAE * Math.PI / 180);
                let AE = LAC * Math.cos(degCAE * Math.PI / 180);

                // 曲线偏移
                const centerX = x1 + (x2 - x1) / 2;
                const centerY = y1 + (y2 - y1) / 2;

                // 方向箭头
                if (reverse) {
                    degCAE = degBAF - deg;
                    CE = LAC * Math.cos(degCAE * Math.PI / 180);
                    AE = LAC * Math.sin(degCAE * Math.PI / 180);
                }

                const Cx = x1 + CE;
                const Cy = y1 + AE;
                let cpx1 = Cx, cpy1 = Cy;

                if (curve) {
                    // 更新三角箭头
                    curve.ar.attr({
                        shape: {
                            x1: cpx1,
                            y1: cpy1,
                            x2,
                            y2,
                        },
                        style: { opacity },
                        z: level
                    })
                    // 更新曲线
                    curve.bc.attr({
                        shape: {
                            x1,
                            y1,
                            x2,
                            y2,
                            cpx1,
                            cpy1,
                        },
                        style: {
                            textOffset: [(cpx1 - centerX) / 2, (cpy1 - centerY) / 2],
                            opacity
                        },
                        z: level
                    })
                    return;
                }
                // 画曲线
                const bc = new zrender.BezierCurve({
                    shape: {
                        x1,
                        y1,
                        x2,
                        y2,
                        cpx1,
                        cpy1,
                    },
                    style: {
                        stroke: strokeColor,
                        text,
                        opacity,
                        transformText: true,
                        textFill: this.drawConfig.relationTextColor,
                        fontSize: this.drawConfig.relationTextSize,
                        textBackgroundColor: '#fff'
                    },
                    z: level
                })
                // 画箭头
                const Arrow = zrender.Path.extend({
                    shape: {
                        x: 0,
                        y: 0,
                        width: 0,
                        height: 0
                    },
                    buildPath: (ctx, shape) => {
                        const { x1, y1, x2, y2 } = shape;
                        this._drawArrow(ctx, x1, y1, x2, y2, 0, 10);

                        ctx.closePath();
                        this.tick();
                    }
                });
                const ar = new Arrow({
                    style: {
                        // stroke: fillColor,
                        fill: fillColor,
                        opacity
                    },
                    shape: {
                        x1: cpx1,
                        y1: cpy1,
                        x2,
                        y2,
                    },
                    z: level
                })
                zr.add(bc);
                zr.add(ar);
                return {
                    bc,
                    ar
                }
            }

            isContain(sub, x, y) {
                const shape = sub.shape;
                const { cx, cy, r } = shape;
                const left = cx - r;
                const top = cy - r;
                const right = cx + r;
                const bottom = cy + r;
                return x >= left && x <= right && y >= top && y <= bottom;
            }
            getPos(e) {
                const { x, y, k } = this.zoomIdentity;
                const ex = (e.offsetX - x) / k;
                const ey = (e.offsetY - y) / k;
                return { x: ex, y: ey }
            }

            // 重置样式
            resetLevel() {
                this.data.links.forEach(link => {
                    link.source.level = 0;
                    link.target.level = 0;
                    link.level = -1;
                })
            }

            resetHighlight(isOpacity) {
                this.data.links.forEach(link => {
                    if (isOpacity) {
                        link.source.circle.attr({ style: { opacity: 1 } })
                        link.target.circle.attr({ style: { opacity: 1 } })
                        link.opacity = 1;
                    }
                    if (link.straightLine) {
                        link.straightLine.line.attr('style', {
                            stroke: this.drawConfig.strokeColor,
                            textFill: this.drawConfig.relationTextColor,
                        })
                    }
                    if (link.curve) {
                        link.curve.bc.attr({
                            style: {
                                stroke: this.drawConfig.strokeColor,
                                textFill: this.drawConfig.relationTextColor,
                            }
                        })
                    }
                })
            }

            // 根据不同节点,获取不同样式
            getRadius(node) {
                const { company_circle_r, man_circle_r } = this.drawConfig;
                return node.type === 'company' ? company_circle_r : man_circle_r;
            }

            getColor(node, stroke) {
                const { company_circle_c, company_circle_stroke, man_circle_c, man_circle_stroke } = this.drawConfig;
                if (stroke) {
                    if (node.isSelf) {
                        return '#ed882b';
                    }
                    return node.type === 'company' ?
                        company_circle_stroke :
                        man_circle_stroke
                }
                if (node.isSelf) {
                    return '#f59429';
                }
                return node.type === 'company' ?
                    company_circle_c :
                    man_circle_c
            }
            getArrowColor(node) {
                const invs = ['投资'];
                if (invs.includes(node.type)) {
                    return '#fe5557'
                }
                return '#4099ea'
            }

            // 绘制更新图元
            createAndUpdateCircle(node) {
                if (node.circle) {
                    let text = node.texts.text1
                    if (node.texts.text2) {
                        text += '\n' + node.texts.text2
                    }
                    if (node.texts.text3) {
                        text += '\n' + node.texts.text3
                    }
                    if (node.texts.text4) {
                        text += '\n' + node.texts.text4
                    }

                    node.circle.attr({
                        shape: {
                            cx: node.fx,
                            cy: node.fy,
                        },
                        style: {
                            text: text,

                        },
                        z: node.level || 0
                    })
                    return;
                }


                const circle = new zrender.Circle({
                    shape: {
                        cx: node.x,
                        cy: node.y,
                        r: this.getRadius(node)
                    },
                    style: {
                        fill: this.getColor(node),
                        stroke: this.getColor(node, true),
                        textLineHeight: this.drawConfig.circleLineHeight,
                        fontSize: this.drawConfig.circleTextSize,
                        textFill: "#fff",
                        strokeNoScale: true,
                        transformText: true
                    },
                })



                const texts = {
                    text1: '',
                    text2: '',
                    text3: '',
                    text4: '',
                }
                const textIns = {};
                const originText = node.name;
                texts.text1 = originText.slice(0, 4)
                texts.text2 = originText.slice(4, 8)
                texts.text3 = originText.slice(8, 12)
                texts.text4 = originText.slice(12, 16)
                node.circle = circle;
                node.textIns = textIns;
                node.texts = texts;
                this.groupMap.add(circle)
            }
            createAndUpdateLine(link) {
                const x1 = link.source.x;
                const y1 = link.source.y;
                const x2 = link.target.x;
                const y2 = link.target.y;

                // 只有一条连线
                if (link.size === 3 && link.linknum === 1 || link.size === 1) {


                    if (link.straightLine) {
                        let textOffset = [0, 0];
                        let x1 = link.source.x;
                        let y1 = link.source.y;
                        let x2 = link.target.x;
                        let y2 = link.target.y;
                        const { company_circle_r, man_circle_r } = this.drawConfig;

                        // 需要偏移的直线距离
                        const needOffset = (company_circle_r - man_circle_r) / 2;
                        // 需要偏移的关系(大小圆半径不同,视觉上需要特殊处理)
                        if (link.source.type === 'man' && link.target.type === 'company') {
                            // 获取角度
                            const a = Math.atan2((y2 - y1), (x2 - x1));
                            // 两点之间的直线长度
                            let totalLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)

                            // 计算实际目标点的坐标
                            x2 = x1 + (totalLength - needOffset) * Math.cos(a)
                            y2 = y1 + (totalLength - needOffset) * Math.sin(a)

                            // 实际目标点-目标点=偏移长度
                            textOffset = [x2 - link.target.x, y2 - link.target.y]
                        }
                        link.straightLine?.line?.attr({
                            shape: {
                                x1: link.source.x,
                                y1: link.source.y,
                                x2: link.target.x,
                                y2: link.target.y,
                            },
                            style: {
                                textOffset,
                                opacity: link.opacity,
                                transformText: true
                            },
                            z: link.level || -1
                        })
                        link.straightLine?.arrow?.attr({
                            shape: {
                                x1: link.source.x,
                                y1: link.source.y,
                                x2: link.target.x,
                                y2: link.target.y,
                            },
                            z: link.level || -1,
                            style: { opacity: link.opacity || 1 }
                        })
                        return;
                    }

                    const line = new zrender.Line({
                        style: {
                            stroke: this.drawConfig.strokeColor,
                            fill: this.getArrowColor(link),
                            text: link.type,
                            textFill: this.drawConfig.relationTextColor,
                            fontSize: this.drawConfig.relationTextSize,
                            textBackgroundColor: '#fff'
                        },
                        shape: {
                            x1, y1, x2, y2,
                        },
                        z: link.level || -1
                    })
                    const Arrow = zrender.Path.extend({
                        type: 'fei',
                        shape: {
                            x: 0,
                            y: 0,
                            width: 0,
                            height: 0
                        },


                        buildPath: (ctx, shape) => {
                            const { x1, y1, x2, y2 } = shape;

                            this._drawArrow(ctx, x1, y1, x2, y2, this.getRadius(link.target), 10);
                            ctx.closePath();
                            this.tick();
                        }
                    });
                    // this._drawArrow(ctx, x1, y1, x2, y2, this.getRadius(link.target), 10);
                    // const polygon = new zrender.Polygon({
                    //     shape: {
                    //         points: [[100, 100], [600, 200], [300, 400]],
                    //         number: [[0, 0], [400, 400]]
                    //     }
                    // })

                    const arrow = new Arrow({
                        style: {
                            fill: this.getArrowColor(link),
                            opacity: link.opacity || 1
                        },
                        shape: {
                            x1: link.source.x,
                            y1: link.source.y,
                            x2: link.target.x,
                            y2: link.target.y,
                        },
                        z: -1
                    })
                    this.groupMap.add(line);
                    this.groupMap.add(arrow)
                    // link.line = line;
                    link.straightLine = {
                        line: line,
                        arrow: arrow
                    }
                    // link.arrow = arrow;
                    return;
                }

                // 曲线连接
                // if (link.linknum > 0) {
                //     if (link.curve) {
                //         this.drawCurvedArrow({
                //             x1,
                //             y1,
                //             x2,
                //             y2,
                //             offset: this.getRadius(link.target),
                //             opacity: link.opacity,
                //             reverse: true,
                //             deg: Math.abs(link.linknum) * 10,
                //             level: link.level,
                //             fillColor: this.getArrowColor(link),
                //         }, link.curve)
                //         return;
                //     }
                //     link.curve = this.drawCurvedArrow({
                //         x1,
                //         y1,
                //         x2,
                //         y2,
                //         strokeColor: this.drawConfig.strokeColor,
                //         text: link.type,
                //         reverse: true,
                //         level: link.level,
                //         fillColor: this.getArrowColor(link),
                //     })
                // }
                // else if (link.linknum < 0) {
                //     if (link.curve) {
                //         this.drawCurvedArrow({
                //             x1,
                //             y1,
                //             x2,
                //             y2,
                //             offset: this.getRadius(link.target),
                //             opacity: link.opacity,
                //             reverse: false,
                //             deg: Math.abs(link.linknum) * 10,
                //             level: link.level,
                //             fillColor: this.getArrowColor(link),
                //         }, link.curve)
                //         return;
                //     }
                //     link.curve = this.drawCurvedArrow({
                //         x1,
                //         y1,
                //         x2,
                //         y2,
                //         offset: this.getRadius(link.target),
                //         strokeColor: this.drawConfig.strokeColor,
                //         reverse: false,
                //         fillColor: this.getArrowColor(link),
                //         text: link.type,
                //         level: link.level
                //     })
                // }



            }
            tick() {
                const { nodes, links } = this.data;
                nodes.forEach(node => {
                    this.createAndUpdateCircle(node)
                })
                links.forEach(link => this.createAndUpdateLine(link))
            }

            // 拖拽
            subject_from_event(e) {
                const ex = this.zoomIdentity.invertX(d3.event.x),
                    ey = this.zoomIdentity.invertY(d3.event.y);

                const node = this.simulation.find(ex, ey);


                // 如果没有点击到目标点
                if (!this.isContain(node.circle, ex, ey)) {
                    return;
                }

                // 重置层级
                this.resetLevel();

                // 提升拖拽相关的元素层级
                this.data.links.forEach(link => {
                    if (link.source === node || link.target === node) {
                        link.source.level = 2;
                        link.target.level = 2;
                        link.level = 1;
                    }
                })
                return node;
            }
            drag_started() {
                d3.event.subject.fx = d3.event.x;
                d3.event.subject.fy = d3.event.y;
                if (!d3.event.active) this.simulation.alphaTarget(0.3).restart();
                d3.event.sourceEvent.stopPropagation();
            }
            dragged() {
                const { x, y, k } = this.zoomIdentity;

                d3.event.subject.fx += d3.event.dx / k;
                d3.event.subject.fy += d3.event.dy / k;
            }
            drag_ended() {
                if (!d3.event.active) this.simulation.alphaTarget(0);
                this.resetLevel();
            }
        }

        const data = {
            nodes: [
                {
                    currency: "万元",
                    id: "130790264",
                    img: "",
                    name: "兆协投资有限公司",
                    organ: false,
                    reg_amount: 0,
                    type: "company",
                },
                {
                    currency: "万元",
                    id: "115804450",
                    img: "",
                    name: "北京兆协食品有限公司上海销售分部",
                    organ: false,
                    reg_amount: 0,
                    type: "company",
                    isSelf: true
                },
                {
                    ex_com_id: 67,
                    id: "126",
                    img: "",
                    name: "翁祖明",
                    type: "man",
                },
                {
                    ex_com_id: "115804450",
                    id: "7065171",
                    name: "杨敬祖",
                    type: "man",
                },
                {
                    ex_com_id: 67932906,
                    id: "8032034",
                    name: "陈家协",
                    type: "man",
                },
                {
                    ex_com_id: 67932906,
                    id: "69542",
                    img: "",
                    name: "林杰",
                    type: "man",
                },
                {
                    ex_com_id: 67932906,
                    id: "630248",
                    img: "",
                    name: "陈钟峰",
                    type: "man",
                },
                {
                    currency: "万美元",
                    id: "67932906",
                    img: "",
                    name: "丹阳钱隆金属材料有限公司",
                    organ: false,
                    reg_amount: 2990,
                    type: "company",
                },
                {
                    ex_com_id: 67932906,
                    id: "17302430",
                    img: "",
                    name: "陈身秀",
                    type: "man",
                },
                {
                    ex_com_id: 67932906,
                    id: "17302431",
                    img: "",
                    name: "陈兴莺",
                    type: "man",
                },
                {
                    currency: "万美元",
                    id: "67",
                    img: "",
                    name: "北京兆协食品有限公司",
                    organ: false,
                    reg_amount: 12,
                    type: "company",
                },
            ],
            links: [

                {
                    src: 67932906,
                    dst: 130790264,
                    type: "投资",
                    with: "company",
                },
                {
                    src: 130790264,
                    dst: 67932906,
                    type: "吃饭",
                    with: "company",
                },
                // {
                //     src: 130790264,
                //     dst: 67932906,
                //     type: "睡觉",
                //     with: "company",
                // },
                {
                    src: 126,
                    dst: 67,
                    type: "法定代表人",
                    with: "man",
                },
                {
                    src: 126,
                    dst: 130790264,
                    type: "投资",
                    with: "man",
                },
                {
                    src: 7065171,
                    dst: 115804450,
                    type: "法定代表人",
                    with: "man",
                },
                {
                    src: 7065171,
                    dst: 130790264,
                    type: "法定代表人",
                    with: "man",
                },
                {
                    src: 8032034,
                    dst: 67932906,
                    type: "董事",
                    with: "man",
                },
                {
                    src: 17302430,
                    dst: 67932906,
                    type: "法定代表人",
                    with: "man",
                },
                {
                    src: 17302431,
                    dst: 67932906,
                    type: "副董事长",
                    with: "man",
                },
                {
                    src: 69542,
                    dst: 67932906,
                    type: "投资",
                    with: "man",
                },
                {
                    src: 630248,
                    dst: 67932906,
                    type: "监事",
                    with: "man",
                },
            ],
        };

        new RelationShip({ data, rootDom: document.querySelector('#main') })
    </script>
</body>

</html>

image.png

企业级实例

力导图分堆需要调控force的力
image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容