首页 > 解决方案 > 特殊移除情况下 D3 链接中断

问题描述

我在图表中添加了一个上下文菜单,我可以在其中使用我的添加和删除功能。无法删除接收连接的节点,警报会通知用户。此外,我可以“无休止地”添加节点。现在是有趣的“问题”部分。

情况1:如果从最高到最小的顺序删除节点,然后添加一个新节点,就可以了。例如,删除节点 8 并在任意位置添加一个新节点。或者删除节点 8、7、6 并在之后添加一个节点也可以。

情况2:移除一个不在最后一个数组位置的节点,然后在任意位置添加一个节点。连接会中断。例如,移除节点 5,在 3 上添加一个新节点。节点 8 的连接将断开。

我对这个问题越来越精神了。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Playground D3v6</title>
    <!-- favcon -->
    <link rel="icon" href="https://networkrepository.com/favicon.png">
    <!-- call external d3.js framework -->
    <script src="https://d3js.org/d3.v6.js"></script>
    <!-- import multiselection framework -->
    <script src="https://d3js.org/d3-selection-multi.v1.js"></script>
    <!-- import "font awesome" stylesheet https://fontawesome.com/ -->
    <script src="https://kit.fontawesome.com/39094309d6.js" crossorigin="anonymous"></script>
</head>

<style>
    body {
        overflow: hidden;
        margin: 0px;
    }

    .canvas {
        background-color: rgb(220, 220, 220);
    }

    .link {
        stroke: rgb(0, 0, 0);
        stroke-width: 1px;
    }

    circle {
        background-color: whitesmoke;
    }

    .tooltip {
        font-family: "Open Sans", sans-serif;
        position: absolute;
        text-align: left;
        background: rgb(245, 245, 245);
        border: 2px;
        border-radius: 6px;
        border-color: rgb(255, 255, 255);
        border-style: solid;
        pointer-events: none;
        line-height: 150%;
        padding: 8px 10px;
    }

    #context-menu {
        font-family: "Open Sans", sans-serif;
        position: fixed;
        z-index: 10000;
        width: 190px;
        background: whitesmoke;
        border: 2px;
        border-radius: 6px;
        border-color: white;
        border-style: solid;
        transform: scale(0);
        transform-origin: top left;
    }

    #context-menu.active {
        transform: scale(1);
        transition: transform 200ms ease-in-out;
    }

    #context-menu .item {
        padding: 8px 10px;
        font-size: 15px;
        color: black;
    }

    #context-menu .item i {
        display: inline-block;
        margin-right: 5px;
    }

    #context-menu hr {
        margin: 5px 0px;
        border-color: whitesmoke;
    }

    #context-menu .item:hover {
        background: lightblue;
    }

</style>

<body>
    <!-- right click context menu -->
    <div id="context-menu">
        <div id="addObject" class="item">
            <i class="fa fa-plus-circle"></i> Add Node
        </div>
        <div id="removeObject" class="item">
            <i class="fa fa-minus-circle"></i> Remove Node
        </div>
    </div>

    <svg id="svg"> </svg>


    <!-- call script where the main application is written -->
    <script>
        var graph = {
            "nodes": [{
                "id": 0,
                "type": "Company",
                "name": "Company",
                "icon": "\uf1ad",
                "parent": 0
            },
            {
                "id": 1,
                "type": "Software",
                "name": "Software_1",
                "icon": "\uf7b1",
                "parent": 0
            },
            {
                "id": 2,
                "type": "Software",
                "name": "Software_2",
                "icon": "\uf78d",
                "parent": 0
            },
            {
                "id": 3,
                "type": "Software",
                "name": "Software_3",
                "icon": "\ue084",
                "parent": 0
            },
            {
                "id": 4,
                "type": "Software",
                "name": "Software_4",
                "icon": "\ue084",
                "parent": 0
            },
            {
                "id": 5,
                "type": "Software",
                "name": "Software_5",
                "icon": "\ue084",
                "parent": 3
            },
            {
                "id": 6,
                "type": "Software",
                "name": "Software_6",
                "icon": "\ue084",
                "parent": 3
            },
            {
                "id": 7,
                "type": "Software",
                "name": "Software_7",
                "icon": "\ue084",
                "parent": 4
            },
            {
                "id": 8,
                "type": "Software",
                "name": "Software_8",
                "icon": "\ue084",
                "parent": 4
            }
            ],
            "links": [{
                "source": 1,
                "target": 0,
                "type": "uses"
            },
            {
                "source": 2,
                "target": 0,
                "type": "uses"
            },
            {
                "source": 3,
                "target": 0,
                "type": "uses"
            },
            {
                "source": 4,
                "target": 0,
                "type": "uses"
            },
            {
                "source": 5,
                "target": 3,
                "type": "uses"
            },
            {
                "source": 6,
                "target": 3,
                "type": "uses"
            },
            {
                "source": 7,
                "target": 4,
                "type": "uses"
            },
            {
                "source": 8,
                "target": 4,
                "type": "uses"
            }
            ]
        }

        // declare initial variables
        var svg = d3.select("svg")
        width = window.innerWidth
        height = window.innerHeight
        node = null
        link = null
        thisNode = null;
        d = null;
        isParent = false;

        // define cavnas area to draw everything
        svg = d3.select("svg")
            .attr("class", "canvas")
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function (event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        // remove zoom on dblclick listener
        d3.select("svg").on("dblclick.zoom", null)

        // append markers to svg
        svg.append('defs').append('marker')
            .attrs({
                'id': 'arrowhead',
                'viewBox': '-0 -5 10 10',
                'refX': 14,
                'refY': 0,
                'orient': 'auto',
                'markerWidth': 30,
                'markerHeight': 30,
                'xoverflow': 'visible'
            })
            .append('svg:path')
            .attr('d', 'M 0,-2 L 4 ,0 L 0,2')
            .attr('fill', 'black')
            .style('stroke', 'none');

        var linksContainer = svg.append("g").attr("class", "linksContainer")
        var nodesContainer = svg.append("g").attr("class", "nodesContainer")

        // iniital force simulation
        var simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function (d) {
                return d.id;
            }).distance(100))
            .force("charge", d3.forceManyBody().strength(-400))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("attraceForce", d3.forceManyBody().strength(70));

        var tooltip = d3.select("body").append("div")
            .attr("class", "tooltip")
            .style("opacity", 0);

        //create links
        link = linksContainer.selectAll(".link")
            .data(graph.links, d => d.id)
            .enter()
            .append("line")
            .attr("class", "link")
            .style("pointer-events", "none")
            .attr('marker-end', 'url(#arrowhead)')

        node = nodesContainer.selectAll(".node")
            .data(graph.nodes, d => d.id)
            .enter()
            .append("g")
            .attr("class", "node")
            .attr("stroke", "white")
            .attr("stroke-width", "2px")
            .call(d3.drag()
            .on("start", dragStarted)
            .on("drag", dragged)
            .on("end", dragEnded)
            )
            

        node.append("circle")
            .attr("r", 30)
            .style("fill", "whitesmoke")
            .on("mouseenter", mouseEnter)
            .on("mouseleave", mouseLeave)
            .on("contextmenu", contextMenu)

        node.append("text")
            .style("class", "icon")
            .attr("font-family", "FontAwesome")
            .attr("dominant-baseline", "central")
            .attr("text-anchor", "middle")
            .attr("font-size", 30)
            .attr("fill", "black")
            .attr("stroke-width", "0px")
            .attr("pointer-events", "none")
            .text(function (d) {
                return d.id
            })

        simulation
            .nodes(graph.nodes)
            .on("tick", ticked);

        simulation
            .force("link")
            .links(graph.links)

            function mouseEnter(event, d) {
            thisNode = d

            d3.select(this)
                .style("fill", "lightblue")
            tooltip.transition()
                .style("opacity", 1)
            tooltip.html(
                "ID: " + d.id + "<br/>" +
                "Name: " + d.name + "<br/>" +
                "Typ: " + d.type + "<br/>" +
                "Parent: " + d.parent)
                .style("left", (event.pageX) + 30 + "px")
                .style("top", (event.pageY - 80) + "px");
        }

        function mouseLeave(d) {
            switch (d.name) {
                case ("power-plug"):
                    tooltip.transition()
                        .style("opacity", 0);

                    return
                default:
                    d3.select(this).style("fill", "whitesmoke")

                    tooltip.transition()
                        .style("opacity", 0);

            }
        }

        function contextMenu(event, d) {
            thisNode = d

            tooltip.transition()
                .style("opacity", 0);

            event.preventDefault()

            var contextMenu = document.getElementById("context-menu")
            contextMenu.style.top = event.clientY + "px"
            contextMenu.style.left = event.clientX + "px"
            contextMenu.classList.add("active")

            window.addEventListener("click", function () {
                contextMenu.classList.remove("active")
            })

            document.getElementById("addObject").addEventListener("click", addNodeClicked)
            document.getElementById("removeObject").addEventListener("click", removeNodeClicked)
        }

        function addNodeClicked() {

            addNode(thisNode)
        }

        function addNode(d) {
            var newID = Math.floor(Math.random()*100000)

            graph.nodes.push({
                "id": newID,
                "type": "software",
                "name": "Software_" + newID,
                "icon": "\ue084",
                "parent": d.id,
            })

            graph.links.push({
                source: newID,
                target: d.id,
                type: "uses"
            })

            link = linksContainer.selectAll(".link")
                .data(graph.links)
                .enter()
                .append("line")
                .attr("class", "link")
                .style("pointer-events", "none")
                .attr('marker-end', 'url(#arrowhead)')
                .style("display", "block")
                .merge(link)

            node = nodesContainer.selectAll(".node")
                .data(graph.nodes)
                .enter()
                .append("g")
                .attr("class", "node")
                .attr("stroke", "white")
                .attr("stroke-width", "2px")
                .call(d3.drag()
                .on("start", dragStarted)
                .on("drag", dragged)
                .on("end", dragEnded)
                )
                
                .merge(node)

            node.append("circle")
                .attr("r", 30)
                .style("fill", "whitesmoke")
                .on("click", addNodeClicked)
                .on("mouseenter", mouseEnter)
                .on("mouseleave", mouseLeave)
                .on("contextmenu", contextMenu)
                .merge(node)

            node.append("text")
                .style("class", "icon")
                .attr("font-family", "FontAwesome")
                .attr("dominant-baseline", "central")
                .attr("text-anchor", "middle")
                .attr("font-size", 30)
                .attr("fill", "black")
                .attr("stroke-width", "0px")
                .attr("pointer-events", "none")
                .text(function (d) {
                    return d.id
                })
                .merge(node)

            simulation.nodes(graph.nodes);
            simulation.force("link").links(graph.links);

            //reheat the simulation
            simulation.alpha(0.3).restart()
            
            /*
            
            console.log("addNode: ")
            console.log(graph.nodes)
            console.log("---------")
            
            console.log("addLink: ")
            console.log(graph.links)
            console.log("---------")
            */
        }

        function removeNodeClicked() {
            removeNode(thisNode)
        }

        function removeNode(d) {
            var hasNeighborNodes = false

            link.filter((l) => {
                if (d.id == l.target.id) {
                    hasNeighborNodes = true
                }

            })

            if (hasNeighborNodes) {
                alert("Object can´t be deleted, beause of incoming connections. Please re-arrange or delete incoming connections first.")
                hasNeighborNodes = false

            } else if (!hasNeighborNodes) {


                var indexOfNodes = graph.nodes.indexOf(d)

                var indexOfLinks = graph.links.findIndex(element => element.source.id == d.id)
                
                graph.links.splice(indexOfLinks, 1)
                
                linksContainer.selectAll(".link")
                .data(graph.links)
                .exit()
                .remove()
                

                graph.nodes.splice(indexOfNodes, 1)

                node
                    .data(graph.nodes, d => d.id)
                    .exit()
                    .remove()

                simulation.nodes(graph.nodes);
                simulation.force("link").links(graph.links);

                //reheat the simulation
                simulation.alpha(0.3).restart()

            }


        }



        function ticked() {
            // update link positions
            link
                .attr("x1", function (d) {
                    return d.source.x;
                })
                .attr("y1", function (d) {
                    return d.source.y;
                })
                .attr("x2", function (d) {
                    return d.target.x;
                })
                .attr("y2", function (d) {
                    return d.target.y;
                });

            // update node positions
            node
                .attr("transform", function (d) {
                    return "translate(" + d.x + ", " + d.y + ")";
                });

        }

        

        
        function dragStarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }
        
        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }
        
        function dragEnded(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = undefined;
            d.fy = undefined;
        }
        
        </script>
</body>

</html>

标签: javascriptarraysd3.js

解决方案


删除节点时,节点选择对象不会被更新。

要更新它,您应该更改:

node
    .data(graph.nodes, d => d.id)
    .exit()
    .remove()

类似于:

// no need to merge with enter, since you're only removing data
node = node
    .data(graph.nodes, d => d.id);
node.exit()
    .remove();

或者,使用推荐的做法selection.join

node = node
    .data(graph.nodes, d => d.id)
    .join();

发生了什么

当你删除一个元素时,如果你不更新node到新的选择,你的选择就会有一个额外的元素(指向一个分离的 DOM 节点)。然后当你添加一个新节点时,这段代码

node = nodesContainer.selectAll(".node")
    .data(graph.nodes)
    .enter().append("g")
    //
    .merge(node);

正在尝试合并两个相同大小的选择。新选择 from.enter()对除最后一个元素之外的所有节点都有空槽(例如,如果您将另一个节点添加到 9 个已经存在的节点,.enter()将返回大小为 10 的选择,但前 9 个是空的) . 旧的选择 ,node已填满所有插槽,尽管其中一个指向一个分离的 DOM 节点。因为它们的大小相同,并且元素仅在插槽为空时才合并到新选择中,因此不会合并旧选择的最后一个元素。如果它不是被移除的节点,那么它不再在选择中,并且不会得到更新。

也更新link

正如评论中所指出的,还有一个错误,如果您删除同一链中的连续节点,它会显示警报。这同样是由于没有更新链接选择对象,它使用过时的数据来检查是否可以删除节点。

要更新link,您应该更改:

linksContainer.selectAll(".link")
    .data(graph.links)
    .exit()
    .remove()

类似于:

link = linksContainer.selectAll(".link")
    .data(graph.links)
    .join()

此外:

您表示您还想更好地了解更新过程。您的代码暗示您可能不太了解其中的细微差别,尤其是在添加新节点时。

使用node.append, 将一个节点附加到选择中的每个组,甚至是已经添加的旧组。您可以通过查看 DOM 检查器来验证这一点,添加几个节点后,每个节点上都有多个circletext元素。相反,您应该只将它们附加到新节点。

它看起来像这样:

var newNodes = nodesContainer.selectAll(".node")
    .data(graph.nodes)
    .enter().append("g")
    // ...

newNodes.append("circle")
    // ...

node = newNodes.merge(node);

此外,您使用 结束某些链.merge(node),这会创建一个新选择,该选择尝试将新附加的元素(circles 或texts )与其父元素组合,并且如果您不使用它或将其分配给它,则不会做任何事情任何事物。您可能想.merge了解它的作用和用途,尽管您可能只想了解更新的.join方法。

全部一起:

var graph = {
    "nodes": [{
        "id": 0,
        "type": "Company",
        "name": "Company",
        "icon": "\uf1ad",
        "parent": 0
    },
    {
        "id": 1,
        "type": "Software",
        "name": "Software_1",
        "icon": "\uf7b1",
        "parent": 0
    },
    {
        "id": 2,
        "type": "Software",
        "name": "Software_2",
        "icon": "\uf78d",
        "parent": 0
    },
    {
        "id": 3,
        "type": "Software",
        "name": "Software_3",
        "icon": "\ue084",
        "parent": 0
    },
    {
        "id": 4,
        "type": "Software",
        "name": "Software_4",
        "icon": "\ue084",
        "parent": 0
    },
    {
        "id": 5,
        "type": "Software",
        "name": "Software_5",
        "icon": "\ue084",
        "parent": 3
    },
    {
        "id": 6,
        "type": "Software",
        "name": "Software_6",
        "icon": "\ue084",
        "parent": 3
    },
    {
        "id": 7,
        "type": "Software",
        "name": "Software_7",
        "icon": "\ue084",
        "parent": 4
    },
    {
        "id": 8,
        "type": "Software",
        "name": "Software_8",
        "icon": "\ue084",
        "parent": 4
    }
    ],
    "links": [{
        "source": 1,
        "target": 0,
        "type": "uses"
    },
    {
        "source": 2,
        "target": 0,
        "type": "uses"
    },
    {
        "source": 3,
        "target": 0,
        "type": "uses"
    },
    {
        "source": 4,
        "target": 0,
        "type": "uses"
    },
    {
        "source": 5,
        "target": 3,
        "type": "uses"
    },
    {
        "source": 6,
        "target": 3,
        "type": "uses"
    },
    {
        "source": 7,
        "target": 4,
        "type": "uses"
    },
    {
        "source": 8,
        "target": 4,
        "type": "uses"
    }
    ]
}

// declare initial variables
var svg = d3.select("svg")
width = window.innerWidth
height = window.innerHeight
node = null
link = null
thisNode = null;
d = null;
isParent = false;

// define cavnas area to draw everything
svg = d3.select("svg")
    .attr("class", "canvas")
    .attr("width", width)
    .attr("height", height)
    .call(d3.zoom().on("zoom", function (event) {
        svg.attr("transform", event.transform)
    }))
    .append("g")

// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)

// append markers to svg
svg.append('defs').append('marker')
    .attrs({
        'id': 'arrowhead',
        'viewBox': '-0 -5 10 10',
        'refX': 14,
        'refY': 0,
        'orient': 'auto',
        'markerWidth': 30,
        'markerHeight': 30,
        'xoverflow': 'visible'
    })
    .append('svg:path')
    .attr('d', 'M 0,-2 L 4 ,0 L 0,2')
    .attr('fill', 'black')
    .style('stroke', 'none');

var linksContainer = svg.append("g").attr("class", "linksContainer")
var nodesContainer = svg.append("g").attr("class", "nodesContainer")

// iniital force simulation
var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function (d) {
        return d.id;
    }).distance(100))
    .force("charge", d3.forceManyBody().strength(-400))
    .force("center", d3.forceCenter(width / 2, height / 2))
    .force("attraceForce", d3.forceManyBody().strength(70));

var tooltip = d3.select("body").append("div")
    .attr("class", "tooltip")
    .style("opacity", 0);

//create links
link = linksContainer.selectAll(".link")
    .data(graph.links, d => d.id)
    .enter()
    .append("line")
    .attr("class", "link")
    .style("pointer-events", "none")
    .attr('marker-end', 'url(#arrowhead)')

node = nodesContainer.selectAll(".node")
    .data(graph.nodes, d => d.id)
    .enter()
    .append("g")
    .attr("class", "node")
    .attr("stroke", "white")
    .attr("stroke-width", "2px")
    .call(d3.drag()
    .on("start", dragStarted)
    .on("drag", dragged)
    .on("end", dragEnded)
    )
    

node.append("circle")
    .attr("r", 30)
    .style("fill", "whitesmoke")
    .on("mouseenter", mouseEnter)
    .on("mouseleave", mouseLeave)
    .on("contextmenu", contextMenu)

node.append("text")
    .style("class", "icon")
    .attr("font-family", "FontAwesome")
    .attr("dominant-baseline", "central")
    .attr("text-anchor", "middle")
    .attr("font-size", 30)
    .attr("fill", "black")
    .attr("stroke-width", "0px")
    .attr("pointer-events", "none")
    .text(function (d) {
        return d.id
    })

simulation
    .nodes(graph.nodes)
    .on("tick", ticked);

simulation
    .force("link")
    .links(graph.links)

    function mouseEnter(event, d) {
    thisNode = d

    d3.select(this)
        .style("fill", "lightblue")
    tooltip.transition()
        .style("opacity", 1)
    tooltip.html(
        "ID: " + d.id + "<br/>" +
        "Name: " + d.name + "<br/>" +
        "Typ: " + d.type + "<br/>" +
        "Parent: " + d.parent)
        .style("left", (event.pageX) + 30 + "px")
        .style("top", (event.pageY - 80) + "px");
}

function mouseLeave(d) {
    switch (d.name) {
        case ("power-plug"):
            tooltip.transition()
                .style("opacity", 0);

            return
        default:
            d3.select(this).style("fill", "whitesmoke")

            tooltip.transition()
                .style("opacity", 0);

    }
}

function contextMenu(event, d) {
    thisNode = d

    tooltip.transition()
        .style("opacity", 0);

    event.preventDefault()

    var contextMenu = document.getElementById("context-menu")
    contextMenu.style.top = event.clientY + "px"
    contextMenu.style.left = event.clientX + "px"
    contextMenu.classList.add("active")

    window.addEventListener("click", function () {
        contextMenu.classList.remove("active")
    })

    document.getElementById("addObject").addEventListener("click", addNodeClicked)
    document.getElementById("removeObject").addEventListener("click", removeNodeClicked)
}

function addNodeClicked() {

    addNode(thisNode)
}

function addNode(d) {
    var newID = Math.floor(Math.random()*100000)

    graph.nodes.push({
        "id": newID,
        "type": "software",
        "name": "Software_" + newID,
        "icon": "\ue084",
        "parent": d.id,
    })

    graph.links.push({
        source: newID,
        target: d.id,
        type: "uses"
    })

    link = linksContainer.selectAll(".link")
        .data(graph.links)
        .enter()
        .append("line")
        .attr("class", "link")
        .style("pointer-events", "none")
        .attr('marker-end', 'url(#arrowhead)')
        .style("display", "block")
        .merge(link)

    var newNodes = nodesContainer.selectAll(".node")
        .data(graph.nodes)
        .enter()
        .append("g")
        .attr("class", "node")
        .attr("stroke", "white")
        .attr("stroke-width", "2px")
        .call(d3.drag()
            .on("start", dragStarted)
            .on("drag", dragged)
            .on("end", dragEnded)
        )
        
    newNodes.append("circle")
        .attr("r", 30)
        .style("fill", "whitesmoke")
        .on("click", addNodeClicked)
        .on("mouseenter", mouseEnter)
        .on("mouseleave", mouseLeave)
        .on("contextmenu", contextMenu);

    newNodes.append("text")
        .style("class", "icon")
        .attr("font-family", "FontAwesome")
        .attr("dominant-baseline", "central")
        .attr("text-anchor", "middle")
        .attr("font-size", 30)
        .attr("fill", "black")
        .attr("stroke-width", "0px")
        .attr("pointer-events", "none")
        .text(function (d) {
            return d.id
        });

    node = newNodes.merge(node)


    simulation.nodes(graph.nodes);
    simulation.force("link").links(graph.links);

    //reheat the simulation
    simulation.alpha(0.3).restart()
    
    /*
    
    console.log("addNode: ")
    console.log(graph.nodes)
    console.log("---------")
    
    console.log("addLink: ")
    console.log(graph.links)
    console.log("---------")
    */
}

function removeNodeClicked() {
    removeNode(thisNode)
}

function removeNode(d) {
    var hasNeighborNodes = false

    link.filter((l) => {
        if (d.id == l.target.id) {
            hasNeighborNodes = true
        }

    })

    if (hasNeighborNodes) {
        alert("Object can´t be deleted, beause of incoming connections. Please re-arrange or delete incoming connections first.")
        hasNeighborNodes = false

    } else if (!hasNeighborNodes) {


        var indexOfNodes = graph.nodes.indexOf(d)

        var indexOfLinks = graph.links.findIndex(element => element.source.id == d.id)
        
        graph.links.splice(indexOfLinks, 1)
        
        link = linksContainer.selectAll(".link")
          .data(graph.links)
          .join();
        

        graph.nodes.splice(indexOfNodes, 1)

        node = node
            .data(graph.nodes, d => d.id)
        node.exit()
            .remove()

        simulation.nodes(graph.nodes);
        simulation.force("link").links(graph.links);

        //reheat the simulation
        simulation.alpha(0.3).restart()

    }


}



function ticked() {
    // update link positions
    link
        .attr("x1", function (d) {
            return d.source.x;
        })
        .attr("y1", function (d) {
            return d.source.y;
        })
        .attr("x2", function (d) {
            return d.target.x;
        })
        .attr("y2", function (d) {
            return d.target.y;
        });

    // update node positions
    node
        .attr("transform", function (d) {
            return "translate(" + d.x + ", " + d.y + ")";
        });

}




function dragStarted(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
}

function dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
}

function dragEnded(event, d) {
    if (!event.active) simulation.alphaTarget(0);
    d.fx = undefined;
    d.fy = undefined;
}
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Playground D3v6</title>
    <!-- favcon -->
    <link rel="icon" href="https://networkrepository.com/favicon.png">
    <!-- call external d3.js framework -->
    <script src="https://d3js.org/d3.v6.js"></script>
    <!-- import multiselection framework -->
    <script src="https://d3js.org/d3-selection-multi.v1.js"></script>
    <!-- import "font awesome" stylesheet https://fontawesome.com/ -->
    <script src="https://kit.fontawesome.com/39094309d6.js" crossorigin="anonymous"></script>
</head>

<style>
    body {
        overflow: hidden;
        margin: 0px;
    }

    .canvas {
        background-color: rgb(220, 220, 220);
    }

    .link {
        stroke: rgb(0, 0, 0);
        stroke-width: 1px;
    }

    circle {
        background-color: whitesmoke;
    }

    .tooltip {
        font-family: "Open Sans", sans-serif;
        position: absolute;
        text-align: left;
        background: rgb(245, 245, 245);
        border: 2px;
        border-radius: 6px;
        border-color: rgb(255, 255, 255);
        border-style: solid;
        pointer-events: none;
        line-height: 150%;
        padding: 8px 10px;
    }

    #context-menu {
        font-family: "Open Sans", sans-serif;
        position: fixed;
        z-index: 10000;
        width: 190px;
        background: whitesmoke;
        border: 2px;
        border-radius: 6px;
        border-color: white;
        border-style: solid;
        transform: scale(0);
        transform-origin: top left;
    }

    #context-menu.active {
        transform: scale(1);
        transition: transform 200ms ease-in-out;
    }

    #context-menu .item {
        padding: 8px 10px;
        font-size: 15px;
        color: black;
    }

    #context-menu .item i {
        display: inline-block;
        margin-right: 5px;
    }

    #context-menu hr {
        margin: 5px 0px;
        border-color: whitesmoke;
    }

    #context-menu .item:hover {
        background: lightblue;
    }

</style>

<body>
    <!-- right click context menu -->
    <div id="context-menu">
        <div id="addObject" class="item">
            <i class="fa fa-plus-circle"></i> Add Node
        </div>
        <div id="removeObject" class="item">
            <i class="fa fa-minus-circle"></i> Remove Node
        </div>
    </div>

    <svg id="svg"> </svg>
</body>

</html>


推荐阅读