3年前写过一个关于d3绘制横向组织机构的帖子d3绘制横向组织机构
,一晃都3年了,中间有好几个人找我咨询相关代码,一直想重构下,这俩天无聊就重构了下,效果图:
功能如下:
- 可以鼠标拖动svg图
- 可以滚动鼠标来进行放大缩小
- 可以点击展开收缩相应节点
- 可以通过切换层级来展示相应层级的节点
上次封装花了800行,采用的d3.v3版本,这次花了200行,采用了d3.v5版本,总体来说不管从交互还是从代码上都有了长足都进步。当然这次不兼容ie8了,实在不想打开ie浏览器来写代码。
废话不多说了,直接上代码:Json数据代码
{
"name": "集团",
"children": [
{
"name": "analytics",
"children": [
{
"name": "cluster",
"hasSon":true,
"children": [
{"name": "AgglomerativeCluster", "value": 3938},
{"name": "CommunityStructure", "value": 3812},
{"name": "HierarchicalCluster", "value": 6714},
{"name": "MergeEdge", "value": 743}
]
},
{
"name": "graph",
"hasSon":true,
"children": [
{"name": "BetweennessCentrality", "value": 3534, "children": [
{"name": "AspectRatioBanker", "value": 7074}
]},
{"name": "LinkDistance", "value": 5731},
{"name": "MaxFlowMinCut", "value": 7840},
{"name": "ShortestPaths", "value": 5914},
{"name": "SpanningTree", "value": 3416}
]
},
{
"name": "optimization",
"children": [
{"name": "AspectRatioBanker", "value": 7074}
]
}
]
}
]
}
2、vue界面代码:
<template>
<div class="go_tab_ctl">
<div ref="tool_bar">
<Row
style="background-color: #F5F7F8;height:40px;line-height: 30px;padding-right: 15px;padding-top: 5px;"
>
<!-- resize按钮-->
<div
class="pull-right resizeIcon icon_item"
@click="sizeChangeBtn('resize')"
>
<i style="font-size: 20px;" class="icon goingicon go-resize"></i>
</div>
<!-- 缩小按钮-->
<div
class="pull-right smallIcon icon_item"
@click="sizeChangeBtn('small')"
>
<i style="font-size: 20px;" class="icon goingicon go-suoxiao"></i>
</div>
<!-- 大小label-->
<div class="pull-right sizeLabel label_item">{{ sizeLabel }}</div>
<!-- 放大按钮-->
<div
class="pull-right bigIcon icon_item"
@click="sizeChangeBtn('big')"
>
<i style="font-size: 20px;" class="icon goingicon go-fangda1"></i>
</div>
<!-- expandItem-->
<div
class="pull-right expandLabel"
style="margin-right: 6px;font-size: 13px;"
>
鼠标可拖动查看架构图
</div>
<!-- 层级显示drop-->
<div class="pull-right showLevel" style="margin-right: 6px;">
<go-drop
v-model="showLevel"
@selDrop="selDrop"
:clearValue="false"
:search="false"
width="70"
:item="showLevelItem"
></go-drop>
</div>
</Row>
</div>
<div id="treeContainer" style="overflow: auto;width: 100%;height: calc(100% - 40px);position: relative;">
</div>
</div>
</template>
<script>
import GoingSvgTree from './goingSvgTree';
import { QueryCommand } from "goingutils";
import * as d3 from 'd3';
import data from './json/flare-2.json';
export default {
name: "accountTreeSvg",
data: function() {
return {
svgCtlObj:null,
treeSvgCtl:null,
pageTabObj: null,
showLevel: 3,
sizeLabel: "100%",
showLevelItem: {
id: "showLevel",
editorType: "staticDrop",
label: "显示层级",
defVal: "3",
items: [
{ id: "1", label: "1" },
{ id: "2", label: "2" },
{ id: "3", label: "3" },
{ id: "4", label: "4" }
]
},
};
},
props: {},
computed: {
contentStyle() {
}
},
methods: {
selDrop(){
if(this.svgCtlObj){
this.svgCtlObj.destruction();
this.initTreeCtl();
}
},
changeSizeLabel(sizeLabel){
this.sizeLabel=parseFloat((parseFloat(sizeLabel+"")/100).toFixed(2)*100).toFixed(0)+"%";
},
//svg图像大小变化按钮
sizeChangeBtn(type) {
if (this.svgCtlObj != null) {
this.svgCtlObj.sizeChange(type);
this.sizeLabel = this.svgCtlObj.getSizeLabel();
}
},
initTreeCtl(){
let width = $("#treeContainer").width();
let height = $("#treeContainer").height();
this.svgCtlObj=new GoingSvgTree(d3,{containerId:'treeContainer',width,height,changeSizeLabel:this.changeSizeLabel,showLevel:(this.showLevel-1)},data);
}
},
mounted() {
this.$nextTick(()=>{
this.initTreeCtl();
});
console.log(data,"==========");
},
watch: {},
components: {},
beforeDestroy(){
}
};
</script>
<style>
.svg-body-container{
height: calc(100% - 40px);overflow: auto;padding-top: 20px
}
.nodeChildrenFlag{
width: 24px;
height: 24px;
border-radius: 50%;
position: absolute;
}
/*.svgTree{*/
/*transform:rotate(90deg);*/
/*}*/
/*.treeText{*/
/*transform:rotate(-90deg);*/
/*}*/
</style>
3、组件封装的代码:
class GoingSvgTree {
constructor(d3, options, data) {
let {width, containerId, changeSizeLabel} = options;
if (changeSizeLabel != null) {
this.changeSizeLabel = changeSizeLabel;
}
this._initOptions(options);
this.d3=d3;
this._sizeLabel = 100;
this.nodeBgColor="#2D7EDC";
this.containerId = containerId;
this.drawTree({width, d3, data});
}
}
/**
* 初始化相关参数
* @param options
* @private
*/
GoingSvgTree.prototype._initOptions = function (options) {
//默认配置
let defConfi={
nodeColor:'#2D7EDC',
nodeOverColor:"#F6A42E",
nodeHeight:33,
defWidth:110,
height:0,
showLevel:2,
width:0,
nodeLeftSpace:160,
nodeTopSpace:40,
nodeWidth:[80,90,120]
};
this.options=$.extend(defConfi, options);
};
/**
* 设置显示对层数
* @param showLevel
* @returns {number}
*/
GoingSvgTree.prototype.setShowLevel= function (showLevel) {
return this.options["showLevel"]=parseInt(showLevel)-1;
};
/**
* 销毁当前的树对象
*/
GoingSvgTree.prototype.destruction = function () {
$("#"+ this.containerId).html("");
for (let pro in this.options) {
this.options[pro] = null;
}
for (let pro in this) {
this[pro] = null;
}
};
/**
* 对图像进行大小变化
* @param type
*/
GoingSvgTree.prototype.changeSvgSize = function () {
let treeObj=this;
treeObj.svg.call(treeObj.zoom).transition().duration(10).call(
treeObj.zoom.transform,
treeObj.d3.zoomIdentity.scale(parseFloat(treeObj._sizeLabel)/100)
);
};
/**
* 获取图像size的label
* @param type
*/
GoingSvgTree.prototype.getSizeLabel = function () {
return this._sizeLabel + "%";
};
/**
* 根据类型进行图像大小的变化
* @param type
*/
GoingSvgTree.prototype.sizeChange = function (type) {
if (type === "big") {
//如果放到到200%则不能再放大了
this._sizeLabel = parseFloat(this._sizeLabel) + 10;
if (parseFloat(this._sizeLabel) > 200) {
this._sizeLabel = parseFloat(this._sizeLabel) - 10;
return;
}
} else if (type === "small") {
//如果缩小大50%了则不能再缩小了
this._sizeLabel = parseFloat(this._sizeLabel) - 10;
if (parseFloat(this._sizeLabel) < 50) {
this._sizeLabel = parseFloat(this._sizeLabel) + 10;
return;
}
} else {
this._sizeLabel = 100;
}
this.changeSvgSize();
};
/**
* 对svg添加缩放功能
* @param d3
* @param svg
* @param root
* @private
*/
GoingSvgTree.prototype._initZoom = function (d3, svg, root, g, x0) {
let treeObj = this;
let sizeTimer = null;
//进行大小变化
function zoomed() {
treeObj._sizeLabel = parseFloat(d3.event.transform.k).toFixed(2)*100+"%";
treeObj._sizeLabel=parseFloat(treeObj._sizeLabel).toFixed(2);
if (treeObj.changeSizeLabel != null) {
if (sizeTimer != null) {
clearTimeout(sizeTimer);
}
sizeTimer = setTimeout(() => {
treeObj.changeSizeLabel(treeObj._sizeLabel);
}, 10);
}
g.attr("transform", d3.event.transform);
}
const zoom = d3.zoom()
.scaleExtent([1 / 2, 10])
.on("zoom", zoomed);
treeObj.zoom=zoom;
//scaleExtent设置放大缩小的方法
svg.call(zoom).transition().duration(10).call(
zoom.transform,
d3.zoomIdentity.translate(100, 100)
);
};
//绘制节点
GoingSvgTree.prototype._drawNodes=function(source,options){
let {svg,root,gNode,d3,width,margin}=options;
if(source.children!=null){
source.children.forEach((itemObj,index)=>{
if(index===0){
itemObj["first"]=true;
}else if(index===(source.children.length-1)){
itemObj["last"]=true;
}else{
itemObj["path"]=false;
}
});
d3.select("#cir_"+source.id).attr("opacity", 1);
d3.select("#icon_"+source.id).attr("opacity", 1).attr("xlink:href",window.SysConfigUtils.go_eplat_adress+"/static/svg/line.svg");
console.log("---sss-1--");
}else{
if(source.hasChildren){
d3.select("#cir_"+source.id).attr("opacity", 1);
d3.select("#icon_"+source.id).attr("opacity", 1);
d3.select("#icon_"+source.id).attr("xlink:href",window.SysConfigUtils.go_eplat_adress+"/static/svg/plus.svg");
}else{
d3.select("#cir_"+source.id).attr("opacity", 0);
d3.select("#icon_"+source.id).attr("opacity", 0);
}
}
const duration = d3.event && d3.event.altKey ? 2500 : 250;
let treeObj=this;
let left = root;
const nodes = root.descendants().reverse();
// Update the nodes…
const node = gNode.selectAll("g")
.data(nodes, d => d.id);
const transition = svg.transition()
.duration(duration)
.attr("class", "svgTree")
.attr("viewBox", [-margin.left, left.x - margin.top, width, this.viewHeight])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("transform", (d) => {
if(d.children!=null){
d.children.forEach((itemObj,index)=>{
if(index===0){
itemObj["first"]=true;
}else if(index===(d.children.length-1)){
itemObj["last"]=true;
}else{
itemObj["path"]=false;
}
});
}
return `translate(${source.y0},${source.x0+40})`;
})
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", d => {
if(d["depth"]===0){
return void(0);
}
d.children = d.children ? null : d._children;
treeObj.expandNode(d,options);
});
//绘制节点
nodeEnter.append("rect")
.attr("rx", 4)
.attr("ry", 4)
.attr("width", (d)=> {
d.nodeWidth=this.options["nodeWidth"][d.depth]||this.options["defWidth"];
return d.nodeWidth;
})
.attr("height", (d)=> {
d.nodeHeigh=this.options["nodeHeight"];
return this.options["nodeHeight"];
})
.attr("fill", treeObj.options["nodeColor"])
.on("mouseover",function(d){
d3.select(this)
.attr("fill",treeObj.options["nodeOverColor"]);
})
.on("mouseout",function(d){
d3.select(this)
.transition()
.duration(30)
.attr("fill",treeObj.options["nodeColor"]);
})
.attr("stroke-width", 10);
nodeEnter.append("circle")
.attr("r", 7)
.attr("fill", "#fff")
.attr("stroke", treeObj.options["nodeOverColor"])
.attr("transform", (d) => {
return `translate(${d.nodeWidth},${parseFloat(this.options.nodeHeight)/2-2})`;
})
.attr("id", (d) => {
return "cir_"+ d.id;
})
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1)
.attr("stroke-width", 1);
nodeEnter.append("image")
.attr("xlink:href",(d)=>{
if(d.depth>=treeObj.options["showLevel"]&&d.hasChildren){
return window.SysConfigUtils.go_eplat_adress+"/static/svg/plus.svg";
}else{
if(d.children){
console.log(d,"-----ddd")
return window.SysConfigUtils.go_eplat_adress+"/static/svg/line.svg";
}else{
d3.select("#cir_"+d.id).attr("opacity", 0);
d3.select("#icon_"+d.id).attr("opacity", 0);
}
}
})
.attr("width", 12)
.attr("id", (d) => {
return "icon_"+d.id;
})
.attr("x", (d) => {
return d.nodeWidth-6;
})
.attr("y", (d) => {
return 8;
})
.attr("opacity", 1)
.attr("height", 12)
nodeEnter.append("text")
.attr("y", parseFloat(this.options.nodeHeight)/2+3)
.attr("class", "treeText")
.attr("x", d => d._children ? 8 : 8)
.text(d => d.data.name)
// .clone(true).lower()
.attr("stroke-linejoin", "round")
.attr("stroke-width", "1px")
.style('stroke',"#FFF")
.attr("stroke", "#fff");
// Transition nodes to their new position.
const nodeUpdate = node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
};
/**
* 展开节点
* @param source
* @param options
*/
GoingSvgTree.prototype.expandNode=function(source,options){
let diagonal=this.diagonal;
let {svg,root,gLink,d3,tree,width,margin}=options;
const duration = d3.event && d3.event.altKey ? 2500 : 250;
const links = root.links();
// Compute the new tree layout.
tree(root);
let left = root;
let right = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
});
const transition = svg.transition()
.duration(duration)
.attr("class", "svgTree")
.attr("viewBox", [-margin.left, left.x - margin.top, width, this.viewHeight])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
//绘制节点
this._drawNodes(source,options);
// Update the links…
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
// Enter any new links at the parent's previous position.
const linkEnter = link.enter().append("path")
.attr("d", d => {
const o = {x: source.x0, y: source.y0,path:source.path};
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.merge(linkEnter).transition(transition)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition(transition).remove()
.attr("d", d => {
const o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
});
// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
};
/**
* 这里需要分节点类型来进行区分绘制 目的在于减少重复线条对绘制。
* @param d
* @returns {string}
*/
GoingSvgTree.prototype.diagonal=function(d){
let nodeDefPadding=60;
function getNodePadding(d){
return (d.nodeWidth||80)+nodeDefPadding-33;
}
let yMover=parseInt((d.source.nodeHeight||33)/2);
var path=null;
path = "M" + (d.source.y ) + " " + (d.source.x+yMover) +
"L" + (d.source.y + getNodePadding(d.source)) + " " + ( d.source.x+yMover) +
" L" + (d.source.y + getNodePadding(d.source)) + " " + (d.target.x+yMover) + " L" +
(d.target.y ) + " " + (d.target.x+yMover);
return path;
}
/**
* 绘制树
* @param width
* @param d3
* @param data
* @returns {*}
*/
GoingSvgTree.prototype.drawTree = function ({width, d3, data}) {
let dx = this.options.nodeTopSpace;
let dy = this.options.nodeLeftSpace;
let margin = ({top: 10, right: 120, bottom: 10, left: 40});
const tree = d3.tree().nodeSize([dx, dy]);
const root = d3.hierarchy(data);
root.x0 = dy / 2;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if(d.depth >=this.options.showLevel){
if(d.children!=null){
d.hasChildren=true;
}
d.children = null;
}
// if (d.depth && d.data.name.length !== 7) d.children = null;
});
this.viewHeight=this.options["height"]-20;
const svg = d3.select('#' + this.containerId).append('svg')
.attr("viewBox", [-margin.left, -margin.top, width, this.viewHeight])
.style("font", "10px sans-serif")
.style("user-select", "none");
this.svg=svg;
//拖拽svg图像
function dragged(d) {
if (d != null) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
}
const g = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.call(d3.drag().on("drag", dragged));
this.svgObj = g;
//对svg添加缩放功能
this._initZoom(d3, svg, root, g, root.x0);
const gLink = g.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);
const gNode = g.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");
this.expandNode(root,{svg,root,gLink,gNode,d3,g,tree,width,margin});
};
export default GoingSvgTree;
到此就大功告成了。