前言:因为最近公司业务需求,需要在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>