d3.js v4实现关系拓扑图

前言:因为最近公司业务需求,需要在App中实现关系拓扑图。由于自己之前学习了下Vue.js和一些前端知识和ThinkPHP。就自告奋勇把任务接下来了。后来才发现真的是too young too simple,后端数据接口和Android部分都还好说,但这个拓扑图真是让我欲哭无泪,后来了解了Vis.js和百度出品的echart,也都尝试着去实现自己想要的功能。却都因为可定制化差强人意,不能满足我的强迫症而放弃。这时,我的目光落在了d3.js上。比起上面两个框架,虽然d3.js可定制化能力强,但这也意味着学习成本更高,实现难度更大。而最巧的是d3.js从v3版本升级到了v4版本,其中的接口都发生了很大的变化,网上的相关的资料并不多,而且官方文档又是英文比较“简洁”。对新手来说真的十分不友好,我只好看着文档和网上仅有的资料不断抹着眼泪,不断尝试。自己接的任务,跪着也要写完。

以下是核心代码:

主要分为三块:html代码和js代码、css代码,这里css用的预编译器是stylus

<template>
  <div id="app" class="container">
    <button type="button" class="exit" @click="jsBack" ref="exit" v-if="isPc">退出</button>
  </div>
</template>

<script type="text/ecmascript-6">
  //  import vis from 'vis'
  import * as d3 from 'd3'

  export default {
    name: 'app',
    data () {
      return {
        relation: {},
        cname: '',
        isPc: false
      }
    },
    methods: {
      jsBack () {
//        返回按钮
        var u = navigator.userAgent
        var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1
        var isiOS = u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) //ios终端
        if (isAndroid) {
//          Android端
          window.android.back()
        } else if (isiOS) {
//          ios端
        }
      },
      showd3 () {
//        获取body高度和宽度
        let height = document.body.clientHeight
        let width = document.body.clientWidth

        //移动端设备横竖屏重新加载页面
        function change () {
          window.location.reload()
        }

        window.addEventListener('onorientationchange' in window ? 'orientationchange' : 'resize', change, false)
//        节点大小(圆圈大小)
        const nodeSize = 35
//        初始化时连接线的距离长度
        const linkDistance = 130
//        赋值数据集
        var nodes = this.relation.nodes
        var links = this.relation.links
//      设置画布,获取id为app的对象,添加svg,这里的图像用了svg,意为可缩放矢量图形,它与其他图片格式相比较,svg更加小,因为是矢量图,放大不会失帧。具体可以自行百度svg相关知识
        var svg = d3.select('#app').append('svg')
          .attr('xmlns', 'http://www.w3.org/2000/svg')
          .attr('version', '2.0')
          .attr('class', 'svg')//给svg设置了一个class样式,主要作用是长宽设置为100%
//        设置力布局,使用d3 v4版本的力导向布局
        var force = d3.forceSimulation()
          .force('center', d3.forceCenter(width / 2, height / 2))//设置力导向布局的中心点,创建一个力中心,设置为画布长宽的一半,所以拓扑图会在画布的中心点
          .force('charce', d3.forceManyBody().strength(-70))//节点间的作用力,如果不设置.strength(-60)的话,默认是-30
          .force('collide', d3.forceCollide())//使用默认的半径创建一个碰撞作用力。radius默认所有的节点都为1

//        设置缩放
//        svg下嵌套g标签,缩放都在g标签上进行
        var g = svg.append('g')
//        d3.zoom是设置缩放,pc端是滚轮进行缩放,在移动端可以通过两指进行缩放
        var zoomObj = d3.zoom()
          .scaleExtent([0.5, 1.2]) // 设置缩放范围
          .on('zoom', () => {
            //监听zoom事件,zoom发生时,调用该方法
            const transform = d3.event.transform //获取缩放和偏移的数据,不懂得同学可以自行通过console.log(d3.event.transform)滑动滚轮查看数据变化
            g.attr('transform', transform)   // 设置缩放和偏移量 transform对象自带toString()方法
          })
          .on('end', () => {
//            该方法在缩放时间结束后回调
            // code
          })
        svg.call(zoomObj)
//        绘制箭头
        //箭头
        // eslint-disable-next-line no-unused-vars
        var markerBlue =
          g.append('marker')
            .attr('id', 'resolvedBlue')
            //.attr("markerUnits","strokeWidth")//设置为strokeWidth箭头会随着线的粗细发生变化
            .attr('markerUnits', 'userSpaceOnUse')//用于确定marker是否进行缩放。取值strokeWidth和userSpaceOnUse,
            .attr('viewBox', '0 -5 10 10')//坐标系的区域
            .attr('refX', 39)//箭头坐标
            .attr('refY', 0)
            .attr('markerWidth', 12)//标识的大小
            .attr('markerHeight', 12)
            .attr('orient', 'auto')//绘制方向,可设定为:auto(自动确认方向)和 角度值
            .attr('stroke-width', 2)//箭头宽度
            .append('path')
            .attr('d', 'M0,-5L10,0L0,5')//箭头的路径
            .attr('fill', '#029ed9')//箭头颜色
        // eslint-disable-next-line no-unused-vars
        var markerRed =
          g.append('marker')
            .attr('id', 'resolvedRed')
            //.attr("markerUnits","strokeWidth")//设置为strokeWidth箭头会随着线的粗细发生变化
            .attr('markerUnits', 'userSpaceOnUse')//用于确定marker是否进行缩放。取值strokeWidth和userSpaceOnUse,
            .attr('viewBox', '0 -5 10 10')//坐标系的区域
            .attr('refX', 39)//箭头坐标
            .attr('refY', 0)
            .attr('markerWidth', 12)//标识的大小
            .attr('markerHeight', 12)
            .attr('orient', 'auto')//绘制方向,可设定为:auto(自动确认方向)和 角度值
            .attr('stroke-width', 2)//箭头宽度
            .append('path')
            .attr('d', 'M0,-5L10,0L0,5')//箭头的路径
            .attr('fill', '#ff4238')//箭头颜色

//        设置连线
        var edgesLine = g.selectAll('line')
          .data(links)
          .enter()
          .append('path')
          .attr('class', 'edgelabel')//添加class样式
          .attr('class', (d, i) => {
            if (d.relation === '投资') {
              return 'nodeBlue'
            } else if (d.relation === '股东') {
              return 'nodeRed'
            }
          })//添加颜色
          .style('stroke-width', 1)//连接线粗细度
          .attr('marker-end', (d, i) => {
            if (d.relation === '投资') {
              return 'url(#resolvedBlue)'
            } else if (d.relation === '股东') {
              return 'url(#resolvedRed)'
            }
          })
        //设置线的末尾为刚刚的箭头
//        设置连接线中间关系文本
        var edgesText = g.selectAll('.linetext')
          .data(links)
          .enter()
          .append('text')
          .attr('class', (d, i) => {
            if (d.relation === '投资') {
              return 'linetextBlue'
            } else if (d.relation === '股东' || d.relation === '分支机构') {
              return 'linetextRed'
            }
          })
          .text((d) => {
//          设置关系文本
            return d.relation
          })
//        设置拖拽
        var drag = d3.drag()
          .on('start', (d, i) => {
            if (!d3.event.active) {
//              拖拽开始回调
              force.alphaTarget(0.1).restart() // 这个方法可以用在在交互时重新启动仿真,比如拖拽了某个节点,重新进行布局。这个必须要进行设置不然会拖动不了。
            }
            d.fixed = true //偏移后固定不动
//            d3.event.sourceEvent.stopPropagation()
            d.fx = d.x//记录当前默认位置(x - 节点当前的 x-位置,如果要为某个节点设置默认的位置,则需要为该节点设置如下两个属性:fx =x位置)
            d.fy = d.y
          })
          .on('drag', (d, i) => {
//            拖动时,设置拖动后默认位置的x,y
            d.fx = d3.event.x
            d.fy = d3.event.y
          })
          .on('end', (d, i) => {
//            拖动结束后
            if (!d3.event.active) {
              force.alphaTarget(0)
            }
          })
        // eslint-disable-next-line no-unused-vars
        var nodeGroup = g.selectAll('g').data(nodes)
          .enter()
          .append('g')
          .attr('id', function (d, i) {
            return 'nodeGroup' + i
          })
          .each(function (d, i) {
            var self = this
            d3.select(this)
              .append('circle')
              .attr('r', nodeSize)
              .attr('class', (d, i) => {
//            为不同的节点设置不同的css样式
                if (d.type === 0) {
                  return 'nodeOrange'
                } else if (d.type === 1) {
                  return 'nodeBlue'
                } else if (d.type === 2) {
                  return 'nodeRed'
                }
              })
              .attr('id', (d, i) => {
//            为每个节点设置不同的id
                return 'node' + i
              })
              .on('touchmove', (d, i) => {
//            设置鼠标监听时间,当移动端手指移动时,设置关系文本透明度
                edgesText.style('fill-opacity', function (edge) {
                  if (edge.source === d || edge.target === d) {
                    return 1.0
                  } else {
                    return 0
                  }
                })
                /**
                 * 改本svg的层级,这个主要是因为在svg中z-index是无效的,svg根据绘制的先后顺序,后绘制的排在最上面,就像贴纸,
                 * 后贴的会盖住前面贴的。所以我们希望在被选中时,能够把节点和节点对应的文字提到最上一层,我们就可以通过d3来选择到点击的对象,然后通过raise方法来提到最上一层
                 * 下同
                 */
                d3.select(self).raise()
              })
              .on('touchend', (d, i) => {
//            手指移开后,所有关系文本设置透明度为1
                edgesText.style('fill-opacity', function (edge) {
                  return 1.0
                })
              })
              .on('mousedown', (d, i) => {
                edgesText.style('fill-opacity', function (edge) {
                  if (edge.source === d || edge.target === d) {
                    return 1.0
                  } else {
                    return 0
                  }
                })
                d3.select(self).raise()
              })
              .on('mouseout', (d, i) => {
                edgesText.attr('fill-opacity', function (edge) {
                  return 1
                })
              })
              .call(drag)//监听拖动事件
            d3.select(this)
              .append('text')
              .attr('text-anchor', 'middle')
              .attr('class', 'nodetext')
              .attr('id', (d, i) => {
                return 'nodetext' + i
              })
              .attr('x', function (d, i) {
                /**
                 * 由于svg的text不能进行换行,所以下面文字使用了tspan进行换行操作
                 */
                  //正则表达式
                var reEn = /[a-zA-Z]+/g
                //如果全英文则不换行
                if (d.name.match(reEn)) {
                  d3.select(this).append('tspan')
                    .attr('class', 'nodetext')
                    .attr('fill', '#ff7438')
                    .text(function () { return d.name })
                } else if (d.name.length <= 4) {
                  //文中小于4个字不换行
                  d3.select(this).append('tspan')
                    .attr('class', 'nodetext')
                    .attr('fill', '#ff7438')
                    .text(function () { return d.name })
                } else {
                  if (d.name.length <= 8) {
                    //中文小于八个字,则分段进行换行
                    let top = d.name.substring(0, 4)
                    let bot = d.name.substring(4, 8)
                    //这里的this指代text dom,不懂的可以自行打印this查看
                    d3.select(this).append('tspan')
                      .text(function () { return top })
                    d3.select(this).append('tspan')
                      .attr('dy', '1.2em')//设置偏移
                      .text(function () { return bot })
                  } else {
                    //中文大于8个字,分段并用...代替后面的字符
                    let top = d.name.substring(0, 4)
                    let bot = d.name.substring(4, 7) + '...'
                    d3.select(this).append('tspan')
                      .text(function () { return top })
                    d3.select(this).append('tspan')
                      .attr('dy', '1.2em')
                      .text(function () { return bot })
                  }
                }
              })
              .attr('cursor', 'default')//设置鼠标样式
              .on('touchmove', (d, i) => {
                edgesText.style('fill-opacity', function (edge) {
                  if (edge.source === d || edge.target === d) {
                    return 1.0
                  } else {
                    return 0
                  }
                })
                //改本svg的层级
                d3.select(self).raise()
              })
              .on('touchend', (d, i) => {
                edgesText.style('fill-opacity', function (edge) {
                  return 1.0
                })
              })
              .on('mousedown', (d, i) => {
                edgesText.style('fill-opacity', function (edge) {
                  if (edge.source === d || edge.target === d) {
                    return 1.0
                  } else {
                    return 0
                  }
                })
                d3.select(self).raise()
              })
              .on('mouseout', (d, i) => {
                edgesText.style('fill-opacity', function (edge) {
                  return 1.0
                })
              })
              .call(drag)
          })

//        设置node和edge
        force.nodes(nodes)
          .force('link', d3.forceLink(links).distance(linkDistance).strength(0.1))
          .restart()
//        tick 表示当运动进行中每更新一帧时
        force.on('tick', function () {
//          //更新连接线的位置
          edgesLine.attr('d', function (d) {
            var path = 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y
            return path
          })

          //更新连接线上文字的位置
          edgesText.attr('x', function (d) {
            return (d.source.x + d.target.x) / 2
          })
          edgesText.attr('y', function (d) { return (d.source.y + d.target.y) / 2 })

          //更新结点和文字
          d3.selectAll('circle').attr('cx', function (d) {
            return d.x
          })
          d3.selectAll('circle').attr('cy', function (d) { return d.y })
          d3.selectAll('.nodetext').attr('x', function (d) { return d.x })
          d3.selectAll('.nodetext').attr('y', function (d) { return d.y })
          //动态更新sptan 的x的坐标
          d3.selectAll('.nodetext').selectAll('tspan')
            .attr('x', function (d) {
              return d.x
            })
        })
      }
    },
    created () {
      this.$nextTick(() => {
        this.relation = JSON.parse('{"nodes":[{"name":"vicky","type":0},{"name":"海南荣恒投资控股有限公司","type":1},{"name":"长白山保护开发区恒力建设有限公司","type":1},{"name":"海宁市恒立房地产开发有限公司","type":1},{"name":"湖州白领氏房地产开发有限公司","type":1},{"name":"左建平","type":2},{"name":"恒力建设集团","type":2},{"name":"张金良","type":2},{"name":"林德仙","type":2},{"name":"钟李彬","type":2},{"name":"黄萍","type":2},{"name":"冯培华","type":2},{"name":"张乐英","type":2},{"name":"董志坚","type":2},{"name":"凌勇","type":2},{"name":"钟云锋","type":2},{"name":"翟鑫森","type":2},{"name":"糜妙娟","type":2}],"links":[{"source":0,"target":1,"relation":"投资"},{"source":0,"target":2,"relation":"投资"},{"source":0,"target":3,"relation":"投资"},{"source":0,"target":4,"relation":"投资"},{"source":5,"target":0,"relation":"股东"},{"source":6,"target":0,"relation":"股东"},{"source":7,"target":0,"relation":"股东"},{"source":8,"target":0,"relation":"股东"},{"source":9,"target":0,"relation":"股东"},{"source":10,"target":0,"relation":"股东"},{"source":11,"target":0,"relation":"股东"},{"source":12,"target":0,"relation":"股东"},{"source":13,"target":0,"relation":"股东"},{"source":14,"target":0,"relation":"股东"},{"source":15,"target":0,"relation":"股东"},{"source":16,"target":0,"relation":"股东"},{"source":17,"target":0,"relation":"股东"}],"code":200,"message":"请求成功"}')
        console.log(this.relation)
        this.showd3()
      })
    }
  }
</script>

<style lang="stylus" rel="stylesheet/stylus">
  .container
    height 100%
    .exit
      position absolute
      top 20px
      left 20px
      width 60px
      height 25px
      background #0583f2
      border none
      border-radius 2px
      color #fff
      z-index 200
      &:hover
        background #1e82d9

  .labeltext
    font-size: 16px;
    font-family: SimSun;
    fill: #ff7438;

  .nodetext
    font-size: 12px;
    font-family: SimSun;
    fill: #fff;
    position relative

  .linetextRed
    font-size: 12px
    font-weight bold
    font-family: SimSun
    fill: #ff4238 !important
    color #ff4238
    fill-opacity: 1.0

  .linetextBlue
    font-size: 12px
    font-weight bold
    font-family: SimSun
    fill: #029ed9 !important
    color #029ed9
    fill-opacity: 1.0

  .svg
    position relative
    width 100%
    height 100%

  .edgepath
    pointer-events none
    stroke-width 0.5px

  .nodeOrange
    position relative
    fill #ff7438 !important
    stroke #ff7438

  .nodeRed
    position relative
    fill #ff4238 !important
    stroke #ff4238

  .nodeBlue
    position relative
    fill #029ed9 !important
    stroke #029ed9
</style>

stylus学习请移步:http://www.zhangxinxu.com/jq/stylus/

如有疑问:可加QQ394259438 一起学习讨论

参考文章:https://github.com/xswei/d3js_doc/tree/master/API/d3-force-master#simulation_alphaTarget  d3中文文档

                    http://blog.csdn.net/lzhlzz/article/details/40918561   参考项目

                    http://wiki.jikexueyuan.com/project/d3wiki/introduction.html   d3.js v3基础教程


项目地址:https://github.com/VickyGit/topo


  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
要使用D3.js实现拖拽生成拓扑图,你可以使用D3.js的拖拽行为(drag behavior)和力导向图(layout)来实现。首先,你需要创建一个SVG容器来放置你的拓扑图。然后,使用D3.js的拖拽行为来使节点可拖拽。你可以通过调用`d3.drag()`函数来创建一个拖拽行为,并将其应用到节点上。在拖拽行为的回调函数中,你可以更新节点的位置。接下来,你可以使用D3.js的力导向图布局(layout)来计算节点的位置。你可以使用`d3.forceSimulation()`函数创建一个力导向图布局,并设置力的参数,如引力、斥力等。然后,将节点和边添加到力导向图布局中,并在每一帧更新节点的位置。最后,你可以使用D3.js的选择集(selection)和绑定数据(data binding)来绘制节点和边。你可以使用`d3.select()`函数选择SVG容器,并使用`selection.data()`函数绑定节点和边的数据。然后,使用`selection.enter()`函数创建新的节点和边,并使用`selection.exit()`函数删除不需要的节点和边。通过设置节点和边的位置和样式,你可以绘制出拖拽生成的拓扑图。\[2\]\[3\] #### 引用[.reference_title] - *1* [【工具推荐】使用D3.js制作网页版网络拓扑图,可拖转可跳转链接](https://blog.csdn.net/diandianxiyu/article/details/131003483)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [使用 d3.js 绘制资源拓扑图](https://blog.csdn.net/weixin_38625669/article/details/103763186)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值