精选文章 52.在vue中使用d3创建力导向图并动态新增、删除节点及联系

52.在vue中使用d3创建力导向图并动态新增、删除节点及联系

作者:夏小拙 时间: 2020-07-27 04:25:39
夏小拙 2020-07-27 04:25:39

0.写在前面

要实现的功能如图:

52.在vue中使用d3创建力导向图并动态新增、删除节点及联系1

1.安装d3

npm install d3 --save-dev

2.在页面中引入d3

import * as d3 from 'd3'

3.在页面中增加热词tag和类名为container的div元素


  
  • {{ item }}
生成结论

4.初始化力导向图

(1)调接口获取nodes和links后走initGraph方法

async getKeyWord(data) {
  const response = await get_keyword(data)
  this.testGraph['nodes'] = response.data.nodes
  this.testGraph['links'] = response.data.links
  this.initGraph(this.testGraph)
},

(2)初始化前先清除上次的图谱 获取的数据存成全局变量

由于要注解,就不把所有代码一次性粘贴上了

如下图,在initGraph方法里,通过d3.select('#the_SVG_ID').remove() 清除上次的图谱;

由于更新图谱时需要往nodes和links数组里添加节点及联系,存成全局变量方便在其他方法里使用。这里需要引入Vue;

import Vue from 'vue'
// 存入全局变量
Vue.prototype.$links = links
Vue.prototype.$nodes = nodes

// 引用全局变量
that.$links
that.$nodes

下边3块代码分别是添加碰撞力和引力及控制link的长度、在container元素中创建svg元素用来放节点和连线、缩放;

 这里应该会注意到 that ,这个that使用的是全局变量,因为点击node节点的箭头函数里this不生效,所以设置了that代替this:

52.在vue中使用d3创建力导向图并动态新增、删除节点及联系2

initGraph(data) {
   d3.select('#the_SVG_ID').remove()
   const links = data.links.map(d => Object.create(d))
   const nodes = data.nodes.map(d => Object.create(d))
   Vue.prototype.$links = links
   Vue.prototype.$nodes = nodes

   // .distance(160))  改变link的长度
   that.simulation = d3.forceSimulation(that.$nodes)
     .force('link', d3.forceLink(links).id(d => d.keyword).distance(150))
     .force('collide', d3.forceCollide().radius(() => 90)) //  碰撞力
     .force('charge', d3.forceManyBody().strength(-90)) // 引力
     .force('center', d3.forceCenter(that.width / 2, that.height / 2));
  
   // 创建svg元素 初始化样式
   const svg = d3.select('.container')
     .append('svg')
     .attr('id', 'the_SVG_ID')
     .attr('viewBox', [0, 0, that.width, that.height])
     .style('width', 1600)
     .style('height', 800)

   // 缩放
   svg.call(d3.zoom().on('zoom', function() {
     g.attr('transform', d3.event.transform)
   }))
}

(3)设置箭头控制方向、svg元素中创建g元素并向其中加入svg_links和linksName

// 两个marker控制箭头方向  stroke-width 箭头粗细  refX 偏移  orient 朝向
const positiveMarker = svg.append('marker')
  .attr('id', 'positiveMarker')
  .attr('orient', 'auto')
  .attr('stroke-width', 2)
  .attr('markerUnits', 'strokeWidth')
  .attr('markerUnits', 'userSpaceOnUse')
  .attr('viewBox', '0 -5 10 10')
  .attr('refX', 26)
  .attr('refY', 0)
  .attr('markerWidth', 12)
  .attr('markerHeight', 12)
  .append('path')
  .attr('d', 'M 0 -5 L 10 0 L 0 5')
  .attr('fill', '#999')
  .attr('stroke-opacity', 0.6)

const negativeMarker = svg.append('marker')
  .attr('id', 'negativeMarker')
  .attr('orient', 'auto')
  .attr('stroke-width', 2)
  .attr('markerUnits', 'strokeWidth')
  .attr('markerUnits', 'userSpaceOnUse')
  .attr('viewBox', '0 -5 10 10')
  .attr('refX', -16)
  .attr('refY', 0)
  .attr('markerWidth', 12)
  .attr('markerHeight', 12)
  .append('path')
  .attr('d', 'M 10 -5 L 0 0 L 10 5')
  .attr('fill', '#999')
  .attr('stroke-opacity', 0.6)

// 在svg中创建g元素 将node和link放在g元素中 更精确
const g = svg.append('g')

// .attr("marker-end","url(#direction)") 添加箭头
that.svg_links = g.append('g')
  .attr('stroke', '#999')
  .attr('stroke-opacity', 0.6)
  .attr('marker-end', 'url(#direction)')
  .selectAll('path')
  .data(that.$links)
  .join('path')
  .attr('stroke-width', d => Math.sqrt(d.value))
  .attr('id', function(d) {
    if (typeof (d.source) === 'object') {
      return d.source.keyword + '_' + d.relationship + '_' + d.target.keyword
    } else {
      return d.source + '_' + d.relationship + '_' + d.target
    }
  })

// linksName 连线上的文字 text-anchor 锚点 startOffset 开始偏移  这两个属性实现居中
that.linksName = g.append('g')
  .selectAll('text')
  .data(that.$links)
  .join('text')
  // .attr('x', 70)
  // .attr('y', 60)
  .style('text-anchor', 'middle')
  .style('fill', '#595959')
  .style('font-size', '12px')
  .style('font-weight', 'bold')
  .append('textPath')
  .attr(
    'xlink:href', function(d) {
      if (typeof (d.source) === 'object') {
        return '#' + d.source.keyword + '_' + d.relationship + '_' + d.target.keyword
      } else {
        return '#' + d.source + '_' + d.relationship + '_' + d.target
      }
    }
  )
  .attr('startOffset', '50%')
  // .attr('dx', 10)
  // .attr('dy', 10)
  .text(function(d) {
    if (d.count) {
      return '数量:' + d.count
    } else {
      return '数量:' + 1
    }
  })

(4)向g元素中添加svg_nodes 点击节点时向热词列表添加热词并调接口获取该节点相关图谱

如下图:点击节点时,先判断热词列表里有没有该节点,并且group为2(评论)也不添加;

获取的新nodes和links肯定有重复的,所以需要判断是否重复;

最后添加到全局变量that.$nodes和that.$links中,并调updateGraph方法动态更新图谱

that.svg_nodes = g.append('g')
  .attr('stroke', '#fff')
  .attr('stroke-width', 1.5)
  .selectAll('circle')
  .data(that.$nodes)
  .join('circle')
  .attr('r', function(d) {
    if (d.group === 2) {
      return 25
    } else {
      return 20
    }
  })
  .attr('class', 'node')
  .attr('fill', that.color)
  // 点击元素获取对应信息
  .on('click', function(d, i) {
    that.isTrue = false
    that.wordData = []
    that.clickWord.map((item,index) =>{
      if(d.group === 2){
        return
      }else if(that.clickWord.indexOf(d.keyword) != -1){
        return 
      }else{
        that.clickWord.push(d.keyword)
      }
    })
    // 根据人名d.keyword 查询到对应的link联系
    const data = {
      'word': d.keyword,
      'start_time': that.start_time,
      'end_time': that.end_time
    }
    get_keyword(data)
      .then(function(res) {
        if (res.status) {
          res.data.nodes.map(item => {
            let flag = true
            for (var j = 0; j < that.$nodes.length; j++) {
              if (that.$nodes[j].keyword === item.keyword) {
                flag = false
                break
              }
            }
            if (flag) {
              that.$nodes.push(item)
            }
          })
          res.data.links.map(item1 => {
            let flag = true
            for (var j = 0; j < that.$links.length; j++) {
              if (that.$links[j].target.keyword === item1.source === d.keyword) {
                that.$links.splice(j, 1)
              }
              if (that.$links[j].source.keyword === item1.source) {
                flag = false
                break
              }
            }
            if (flag) {
              that.$links.push(item1)
            }
          })
          that.updateGraph(d.keyword)
        }
      })
      .catch(function(err) {
        console.log(err)
      })
  })
  .call(that.drag(that.simulation))

that.svg_nodes.append('title')
  .text(function(d) {
    return d.keyword
  })

(5)向g元素中添加节点名称nodesName 设置力图布局

设置力图布局这里就用到了两个箭头常量:#positiveMarker   #negativeMarker

// nodesName title显示在node下方
that.nodesName = g.append('g')
  .selectAll('text')
  .data(that.$nodes)
  .join('text')
  .text(function(d) {
    if (d.keyword.length > 2) {
      return d.keyword.slice(0, 2) + '...'
    } else {
      return d.keyword
    }
  })
  // .attr('dx', function() {
  //   return this.getBoundingClientRect().width / 2 * (-1)
  // })
  .attr('dx', -15)
  .attr('dy', 10)
  .attr('class', 'nodeName')

// 力图布局
that.simulation.on('tick', () => {
  that.svg_links
    .attr('d', function(d) {
      if (d.source.x < d.target.x) {
        return 'M' + d.source.x + ' ' + d.source.y + 'L' + d.target.x + ' ' + d.target.y
      } else {
        return 'M' + d.target.x + ' ' + d.target.y + 'L' + d.source.x + ' ' + d.source.y
      }
    })
    .attr('marker-end', function(d) {
      if (d.source.x < d.target.x) {
        return 'url(#positiveMarker)'
      } else {
        return null
      }
    })
    .attr('marker-start', function(d) {
      if (d.source.x < d.target.x) {
        return null
      } else {
        return 'url(#negativeMarker)'
      }
    })

  that.svg_nodes
    .attr('cx', d => d.x)
    .attr('cy', d => d.y)

  that.nodesName
    .attr('x', d => d.x)
    .attr('y', d => d.y)
})

5.动态更新图谱方法

在点击图谱中的某个热词时,会先调接口获取与之相关的热词及联系,然后再调这个updateGraph方法

(1)遍历节点将点击节点改变填充颜色 遍历连线改变箭头方向

如下图:在这个更新图谱方法中,首先先去遍历所有节点,截取其id,判断哪个包含点击的热词,将其填充颜色改成绿色;

上步完成,就是遍历所有连线,将之前的评论与你点击的热词间的连线删掉,因为新的关系出来,连线箭头会改变

updateGraph(keyword){
  var sel = d3.select(that.svg_nodes)._groups[0][0]._groups[0]
  sel.map((item,index) =>{
    let tempArr = []
    tempArr = item.innerHTML.split('')
    const newStr = tempArr.join('')
    let tempArr1 = []
    tempArr1 = newStr.split('')
    const newStr1 = tempArr1.join('')
    if(newStr1.length < 20 && item.__data__.group == 1){
      if(newStr1.indexOf(keyword) != -1){
        sel[index].style.fill = '#82E0AA'
      }
    }
  })
  
  that.svg_links._groups[0].map((item,index) =>{
    if(item.id.slice(0,15).indexOf('app_keyword') != -1){
      return
    }else{
      let uid = item.id.substring(item.id.length - 5)
      if(uid.indexOf(keyword) != -1){
        d3.select(that.svg_links._groups[0][index]).remove()
      }
    }
  })
}

(2)向初始节点数组中添加新节点 点击节点再次调用更新图谱方法

 如下图:在点击节点的方法中,先遍历所有连线,判断点击的节点是否不是之前评论相连的节点,并且是新的评论相连节点

如果两个条件都满足,再做一下限制,之前点过的节点和group为2即为评论的节点都不能再点击

最后和初始化里一样,先调接口获取点击的节点相关联的热词和联系,再调updateGraph方法

that.svg_nodes = that.svg_nodes
  .data(that.$nodes)
  .enter()
  .append('circle')
  .attr('r', function(d) {
    if (d.group === 2) {
      return 25
    } else {
      return 20
    }
  })
  .attr('fill', that.color)
  .attr('class', 'node')
  .merge(that.svg_nodes)
  .on('click', function(d, i) {
    for(var i=0;i{
          if(d.group === 2){
            return
          }else if(that.clickWord.indexOf(d.keyword) != -1){
            return 
          }else{
            that.clickWord.push(d.keyword)
          }
        })
        // 根据人名d.keyword 查询到对应的link联系
        const data = {
          'word': d.keyword,
          'start_time': that.start_time,
          'end_time': that.end_time
        }
        get_keyword(data)
          .then(function(res) {
            if (res.status) {
              res.data.nodes.map(item => {
                let flag = true
                for (var j = 0; j < that.$nodes.length; j++) {
                  if (that.$nodes[j].keyword === item.keyword) {
                    flag = false
                    break
                  }
                }
                if (flag) {
                  that.$nodes.push(item)
                }
              })
              res.data.links.map(item1 => {
                let flag = true
                for (var j = 0; j < that.$links.length; j++) {
                  if (that.$links[j].target.keyword === item1.source === d.keyword) {
                    that.$links.splice(j, 1)
                  }
                  if (that.$links[j].source.keyword === item1.source) {
                    flag = false
                    break
                  }
                }
                if (flag) {
                  that.$links.push(item1)
                }
              })
              that.updateGraph(d.keyword)
            }
          })
          .catch(function(err) {
            console.log(err)
          })
      }
    }
  })
  .call(that.drag(that.simulation))

(3)添加新节点名称、新连线、新连线名称 并重新启动simulation

that.svg_nodes.append('title')
  .text(function(d) {
    return d.keyword
  })

// nodesName title显示在node下方
that.nodesName = that.nodesName
  .data(that.$nodes)
  .enter()
  .append('text')
  .merge(that.nodesName)
  .text(function(d) {
    if (d.keyword.length > 2) {
      return d.keyword.slice(0, 2) + '...'
    } else {
      return d.keyword
    }
  })
  .attr('dx', -10)
  .attr('dy', 8)
  .attr('class', 'nodeName')

that.svg_links = that.svg_links
  .data(that.$links)
  .enter()
  .append('path')
  .attr('stroke', '#999')
  .attr('stroke-opacity', 0.6)
  .attr('stroke-width', d => Math.sqrt(d.value))
  .attr('marker-end', 'url(#direction)')
  .attr('id', function(d) {
    if (typeof (d.source) === 'object') {
      return d.source.keyword + '_' + d.relationship + '_' + d.target.keyword
    } else {
      return d.source + '_' + d.relationship + '_' + d.target
    }
  })
  .merge(that.svg_links)

// linksName 连线上文字
that.linksName = that.linksName
  .data(that.$links)
  .enter()
  .append('text')
  .style('text-anchor', 'middle')
  .style('fill', 'black')
  .style('font-size', '10px')
  .style('font-weight', 'bold')
  .append('textPath')
  .attr(
    'xlink:href', function(d) {
      if (typeof (d.source) === 'object') {
        return '#' + d.source.keyword + '_' + d.relationship + '_' + d.target.keyword
      } else {
        return '#' + d.source + '_' + d.relationship + '_' + d.target
      }
    }
  )
  .attr('startOffset', '50%')
  .merge(that.linksName)
  .text(function(d) {
    if (d.count) {
      return '数量:' + d.count
    } else {
      return '数量:' + 1
    }
  })

that.simulation.nodes(that.$nodes)
that.simulation.force('link').links(that.$links)
that.simulation.alpha(1).restart()

6.删除选词

这里根据需求,设置的是当删除的是第一个选词的话,会清空图谱,删除其他的再去判断

如下图:在删除其他选词时,会先循环选词数组,如果删除的选词与数组的某一个相同则删除,

然后遍历links数组,如果某个连线的id前几个词包含选词数组某个词则也跟着删除;

这里的意思就是,当删除的那个词后边的词是由你删除的词散开的词,则跟着删除,否则不删

最后就是遍历选词数组剩余的词,如果是第一个还走 initGraph 方法;否则走 updateGraph 方法

// 删除选词结果
delWord(val) {
  if (val == that.clickWord[0]) {
    d3.select('#the_SVG_ID').remove()
    that.clickWord = []
    that.isTrue = false
    that.wordValue = ''
    that.wordData = []
    that.wordFuzzyData = []
  }else{
    for(var i=1;i{
          if(typeof(that.clickWord[i]) != 'undefined'){
            let uid = ''
            if(that.clickWord[i].length > 2){
              uid = item.id.substring(0,4)
            }else{
              uid = item.id.substring(0,2)
            }
            if(uid.indexOf(that.clickWord[i]) != -1){
              that.clickWord.splice(i,1)
            }
          }
        })
      }
    }
  }
  that.clickWord.map((item,index) =>{
    if(index == 0){
      const data = {
        'word': item,
        'start_time': this.start_time,
        'end_time': this.end_time
      }
      this.getKeyWord(data)
    }
    if(index != 0){
      const data = {
        'word': item,
        'start_time': that.start_time,
        'end_time': that.end_time
      }
      get_graphdata_app_keyword(data)
        .then(function(res) {
          if (res.status) {
            res.data.nodes.map(item2 => {
              let flag = true
              for (var j = 0; j < that.$nodes.length; j++) {
                if (that.$nodes[j].keyword === item2.keyword) {
                  flag = false
                  break
                }
              }
              if (flag) {
                that.$nodes.push(item2)
              }
            })
            res.data.links.map(item3 => {
              let flag = true
              for (var j = 0; j < that.$links.length; j++) {
                if (that.$links[j].target.keyword === item3.source === item) {
                  that.$links.splice(j, 1)
                }
                if (that.$links[j].source.keyword === item3.source) {
                  flag = false
                  break
                }
              }
              if (flag) {
                that.$links.push(item3)
              }
            })
            that.updateGraph(item)
          }
        })
        .catch(function(err) {
          console.log(err)
        })
    }
  })
},

(本文完)

勿删,copyright占位
分享文章到微博
分享文章到朋友圈

上一篇:Hbuildx--启动微信小程序项目

下一篇:PowerDesigner165 安装教程

您可能感兴趣

  • Vue render函数

    前几天想学学Vue中怎么编写可复用的组件,提到要对Vue的render函数有所了解。可仔细一想,对于Vue的render函数自己只是看了官方的一些介绍,并未深入一点去了解这方面的知识。为了更好的学习后续的知识,又折回来了解Vue中的render函数,这一切主要都是为了后续能更好的学习Vue的知识。 回忆Vue的一些基本概念 今天我们学习的目的是了解和学习Vue的render函数。如果想要更好...

  • SQL Server中的聚集索引与堆

    摘要 (Summary) There are few topics so widely misunderstood and that generates such frequent bad advice as that of the decision of how to index a table. Specifically, the decision to use a heap over ...

  • vue知识点总结

    # 1.ES6新增? *1、变量的改变* *let:代码块内有效;不能重复声明;不存在变量提升* *const:只读变量,声明之后不允许改变。意味着,一旦声明必须初始化,否则会报错。;* *2、模版字符串(``)* *3、函数* **1、箭头函数 (sender) => { } 箭头函数最直观的三个特点。 不需要function关键字 可以省略return关键字 继承当前上下文的this关键...

  • C++程序员应了解的那些事(8) PIMPL模式(指向实现的指针)

    【1】C++之善用PIMPL技巧解決/改善C++编码时常碰到的2大问题。 (1)class增加private/protected成员时,使用此class的相关 .cpp(s) 需要重新编译。 (2)定义冲突与跨平台编译。 (1)class增加private/protected成员时,使用此class的相关 .cpp(s) 需要重新编译。 假设我们有一个A.h(class A),并且有A/B/...

  • ZooKeeper基本原理

    ZooKeeper简介 ZooKeeper是一个开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。 ZooKeeper设计目的 1.最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。 2.可靠性:具有简单、健壮、良好的性能,如果消息m被到一台服务器接受,那么它将被...

  • 个人理解数据中台与大数据平台区别

    个人理解数据中台与大数据平台区别 概念介绍 本文主要介绍如下几个数据概念: 数据库 数据库是“按照数据结构来组织、存储和管理数据的仓库”。是一个长期存储在计算机内的、有组织的、有共享的、统一管理的数据集合。 数据库是以一定方式储存在一起、能与多个用户共享、具有尽可能小的冗余度、与应用程序彼此独立的数据集合,可视为电子化的文件柜——存储电子文件的处所,用户可以对文件中的数据进行新增、查询、更新...

  • Redisson基础资料汇总

    一.前言 分布式系统有一个著名的理论CAP,指在一个分布式系统中,最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项,CAP原则是常用的一个原则。 C一致性: 在分布式环境当中,对于多个数据源,多个数据库对数据的访问能不能满足隔离性,一致性,原子性等要求,是分布式系统的重点和难点 A可用性:...

  • 2020年iOS面试反思总结

    Object-C系列面试题总结 基础题: 1.Objective-C的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方式用继承好还是分类好?为什么不要在category中重写一个类原有的方法? 答: Objective-c的类不可以有多继承,OC里面都是单继承,多继承可以用protocol委托代理来模拟实现 可以实现多个接口,可以通过实现多个接口完成OC的多重继承...

华为云40多款云服务产品0元试用活动

免费套餐,马上领取!
CSDN

CSDN

中国开发者社区CSDN (Chinese Software Developer Network) 创立于1999年,致力为中国开发者提供知识传播、在线学习、职业发展等全生命周期服务。