<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body, html {
            margin: 0;
            height: 100%;
            width: 100%;
        }

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

        .link {
            fill: none;
            stroke: #cccccc;
            stroke-width: 1.5px;
        }

        .textShadow {
            text-shadow: 5px 5px 3px #000000;
        }

        div {
            position: absolute;
            top: 10px;
            left: 10px;
        }
    </style>
</head>
<body>
<div data-index="test">
    <button onclick="changeTheLine('C')">曲线</button>
    <button onclick="changeTheLine('L')">直线</button>
    <button onclick="changeLayout('left')">左布局</button>
    <button onclick="changeLayout('right')">右布局</button>
    <button onclick="changeLayout('top')">上布局</button>
    <span>点击树节点文字可标注父级路径</span>
</div>
<script src="d3.v6.js"></script>
<script>
    let doc = document
    let w = doc.documentElement.clientWidth / 2;
    let h = doc.documentElement.clientHeight;

    let sourceY = 0;
    let text = null;
    let node = null;
    let link = null;
    let line = null;
    let firstText = null;          // 第一级文字
    let textWidthOrHeight = 0;
    let rememberShape = 'C';       // C = 曲线/L = 直线
    let rememberLayout = 'right';  // left = 左布局/right = 右布局/top = 上布局
    let paddingY = 50;
    let strokeLength = 150;        // 线条长度

    let path = null

    /**
     * 改变线条及文字坐标
     **/
    function updateLink() {
        link.data(line)
            .enter()
            .append('path')
            .attr('class', 'link')
            .merge(link)
            .attr('d', function (d, i) {
                // 生成标注线
                if (d.target.taggingArr) {
                    for (let i of d.target.taggingArr) {
                        // !link._groups[0][i].line 避免重复克隆dom
                        if (!link._groups[0][i].line) {
                            link._groups[0][i].line = true
                            path = doc.querySelector('g').appendChild(link._groups[0][i].cloneNode(true));
                            path.style.stroke = '#000000';
                            path.setAttribute('data-index', `index${i}`);
                        }
                    }
                }
                d.target.widthOrHeight = text._groups[0][i + 1].getBBox()[rememberLayout === 'top' ? 'height' : 'width'];
                return lineShape(d, node._groups[0][i + 1]);
            });
    }

    /**
     * 改变线条
     * @param {String} shape - C = 曲线/L = 直线
     * @see removePath
     * @see updateLink
     **/
    function changeTheLine(shape = rememberShape) {
        rememberShape = shape; // 新赋值曲线
        removePath({removeAll: true});
        updateLink();
    }

    /**
     * 改变布局
     * @param {String} layout - left = 左布局/right = 右布局/top = 上布局
     * @see removePath
     * @see updateLink
     * @see firstTextTranslate
     **/
    function changeLayout(layout = rememberLayout) {
        removePath({removeAll: true});
        rememberLayout = layout;  // 新赋值布局方向
        updateLink();
        firstTextTranslate(layout)
    }

    /**
     * 改变线条及布局,方法调用
     * @see topLayout
     * @see leftLayout
     * @see rightLayout
     * @param {Object}      d - d3返回的树形数据
     * @param {HTMLElement} g - 文字的g标签
     **/
    function lineShape(d, g) {
        textWidthOrHeight = d.target.parent.widthOrHeight ? d.target.parent.widthOrHeight : d.target.widthOrHeight;

        return this[`${rememberLayout}Layout`](d, g, textWidthOrHeight);
    }

    /**
     * 上布局
     * @param {Object}          d - d3返回的树形数据
     * @param {HTMLElement}     g - 文字的g标签
     * @param {Number} textHeight - 文字高度
     **/
    function topLayout(d, g, textHeight) {
        sourceY = d.source.y + paddingY;

        if (d.source.depth === 0) {
            d.target.M0 = sourceY;
        } else {
            d.target.M0 = d.target.parent.C4 + textHeight;
        }

        d.target.C4 = d.target.M0 + strokeLength;
        d.target.C0 = d.target.M0 + strokeLength / 2;
        d.target.C2 = d.target.M0 + strokeLength / 2;

        g.setAttribute('transform', `translate(${d.target.x - (g.getBBox().width / 2)},${(d.target.C4 + textHeight / 2) + 2})`);

        return `M${d.source.x},${d.target.M0} ${rememberShape}${d.source.x},${d.target.C0} ${d.target.x},${d.target.C2} ${d.target.x},${d.target.C4}`;
    }

    /**
     * 左布局
     * @param {Object}         d - d3返回的树形数据
     * @param {HTMLElement}    g - 文字的g标签
     * @param {Number} textWidth - 文字宽度
     **/
    function leftLayout(d, g, textWidth) {
        sourceY = d.source.y + paddingY;

        if (d.source.depth === 0) {
            d.target.M0 = sourceY;
        } else {
            d.target.M0 = d.target.parent.L4 + textWidth;
        }

        d.target.L4 = d.target.M0 + strokeLength;
        d.target.L0 = d.target.M0 + strokeLength / 2;
        d.target.L2 = d.target.M0 + strokeLength / 2;

        g.setAttribute('transform', `translate(${d.target.L4},${d.target.x})`);

        return `M${d.target.M0},${d.source.x} ${rememberShape}${d.target.L0},${d.source.x} ${d.target.L2},${d.target.x} ${d.target.L4},${d.target.x}`;
    }

    /**
     * 右布局
     * @param {Object}         d - d3返回的树形数据
     * @param {HTMLElement}    g - 文字的g标签
     * @param {Number} textWidth - 文字宽度
     **/
    function rightLayout(d, g, textWidth) {
        sourceY = (w - d.source.depth) - paddingY;

        if (d.source.depth === 0) {
            d.target.M0 = sourceY;
        } else {
            d.target.M0 = d.target.parent.L4 - textWidth;
        }

        d.target.L4 = d.target.M0 - strokeLength;
        d.target.L0 = d.target.M0 - strokeLength / 2;
        d.target.L2 = d.target.M0 - strokeLength / 2;
        d.target.translateX = d.target.L4 - d.target.widthOrHeight;

        g.setAttribute('transform', `translate(${d.target.translateX},${d.target.x})`);

        return `M${d.target.M0},${d.source.x} ${rememberShape}${d.target.L0},${d.source.x} ${d.target.L2},${d.target.x} ${d.target.L4},${d.target.x}`;
    }

    let count = 1;

    /**
     * 只设置第一级文字坐标
     * @param {String} layout - left = 左布局/right = 右布局/top = 上布局
     **/
    function firstTextTranslate(layout = rememberLayout) {
        firstText.attr('transform', function (d) {
            if (layout === 'right') {
                return `translate(${w - d.depth * 100 - paddingY},${d.x})`
            } else if (layout === 'left') {
                return `translate(${d.y + paddingY - firstText.node().getBBox().width - 1},${d.x})`
            } else if (layout === 'top') {
                return `translate(${d.x - firstText.node().getBBox().width / 2},${d.y + paddingY - firstText.node().getBBox().height / 2})`
            }
        });
    }

    /**
     * 递归父级
     **/
    function find(data) {
        let arr = [];
        return new Promise(resolve => {
            function f(val) {
                if (!val.parent) {
                    return resolve(arr)
                }

                arr.push(val.index);
                if (val.parent) {
                    f(val.parent)
                }
            }

            return f(data)
        })
    }

    /**
     * 搜集当前点击节点下的子节点的所有 taggingArr
     * @param {Array} data - 子节点
     **/
    function changeTagging(data) {
        let childs = []
        return new Promise(resolve => {
            function f(data) {
                for (let item of data) {
                    if (item.taggingArr) {
                        childs.push(...item.taggingArr)
                        item.taggingArr = null
                    }

                    if (item.children) {
                        f(item.children)
                    }
                }
            }

            f(data)
            return resolve(childs)
        })
    }

    /**
     * 删除标注线dom
     * @param {Array}    indexArr - 要删除的标注线data-index
     * @param {Boolean} removeAll - 删除所有的标注线
     **/
    function removePath({removeArr, removeAll = false}) {
        let lineDom = null
        let textDom = null

        if (typeof removeAll === 'boolean' && removeAll) {
            // 删除标注线
            lineDom = doc.querySelectorAll(`path[data-index]`)
            for (let item of lineDom) {
                item.remove()
            }

            // 删除text选中的class
            textDom = doc.querySelectorAll('.textShadow')
            for (let item of textDom) {
                item.classList.remove('textShadow')
            }
        } else if (removeArr.length > 0) {
            for (let i of removeArr) {
                link._groups[0][i].line = false
                lineDom = doc.querySelector(`path[data-index=index${i}]`)
                if (lineDom) {
                    lineDom.remove()
                }
            }
        }
    }

    // 集群
    let cluster = d3.cluster()
        .size([w, h])
        .separation(function (a, b) {
            return (a.parent === b.parent ? 1 : 2)
        });

    // 树形
    let tree = d3.tree()
        .size([w, h])
        .separation((a, b) => {
            return (a.parent === b.parent ? 1 : rememberLayout === 'top' ? 1 : 2)
        });

    d3.json('city.json').then(root => {
        let hierarchyData = d3.hierarchy(root);

        let svg = d3.select("body")
            .append("svg")
            .attr("width", '100%')
            .attr("height", '100%')
            .attr('viewBox', `0 0 ${w} ${h}`);

        let g = svg.append('g');

        let zoom = d3.zoom()
            // .scaleExtent([0, 1])
            .on('zoom', function ({transform}) {
                g.attr("transform", transform);
            });

        svg.call(zoom);
        // 初始放大比例
        // zoom.scaleTo(svg,0.9);

        let treeData = tree(hierarchyData);
        let nodes = treeData.descendants();
        line = treeData.links();


        node = g.selectAll('.node')
            .data(nodes, function (d, i) {
                d.index = i - 1;
                return i
            })
            .enter()
            .append('g')
            .classed('node', true)
            .on('click', async function (ev, current) {
                // 重复点击选中的节点
                if (current.taggingArr) {
                    this.querySelector('text').classList.remove('textShadow')
                    removePath({removeArr: current.taggingArr})
                    current.taggingArr = null
                } else {
                    // xx.line为true,表示被克隆过
                    if (link._groups[0][current.index].line) {
                        let val = [...new Set(await changeTagging(current.children))]

                        if (val.length) {
                            let textDom = null
                            let arr = []
                            current.taggingArr = []

                            for (let item of val) {
                                // 比当前index大的用于删除dom,比当前index小的保留
                                if (item > current.index) {
                                    // 删除text选中的class
                                    textDom = node._groups[0][item + 1].querySelector('.textShadow')
                                    if (textDom) {
                                        textDom.classList.remove('textShadow')
                                    }

                                    arr.push(item)
                                } else {
                                    // 保留的节点
                                    current.taggingArr.push(item)
                                }
                            }
                            removePath({removeArr: arr})
                        }
                    }
                    // xx.line为false,表示为新节点
                    else {
                        current.taggingArr = await find(current)
                    }
                }

                // 更新
                updateLink()
            });

        text = node.append('text')
            .attr('dy', 3)
            .text(function (d, i) {
                return d.data.name
            })
            .on('click', function () {
                this.classList.add('textShadow')
            });

        firstText = g.select('.node');

        firstTextTranslate();

        link = g.selectAll('.link')
            .data(line)
            .enter()
            .append('path')
            .attr('class', 'link')
            .attr('d', function (d, i) {
                d.target.widthOrHeight = text._groups[0][i + 1].getBBox()[rememberLayout === 'top' ? 'height' : 'width'];
                return lineShape(d, node._groups[0][i + 1]);
            })
    })
</script>
</body>
</html>

Copy the code

Data format: {“name”: “region “, “children”: [” {}”]