Практический пример выбора D3.join()

#javascript #d3.js

#javascript #d3.js

Вопрос:

Я пытаюсь понять join() функцию в D3v6. Очевидно, я неправильно понимаю официальную документацию D3 join(). https://github.com/d3/d3-selection/blob/v2.0.0/README.md#selection_join , потому что я думал, что могу использовать join() вместо обычного шаблона append(), enter(), remove(), exit() и сэкономить тонны кода.

Приведенный ниже код создает 3 узла в начале с контекстным меню. Я могу добавлять и удалять данные initialize() , снова вызывая функцию, чтобы перерисовать узлы. Проблема в том, что «старые узлы» все еще существуют. Вместо добавления только 1 узла перерисовываются целые группы узлов.

Что я пропустил?

 <!DOCTYPE html>
<html>

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

    <title>D3 JOIN Test</title>
    <!-- call external d3.js framework -->
    <script src="https://d3js.org/d3.v6.js"></script>
</head>

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

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

    #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>

    <script>
        var nodes = [
            {
                "id": 1,
            },
            {
                "id": 2,
            },
            {
                "id": 3,
            }
        ]

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

        // 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)

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

        // iniital force simulation
        var simulation = d3.forceSimulation()
            .force("charge", d3.forceManyBody().strength(-100))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("attraceForce", d3.forceManyBody().strength(70));

        initialze()

        function initialze() {
            
            node = nodesContainer.selectAll(".node")
            .data(nodes, d => d.id)
                .join("circle")
                .attr("r", 30)
                .attr("fill", "whitesmoke")
                .attr("stroke", "white")
                .call(d3.drag()
                    .on("start", dragStarted)
                    .on("drag", dragged)
                    .on("end", dragEnded)
                )
                .on("mouseenter", mouseEnter)
                .on("mouseleave", mouseLeave)
                .on("contextmenu", contextMenu)
                
                simulation
                .nodes(nodes)
                .on("tick", ticked);
                
        }
                

        function mouseEnter(event, d) {
            thisNode = d

            d3.select(this).style("fill", "lightblue")
        }

        function mouseLeave(d) {
            d3.select(this).style("fill", "whitesmoke")
        }

        function contextMenu(event, d) {
            thisNode = d

            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)

            nodes.push({ "id": newID, })

            initialze()
        }

        function removeNodeClicked() {
            removeNode(thisNode)
        }

        function removeNode(d) {      
            var indexOfNodes = nodes.indexOf(d)

            nodes.splice(indexOfNodes, 1)

            initialze()
        }

        function ticked() {
            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> 

Ответ №1:

Вы выбираете по классу, который вы никогда не устанавливали. Поэтому просто сделайте:

 .attr("class", "node")
 

Вот ваш код с этим изменением:

 <!DOCTYPE html>
<html>

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

    <title>D3 JOIN Test</title>
    <!-- call external d3.js framework -->
    <script src="https://d3js.org/d3.v6.js"></script>
</head>

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

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

    #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>

    <script>
        var nodes = [
            {
                "id": 1,
            },
            {
                "id": 2,
            },
            {
                "id": 3,
            }
        ]

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

        // 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)

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

        // iniital force simulation
        var simulation = d3.forceSimulation()
            .force("charge", d3.forceManyBody().strength(-100))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("attraceForce", d3.forceManyBody().strength(70));

        initialze()

        function initialze() {
            
            node = nodesContainer.selectAll(".node")
            .data(nodes, d => d.id)
                .join("circle")
                .attr("r", 30)
                .attr("class", "node")
                .attr("fill", "whitesmoke")
                .attr("stroke", "white")
                .call(d3.drag()
                    .on("start", dragStarted)
                    .on("drag", dragged)
                    .on("end", dragEnded)
                )
                .on("mouseenter", mouseEnter)
                .on("mouseleave", mouseLeave)
                .on("contextmenu", contextMenu)
                
                simulation
                .nodes(nodes)
                .on("tick", ticked);
                
        }
                

        function mouseEnter(event, d) {
            thisNode = d

            d3.select(this).style("fill", "lightblue")
        }

        function mouseLeave(d) {
            d3.select(this).style("fill", "whitesmoke")
        }

        function contextMenu(event, d) {
            thisNode = d

            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)

            nodes.push({ "id": newID, })

            initialze()
        }

        function removeNodeClicked() {
            removeNode(thisNode)
        }

        function removeNode(d) {      
            var indexOfNodes = nodes.indexOf(d)

            nodes.splice(indexOfNodes, 1)

            initialze()
        }

        function ticked() {
            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>