专栏首页一个会写诗的程序员的博客Kotlin 函数式编程 + 递归 : 构建 "树" 数据结构通用模型

Kotlin 函数式编程 + 递归 : 构建 "树" 数据结构通用模型

Kotlin 函数式编程 + 递归 : 构建 "树" 数据结构通用模型

源代码: https://github.com/AK-47-D/cms-spider 分支: i9102_20190407

效果图

原始数据

原始 JSON 层级化的数据:

[
    {
        "code": "100000",
        "parentCode": "-1",
        "category": "COUNTRY",
        "name": "中国",
        "children": [
            {
                "code": "110000",
                "parentCode": "100000",
                "name": "北京市"
            },
            {
                "code": "120000",
                "parentCode": "100000",
                "name": "天津市"
            },
            {
                "code": "130000",
                "parentCode": "100000",
                "name": "河北省"
            },
            {
                "code": "140000",
                "parentCode": "100000",
                "name": "山西省"
            },
            {
                "code": "150000",
                "parentCode": "100000",
                "name": "内蒙古自治区"
            },
            {
                "code": "210000",
                "parentCode": "100000",
                "name": "辽宁省"
            },
            {
                "code": "220000",
                "parentCode": "100000",
                "name": "吉林省"
            },
            {
                "code": "230000",
                "parentCode": "100000",
                "name": "黑龙江省"
            },
            {
                "code": "310000",
                "parentCode": "100000",
                "name": "上海市"
            },
            {
                "code": "320000",
                "parentCode": "100000",
                "name": "江苏省"
            },
            {
                "code": "330000",
                "parentCode": "100000",
                "name": "浙江省"
            },
            {
                "code": "340000",
                "parentCode": "100000",
                "name": "安徽省"
            },
            {
                "code": "350000",
                "parentCode": "100000",
                "name": "福建省"
            },
            {
                "code": "360000",
                "parentCode": "100000",
                "name": "江西省"
            },
            {
                "code": "370000",
                "parentCode": "100000",
                "name": "山东省"
            },
            {
                "code": "410000",
                "parentCode": "100000",
                "name": "河南省"
            },
            {
                "code": "420000",
                "parentCode": "100000",
                "name": "湖北省"
            },
            {
                "code": "430000",
                "parentCode": "100000",
                "name": "湖南省"
            },
            {
                "code": "440000",
                "parentCode": "100000",
                "name": "广东省"
            },
            {
                "code": "450000",
                "parentCode": "100000",
                "name": "广西壮族自治区"
            },
            {
                "code": "460000",
                "parentCode": "100000",
                "name": "海南省"
            },
            {
                "code": "500000",
                "parentCode": "100000",
                "name": "重庆市"
            },
            {
                "code": "510000",
                "parentCode": "100000",
                "name": "四川省"
            },
            {
                "code": "520000",
                "parentCode": "100000",
                "name": "贵州省"
            },
            {
                "code": "530000",
                "parentCode": "100000",
                "name": "云南省"
            },
            {
                "code": "540000",
                "parentCode": "100000",
                "name": "西藏自治区"
            },
            {
                "code": "610000",
                "parentCode": "100000",
                "name": "陕西省"
            },
            {
                "code": "620000",
                "parentCode": "100000",
                "name": "甘肃省"
            },
            {
                "code": "630000",
                "parentCode": "100000",
                "name": "青海省"
            },
            {
                "code": "640000",
                "parentCode": "100000",
                "name": "宁夏回族自治区"
            },
            {
                "code": "650000",
                "parentCode": "100000",
                "name": "新疆维吾尔自治区"
            },
            {
                "code": "810000",
                "parentCode": "100000",
                "name": "中国香港特别行政区"
            },
            {
                "code": "820000",
                "parentCode": "100000",
                "name": "中国澳门特别行政区"
            }
        ]
    }

]

建立数据模型

package com.ak47.cms.cms.tree

import org.hibernate.annotations.GenericGenerator
import java.util.*
import javax.persistence.*

@Entity
@Table(indexes = arrayOf(Index(name = "idx_category", columnList = "category")))
class Tree {

    @Id
    @Column(length = 100)
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "custom-uuid")
    @GenericGenerator(name = "custom-uuid", strategy = "com.ak47.cms.cms.tree.CustomUUIDGenerator")
    var code: String = "0"

    @Column(length = 100)
    var parentCode: String = "0"

    @Column(length = 200)
    var name = ""

    @Column(length = 100)
    var category = ""

    var gmtCreated = Date()
    var gmtModified = Date()
    var isDeleted = 0
    @Version
    var version = 0
}

注意到这里的@Id 生成策略是我们自定义的: com.ak47.cms.cms.tree.CustomUUIDGenerator

package com.ak47.cms.cms.tree

import org.apache.commons.lang3.reflect.FieldUtils
import org.hibernate.engine.spi.SharedSessionContractImplementor
import org.hibernate.id.UUIDGenerator
import java.io.Serializable


class CustomUUIDGenerator : UUIDGenerator() {

    override fun generate(session: SharedSessionContractImplementor, obj: Any): Serializable {
        val code = FieldUtils.readDeclaredField(obj, "code", true)
        if (code != null) {
            return code as Serializable
        }
        return super.generate(session, obj)
    }

}

其中, val code = FieldUtils.readDeclaredField(obj, "code", true) 这一行的写法要注意.

数据库操作

    private fun initTreeCountry() {
        logger.info("initTreeCountry")
        // clean
        // treeRepository.deleteAll()

        // read init file data
        val cp = Thread.currentThread().contextClassLoader.getResource("").path;
        val filePath = "$cp/tree/country.js"
        val initCountryJson = File(filePath).readText()

        val nodes = JSON.parseArray(initCountryJson, TreeVO::class.java)

        nodes.forEach {
            val node = it

            TreeUtil.visitTree(node) {
                val t = Tree()
                t.code = it.code
                t.name = it.name
                t.parentCode = it.parentCode
                t.category = TreeCategory.COUNTRY.name
                try {
                    treeRepository.save(t)
                } catch (e: Exception) {
                    logger.error("t:{}", JSON.toJSONString(t))
                    logger.error("treeRepository.save(t): ", e)
                }
            }
        }


    }

构建树结构

package com.ak47.cms.cms.tree

import com.ak47.cms.cms.vo.TreeVO

object TreeUtil {

    /**
     * 递归遍历树
     * @param node 当前树节点
     * @param visitor 游历函数
     */
    fun visitTree(node: TreeVO, visitor: (TreeVO) -> Unit) {
        // 调用游历函数
        visitor(node)

        node.children.forEach {
            visitTree(it, visitor)
        }
    }


    /**
     * 根据节点列表,递归构建一棵树
     * @param nodes 节点列表
     * @param category 树的类型
     */
    fun buildTree(nodes: List<TreeVO>, category: TreeCategory): TreeVO {
        val treeNodes = mutableListOf<TreeVO>()

        val rootNodes = nodes.filter { it.parentCode == "-1" && it.category == category.name }

        rootNodes.forEach {
            buildChildren(it, nodes)
            treeNodes.add(it)
        }

        val treeVO = TreeVO()
        treeVO.children = treeNodes
        treeVO.category = TreeCategory.COUNTRY.name
        treeVO.name = TreeCategory.COUNTRY.name
        return treeVO
    }

    /**
     * 递归构建当前节点的的孩子列表
     * @param node 当前节点
     * @param nodes 节点列表
     */
    private fun buildChildren(node: TreeVO, nodes: List<TreeVO>) {
        val nodeCode = node.code
        val children = nodes.filter { it.parentCode == nodeCode }
        children.forEach {
            buildChildren(it, nodes)
        }
        node.children = children
    }
}

Controller 层

package com.ak47.cms.cms.tree

import com.ak47.cms.cms.dao.TreeRepository
import com.ak47.cms.cms.vo.TreeVO
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class TreeCountryController {
    @Autowired
    lateinit var treeRepository: TreeRepository


    @GetMapping("/tree/country.json")
    fun country(): TreeVO {
        val trees = treeRepository.findByCategory(TreeCategory.COUNTRY.name)

        val treeVOList = mutableListOf<TreeVO>()
        trees.forEach {
            val vo = TreeVO()
            vo.category = it.category
            vo.code = it.code
            vo.name = it.name
            vo.parentCode = it.parentCode
            treeVOList.add(vo)
        }

        return TreeUtil.buildTree(treeVOList, TreeCategory.COUNTRY)

    }
}

前端视图层

country.ftl

<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">

    .node {
        cursor: pointer;
    }

    .overlay{
        background-color:#fff;
    }

    .node circle {
        fill: #fff;
        stroke: steelblue;
        stroke-width: 1px;
    }

    .node text {
        font-size:12px;
        font-family:sans-serif;
    }

    .link {
        fill: none;
        stroke: #ccc;
        stroke-width: 2px;
    }

    .templink {
        fill: none;
        stroke: red;
        stroke-width: 3px;
    }

    .ghostCircle.show{
        display:block;
    }

    .ghostCircle, .activeDrag .ghostCircle{
        display: none;
    }

</style>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="/tree/country.js"></script>
<body>
<div id="tree-container"></div>
</body>
</html>

country.js

/*Copyright (c) 2013-2016, Rob Schmuecker
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* The name Rob Schmuecker may not be used to endorse or promote products
  derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.*/

// Get JSON data
treeJSON = d3.json("/tree/country.json", function(error, treeData) {
    // Calculate total nodes, max label length
    var totalNodes = 0;
    var maxLabelLength = 0;
    // variables for drag/drop
    var selectedNode = null;
    var draggingNode = null;
    // panning variables
    var panSpeed = 200;
    var panBoundary = 20; // Within 20px from edges will pan when dragging.
    // Misc. variables
    var i = 0;
    var duration = 750;
    var root;

    // size of the diagram
    var viewerWidth = $(document).width();
    var viewerHeight = $(document).height();

    var tree = d3.layout.tree()
        .size([viewerHeight, viewerWidth]);

    // define a d3 diagonal projection for use by the node paths later on.
    var diagonal = d3.svg.diagonal()
        .projection(function(d) {
            return [d.y, d.x];
        });

    // A recursive helper function for performing some setup by walking through all nodes

    function visit(parent, visitFn, childrenFn) {
        if (!parent) return;

        visitFn(parent);

        var children = childrenFn(parent);
        if (children) {
            var count = children.length;
            for (var i = 0; i < count; i++) {
                visit(children[i], visitFn, childrenFn);
            }
        }
    }

    // Call visit function to establish maxLabelLength
    visit(treeData, function(d) {
        totalNodes++;
        maxLabelLength = Math.max(d.name.length, maxLabelLength);

    }, function(d) {
        return d.children && d.children.length > 0 ? d.children : null;
    });


    // sort the tree according to the node names

    function sortTree() {
        tree.sort(function(a, b) {
            return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
        });
    }
    // Sort the tree initially incase the JSON isn't in a sorted order.
    sortTree();

    // TODO: Pan function, can be better implemented.

    function pan(domNode, direction) {
        var speed = panSpeed;
        if (panTimer) {
            clearTimeout(panTimer);
            translateCoords = d3.transform(svgGroup.attr("transform"));
            if (direction == 'left' || direction == 'right') {
                translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
                translateY = translateCoords.translate[1];
            } else if (direction == 'up' || direction == 'down') {
                translateX = translateCoords.translate[0];
                translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
            }
            scaleX = translateCoords.scale[0];
            scaleY = translateCoords.scale[1];
            scale = zoomListener.scale();
            svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
            d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
            zoomListener.scale(zoomListener.scale());
            zoomListener.translate([translateX, translateY]);
            panTimer = setTimeout(function() {
                pan(domNode, speed, direction);
            }, 50);
        }
    }

    // Define the zoom function for the zoomable tree

    function zoom() {
        svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
    }


    // define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
    var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);

    function initiateDrag(d, domNode) {
        draggingNode = d;
        d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none');
        d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show');
        d3.select(domNode).attr('class', 'node activeDrag');

        svgGroup.selectAll("g.node").sort(function(a, b) { // select the parent and sort the path's
            if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back
            else return -1; // a is the hovered element, bring "a" to the front
        });
        // if nodes has children, remove the links and nodes
        if (nodes.length > 1) {
            // remove link paths
            links = tree.links(nodes);
            nodePaths = svgGroup.selectAll("path.link")
                .data(links, function(d) {
                    return d.target.id;
                }).remove();
            // remove child nodes
            nodesExit = svgGroup.selectAll("g.node")
                .data(nodes, function(d) {
                    return d.id;
                }).filter(function(d, i) {
                    if (d.id == draggingNode.id) {
                        return false;
                    }
                    return true;
                }).remove();
        }

        // remove parent link
        parentLink = tree.links(tree.nodes(draggingNode.parent));
        svgGroup.selectAll('path.link').filter(function(d, i) {
            if (d.target.id == draggingNode.id) {
                return true;
            }
            return false;
        }).remove();

        dragStarted = null;
    }

    // define the baseSvg, attaching a class for styling and the zoomListener
    var baseSvg = d3.select("#tree-container").append("svg")
        .attr("width", viewerWidth)
        .attr("height", viewerHeight)
        .attr("class", "overlay")
        .call(zoomListener);


    // Define the drag listeners for drag/drop behaviour of nodes.
    dragListener = d3.behavior.drag()
        .on("dragstart", function(d) {
            if (d == root) {
                return;
            }
            dragStarted = true;
            nodes = tree.nodes(d);
            d3.event.sourceEvent.stopPropagation();
            // it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none');
        })
        .on("drag", function(d) {
            if (d == root) {
                return;
            }
            if (dragStarted) {
                domNode = this;
                initiateDrag(d, domNode);
            }

            // get coords of mouseEvent relative to svg container to allow for panning
            relCoords = d3.mouse($('svg').get(0));
            if (relCoords[0] < panBoundary) {
                panTimer = true;
                pan(this, 'left');
            } else if (relCoords[0] > ($('svg').width() - panBoundary)) {

                panTimer = true;
                pan(this, 'right');
            } else if (relCoords[1] < panBoundary) {
                panTimer = true;
                pan(this, 'up');
            } else if (relCoords[1] > ($('svg').height() - panBoundary)) {
                panTimer = true;
                pan(this, 'down');
            } else {
                try {
                    clearTimeout(panTimer);
                } catch (e) {

                }
            }

            d.x0 += d3.event.dy;
            d.y0 += d3.event.dx;
            var node = d3.select(this);
            node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")");
            updateTempConnector();
        }).on("dragend", function(d) {
            if (d == root) {
                return;
            }
            domNode = this;
            if (selectedNode) {
                // now remove the element from the parent, and insert it into the new elements children
                var index = draggingNode.parent.children.indexOf(draggingNode);
                if (index > -1) {
                    draggingNode.parent.children.splice(index, 1);
                }
                if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
                    if (typeof selectedNode.children !== 'undefined') {
                        selectedNode.children.push(draggingNode);
                    } else {
                        selectedNode._children.push(draggingNode);
                    }
                } else {
                    selectedNode.children = [];
                    selectedNode.children.push(draggingNode);
                }
                // Make sure that the node being added to is expanded so user can see added node is correctly moved
                expand(selectedNode);
                sortTree();
                endDrag();
            } else {
                endDrag();
            }
        });

    function endDrag() {
        selectedNode = null;
        d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
        d3.select(domNode).attr('class', 'node');
        // now restore the mouseover event or we won't be able to drag a 2nd time
        d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
        updateTempConnector();
        if (draggingNode !== null) {
            update(root);
            centerNode(draggingNode);
            draggingNode = null;
        }
    }

    // Helper functions for collapsing and expanding nodes.

    function collapse(d) {
        if (d.children) {
            d._children = d.children;
            d._children.forEach(collapse);
            d.children = null;
        }
    }

    function expand(d) {
        if (d._children) {
            d.children = d._children;
            d.children.forEach(expand);
            d._children = null;
        }
    }

    var overCircle = function(d) {
        selectedNode = d;
        updateTempConnector();
    };
    var outCircle = function(d) {
        selectedNode = null;
        updateTempConnector();
    };

    // Function to update the temporary connector indicating dragging affiliation
    var updateTempConnector = function() {
        var data = [];
        if (draggingNode !== null && selectedNode !== null) {
            // have to flip the source coordinates since we did this for the existing connectors on the original tree
            data = [{
                source: {
                    x: selectedNode.y0,
                    y: selectedNode.x0
                },
                target: {
                    x: draggingNode.y0,
                    y: draggingNode.x0
                }
            }];
        }
        var link = svgGroup.selectAll(".templink").data(data);

        link.enter().append("path")
            .attr("class", "templink")
            .attr("d", d3.svg.diagonal())
            .attr('pointer-events', 'none');

        link.attr("d", d3.svg.diagonal());

        link.exit().remove();
    };

    // Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.

    function centerNode(source) {
        scale = zoomListener.scale();
        x = -source.y0;
        y = -source.x0;
        x = x * scale + viewerWidth / 2;
        y = y * scale + viewerHeight / 2;
        d3.select('g').transition()
            .duration(duration)
            .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
        zoomListener.scale(scale);
        zoomListener.translate([x, y]);
    }

    // Toggle children function

    function toggleChildren(d) {
        if (d.children) {
            d._children = d.children;
            d.children = null;
        } else if (d._children) {
            d.children = d._children;
            d._children = null;
        }
        return d;
    }

    // Toggle children on click.

    function click(d) {
        if (d3.event.defaultPrevented) return; // click suppressed
        d = toggleChildren(d);
        update(d);
        centerNode(d);
    }

    function update(source) {
        // Compute the new height, function counts total children of root node and sets tree height accordingly.
        // This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
        // This makes the layout more consistent.
        var levelWidth = [1];
        var childCount = function(level, n) {

            if (n.children && n.children.length > 0) {
                if (levelWidth.length <= level + 1) levelWidth.push(0);

                levelWidth[level + 1] += n.children.length;
                n.children.forEach(function(d) {
                    childCount(level + 1, d);
                });
            }
        };
        childCount(0, root);
        var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line
        tree = tree.size([newHeight, viewerWidth]);

        // Compute the new tree layout.
        var nodes = tree.nodes(root).reverse(),
            links = tree.links(nodes);

        // Set widths between levels based on maxLabelLength.
        nodes.forEach(function(d) {
            d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px
            // alternatively to keep a fixed scale one can set a fixed depth per level
            // Normalize for fixed-depth by commenting out below line
            // d.y = (d.depth * 500); //500px per level.
        });

        // Update the nodes…
        node = svgGroup.selectAll("g.node")
            .data(nodes, function(d) {
                return d.id || (d.id = ++i);
            });

        // Enter any new nodes at the parent's previous position.
        var nodeEnter = node.enter().append("g")
            .call(dragListener)
            .attr("class", "node")
            .attr("transform", function(d) {
                return "translate(" + source.y0 + "," + source.x0 + ")";
            })
            .on('click', click);

        nodeEnter.append("circle")
            .attr('class', 'nodeCircle')
            .attr("r", 0)
            .style("fill", function(d) {
                return d._children ? "lightsteelblue" : "#fff";
            });

        nodeEnter.append("text")
            .attr("x", function(d) {
                return d.children || d._children ? -10 : 10;
            })
            .attr("dy", ".35em")
            .attr('class', 'nodeText')
            .attr("text-anchor", function(d) {
                return d.children || d._children ? "end" : "start";
            })
            .text(function(d) {
                return d.name;
            })
            .style("fill-opacity", 0);

        // phantom node to give us mouseover in a radius around it
        nodeEnter.append("circle")
            .attr('class', 'ghostCircle')
            .attr("r", 30)
            .attr("opacity", 0.2) // change this to zero to hide the target area
            .style("fill", "red")
            .attr('pointer-events', 'mouseover')
            .on("mouseover", function(node) {
                overCircle(node);
            })
            .on("mouseout", function(node) {
                outCircle(node);
            });

        // Update the text to reflect whether node has children or not.
        node.select('text')
            .attr("x", function(d) {
                return d.children || d._children ? -10 : 10;
            })
            .attr("text-anchor", function(d) {
                return d.children || d._children ? "end" : "start";
            })
            .text(function(d) {
                return d.name;
            });

        // Change the circle fill depending on whether it has children and is collapsed
        node.select("circle.nodeCircle")
            .attr("r", 4.5)
            .style("fill", function(d) {
                return d._children ? "lightsteelblue" : "#fff";
            });

        // Transition nodes to their new position.
        var nodeUpdate = node.transition()
            .duration(duration)
            .attr("transform", function(d) {
                return "translate(" + d.y + "," + d.x + ")";
            });

        // Fade the text in
        nodeUpdate.select("text")
            .style("fill-opacity", 1);

        // Transition exiting nodes to the parent's new position.
        var nodeExit = node.exit().transition()
            .duration(duration)
            .attr("transform", function(d) {
                return "translate(" + source.y + "," + source.x + ")";
            })
            .remove();

        nodeExit.select("circle")
            .attr("r", 0);

        nodeExit.select("text")
            .style("fill-opacity", 0);

        // Update the links…
        var link = svgGroup.selectAll("path.link")
            .data(links, function(d) {
                return d.target.id;
            });

        // Enter any new links at the parent's previous position.
        link.enter().insert("path", "g")
            .attr("class", "link")
            .attr("d", function(d) {
                var o = {
                    x: source.x0,
                    y: source.y0
                };
                return diagonal({
                    source: o,
                    target: o
                });
            });

        // Transition links to their new position.
        link.transition()
            .duration(duration)
            .attr("d", diagonal);

        // Transition exiting nodes to the parent's new position.
        link.exit().transition()
            .duration(duration)
            .attr("d", function(d) {
                var o = {
                    x: source.x,
                    y: source.y
                };
                return diagonal({
                    source: o,
                    target: o
                });
            })
            .remove();

        // Stash the old positions for transition.
        nodes.forEach(function(d) {
            d.x0 = d.x;
            d.y0 = d.y;
        });
    }

    // Append a group which holds all nodes and which the zoom Listener can act upon.
    var svgGroup = baseSvg.append("g");

    // Define the root
    root = treeData;
    root.x0 = viewerHeight / 2;
    root.y0 = 0;

    // Layout the tree initially and center on the root node.
    update(root);
    centerNode(root);
});


Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 解决Spring Spring Data JPA 错误: Page 1 of 1 containing UNKNOWN instances解决Spring Spring Data JPA 错误:

    后台得不到数据, 并提示 Page 1 of 1 containing UNKNOWN instances

    一个会写诗的程序员
  • Kotlin 's Function 《Kotlin极简教程》正式上架:

    Functions are declared using the fun keyword, followed by a function name and an...

    一个会写诗的程序员
  • Kotlin + Spring Boot (Gradle) + React.js (Nowa) 集成 Web 开发

    Kotlin + Spring Boot (Gradle) + React.js (Nowa) 集成 Web 开发

    一个会写诗的程序员
  • 并发编程包--java.util.Concurrent

    SuperHeroes
  • 网页中Office和pdf相关文件导出

    最近被派去维护和开发一些做了一半、年久失修的项目。有一部分内容是关于word文件导出,顺带着把excel、pdf文件的导出也调研下吧,我想未来开发我应该会遇到的...

    丰臣正一
  • 深圳大学校长李清泉谈地方高校探索高校改革

    近日深圳大学校长李清泉做客人民网百名校长访谈录节目。与网友们一起交流深圳大学在高校体制改革中的新使命和新举措。 采访原文链接:http://edu.people...

    腾讯高校合作
  • 译文 | 与TensorFlow的第一次接触第二篇:线性回归

    本章中,将会利用TensorFlow实现一个简单的模型:线性回归。通过本示例,我会分析一些代码基础及说明如何在学习过程中调用各种重要组件,比如cost func...

    用户1332428
  • LESS第六课(内置函数 层级结构 继承()注意是extend,不是extends)

    /* 由于less的底层就是用JavaScript实现的, 所以JavaScript中常用的一些函数在less中都支持 */ 不想介绍,因为太他妈多了。...

    用户7873631
  • 阿里面试Java必问210题解析:Spring+微服务+Redis+MySQL+中间件等

    很多人面试之前,可能没有在互联网公司工作过或者说工作过但年头较短,不知道互联网公司技术面试都会问哪些问题? 再加上可能自己准备也不充分,去面试没几个回合就被面试...

    用户5546570
  • WiredTiger存储引擎之六:Cache分配规则与Page的淘汰机制

    WiredTiger存储引擎系列文章将从逻辑正确、内容完整的角度全面介绍WiredTiger存储引擎。本篇作为WiredTiger存储引擎介绍系列文章第六篇,也...

    MongoDB中文社区

扫码关注云+社区

领取腾讯云代金券