vue项目中使用d3实现树结构
1. vue.js
安装vue.js
最新稳定版本
$ npm install vue
最新稳定 CSP 兼容版本
$ npm install vue@csp
2. d3
安装d3
$ npm i d3 --save
vue中引入
import * as d3 from 'd3'
3. 代码部分
(1)生成树实现
参考:https://www.jianshu.com/p/4e517d4c3885
参考文章里实现了点击节点会收起子节点功能,由于我的需求里没有用到就删除了那部分,如果有需要的去原文章里看哦~
(在实现中根据需求我将树的圆形结点改为了矩形结点,需要圆形结点可将)
nodeEnter.append("rect")
改为
nodeEnter.append("circle")
生成树部分代码
<script>
const dataset = {
name:"标题1",
children:[
{
name:"标题2标题2标题2标",
children:[
{
name:"标题3",
children:[
{name:"标题4",value:100},
]
},
{name:"标题3",value:100},
{name:"标题3",value:100},
]
},
{
name:"标题2标题2标",
children:[
{name:"标题3" ,value:100},
{name:"标题3",value:100},
{name:"标题3",value:100},
]
}
]
}
import * as d3 from 'd3'
export default {
name: 'Scale',
data() {
return {
id: '',
zoom: null,
index: 0,
duration: 750,
root: null,
nodes: [],
links: [],
dTreeData: null,
transform: null,
margin: { top: 0, right: 90, bottom: 30, left: 180 }
}
},
methods: {
uuid () {
function s4 () {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1)
}
return (
s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
)
},
/**
* @description 获取构造根节点
*/
getRoot () {
let root = d3.hierarchy(dataset, d => {
return d.children
})
root.x0 = this.height / 2
root.y0 = 0
return root
},
clicktext (d) {
this.$alert(d.data.name, '标题名称', {
confirmButtonText: '确定',
});
},
diagonal (s, d) {
return `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
},
/**
* @description 获取构造的node数据和link数据
*/
getNodesAndLinks () {
// 树状图根据根节点生成新的x、y坐标,
// 所以不能使用vue的计算螺旋桨
this.dTreeData = this.treemap(this.root)
this.nodes = this.dTreeData.descendants()
this.links = this.dTreeData.descendants().slice(1)
},
/**
* @description 数据与Dom进行绑定
*/
update (source) {
this.getNodesAndLinks()
this.nodes.forEach(d => {
d.y = d.depth * 140
})
// *************************** Nodes section *************************** //
// 更新节点
const svg = d3.select(this.$el).select('svg.d3-tree')
const container = svg.select('g.container')
let node = container.selectAll('g.node')
.data(this.nodes, d => {
return d.id || (d.id = ++this.index)
})
// 在父级之前的位置输入任何新源
let nodeEnter = node.enter().append('g')
.attr('class', 'node')
//.on('click', this.clickNode)
.attr('transform', d => {
return 'translate(' + source.y0 + ',' + source.x0 + ')'
})
nodeEnter.append("rect")
.attr("width", 85)
.attr("height",30)
.attr("x",function(d){
return d.children?-50:-1;
})
.attr("y",-16)
.attr("dy",10)
.attr("rx",5)
.attr("ry",5)
.attr("fill","#0076B3")
.attr("stroke","#0076B3")
.attr("stroke-width",1)
nodeEnter.append("foreignObject")
.attr("x",function(d){
return d.children?-30:20;
})
.attr("y",-10)
.attr("dy",10)
.attr("width","50")
.attr("height","20")
.on("click", this.clicktext)
.text(function(d){
return d.data.name;
})
.style("font-size","14px");
let nodeUpdate = nodeEnter.merge(node)
.transition()
.duration(this.duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// *************************** Links section *************************** //
// 更新链接
let link = container.selectAll('path.link')
.data(this.links, d => { return d.id })
// 在父级之前的位置输入任何新链接
let linkEnter = link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", d => {
let o = {x: source.x0, y: source.y0};
return this.diagonal(o, o)
})
.attr("fill", 'none')
.attr("stroke-width", 1)
.attr('stroke', '#ccc')
// 过渡链接到他们的新位置
let linkUpdate = linkEnter.merge(link)
linkUpdate.transition()
.duration(this.duration)
.attr('d', d => { return this.diagonal(d, d.parent) })
},
/**
* @description 控制画布放大或缩小
*/
zoomed () {
d3.select(this.$el).select('g.container').attr('transform', d3.event.transform)
}
},
created() {
this.id = this.uuid()
},
mounted () {
//创建svg画布
this.width = document.getElementById(this.id).clientWidth
this.height = document.getElementById(this.id).clientHeight
const svg = d3.select(this.$el).select('svg.d3-tree')
.attr('width', this.width)
.attr('height', this.height)
const transform = d3.zoomIdentity.translate(this.margin.left, this.margin.top).scale(1)
const container = svg.select('g.container')
// 初始化缩放行为,它既是对象又是函数
this.zoom = d3.zoom()
.scaleExtent([1 / 2, 8])
.on('zoom', this.zoomed)
container.transition().duration(750).call(this.zoom.transform, transform)
//svg.call(this.zoom) 可随意移动树的位置
this.root = this.getRoot()
this.update(this.root)
},
computed: {
treemap () {
return d3.tree().size([this.height, this.width])
}
}
}
</script>
(2)如果想要树的连接线是直线
diagonal (s, d) {
return `M ${s.y} ${s.x}
L ${(s.y + d.y) / 2} ${s.x},
L ${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
},
(3)处理文字溢出,溢出部分显示…
a. 坑1
需求是文字溢出要显示…,当光标移上去时显示所有文字
一看到这个我想这好办吖,overflow ellipsis 吧啦吧啦不就得了。
可是不管我怎么写都不行的时候我发现我错了,原来使用svg里面的text是没有width属性的,按传统css方法overflow不起作用,快把百度翻遍了也找不到一个合适的方法,突然看到一个回答说使用foreignObject,赶紧试了一下,样式终于起作用了!
.d3-tree
.node foreignObject
font: 14px sans-serif;
color #fff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover
overflow: visible;
(4)鼠标点击提示框显示完整标题
其实完全可以直接使用alert,但这里使用了element ui组件的提示框,我jiao的比较好看~
(使用要安装element-ui哦)
$ npm install element-ui -S
main.js
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
Vue.use(ElementUI)
clicktext (d) {
this.$alert(d.data.name, '标题名称', {
confirmButtonText: '确定',
});
}
(5)全部样式
<style lang='stylus'>
.down{
border: 1px solid rgb(196, 196, 196);
width: 822px;
height: 387px;
margin-top: 24px;
background-color:rgb(255, 255, 255);
}
.p_title
width:48px;
height:20px;
font-size:14px;
font-family:PingFangSC-Regular;
font-weight:400;
color:rgba(45,100,119,1);
line-height:20px;
margin: 30px 640px 0px 134px;
.tree-container
width: 100%;
height: 320px;
.d3-tree
.node foreignObject
font: 14px sans-serif;
color #fff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover
overflow: visible;
</style>
a. 坑2
一开始在样式处加了 scoped,使用css修改树结点样式总是不起作用,发现原来是 scoped在作祟,最终删除 scoped就能正常显示了
//<style lang="stylus" scoped>
<style lang="stylus">
(6)HTML部分
<template lang='pug'>
<div class="down">
<p class="p_title">目录树</p>
div.tree-container(:id="id")
svg.d3-tree
g.container
</div>
</template>
a. 坑3
Cannot find module 'pug'
记得要安装pug吖
啥事是pug?
是一个HTML模板引擎,它是HAML在JavaScript上的实现,在这里是使用缩进排列替代成对标签。
4. 结果展示
5. 完整代码
<template lang='pug'>
<div class="down">
<p class="p_title">目录树</p>
div.tree-container(:id="id")
svg.d3-tree
g.container
</div>
</template>
<script>
const dataset = {
name:"标题1",
children:[
{
name:"标题2标题2标题2标",
children:[
{
name:"标题3",
children:[
{name:"标题4",value:100},
]
},
{name:"标题3",value:100},
{name:"标题3",value:100},
]
},
{
name:"标题2标题2标",
children:[
{name:"标题3" ,value:100},
{name:"标题3",value:100},
{name:"标题3",value:100},
]
}
]
}
import * as d3 from 'd3'
export default {
name: 'Scale',
data() {
return {
id: '',
zoom: null,
index: 0,
duration: 750,
root: null,
nodes: [],
links: [],
dTreeData: null,
transform: null,
margin: { top: 0, right: 90, bottom: 30, left: 180 }
}
},
methods: {
uuid () {
function s4 () {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1)
}
return (
s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
)
},
/**
* @description 获取构造根节点
*/
getRoot () {
let root = d3.hierarchy(dataset, d => {
return d.children
})
root.x0 = this.height / 2
root.y0 = 0
return root
},
clicktext (d) {
this.$alert(d.data.name, '标题名称', {
confirmButtonText: '确定',
});
},
diagonal (s, d) {
return `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
// return `M ${s.y} ${s.x}
// L ${(s.y + d.y) / 2} ${s.x},
// L ${(s.y + d.y) / 2} ${d.x},
// ${d.y} ${d.x}`
},
/**
* @description 获取构造的node数据和link数据
*/
getNodesAndLinks () {
// 树状图根据根节点生成新的x、y坐标,
// 所以不能使用vue的计算螺旋桨
this.dTreeData = this.treemap(this.root)
this.nodes = this.dTreeData.descendants()
this.links = this.dTreeData.descendants().slice(1)
},
/**
* @description 数据与Dom进行绑定
*/
update (source) {
this.getNodesAndLinks()
this.nodes.forEach(d => {
d.y = d.depth * 140
})
// *************************** Nodes section *************************** //
// 更新节点
const svg = d3.select(this.$el).select('svg.d3-tree')
const container = svg.select('g.container')
let node = container.selectAll('g.node')
.data(this.nodes, d => {
return d.id || (d.id = ++this.index)
})
// 在父级之前的位置输入任何新源
let nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr('transform', d => {
return 'translate(' + source.y0 + ',' + source.x0 + ')'
})
nodeEnter.append("rect")
.attr("width", 85)
.attr("height",30)
.attr("x",function(d){
return d.children?-50:-1;
})
.attr("y",-16)
.attr("dy",10)
.attr("rx",5)
.attr("ry",5)
.attr("fill","#0076B3")
.attr("stroke","#0076B3")
.attr("stroke-width",1)
nodeEnter.append("foreignObject")
.attr("x",function(d){
return d.children?-30:20;
})
.attr("y",-10)
.attr("dy",10)
.attr("width","50")
.attr("height","20")
.on("click", this.clicktext)
.text(function(d){
return d.data.name;
})
.style("font-size","14px");
let nodeUpdate = nodeEnter.merge(node)
.transition()
.duration(this.duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// *************************** Links section *************************** //
// 更新链接
let link = container.selectAll('path.link')
.data(this.links, d => { return d.id })
// 在父级之前的位置输入任何新链接
let linkEnter = link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", d => {
let o = {x: source.x0, y: source.y0};
return this.diagonal(o, o)
})
.attr("fill", 'none')
.attr("stroke-width", 1)
.attr('stroke', '#ccc')
// 过渡链接到他们的新位置
let linkUpdate = linkEnter.merge(link)
linkUpdate.transition()
.duration(this.duration)
.attr('d', d => { return this.diagonal(d, d.parent) })
},
/**
* @description 控制画布放大或缩小
*/
zoomed () {
d3.select(this.$el).select('g.container').attr('transform', d3.event.transform)
}
},
created() {
this.id = this.uuid()
},
mounted () {
//创建svg画布
this.width = document.getElementById(this.id).clientWidth
this.height = document.getElementById(this.id).clientHeight
const svg = d3.select(this.$el).select('svg.d3-tree')
.attr('width', this.width)
.attr('height', this.height)
const transform = d3.zoomIdentity.translate(this.margin.left, this.margin.top).scale(1)
const container = svg.select('g.container')
// 初始化缩放行为,它既是对象又是函数
this.zoom = d3.zoom()
.scaleExtent([1 / 2, 8])
.on('zoom', this.zoomed)
container.transition().duration(750).call(this.zoom.transform, transform)
//svg.call(this.zoom) 可随意移动树的位置
this.root = this.getRoot()
this.update(this.root)
},
computed: {
treemap () {
return d3.tree().size([this.height, this.width])
}
}
}
</script>
<style lang='stylus'>
.down{
border: 1px solid rgb(196, 196, 196);
width: 822px;
height: 387px;
margin-top: 24px;
background-color:rgb(255, 255, 255);
}
.p_title
width:48px;
height:20px;
font-size:14px;
font-family:PingFangSC-Regular;
font-weight:400;
color:rgba(45,100,119,1);
line-height:20px;
margin: 30px 640px 0px 134px;
.tree-container
width: 100%;
height: 320px;
.d3-tree
.node foreignObject
font: 14px sans-serif;
color #fff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover
overflow: visible;
</style>
结语
初接触前端vue.js,有问题还请多多指出。
一点儿心得…遇到问题没关系,只要不放弃,总能解决…