d3.js画树状图
我们在项目开发过程中,会遇到项目要求:可视化的树状图,1:满足收缩展开。2:点击跳转。 3:放大缩小,平移。4:鼠标移到某一范围,弹框信息提示 我 研究了一下echart.js中的树状图,echart.js可满足 1:收缩展开,2:放大缩小,平移。3:鼠标移到某一范围, 弹框信息提示。**而且echart.js中展开收缩展开和点击跳转事件冲突 **,考虑以上种种, 我就使用 d3.js构建树状图了。
效果图:
直接上代码
<template>
<div :id="id" class="tree-container">
<svg class="d3-tree ">
<g class="container"></g>
</svg>
<!-- 加字图片 -->
<svg id="mySvg_jia" width="0" height="0" >
<defs id="mdef">
<pattern id="image_jia" x="0" y="0" height="20" width="20">
<image x="0" y="0" width="20" height="20" xlink:href="@/assets/images/taskManage/jia.svg"></image>
</pattern>
</defs>
</svg>
<!-- 减字图片 -->
<svg id="mySvg_jian" width="0" height="0" >
<defs id="mdef">
<pattern id="image_jian" x="0" y="0" height="20" width="20">
<image x="0" y="0" width="20" height="20" xlink:href="@/assets/images/taskManage/jian.svg"></image>
</pattern>
</defs>
</svg>
<div class=" tip_content" v-if="visible" :style="{left:clientX+'px',top:clientY+'px'}">
<div>任务名称:{{popoverData.taskName?popoverData.taskName:''}}</div>
<div>{{popoverData.taskLevel===1||popoverData.taskLevel===2?"创建人":"分解人"}}:{{popoverData.createName?popoverData.createName:''}}</div>
<div>{{popoverData.taskLevel===1?'审核人:':'上级审核人:'}}{{popoverData.superAuditorName?popoverData.superAuditorName:''}}</div>
<div>处理人:{{popoverData.taskHandlerUserStr?popoverData.taskHandlerUserStr:''}}</div>
<div>任务完成进度:{{popoverData.taskProgress?popoverData.taskProgress:'0/0'}}</div>
<div>当前任务状态:{{popoverData.taskStatusName?popoverData.taskStatusName:''}}</div>
<div>当前反馈起止时间:{{popoverData.taskStartTime?popoverData.taskStartTime:'无'}} - {{popoverData.taskEndTime?popoverData.taskEndTime:'无'}}</div>
<div :class="popper_arrow==='right'?'arrow_right':'arrow_left'" ></div>
</div>
<trajectoryTable :dialogShow.sync="dialogShow" v-if="dialogShow" :clickData="clickData" :orderMes="orderMes"></trajectoryTable>
<!-- 新增任务,编辑任务 -->
<addMes :dialogShow.sync="addShow" v-if="addShow" :orderMes="trajectoryMes" :organizationObj="organizationObj" @close="closeDia" ></addMes>
</div>
</template>
mounted () {
this.findTaskTrack()
//创建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');
const transform = d3.zoomIdentity.translate(this.width/2, 140).scale(1) ;
// init zoom behavior, which is both an object and function
this.zoom = d3.zoom()
.scaleExtent([1 / 8, 8])
.on('zoom', (e)=>{
this.visible = false;
this.x = d3.event.transform.x ;
this.y = d3.event.transform.y ;
this.scale = d3.event.transform.k
d3.select(this.$el).select('g.container').attr('transform', d3.event.transform)
})
svg.call(this.zoom)
svg.transition().duration(750).call(this.zoom.transform, transform)
},
computed: {
treemap () {
return d3.tree().size([this.height, this.width]).nodeSize([240, 200]);
}
},
methods: {
// 编辑 分解
editMes(row,type){
this.trajectoryMes = {
type: type,
data: {...row,id:row.taskId},
taskMethod: 1,
verifyPermissions: 'none'
};
this.addShow = true;
},
// 关闭弹框
closeDia(bol){
this.addShow = false;
this.findTaskTrack()
// this.visibleShow = false;
// this.showMes = false;
// bol?this.handleSearch():this.handleSearchClear()
},
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(this.treeData, d => {
return d.subTaskList
})
root.x0 = this.width / 2
root.y0 = 140
return root
},
/**
* @description 点击节点,展开or收缩
*/
clickNode (d) {
this.stopDialog = true;
if (!d._children && !d.children)
return
if (d.children) {
this.$set(d, '_children', d.children)
d.children = null
} else {
this.$set(d, 'children', d._children)
d._children = null
}
this.$nextTick(
() => {
this.update(d)
}
)
},
// 点击穿透
clickUrl(d){
console.log(d)
if(!this.planOrGroundBol) return;
this.clickData = d.data;
this.dialogShow = true;
},
// 点击分解按钮
decompose(d){
var e = d3.event;
if(e.stopPropagation){
e.stopPropagation();
}else{
window.event.returnValue == false;
}
this.clickData = d.data;
this.editMes(d.data,'dis')
},
// 鼠标移上效果
addVisible(d){
if(this.stopDialog) return this.stopDialog = false;
this.visible = true;
this.popoverData = d.data;
if(this.x+(d.x+110)*this.scale>this.width-320) {
this.popper_arrow = 'left';
this.clientX = this.x+(d.x-110)*this.scale-336;
}else{
this.popper_arrow = 'right';
this.clientX = 16+this.x+(d.x+110)*this.scale;
}
this.clientY = this.y+(d.y-120)*this.scale;
},
delVisible(d){
this.visible = false
},
diagonal (s, d) {
// `M ${s.x} ${s.y}
// C ${(s.x + d.x) / 2} ${s.y},
// ${(s.x + d.x) / 2} ${d.y},
// ${d.x} ${d.y}`
return `M ${s.x} ${s.y}
C ${s.x} ${d.y},
${s.x} ${d.y},
${d.x} ${d.y}`
},
/**
* @description 获取构造的node数据和link数据
*/
getNodesAndLinks () {
// treemap generate new x、y coordinate according to root node,
// so can‘t use computed propter of 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 * 180
})
// *************************** Nodes section *************************** //
// Update the nodes...
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)
})
// Enter any new sources at the parent's previous position.
let nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr('transform', d => {
return 'translate(' + source.x0 + ',' + source.y0 + ')'
})
nodeEnter.append("circle")
.attr("r", 10)
.attr("stroke", "#fff")
.attr("stroke-width", 1)
.on('click', this.clickNode)
.style("fill", function(d) {
if(d.children) return "url(#image_jian)";
if(d._children) return "url(#image_jia)";
return "#fff";
});
// 创建一个
let rectText = nodeEnter.append("g")
.on('click', this.clickUrl)
.on('mouseenter', this.addVisible)
.on('mouseleave', this.delVisible)
.style("fill-opacity", 1)
.style("cursor", 'pointer')
.attr("transform", function(d) { return "translate(" + 0 + "," + 0 + ")"; });
// 节点背景图
rectText.append("rect")
.attr("x", -110)
.attr("y", -120)
.attr("rx", 6)
.attr("ry", 6)
.attr("width", 220)
.attr("height", 120)
.attr("fill", "white")
.attr("stroke", (d)=>{
if(d.data.taskLevel===1){
return "#33a39c"
}else{
return "#999"
}
})
// .html(function(d,i) { "#33a39c"
// return 'some text' + '<p class="eqe">1545</p>' + d.data.name;
// })
.style("fill-opacity", 1);
// 标题区域
let titleText = rectText.append("g")
.attr("transform", function(d) { return "translate(" +-110 + "," + -120 + ")"; });
titleText.append("rect")
.attr("width", 220)
.attr("height", 40)
.attr("rx", 6)
.attr("ry",6)
.attr("fill", "#33a39c")
.attr("stroke", "#33a39c")
// .html(function(d,i) {
// return 'some text' + '<p class="eqe">1545</p>' + d.data.name;
// })
.style("fill-opacity", 1);
// 标题
titleText.append("text")
.attr("x",(d)=>{
return this.orderMes.source==='all'&&d.data.taskLevel!==1?90:110
})
.attr("y", 0)
.attr("dy", 26)
.attr("fill", "#fff")
.attr("text-anchor", 'middle' )
.text(function(d) {
if(d.data.taskName&&d.data.taskName.length>=7){
return `${d.data.taskLevel===1?'':'子'}任务名称:${d.data.taskName.substring(0,6)}...`
}else{
return `${d.data.taskLevel===1?'':'子'}任务名称:${d.data.taskName}`
}
})
.style("fill-opacity", 1e-6);
// 分解按钮
titleText.append("text")
.on('click', this.decompose)
.attr("x",160)
.attr("y", 0)
.attr("dy", 26)
.attr("fill", "#0000dd")
.attr("text-anchor", 'right' )
.text('(分解)')
.style("display",(d)=>{
//return 'block'
return this.orderMes.source==='all'&&d.data.taskLevel!==1?'block':'none'
})
.style("text-decoration","underline")
.style("fill-opacity", 1);
// 创建人
rectText.append("text")
.attr("x",-100)
.attr("y", -75)
.attr("dy", 20)
.attr("text-anchor", 'left' )
.text(function(d) { return '创建人:'+d.data.createName })
.style("fill-opacity", 1);
// 处理人
rectText.append("text")
.attr("x",-100)
.attr("y", -40)
.attr("dy", 20)
.attr("text-anchor", 'left' )
.text(function(d) {
if(d.data.taskHandlerUserStr&&d.data.taskHandlerUserStr.length>=7){
return '处理人 :'+d.data.taskHandlerUserStr.substring(0,6)+'...'
}else{
return '处理人 :'+d.data.taskHandlerUserStr
}
})
.style("fill-opacity", 1);
// 进度条
let circleText = rectText.append("g")
.attr("transform", function(d) { return "translate(" + 80 + "," + -50 + ")"; });
circleText.append("circle")
.attr("r", 16)
.style("fill", function(d) {
if(d.data.taskStatus===1){
return "#f7c692";
}else if(d.data.taskStatus===2){
return "#FA8C15";
}else if(d.data.taskStatus===3){
return "#64D16D";
} else if(d.data.taskStatus===4){
return "#1592E6";
} else if(d.data.taskStatus===5){
return "#666565";
}else{
return "#E62412";
}
});
// 进度值
circleText.append("text")
.attr("y", 40)
.attr("text-anchor", 'middle' )
.attr("fill", function(d) {
if(d.data.taskStatus===1){
return "#f7c692";
}else if(d.data.taskStatus===2){
return "#FA8C15";
}else if(d.data.taskStatus===3){
return "#64D16D";
} else if(d.data.taskStatus===4){
return "#1592E6";
} else if(d.data.taskStatus===5){
return "#666565";
}else{
return "#E62412";
}
})
.text(function(d) { return d.data.taskProgress })
.style("fill-opacity", 1);
// Transition nodes to their new position.
let nodeUpdate = nodeEnter.merge(node)
.transition()
.duration(this.duration)
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
// 节点更新
nodeUpdate.select("circle")
.attr("r", 10)
.attr("stroke", "#fff")
.attr("stroke-width", 1)
.style("fill", function(d) {
if(d.children) return "url(#image_jian)";
if(d._children) return "url(#image_jia)";
return "#fff";
})
.attr("transform", function(d) { return "translate(" + 0 + "," + 10 + ")"; });
nodeUpdate.select("text")
.style("fill-opacity", 1)
// Transition exiting nodes to the parent's new position.
let nodeExit = node.exit()
.transition()
.duration(this.duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// *************************** Links section *************************** //
// Update the links…
let link = container.selectAll('path.link')
.data(this.links, d => { return d.id })
// Enter any new links at the parent's previous position.
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')
// Transition links to their new position.
let linkUpdate = linkEnter.merge(link)
linkUpdate.transition()
.duration(this.duration)
.attr('d', d => { return this.diagonal(d, d.parent) })
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(this.duration)
.attr("d", d => {
let o = {x: source.x, y: source.y};
return this.diagonal(o, o)
})
.remove();
// Stash the old positions for transition.
this.nodes.forEach(d => {
d.x0 = d.x
d.y0 = d.y
})
},
``
欢迎互相学习, 不动了发我邮箱 ty_html5@163.com