Bootstrap

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

实现两个节点之间多条关系线的显示,我们需要使用二次贝塞尔曲线来画图,主要是对两个节点之间的关系线进行编号或者分类,另一种写法在这里:https://blog.csdn.net/m0_54479027/article/details/140517284?spm=1001.2014.3001.5501
首先看一下数据

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: 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: '下属'},
      ],

先看一下实现的效果
在这里插入图片描述

先计算每个节点的关系的数量,并设置关系线的弧线

if (links.length > 0) {
        _.each(links, function (link) {
          let same = _.filter(links, {
            source: link.source,
            target: link.target
          });
          let sameAlt = _.filter(links, {
            source: link.target,
            target: link.source
          });
          let sameAll = same.concat(sameAlt);
          _.each(sameAll, function (s, i) {
            s.sameIndex = i + 1; //当前关系线在相同的起始节点和目标节点组合中的索引(从1开始)。
            s.sameTotal = sameAll.length; //相同的起始节点和目标节点组合中的关系线总数。
            s.sameTotalHalf = s.sameTotal / 2; //相同的起始节点和目标节点组合中关系线总数的一半。
            s.sameUneven = s.sameTotal % 2 !== 0; //判断相同的起始节点和目标节点组合中的关系线总数是否为奇数。
            s.sameMiddleLink =
              s.sameUneven === true &&
              Math.ceil(s.sameTotalHalf) === s.sameIndex; //判断当前关系线是否处于奇数个关系线中的中间位置。
            s.sameLowerHalf = s.sameIndex <= s.sameTotalHalf; //判断当前关系线是否处于关系线总数的前半部分。
            s.sameArcDirection = 1; //弧线方向,通常为0或1。
            s.sameArcDirection = s.sameLowerHalf ? 0 : 1;
            s.sameIndexCorrected = s.sameLowerHalf
              ? s.sameIndex
              : s.sameIndex - Math.ceil(s.sameTotalHalf);
          }); //修正后的关系线索引,根据是否处于关系线总数的前半部分进行调整。
        });
        let maxSame = _.chain(links) //关系线总数的一半,用于计算弯曲路径时的参数。
          .sortBy(function (x) {
            return x.sameTotal;
          })
          .last()
          .value().sameTotal;

        _.each(links, function (link) {
          link.maxSameHalf = Math.round(maxSame / 2);
        });
      }

创建关系,注意曲线用path,line是直线

// 创建关系线
      const link = g.selectAll('path')
        .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}`);

添加关系的描述

// 添加关系线的relate文字
      const linkText = link.append('text')
        .attr('class', 'linktext')
        .style('fill', 'black')
        .style('font-size', 8)
        .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;
        let dr = Math.sqrt(dx * dx + dy * dy),
          unevenCorrection = d.sameUneven ? 0 : 0.5;
        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) {
          dr = 40 / d.sameTotal;
          return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y + 3);
        }
        let curvature = 2,
          arc =
            (1.0 / curvature) *
            ((dr * d.maxSameHalf) / (d.sameIndexCorrected - unevenCorrection));
        if (d.sameMiddleLink) {
          arc = 0;
        }
        let dd = "M" + startX + "," + startY + "A" + arc + "," + arc + " 0 0," + d.sameArcDirection + " " + endX + "," + endY;
        return dd;
      }

最后附上完整代码

<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: 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;

      //关系定义
      if (links.length > 0) {
        _.each(links, function (link) {
          let same = _.filter(links, {
            source: link.source,
            target: link.target
          });
          let sameAlt = _.filter(links, {
            source: link.target,
            target: link.source
          });
          let sameAll = same.concat(sameAlt);
          _.each(sameAll, function (s, i) {
            s.sameIndex = i + 1; //当前关系线在相同的起始节点和目标节点组合中的索引(从1开始)。
            s.sameTotal = sameAll.length; //相同的起始节点和目标节点组合中的关系线总数。
            s.sameTotalHalf = s.sameTotal / 2; //相同的起始节点和目标节点组合中关系线总数的一半。
            s.sameUneven = s.sameTotal % 2 !== 0; //判断相同的起始节点和目标节点组合中的关系线总数是否为奇数。
            s.sameMiddleLink =
              s.sameUneven === true &&
              Math.ceil(s.sameTotalHalf) === s.sameIndex; //判断当前关系线是否处于奇数个关系线中的中间位置。
            s.sameLowerHalf = s.sameIndex <= s.sameTotalHalf; //判断当前关系线是否处于关系线总数的前半部分。
            s.sameArcDirection = 1; //弧线方向,通常为0或1。
            s.sameArcDirection = s.sameLowerHalf ? 0 : 1;
            s.sameIndexCorrected = s.sameLowerHalf
              ? s.sameIndex
              : s.sameIndex - Math.ceil(s.sameTotalHalf);
          }); //修正后的关系线索引,根据是否处于关系线总数的前半部分进行调整。
        });
        let maxSame = _.chain(links) //关系线总数的一半,用于计算弯曲路径时的参数。
          .sortBy(function (x) {
            return x.sameTotal;
          })
          .last()
          .value().sameTotal;

        _.each(links, function (link) {
          link.maxSameHalf = Math.round(maxSame / 2);
        });
      }

      // 添加箭头定义
      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(150)) // 增大节点间距
        .force('charge', d3.forceManyBody().strength(-200)) // 增大节点间斥力
        .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 link = g.selectAll('path')
        .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}`);
      // 添加关系线的relate文字
      const linkText = link.append('text')
        .attr('class', 'linktext')
        .style('fill', 'black')
        .style('font-size', 8)
        .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;
        let dr = Math.sqrt(dx * dx + dy * dy),
          unevenCorrection = d.sameUneven ? 0 : 0.5;
        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) {
          dr = 40 / d.sameTotal;
          return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y + 3);
        }
        let curvature = 2,
          arc =
            (1.0 / curvature) *
            ((dr * d.maxSameHalf) / (d.sameIndexCorrected - unevenCorrection));
        if (d.sameMiddleLink) {
          arc = 0;
        }
        let dd = "M" + startX + "," + startY + "A" + arc + "," + arc + " 0 0," + d.sameArcDirection + " " + endX + "," + endY;
        return dd;
      }

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

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


    }
  }
};
</script>

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

;