Fork me on GitHub

d3 v7树图实现动态边框,新增/编辑兄弟节点、子节点,删除节点和拖拽、缩放,动态边框

d3版本:v7。

PS:在用d3之前需要先了解SVG和CSS相关知识。树图生成部分和部分效果都是用SVG相关标签完成的。

 

效果图:

 

 

全部代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>d3 树状图实现动态边框,新增/编辑兄弟节点、子节点,删除节点和拖拽、缩放</title>

    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style type="text/css">
        body,
        html {
            width: 100%;
            height: 100%;
            position: relative;
            background-color: #f7f7f9;
        }

        g.link {
            fill: none;
            stroke: #ccc;
            stroke-width: 1.5;
        }

        text.tip {
            fill: #2553ff;
            font-size: 12px;
            text-anchor: middle;
            paint-order: stroke;
        }

        rect.out {
            border: none;
            stroke: #cfcfcf;
            fill: white;
            cursor: pointer;
        }

        rect.rightTip {
            border-left: #ccc;
            stroke: #555;
            fill: #fff;
            cursor: pointer;
        }

        rect.textPath {
            fill: #f5f5f5;
        }

        rect.showMenu {
            cursor: pointer;
            fill: none;
        }

        circle {
            fill: red;
        }

        .openClick {
            width: 15px;
            height: 15px;
            border: 1px solid red;
            border-radius: 50%;
            color: red;
            text-align: center;
            line-height: 15px;
            cursor: pointer;
            margin-left: 91px;
            margin-top: 2px;
            background-color: #fff;
        }


        .d3-context-menu {
            position: absolute;
            border: 1px solid #ddd;
            display: none;
            background-color: #fff;
            border-radius: 5px;
            font-size: 14px;
            box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.2);

        }

        line.rightLine {
            stroke: #d9d9d9;
        }

        ul {
            padding: 0;
            padding-left: 15px;
            min-width: 100px;
            min-height: 50px;
        }

        li {
            line-height: 30px;
            list-style: none;
        }

        li:hover {
            color: red;
            text-decoration: underline;
        }

        /* 边框动态线圈 */
        rect.out-animation {
            stroke-dasharray: 800;
            animation: strok 1.5s infinite;
            animation-direction: reverse;
        }

        @keyframes strok {
            100% {
                stroke: blue;
                stroke-dashoffset: 800;
            }
        }
    </style>
</head>

<body>
    <div id="tree"></div>

    <div class="d3-context-menu">
        <ul>
            <li id="nodeSonAdd">添加子节点</li>
            <li id="nodeAdd">添加兄弟节点</li>
            <li id="nodeDelete">删除节点</li>
        </ul>
    </div>
</body>

<script>
    //随机数,用于绑定id
    function uuid() {
        return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
    }

    let data = {
        'nodeName': '开始节点',
        id: '0',
        'children': []
    }


    let tree = d3.tree()
        .nodeSize([150, 50]) // 设置节点大小
        .separation((a, b) => a.parent === b.parent ? 2 : 2.5), // 设置节点间隔
        width = document.body.clientWidth,
        height = document.body.clientHeight,
        svg = d3.select('#tree').append('svg')
            .attr('width', width).attr('height', height) // 设置svg的宽高和缩放、拖拽效果
            .call(d3.zoom().scaleExtent([0.2, 2]).on('zoom', (event) => { // 缩放和拖拽
                if (event.sourceEvent.type != 'dblclick') {
                    g.attr("transform", event.transform);
                    d3.select('.d3-context-menu').style('display', 'none');
                }
            })),
        g = svg.append("g").attr("id", "g_main").attr("transform", "translate(" + width / 2 + "," + 100 + ")"), // 设置g的id和transform 属性

        duration = 300, // transition的持续时间
        rectHight = 120, // rect的高
        rectWidth = 200,// rect的宽
        rootData,
        clickNodeData; //点击的节点数据


    let link = g.append("g").attr("class", "link");// 树的线条使用的g
    let nodeG = g.append('g').attr("id", "id_node").attr("stroke-width", 1); // 树的节点使用的g

    document.getElementById('nodeSonAdd').addEventListener('click', function () {
        addNode('append', clickNodeData)
    })

    document.getElementById('nodeAdd').addEventListener('click', function () {
        addNode('insert', clickNodeData)
    })

    document.getElementById('nodeDelete').addEventListener('click', function () {
        deleteNode('', clickNodeData)
    })

    createRootData();
    function createRootData() {
        rootData = null;
        rootData = tree(d3.hierarchy(data)); // 树节点添加X,y属性
        rootData.each(d => {
            d.y = d.depth * 240; 
            d.id = d.data.id || uuid();
        });

        createLink();
        createNode();
    }

    // 添加线条
    function createLink() {
        // rootData.links() 树的连线节点
        let links = link.selectAll('path.linkPath').data(rootData.links())
        let linkEnter = links.enter().append('path').attr("class", "linkPath").attr("d", (d) => drawLink(d));
      linkEnter.merge(links).transition().duration(
150).attr("d", (d) => drawLink(d));
links.exit().transition().duration(duration
/ 2).remove(); } // 生成连线 function drawLink({ source, target }) { let s = source, t = target; return `M ${s.x} ${s.y} V ${s.y + rectHight + (rectHight / 2)} H ${t.x}, V ${t.y}`; } // 生成节点 function createNode() { // rootData.descendants() 树节点派生,当前节点信息和父节点信息 let nodes = nodeG.selectAll('g.node').data(rootData.descendants(), d => d.data.nodeName); // 添加节点及节点属性 let nodeEnter = nodes.enter().append('g') .attr('id', d => 'node_' + d.id).attr('class', 'node') .attr('transform', d => `translate(${d.x},${d.y})`); nodeEnter.append("rect").attr('class', 'out out-animation') .attr('width', rectWidth).attr('height', rectHight).attr('x', -rectWidth / 2) .attr('rx', '5').attr('ry', '5').on('dblclick', showD3Menu); nodeEnter.append("text").attr('class', 'text').attr("x", -20).attr("y", rectHight / 2).text(d => d.data.nodeName); nodeEnter.append("rect").attr('class', 'textPath').attr('width', rectWidth).attr('height', 20).attr("x", -rectWidth / 2).attr("y", rectHight - 21).attr('rx', '5'); nodeEnter.append("text").attr('class', 'tip').attr("x", '1').attr("y", rectHight - 5).text('双击添加子节点'); // 更新节点信息 let nodeUpdate = nodes.merge(nodeEnter); // 节点合并 nodeUpdate.transition().duration(300).attr('transform', d => `translate(${d.x},${d.y})`); nodeUpdate.select('text.text').text(d => d.data.nodeName); // 删除节点 nodes.exit().transition().duration(duration).attr('transform', (d) => "translate(" + d.parent.x + "," + d.parent.y + ")").remove(); } function showD3Menu(event, node) { clickNodeData = node; d3.select('.d3-context-menu').style('display', 'block').style('left', event.pageX + 66 + 'px').style('top', event.pageY - 50 + 'px'); d3.select('#nodeAdd').style('display', node && node.parent ? 'block' : 'none'); d3.select('#nodeDelete').style('display', node && node.parent ? 'block' : 'none'); } // 添加节点 function addNode(event, nodeData) { if (event == 'insert') { addBroNode([data] || [], nodeData); } else if (event == 'append') { addSonNode(data.children || [], nodeData); } createRootData(); } // 添加子节点 function addSonNode(data1 = [], nodeData) { if (nodeData.data.id == 0) { let uid = uuid(); data.children.push({ nodeName: '新增-' + uid, id: uid, isNotSaveNode: true, parentId: 0 }) } else { for (let i = 0; i < data1.length; i++) { let item = data1[i]; if (item.nodeName == nodeData.data.nodeName) { if (!item.children) { item.children = []; } let uid = uuid(); item.children.push({ nodeName: '新增-' + uid, id: uid, parentId: nodeData.data.id, isNotSaveNode: true, }) } else { addSonNode(item.children, nodeData) } } } } // 添加兄弟节点 function addBroNode(data1 = [], nodeData) { data1.map((item, index) => { if (item.id == nodeData.data.parentId) { let uid = uuid(); item.children.push({ nodeName: '新增-' + uid, id: uid, parentId: nodeData.data.parentId, isNotSaveNode: true, }) } if (item.children && item.children.length > 0) { addBroNode(item.children, nodeData) } }) } // 更新节点 function updateNode() { createNode(); createLink(); d3.select('.d3-context-menu').style('display', 'none'); } // 删除节点/连线 function deleteNode(event, nodeName) { let data1 = filterDel(rootData.children, nodeName); let data2 = filterDel(rootData.data.children, nodeName); rootData.children = data1; rootData.data.children = data2 updateNode(); } // 过滤数据 function filterDel(data1, node) { data1.forEach((item, index) => { if (item.id == node.id) { data1.splice(index, 1) } if (item.children && item.children.length > 0) { filterDel(item.children, node) } }) return data1 } // 子节点收起、展开 function expandOrCollapse(event, node) { if (node.children) { node._children = node.children; node.children = null; event.target.innerText = '+'; } else { node.children = node._children; node._children = null; event.target.innerText = '-'; } updateNode() } </script> </html>

 

posted @ 2024-05-22 13:24  元芳啊  阅读(186)  评论(0编辑  收藏  举报