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

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

时间:2024-05-22 13:29:06浏览次数:14  
标签:node attr d3 节点 边框 动态 data children

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>

 

标签:node,attr,d3,节点,边框,动态,data,children
From: https://www.cnblogs.com/liuyuanfang/p/18206067

相关文章

  • 输入自带边框的文字
    输入自带边框的文字首先,我们选中文本工具,在Illustrator中输入文字。然后,依次点击窗口,外观,再点击添加新描边。点击“外观”面板下方的第三个“添加新效果”按钮,在弹出的菜单中,选择转换为形状。参考链接:https://blog.csdn.net/weixin_35387135/article/details/112739759......
  • 一个动态指定logback日志路径的方法
    基于版本:<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.3.14</version></dependency>logback.xml:<?xmlversion="1.0"encoding="UTF-8......
  • sql server 动态查询空格问题
    一个奇怪的bug,之前遇到过,今天再次遇到了,这里说一下,先看如下SQL:--存在这个表则删除IFOBJECT_ID(N't_pl_table',N'U')isnotNULLBEGINDROPTABLEt_pl_tableEND--创建一个表用来测试CREATETABLEt_pl_table(item_nochar(20))--随便插入一条数据INSERTINTO......
  • 数据结构与算法学习——动态规划
    动态规划动态规划(英语:Dynamicprogramming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题[1]和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素......
  • 动态规划
    DP常见考虑角度状态表示以0/1背包问题为例:所求解问题用几维变量表示。常见的两维:$f_{i,j}$。两个考虑角度:集合+属性表示的集合是什么:所有选法的集合。条件:只从前\(i\)个物品里面选,总体积\(\leqslantj\)。集合的属性是什么:最大值、最小值、元素数量......
  • UE4 动态生成网格
    说明在游戏中动态改变网格数量和形状等,该功能是寻路功能的前期准备,即在基础移动地基上方,构建一层网格,任何移动的操作都可以基于该网格进行计算。从而在编辑器模式下能够更方便进行调试InstancedStaticMeshComponent其是一种用于优化静态网格渲染性能的技术。InstancedStaticMes......
  • K8S多master节点更换证书
    以下命令master节点均需要执行1.备份cp-a/etc/kubernetes{,.bak}cp-a/var/lib/kubelet{,.bak}cp-a/var/lib/etcd/var/lib/etcd.bak2.生成kubeadm-configkubectl-nkube-systemgetcmkubeadm-config-oyaml>kubeadm-config-20240521.yaml3.刷新证书到期时间再......
  • rancher添加k8s节点时显示节点已添加
    由于几台服务器都用相同的外部externalip,而添加k8s节点时有外部ip先使用外部ip,所以会显示节点已添加,无法添加成功,此时需要用--address参数来指定内部ip,这样节点就由内部ip来添加如下添加k8s节点的命令:sudodockerrun-d--privileged--restart=unless-stopped--net=ho......
  • 创建hello_world节点、编译、运行
    1、在功能包的src目录下创建源文件cd hello_world_ws/src/hello_world_pkg/srctouch hello_world.cpp#include"rclcpp/rclcpp.hpp"intmain(intargc,char**argv){  rclcpp::init(argc,argv);  autonode=std::make_shared<rclcpp::Node>("hello_w......
  • 自动化部署elasticsearch三节点集群
    什么是Elasticsearch?Elasticsearch是一个开源的分布式搜索和分析引擎,构建在ApacheLucene的基础上。它提供了一个分布式多租户的全文搜索引擎,具有实时分析功能。Elasticsearch最初是用于构建全文搜索引擎,但它的功能已经扩展到包括日志分析、应用程序性能监控、地理信息系统等......