d3(新)绘制横向组织机构树

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;

到此就大功告成了。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值