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>

  • 19
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值