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