d3 使用记录: 树形图

先上效果图:

 

 

 对应源码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="../d3.v5.js"></script>
    <script src="./data.js"></script>
    <style>
        svg{
            display: block;
            margin: 100px auto;
            border: 3px solid #ccc;
            background: darkgray;
        }
        path.link{
            stroke: #333;
            stroke-width: 1.5px;
            fill:transparent;
        }
        /* path.line{
            fill: none;
        } */
        .node circle{
            fill:#fff;
            stroke:steelblue;
            stroke-width: 1.5px;
        }
        .grid-line {
            stroke: #fcfcfc;
            stroke-dasharray: 3 3;
            stroke-opacity: 0.5;
        }
        .x-axis .tick:nth-child(2) .grid-line {
            /*stroke-width: 4px;*/
            stroke: none;
        }
        .y-axis .tick:last-child .grid-line {
            /* stroke: none; */
        }
    </style>
</head>
<body>

    <script>

        var globalId = 1;
        function tree() {
            var _chart = {};
            var _width = 1000,
                _height = 600,
                _margin = { left: 25, right: 15, top: 20, bottom: 20 },
                _colors = d3.scaleOrdinal(d3.schemeCategory10),
                _svg,
                _nodes,
                _i = 0,
                _duration = 300,
                _bodyG,
                _root,
                _curMaxHeight,
                _tree,
                _nodeRectWidth = 100,
                hierarchyData = {},
                textLineHeightCache = {},
                textPadding = [10, 5, 10, 5],
                textLineHeight = 18;

            

            _chart.render = function() {
                if (!_svg) {
                    _svg = d3.select("body").append("svg")
                                .attr("width", _width)
                                .attr("height", _height);
                }

                renderBody(_svg)
            }

            
            function renderBody(svg) {
                if (!_bodyG) {
                    _bodyG = svg.append("g")
                                .attr("class", "body")
                                .attr("transform", function(d) {
                                    return 'translate(' + _margin.left + ',' + _margin.top +')'
                                })
                    addScroll();
                }

                _tree = d3.tree().nodeSize([120, 80]);

                for (let position in _nodes) {

                    // 递归计算节点的偏移, 节点高度; 并缓存;
                    initDistance(_nodes[position], calcTextLineght(_nodes[position].name, false));

                    // 获取树结构数据
                    hierarchyData[position] = d3.hierarchy(_nodes[position])

                    // 设置树的折叠;
                    collapseChildren(hierarchyData[position], position > 'left' ? 1 : 2, position);
                    
                    // 渲染节点树
                    render(hierarchyData[position], _tree, position);
                }
            }

            // 折叠节点:
            function collapseChildren (treeNode, collapseDepth, direction) {
                var preOffset = 0;
                var walk = function(node, depth, direction) {
                    node.direction = direction;
                    node.offset = (node.offset || 0) + preOffset;
                    if ( node.children ) {
                        if (Array.isArray(node.children)) {
                            if (node.depth >= depth) {
                                node._children = node.children;
                                node.children = null
                                node.collapseAble = true;

                                node._children.forEach(function(v) {
                                    walk(v, depth, direction)
                                })
                            } else {
                                node.children.forEach(function(v) {
                                    walk(v, depth, direction)
                                })
                            }
                        } else {
                            if (node.depth >= depth) {
                                node._children = node.children;
                                node.children = null
                                node.collapseAble = true;

                                walk(node._children, depth, direction)
                            } else {
                                walk(node.children, depth, direction)
                            }
                        }
                    }
                }
                walk(treeNode, collapseDepth, direction)
            }

            // 应用缩放
            function addScroll() {
                var zoomHandler = d3.zoom().scaleExtent([0.2, 20]).on('zoom', function () {
                    var transform = d3.event.transform;
                    d3.select('svg g.body').attr('transform', transform) // .transition().duration(100)
                })
                _svg.call(zoomHandler)
            }

            function render(root, tree, position) {
                // 计算树的布局
                tree(root);

                // 将对称的两个树进行偏移, 让其居中, 对称
                setNodePosition(root, position)
                
                renderNodes(root, position);
                renderLinks(root, position);
            }

            // 设置节点偏移, 计算节点高度
            function initDistance(list, preOffSize) {
                preOffSize = preOffSize || 0;
                var iterator = [].forEach;
                iterator.call(list.children || list._children, item => {
                    // item._children = item.children ? (item.children.length ? item.children : null) : null
                    // item.children = null

                    let offSize = 0
                    let height = calcTextLineght(item.name, !!(item._children || item.children))

                    // console.log('高度', height, item);
                    offSize = height + preOffSize
                    item.offSize = preOffSize
                    item.height = height
                    
                    if (item.children || item._children) {

                        initDistance(item, offSize)
                    }
                })
            }

            var canvas2dContext = document.createElement("canvas").getContext("2d");
            function setNodePosition(root, position) {
                var offsetX = _width / 2,
                    offsetY = _height / 2,
                    oneWidth = 120,
                    oneHeight = 80;

                // root.each()
                // console.log('root', root);
                root.each((node,index) => {
                    var text = node.data.name;
                    var textLineHeights = null;
                    var height = null;

                    if (textLineHeightCache[text]) {
                        textLineHeights = textLineHeightCache[text]
                    } else {
                        textLineHeights = calcBreakWord(text, _nodeRectWidth - textPadding[1] - textPadding[3] - (node.collapseAble ? 16 + 10 : 0), 14, canvas2dContext)
                        textLineHeightCache[text] = textLineHeights
                    }

                    // 如果是根节点, 将位置设置到中心
                    if (node.depth === 0) {
                        height = textLineHeights.length * textLineHeight + textPadding[0] + textPadding[2]

                        // 横向的上下图
                        node.x = offsetX;
                        node.y = offsetY;

                        node._width = _nodeRectWidth;
                        node._height = height;

                    } else {
                        // console.log('计算高度', textLineHeights);
                        height = textLineHeights.length * textLineHeight + textPadding[0] + textPadding[2]

                        // cur = root
                        if (position === 'left') {
                            node.x += offsetX;
                            node.y += offsetY;

                            node._width = _nodeRectWidth;
                            node._height = height;
                        } else if (position === 'right') {
                            node.x += offsetX;
                            // node.y -= offsetY;
                            
                            node.y = -(node.y - offsetY)

                            node._width = _nodeRectWidth;
                            node._height = height;
                        } else if (position === 'top') {
                            // 
                            // node.x = -(node.x - gridX / 2);
                            // console.log('Y周偏移', gridX / 2, node.x, offsetX, node.y, offsetY);
                            // // node.y += offsetY * Math.cos(Math.PI / 2)
                            // node.y = gridY / 2 - (node.x - gridX / 2);
                        } else {
                            node.x -= offsetX;
                            node.y += offsetY;

                            node._width = _nodeRectWidth;
                            node._height = height;
                        }
                    }
                })
            }


            // 计算文本高度; 并缓存起来
            function calcTextLineght(text, collapseAble) {
                var textLineHeights = null;
                var height = null;

                if (textLineHeightCache[text]) {
                    textLineHeights = textLineHeightCache[text]
                } else {
                    textLineHeights = calcBreakWord(text, _nodeRectWidth - textPadding[1] - textPadding[3] - (collapseAble ? 16 + 10 : 0), 14, canvas2dContext)
                    textLineHeightCache[text] = textLineHeights
                }
                height = textLineHeights.length * textLineHeight + textPadding[0] + textPadding[2]

                return height
            }

            

            function renderNodes(root, baseId) {
                var nodes = root.descendants(); // .slice(1)
                // console.log('重新计算坐标位置', nodes, root);
                var _offsetX = _width / 2,
                    _offsetY = _height / 2,
                    oneWidth = 120,
                    oneHeight = 80;
                
                var nodeELements = _bodyG.selectAll("g.node._" + baseId)
                                        .data(nodes, function(d){
                                            return d.id || (d.id = ++_i)
                                        });
                var nodeEnter = nodeELements.enter().append("g")
                                    .attr("class", "node _" + baseId);

                nodeEnter.append("rect")
                        .attr("x", function(d) { return -d._width / 2 })
                        .attr("y", function(d) { return 0 })
                        .attr("width", function(d) { return d._width })
                        .attr("height", function(d) { return d._height })
                        .attr("class", "node-background")
                        .attr("fill", function(d) { return d.backgournd || '#fff' })
                nodeEnter.append("circle")
                        .attr("r", function(d) {
                            return d.children || d._children ? 8 : 1e-8
                        })
                        .attr("class", "indicator-circle")
                        .attr("stroke", "#333")
                        .attr("fill", "#fff")
                        .style("cursor", "pointer")
                        .attr("cx", function(d) {
                            return d._width / 2 - textPadding[1] - 8
                        })
                        .attr("cy", function(d) {
                            return d._height / 2
                        })
                        .style("display", function(d) {
                            return d.children || d._children ? "block" : "none"
                        });
                nodeEnter.append("text")
                        .attr("class", "indicator-text")
                        .attr("x", function(d) {
                            return d._width / 2 - textPadding[1] - 8
                        })
                        .attr("y", function(d) {
                            return d._height / 2
                        })
                        .attr("text-anchor", "middle")
                        .attr("dominant-baseline", "middle")
                        .style("cursor", "pointer")
                        .text(function(d) {
                            return d.children ? "-" : d._children ? "+" : ""
                        });
                nodeEnter.attr("transform", function(d) {
                                        
                            if (d.parent) {
                                var offsetY = d.parent.data.offSize || 0;

                                if (baseId === 'right') {
                                    return 'translate(' + d.parent.x + ',' + (d.parent.y - offsetY) + ')'
                                } else {
                                    return 'translate(' + d.parent.x + ',' + (d.parent.y + offsetY) + ')'
                                }
                            } else {
                                var offsetY = d.data.offSize || 0;

                                if (baseId === 'right') {
                                    return 'translate(' + d.x + ',' + (d.y - offsetY) + ')'
                                } else {
                                    return 'translate(' + d.x + ',' + (d.y + offsetY) + ')'
                                }
                            }
                        })
                        .on("click", function(d) {
                            toggle(d);
                            render(hierarchyData[d.direction], _tree, d.direction);
                        })

                // console.log('缓存的行高信息', textLineHeightCache, nodeEnter);
                nodeEnter.call(function (nodeGroup, i) {
                            nodeGroup.selectAll("text.node-text")
                                .data(function(d) {
                                    var cache = textLineHeightCache[d.data.name]
                                    var list = []
                                    if (cache) {
                                        for (var i = 0; i < cache.length; i++) {
                                            list.push({
                                                text: cache[i],
                                                _width: d._width,
                                                _height: d._height
                                            })
                                        }
                                    }
                                    return list
                                })
                                .enter()
                                .append("text")
                                .attr("class", "node-text")
                                .attr("x", function(d) {
                                    return -d._width / 2 + textPadding[3] 
                                })
                                .attr("y", function(d, i) {
                                    // var totalHeight = d._height - textPadding[0] - textPadding[3];
                                    // var Total -d._height / 2 + 
                                    return textPadding[0] + textLineHeight/2 + i * textLineHeight
                                })
                                .text(function(d) { 
                                    return d.text 
                                })
                                .attr("dominant-baseline", "middle")
                        })
                        
                

                var nodeUpdate = nodeEnter.merge(nodeELements)
                                            // 从父节点伸展到节点位置
                                            .attr("transform", function(d) {
                                                // if (d.parent) {
                                                //     var offsetY = d.parent.data.offSize || 0;

                                                //     if (baseId === 'right') {
                                                //         return 'translate(' + d.parent.x + ',' + (d.parent.y - offsetY) + ')'
                                                //     } else {
                                                //         return 'translate(' + d.parent.x + ',' + (d.parent.y + offsetY) + ')'
                                                //     }
                                                // } else {
                                                    var offsetY = d.data.offSize || 0;

                                                    if (baseId === 'right') {
                                                        return 'translate(' + d.x + ',' + (d.y - offsetY) + ')'
                                                    } else {
                                                        return 'translate(' + d.x + ',' + (d.y + offsetY) + ')'
                                                    }
                                                // }
                                                
                                            })
                                            .transition().duration(_duration)
                                            .attr("transform", function(d) {
                                                var offsetY = d.data.offSize || 0;

                                                if (baseId === 'right') {
                                                    return 'translate(' + d.x + ',' + (d.y - offsetY) + ')'
                                                } else {
                                                    return 'translate(' + d.x + ',' + (d.y + offsetY) + ')'
                                                }
                                            });

                nodeUpdate.select("text.indicator-text")
                            .text(function(d) {
                                return d.children ? "-" : d._children ? "+" : ""
                            });
                nodeUpdate.select("circle.indicator-circle")
                            .attr("r", function(d) {
                                return d.children || d._children ? 8 : 1e-8
                            })
                            .style("display", function(d) {
                                return d.children || d._children ? "block" : "none"
                            })

                var nodeExit = nodeELements.exit()
                                            .transition().duration(_duration)
                                            .attr("transform", function(d) {
                                                // if (baseId === 'right') {
                                                //     return 'translate(' + d.x + ',' + d.y + ')'
                                                // } else {
                                                //     return 'translate(' + d.x + ',' + d.y + ')'
                                                // }
                                                // 收缩回父节点
                                                if (d.parent) {
                                                    var offsetY = d.parent.data.offSize || 0;

                                                    if (baseId === 'right') {
                                                        return 'translate(' + d.parent.x + ',' + (d.parent.y - offsetY) + ')'
                                                    } else {
                                                        return 'translate(' + d.parent.x + ',' + (d.parent.y + offsetY) + ')'
                                                    }
                                                } else {
                                                    var offsetY = d.data.offSize || 0;

                                                    if (baseId === 'right') {
                                                        return 'translate(' + d.x + ',' + (d.y - offsetY) + ')'
                                                    } else {
                                                        return 'translate(' + d.x + ',' + (d.y + offsetY) + ')'
                                                    }
                                                }
                                            })
                                            .call(function(node) {
                                                node.select("rect.node-text")
                                                        .attr("r", 1e-6)    // 0.000001
                                                        .remove();
                                                node.select("text.indicator-text")
                                                    .text(function(d) {
                                                        return ""
                                                    })
                                                    .remove();
                                                node.select("circle.indicator-circle")
                                                    .attr("r", function(d) {
                                                        return 1e-8
                                                    })
                                                    .remove();
                                                node.select("rect.node-background")
                                                    .remove()
                                                node.selectAll("text.node-text")
                                                    .remove()
                                            })
                                            .remove();
                
                // reCalcMaxHeight(nodes);
                // renderLabels(nodeEnter, nodeUpdate, nodeExit, root);
            }

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


            function renderLinks(root, baseId) {
                var nodes = root.descendants().slice(1);
                var links = _bodyG.selectAll("path.link._" + baseId)
                                .data(nodes, function(d) {
                                    return d.id || (d.id = ++_i);
                                })

                links.enter()
                        .insert("path")
                    .merge(links)
                        .attr("class", "link _" + baseId)
                        .transition().duration(_duration)
                        .attr("d", function(d) {
                            return generateLinkPath(d, d.parent, baseId)
                        })
                links.exit().remove();
            }

            function generateLinkPath(target, source, baseId) {
                // console.log('顶点位置', target, source);
                // (d.y + offsetY)
                // 直线链接
                // var path = d3.path();
                // if (baseId === 'left') {
                //     path.moveTo(source.x, source.y + (source.data.offSize || 0) + (source._height || 0));
                //     path.lineTo(target.x, target.y + (target.data.offSize || 0));
                // } else if (baseId === 'right') {
                //     path.moveTo(source.x, source.y - (source.data.offSize || 0));
                //     path.lineTo(target.x, target.y - (target.data.offSize || 0) + (target._height || 0));
                // }
                // return path.toString()
                
                // 曲线链接
                // var path = d3.path();
                // // // bezierCurveTo 三阶贝塞尔曲线: 
                // // // 控制点 1 的 x坐标
                // // // 控制点 1 的 y坐标
                // // // 控制点 2 的 x坐标
                // // // 控制点 2 的 y坐标
                // // // 结束点 的 x坐标
                // // // 结束点 的 y坐标
                // // 折线链接方式
                // var centerPointX = source.x - target.x;
                // var sourceY, targetY;
                // if (baseId === 'left') {
                //     sourceY = source.y + (source.data.offSize || 0) + (source._height || 0);
                //     targetY = target.y + (target.data.offSize || 0);
                // } else if (baseId === 'right') {
                //     sourceY = source.y - (source.data.offSize || 0);
                //     targetY = target.y - (target.data.offSize || 0) + (target._height || 0);
                // }
                // var centerPointY = sourceY - targetY;
                // path.moveTo(target.x, targetY);
                // path.bezierCurveTo(
                //     target.x, 
                //     (targetY + sourceY) / 2,
                //     source.x, 
                //     (targetY + sourceY) / 2, 
                //     source.x,
                //     sourceY
                // );
                // return path.toString()
                
                
                // 折线链接方式
                var centerPointX = source.x - target.x;
                var sourceY, targetY;
                if (baseId === 'left') {
                    sourceY = source.y + (source.data.offSize || 0) + (source._height || 0);
                    targetY = target.y + (target.data.offSize || 0);
                } else if (baseId === 'right') {
                    sourceY = source.y - (source.data.offSize || 0);
                    targetY = target.y - (target.data.offSize || 0) + (target._height || 0);
                }
                var centerPointY = sourceY - targetY;
                var d = `M${target.x},${targetY}v${centerPointY / 3}h${centerPointX}v${centerPointY * 2 / 3}`;
                return d
            }

            _chart.nodes = function(node) {
                if(!arguments.length) return _nodes;
                _nodes = node;
                return _chart
            }

            _chart.valueAccessor = function(accessor) {
                if(!arguments.length) return _valueAccessor;
                _valueAccessor = accessor;
                return _chart
            }

            
            _chart.width = function(width) {
                if(!arguments.length) return _width;
                _width = width;
                return _chart
            }
            _chart.height = function(height) {
                if(!arguments.length) return _height;
                _height = height;
                return _chart
            }

            _chart.margins = function(margin) {
                if(!arguments.length) return _margin;
                _margin = margin;
                return _chart
            }

            _chart.colors = function(color) {
                if(!arguments.length) return _colors;
                _colors = color;
                return _chart
            }

            return _chart
        }

        function size(d) {
            return d.size
        }

        function count() {
            return 1
        }

        function clip(d) {
            d.valueAccessor(chart.valueAccessor() == size ? count : size)
        }

        function randomData(base) {
            base = base || 10
            return Math.random() * base
        }

        // 获取系统默认字体
        function getSysFont(text) {
            var span = document.createElement('span');
            var fontFamily = '';
            span.innerHTML = text;
            span.style.display = 'none';
            document.body.appendChild(span);
            fontFamily = getComputedStyle(span).fontFamily;
            span.parentNode.removeChild(span);
            return fontFamily;
        }
        // 计算文本宽度
        function calcTextWidth(text, font, canvas2dContext) {
            
            if (canvas2dContext) {
                canvas2dContext.font = font;
                return canvas2dContext.measureText(text).width
            } else {
                var canvas = document.createElement("canvas");
                var context = canvas.getContext("2d");
                context.font = font;
                var metrics = context.measureText(text);
                canvas = null;
                return metrics.width
            }
        }
        // 文本断行
        function calcBreakWord (text, limitWidth, fontSize, canvas2dContext) {
            var defaultSysFont = getSysFont(text),
                textWidth = calcTextWidth(text, fontSize + 'px ' + defaultSysFont, canvas2dContext), // 计算字符串总宽度;
                res = {length: 0},
                len = text.length,
                i = 0,
                char = '',
                lastCharUnit = '',
                lastIndex;

            if (textWidth > limitWidth) {
                while(i < len) {
                    char += text[i];
                    if (calcTextWidth(char, fontSize + 'px ' + defaultSysFont, canvas2dContext) > limitWidth) {
                        lastIndex = i;
                        lastCharUnit = text[i];

                        res[res.length] = char.slice(0, -1);
                        res.length += 1;
                        char = lastCharUnit;
                    }
                    i++;
                }
                // 添加末尾
                if (lastIndex < len) {
                    res[res.length] = text.slice(lastIndex);
                    res.length += 1;
                }
            } else {
                res[res.length] = text;
                res.length += 1;
            }

            return res;
        }

        var chart = tree();
        chart.nodes(data).valueAccessor(size).render()

    </script>
</body>
</html>
var data = {
    left: {
      name: 'Boss',
      bgColor: '#F6AA65',
      id: 1,
      children: [
        {
          name: 'left-1',
          bgColor: '#D0E1F1',
          total: 10,
          children: [
            { name: 'left-1-1' },
            { name: 'left-1-2' },
            { name: 'left-1-3' },
            { name: 'left-1-4' },
            { name: 'left-1-5',
              children: [
                { name: 'left-1-5-1 来一个我们不一样' }
              ]
            },
            { name: 'left-1-6' },
            { name: 'left-1-7' },
            { name: 'left-1-8' },
            { name: 'left-1-9' },
            { name: 'left-1-10' },
          ]
        },
        {
          name: 'left-2',
          bgColor: '#C3CFEF',
          total: 22,
          children: [
            { name: 'left-2-1 文字来长一点断个行, 计算好叠加的坐标', 
              children: [ 
                { name: 'left-2-1-1 再来一个; 其中之一而已, 要在一个分支上的不断叠加', 
                  children: [
                    {name: 'left-2-1-1-1'},
                    {name: 'left-2-1-1-2'}
                  ] 
                },
                { name: 'left-2-1-2' }
              ] 
            },
            { name: 'left-2-2' },
            { name: 'left-2-3' },
            { name: 'left-2-4' },
            { name: 'left-2-5' },
            { name: 'left-2-6' },
            { name: 'left-2-7' },
            { name: 'left-2-8' },
            { name: 'left-2-9' },
            { name: 'left-2-10' },
            { name: 'left-2-11' },
            { name: 'left-2-12' }
          ]
        },
        { name: 'left-3', bgColor: '#D7D7D7', total: 0, children: [] }
      ]
    },
    right: {
      name: 'Boss',
      bgColor: '#F6AA65',
      id: 1,
      children: [
        {
          name: 'right-1',
          bgColor: '#BFCDE3',
          total: 63,
          children: [
            { name: 'right-1-1' },
            { name: 'right-1-2' },
            { name: 'right-1-3' },
            { name: 'right-1-4' },
            { name: 'right-1-5' },
            { name: 'right-1-6' },
            { name: 'right-1-7' }
          ]
        },
        {
          name: 'right-2',
          bgColor: '#B9D4F3',
          total: 18326,
          children: [
            { name: 'right-2-1' },
            { name: 'right-2-2' },
            { name: 'right-2-3' },
            { name: 'right-2-4' },
            { name: 'right-2-5' }
          ]
        },
        { name: 'right-3', bgColor: '#D7D7D7', total: 0, children: [] }
      ]
    }
}

 

实现上比较粗糙, 但也应该能够应对一般的业务场景了. 像天眼查企业图谱的树形结构, 对展开事件变换处理下即可。

实现上关键点在于

d3.tree().nodeSize([120, 80]) 这一行代码, d3.tree() 会提供我们一个树形布局算法, 支持 [nodeSize | size]两种方式
nodeSize: 为每一个节点设定固定大小的布局
size: 为整个树设定固定大小的布局
separation 额外支持设置相邻节点之间的间隔
设想一下:
在一块画布上, 针对未知层级未知数量的节点进行布局的方案, 如果是需要固定树大小, 那么就需要找出最边界的节点(上 | 下 | 左 | 右),
上下(深度)确定每个节点所占的高度, 左右(跨度)确定每个节点所占宽度;
而如果是固定节点大小, 通过计算当前(父)节点的最大子节点数量和子节点数量, 则可以确定(父)节点的宽度和横向位置, 高度累加;

d3 帮我们完成了从: 树结构数据 -> 包含坐标信息的数据 的一个运算;
得到了坐标信息, 再对坐标进行一个平移, 以达到居中;
如果要把节点内容所占宽高也算进去, 则还需要计算出节点的偏移距离(因为上层节点的位置是会影响的), 在具体绘制时把节点坐标和偏移都进行计算, 这样便达到了最终的布局效果;

 

posted @ 2020-10-10 11:03  芋头圆  阅读(491)  评论(0编辑  收藏  举报