d3.js+vue生成动力图(一) --实现两个节点之间的多条关系线的显示以及关系的描述

实现两个节点之间多条关系线的显示,我们需要使用二次贝塞尔曲线来画图,主要是对两个节点之间的关系线进行编号,首先看一下数据

 nodes: [
        {id: 1, name: '刘备', type: '皇上'},
        {id: 2, name: '关羽', type: '将军'},
        {id: 3, name: '张飞', type: '将军'},
        {id: 4, name: '诸葛亮', type: '丞相'},
        {id: 5, name: '小兵1', type: '士兵'},
        {id: 6, name: '小兵2', type: '士兵'},
      ],
      links: [
        {source: 1, target: 1, relate: '皇上'},
        {source: 1, target: 2, relate: '将军'},
        {source: 1, target: 2, relate: '异性兄弟'},
        {source: 1, target: 3, relate: '将军'},
        {source: 1, target: 4, relate: '丞相'},
        {source: 2, target: 5, relate: '下属'},
        {source: 2, target: 6, relate: '下属'},
        {source: 3, target: 5, relate: '下属'},
      ],

先看一下实现的效果

在这里插入图片描述

先对所有关系进行分组

//关系分组
      const linkGroup = {};
      //对连接线进行统计和分组,不区分连接线的方向,只要属于同两个实体,即认为是同一组
      var linkmap = {}
      for (var i = 0; i < links.length; i++) {
        var key = links[i].source < links[i].target ? links[i].source + ':' + links[i].target : links[i].target + ':' + links[i].source;
        if (!linkmap.hasOwnProperty(key)) {
          linkmap[key] = 0;
        }
        linkmap[key] += 1;
        if (!linkGroup.hasOwnProperty(key)) {
          linkGroup[key] = [];
        }
        linkGroup[key].push(links[i]);
      }
      //为每一条连接线分配size属性,同时对每一组连接线进行编号
      for (var i = 0; i < links.length; i++) {
        var key = links[i].source < links[i].target ? links[i].source + ':' + links[i].target : links[i].target + ':' + links[i].source;
        links[i].size = linkmap[key];
        //同一组的关系进行编号
        var group = linkGroup[key];
        var keyPair = key.split(':');
        var type = 'noself';//标示该组关系是指向两个不同实体还是同一个实体
        if (keyPair[0] == keyPair[1]) {
          type = 'self';
        }
        //给节点分配编号
        setLinkNumber(group, type);
      }

给节点分配编号

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

          var startLinkNumber = maxLinkNumber;
          for (var i = 0; i < smallerLinks.length; i++) {
            smallerLinks[i].linknum = startLinkNumber--;
          }
          var tmpNumber = startLinkNumber;

          startLinkNumber = 1;
          var p = 0;
          while (startLinkNumber <= maxLinkNumber) {
            biggerLinks[p++].linknum = startLinkNumber++;
          }
          //开始负编号
          startLinkNumber = 0 - tmpNumber;
          for (var i = p; i < biggerLinks.length; i++) {
            biggerLinks[i].linknum = startLinkNumber++;
          }
        }
      }

创建关系,注意曲线要用path

	// 创建关系
      const link = g.selectAll('path') //path是曲线,line是直线
        .data(data.links, d => d.id) // 假设d.id是唯一的link标识符,用于绑定数据
        .enter()
        .append('g');

      // 添加关系线的路径
      const paths = link.append('path')
        .attr('fill', 'none')
        .attr('stroke', '#999') // 设置关系线的颜色
        .attr('stroke-width', 2)
        .attr('marker-end', 'url(#arrow)')
        .attr('id', (d, i) => `linkPath${i}`);

添加关系的描述

// 添加关系线的描述文字
      const linkText = link.append('text')
        .attr('class', 'linktext')
        .style('fill', 'black')
        .style('font-size', 10)
        .style('text-anchor', 'middle')
        .style('pointer-events', 'none');
      // 每条边都有各自的路径
      linkText.append('textPath')
        .attr('href', (d, i) => `#linkPath${i}`)
        .attr('startOffset', '50%')
        .text(d => d.relate);

更新节点和边的位置

const linkArc = d => {
        // 计算方向向量
        const dx = d.target.x - d.source.x;
        const dy = d.target.y - d.source.y;
        const length = Math.sqrt(dx * dx + dy * dy);
        const unitX = dx / length;
        const unitY = dy / length;

        // 调整后的起始和终止点
        const startX = d.source.x + unitX * this.nodeRadius;
        const startY = d.source.y + unitY * this.nodeRadius;
        const endX = d.target.x - unitX * this.nodeRadius;
        const endY = d.target.y - unitY * this.nodeRadius;

        // 如果链接连接相同的节点,相应地调整路径
        if (d.target === d.source) {
          const dr = 20 / d.linknum;
          return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y + 3);
        } else if (d.size % 2 !== 0 && d.linknum === 1) {
          return `M${startX} ${startY}L${endX} ${endY}`;
        }

        // 计算曲线半径
        const curve = 1.5;
        const homogeneous = 3;
        const dr = length * (d.linknum + homogeneous) / (curve * homogeneous);

        // 根据链接编号调整路径
        if (d.linknum < 0) {
          const adjustedDr = length * (-1 * d.linknum + homogeneous) / (curve * homogeneous);
          return `M${startX},${startY}A${adjustedDr},${adjustedDr} 0 0,0 ${endX},${endY}`;
        }
        return `M${startX},${startY}A${dr},${dr} 0 0,1 ${endX},${endY}`;
      };
// 在模拟完成后调用updatePositions函数
      simulation.on('tick', updatePositions);

      // 定义更新位置的逻辑
      function updatePositions() {
        paths.attr('d', d => linkArc(d));
        node.attr('transform', d => `translate(${d.x},${d.y})`);
      }

最后附上完整代码

<template>
  <div ref="chart" className="ggraph"></div>
</template>

<script>
import * as d3 from 'd3';

export default {
  data() {
    return {
      nodes: [
        {id: 1, name: '刘备', type: '皇上'},
        {id: 2, name: '关羽', type: '将军'},
        {id: 3, name: '张飞', type: '将军'},
        {id: 4, name: '诸葛亮', type: '丞相'},
        {id: 5, name: '小兵1', type: '士兵'},
        {id: 6, name: '小兵2', type: '士兵'},
      ],
      links: [
        {source: 1, target: 1, relate: '皇上'},
        {source: 1, target: 2, relate: '将军'},
        {source: 1, target: 2, relate: '异性兄弟'},
        {source: 1, target: 3, relate: '将军'},
        {source: 1, target: 4, relate: '丞相'},
        {source: 2, target: 5, relate: '下属'},
        {source: 2, target: 6, relate: '下属'},
        {source: 3, target: 5, relate: '下属'},
      ],
      //节点颜色
      colorScale: undefined,
      //节点半径
      nodeRadius: 18,
    };
  },
  mounted() {
    this.drawChart();
  },
  methods: {
    drawChart() {
      // 清除图表重新绘图
      d3.select(this.$refs.chart).selectAll('*').remove();
      const data = {
        nodes: this.nodes,
        links: this.links
      };
      const height = 600;
      const width = 900;

      this.colorScale = d3.scaleOrdinal(d3.schemeCategory10)
        .domain(this.nodes.map(d => d.type));

      // 创建SVG容器
      const svg = d3.select(this.$refs.chart)
        .append('svg')
        .attr('width', width)
        .attr('height', height);
      const links = data.links;
      //关系分组
      const linkGroup = {};
      //对连接线进行统计和分组,不区分连接线的方向,只要属于同两个实体,即认为是同一组
      var linkmap = {}
      for (var i = 0; i < links.length; i++) {
        var key = links[i].source < links[i].target ? links[i].source + ':' + links[i].target : links[i].target + ':' + links[i].source;
        if (!linkmap.hasOwnProperty(key)) {
          linkmap[key] = 0;
        }
        linkmap[key] += 1;
        if (!linkGroup.hasOwnProperty(key)) {
          linkGroup[key] = [];
        }
        linkGroup[key].push(links[i]);
      }
      //为每一条连接线分配size属性,同时对每一组连接线进行编号
      for (var i = 0; i < links.length; i++) {
        var key = links[i].source < links[i].target ? links[i].source + ':' + links[i].target : links[i].target + ':' + links[i].source;
        links[i].size = linkmap[key];
        //同一组的关系进行编号
        var group = linkGroup[key];
        var keyPair = key.split(':');
        var type = 'noself';//标示该组关系是指向两个不同实体还是同一个实体
        if (keyPair[0] == keyPair[1]) {
          type = 'self';
        }
        //给节点分配编号
        setLinkNumber(group, type);
      }

      // 添加箭头定义
      svg.append("defs")
        .append("marker")
        .attr("id", "arrow")
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", this.nodeRadius - 8)
        .attr("refY", 0)
        .attr("markerWidth", 6)
        .attr("markerHeight", 6)
        .attr("orient", "auto")
        .append("path")
        .attr("d", "M0,-5L10,0L0,5");

      const g = svg.append('g'); // 将 g 元素放在 svg 元素内部
      //新建一个力导向图
      const simulation = d3.forceSimulation(data.nodes)
        .force('link', d3.forceLink(data.links).id(d => d.id).distance(200)) // 增大节点间距
        .force('charge', d3.forceManyBody().strength(-100)) // 增大节点间斥力
        .force('center', d3.forceCenter(width / 2, height / 2));

      //创建节点
      const node = g.selectAll('.node')
        .data(data.nodes)
        .enter()
        .append('g')
        .attr('class', 'node')
        .style('fill', 'black')
        .style('fill', d => {
          this.colorScale(d)
        })

      node.append('circle')
        .attr('r', 18)
        .attr('fill', 'steelblue')
        .style('fill', d => this.colorScale(d))

      node.append('text')
        .text(d => d.name)
        .attr('text-anchor', 'middle')
        .attr('dy', 4)
        .attr('font-size', d => Math.min(2 * d.radius, 20))
        .attr('fill', 'black')
        .style('pointer-events', 'none');

      const linkArc = d => {
        // 计算方向向量
        const dx = d.target.x - d.source.x;
        const dy = d.target.y - d.source.y;
        const length = Math.sqrt(dx * dx + dy * dy);
        const unitX = dx / length;
        const unitY = dy / length;

        // 调整后的起始和终止点
        const startX = d.source.x + unitX * this.nodeRadius;
        const startY = d.source.y + unitY * this.nodeRadius;
        const endX = d.target.x - unitX * this.nodeRadius;
        const endY = d.target.y - unitY * this.nodeRadius;

        // 如果链接连接相同的节点,相应地调整路径
        if (d.target === d.source) {
          const dr = 20 / d.linknum;
          return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y + 3);
        } else if (d.size % 2 !== 0 && d.linknum === 1) {
          return `M${startX} ${startY}L${endX} ${endY}`;
        }

        // 计算曲线半径
        const curve = 1.5;
        const homogeneous = 3;
        const dr = length * (d.linknum + homogeneous) / (curve * homogeneous);

        // 根据链接编号调整路径
        if (d.linknum < 0) {
          const adjustedDr = length * (-1 * d.linknum + homogeneous) / (curve * homogeneous);
          return `M${startX},${startY}A${adjustedDr},${adjustedDr} 0 0,0 ${endX},${endY}`;
        }
        return `M${startX},${startY}A${dr},${dr} 0 0,1 ${endX},${endY}`;
      };

      // 创建关系线
      const link = g.selectAll('path') //path是曲线,line是直线
        .data(data.links, d => d.id) // 假设d.id是唯一的link标识符,用于绑定数据
        .enter()
        .append('g');

      // 添加关系线的路径
      const paths = link.append('path')
        .attr('fill', 'none')
        .attr('stroke', '#999') // 设置关系线的颜色
        .attr('stroke-width', 2)
        .attr('marker-end', 'url(#arrow)')
        .attr('id', (d, i) => `linkPath${i}`);

      // 添加关系线的描述文字
      const linkText = link.append('text')
        .attr('class', 'linktext')
        .style('fill', 'black')
        .style('font-size', 10)
        .style('text-anchor', 'middle')
        .style('pointer-events', 'none');
      // 每条边都有各自的路径
      linkText.append('textPath')
        .attr('href', (d, i) => `#linkPath${i}`)
        .attr('startOffset', '50%')
        .text(d => d.relate);

      // 在模拟完成后调用updatePositions函数
      simulation.on('tick', updatePositions);

      // 定义更新位置的逻辑
      function updatePositions() {
        paths.attr('d', d => linkArc(d));
        node.attr('transform', d => `translate(${d.x},${d.y})`);
      }

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

          var startLinkNumber = maxLinkNumber;
          for (var i = 0; i < smallerLinks.length; i++) {
            smallerLinks[i].linknum = startLinkNumber--;
          }
          var tmpNumber = startLinkNumber;

          startLinkNumber = 1;
          var p = 0;
          while (startLinkNumber <= maxLinkNumber) {
            biggerLinks[p++].linknum = startLinkNumber++;
          }
          //开始负编号
          startLinkNumber = 0 - tmpNumber;
          for (var i = p; i < biggerLinks.length; i++) {
            biggerLinks[i].linknum = startLinkNumber++;
          }
        }
      }


    }
  }
};
</script>

<style>
svg {
  background-color: #d1e9ff;
}
</style>

  • 31
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
这个需求比较复杂,需要一定的前端开发经验。下面是一个简单的示例代码,可以帮助您入门,在实际开发中,您需要根据具体情况进行调整和优化。 首先,您需要引入 VueD3.js 和 Cytoscape.js 的库文件。假设您已经完成了这一步,下面是一个基于 VueD3.js 和 Cytoscape.js 实现企查查关系谱的示例代码: ```html <template> <div id="cy"></div> </template> <script> import cytoscape from 'cytoscape'; import d3 from 'd3'; export default { mounted() { const data = { nodes: [ { id: 'n1', label: 'Node 1' }, { id: 'n2', label: 'Node 2' }, { id: 'n3', label: 'Node 3' }, { id: 'n4', label: 'Node 4' }, { id: 'n5', label: 'Node 5' }, { id: 'n6', label: 'Node 6' }, { id: 'n7', label: 'Node 7' }, { id: 'n8', label: 'Node 8' }, { id: 'n9', label: 'Node 9' }, { id: 'n10', label: 'Node 10' } ], edges: [ { source: 'n1', target: 'n2' }, { source: 'n2', target: 'n3' }, { source: 'n3', target: 'n4' }, { source: 'n4', target: 'n5' }, { source: 'n5', target: 'n6' }, { source: 'n6', target: 'n7' }, { source: 'n7', target: 'n8' }, { source: 'n8', target: 'n9' }, { source: 'n9', target: 'n10' }, { source: 'n10', target: 'n1' } ] }; const container = document.getElementById('cy'); const cy = cytoscape({ container, elements: data, style: [ { selector: 'node', style: { 'background-color': '#666', label: 'data(label)' } }, { selector: 'edge', style: { 'line-color': '#ccc', 'target-arrow-color': '#ccc', 'target-arrow-shape': 'triangle' } } ], layout: { name: 'circle', fit: true, padding: 30, avoidOverlap: true, avoidOverlapPadding: 10, nodeDimensionsIncludeLabels: true, spacingFactor: 1 } }); cy.resize(); } }; </script> <style> #cy { height: 100%; width: 100%; position: absolute; } </style> ``` 在这个示例代码中,我们定义了一个包含10个节点和9条边的形数据,然后使用 Cytoscape.js 将这个数据渲染成一个关系谱。其中,我们使用了 Circle 布局,将节点排列成一个圆形,并且通过 `avoidOverlap` 和 `avoidOverlapPadding` 属性来避免节点元素的重叠。最后,我们通过调用 `cy.resize()` 来居中展示整个形。 当然,在实际开发中,您可能需要根据具体情况进行调整和优化,例如更改节点和边的样式、使用其他布局算法等等。希望这个示例代码能够帮助您入门,祝您编写愉快!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

emo哥老白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值