Home > Back-end >  Forced graph doesn't update after changing data
Forced graph doesn't update after changing data

Time:01-12

I got a small D3 forced graph with 3 main nodes. Those nodes contains a attribute shoes, which hold a integer. On top of the graph are two buttons, to either add or remove shoes. As soon as one of those buttons are clicked, I want to update the D3 forced graph data. Basically the integer value in the blue node should either increase or decrease.

I searched and found several stackoverflow articles which explain the steps to achieve my need. Unforutnately I was not able yet to successfully map those articles on my prototype.

The problem is: It adds an element to the last data node but does not visually change the amount in the blue circle. The console output instead shows the correct value and increases or decreases the amount of shoes correctly.

What do I miss?

        var width = window.innerWidth,
            height = window.innerHeight;

        var buttons = d3.select("body").selectAll("button")
            .data(["add Shoes", "remove Shoes"])
            .enter()
            .append("button")
            .text(function(d) {
                return d;
            })

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .call(d3.zoom().on("zoom", function(event) {
                svg.attr("transform", event.transform)
            }))
            .append("g")

        ////////////////////////
        // outer force layout

        var data = {
            "nodes":[
                { "id": "A", "shoes": 1}, 
                { "id": "B", "shoes": 1},
                { "id": "C", "shoes": 0},
            ],
            "links": [
                { "source": "A", "target": "B"},
                { "source": "B", "target": "C"},
                { "source": "C", "target": "A"}
            ]
        };

        var simulation = d3.forceSimulation()
            .force("size", d3.forceCenter(width / 2, height / 2))
            .force("charge", d3.forceManyBody().strength(-1000))
            .force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
       
        linksContainer = svg.append("g").attr("class", "linkscontainer")
        nodesContainer = svg.append("g").attr("class", "nodesContainer")
       
        var links = linksContainer.selectAll("g")
            .data(data.links)
            .join("g")
            .attr("fill", "transparent")

        var linkLine = linksContainer.selectAll(".linkPath")
            .data(data.links)
            .join("path")
            .attr("stroke", "red")
            .attr("fill", "transparent")
            .attr("stroke-width", 3)
        
        nodes = nodesContainer.selectAll(".nodes")
            .data(data.nodes, function (d) { return d.id; })
            .join("g")
            .attr("class", "nodes")
            .attr("id", function (d) { return d.id; })
            .call(d3.drag()
                .on("start", dragStarted)
                .on("drag", dragged)
                .on("end", dragEnded)
            )

        nodes.selectAll("circle")
            .data(d => [d])
            .join("circle")
            .style("fill", "lightgrey")
            .style("stroke", "blue")
            .attr("r", 40)

        var smallCircle = nodes.selectAll("g")
            //.data(d => d.shoes)
            .data(d => [d])
            .enter()
            .filter(function(d) { 
                return d.shoes !== 0; 
            })
            .append("g")
            .attr("cursor", "pointer")
            .attr("transform", function(d, i) {
                const factor = (i / 40) * (15 / 2) * 5;
                return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
            });
                        
        smallCircle.append('circle')
            .attr("class", "circle-small")
            .attr('r', 15)
            .attr("fill", "blue")

        smallCircle.append("text")
            .attr("font-size", 15)
            .attr("fill", "white")
            .attr("dominant-baseline", "central")
            .style("text-anchor", "middle")
            .attr("pointer-events", "cursor")
            .text(function(d) {
                return d.shoes;
            })

        simulation
            .nodes(data.nodes)
            .on("tick", tick)

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

        function tick() {
            linkLine.attr("d", function(d) {
                var dx = (d.target.x - d.source.x),
                    dy = (d.target.y - d.source.y),
                    dr = Math.sqrt(dx * dx   dy * dy)

                return "M"   d.source.x   ","   d.source.y   "A"   dr   ","   dr   " 0 0,1 "   d.target.x   ","   d.target.y;
            })
                
            nodes
                .attr("transform", d => `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 = null;
            d.fy = null;
        }

        buttons.on("click", function(d) {
            if (d.srcElement.__data__ == "add Shoes") {
                
                data.nodes.forEach(function(item) {
                    item.shoes = item.shoes   1
                })
            } else if (d.srcElement.__data__ == "remove Shoes") {
                data.nodes.forEach(function(item) {
                    if (!item.shoes == 0) {
                        item.shoes = item.shoes - 1
                    }
                })
            }

            restart()
        })

        function restart() {
            // Apply the general update pattern to the nodes.
    
            smallCircle = nodes.selectAll("g")
                .data(d => [d])
                .enter()
                .filter(function(d) { 
                    return d.shoes !== 0; 
                })
                .append("g")
                .attr("cursor", "pointer")
                .attr("transform", function(d, i) {
                    const factor = (i / 40) * (15 / 2) * 5;
                    return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
                });

            smallCircle.append("circle")
                .attr("class", "circle-small")
                .attr('r', 15)
                .attr("fill", "blue")

            smallCircle.append("text")
                .attr("font-size", 15)
                .attr("fill", "white")
                .attr("dominant-baseline", "central")
                .style("text-anchor", "middle")
                .text(function(d) {
                    return d.shoes;
                })

            smallCircle.exit().remove();

            // Update and restart the simulation.
            simulation.nodes(data.nodes);
            simulation.restart()
        }
body {
        background: whitesmoke,´;
        overflow: hidden;
        margin: 0px;
    }
<!DOCTYPE html>
<html>

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

    <title>D3v7</title>
    <!-- d3.js framework -->
    <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
 
</body>

</html>

CodePudding user response:

First of all, do not mess with private variables, conventionally assigned as __foo__. So, instead of...

buttons.on("click", function(d) {
    if (d.srcElement.__data__ == "add Shoes") { etc...

...just do:

buttons.on("click", function(_, d) {
    if (d == "add Shoes") {

Back to the problem: the issue here is an incorrect enter-update-exit pattern. This should be it:

//the update selection:
let smallCircle = nodes.selectAll("g")
    .data(d => d.shoes ? [d] : []);

//the exit selection:
smallCircle.exit().remove();

//the enter selection:
const smallCircleEnter = smallCircle.enter()
    .append("g")
    //etc...

//appending elements in the enter selection only:
smallCircleEnter.append("circle")
    //etc...

smallCircleEnter.append("text")
    //etc...

//merging the enter and the update selections:
smallCircle = smallCircleEnter.merge(smallCircle);

//modifying the update selection
smallCircle.select("text")
    .text(function(d) {
      return d.shoes;
    });

Also, I'm removing that filter and passing an empty array if the number of shoes is zero.

Here's your code with those changes:

var width = window.innerWidth,
  height = window.innerHeight;

var buttons = d3.select("body").selectAll("button")
  .data(["add Shoes", "remove Shoes"])
  .enter()
  .append("button")
  .text(function(d) {
    return d;
  })

var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height)
  .call(d3.zoom().on("zoom", function(event) {
    svg.attr("transform", event.transform)
  }))
  .append("g")

////////////////////////
// outer force layout

var data = {
  "nodes": [{
      "id": "A",
      "shoes": 1
    },
    {
      "id": "B",
      "shoes": 1
    },
    {
      "id": "C",
      "shoes": 0
    },
  ],
  "links": [{
      "source": "A",
      "target": "B"
    },
    {
      "source": "B",
      "target": "C"
    },
    {
      "source": "C",
      "target": "A"
    }
  ]
};

var simulation = d3.forceSimulation()
  .force("size", d3.forceCenter(width / 2, height / 2))
  .force("charge", d3.forceManyBody().strength(-1000))
  .force("link", d3.forceLink().id(function(d) {
    return d.id
  }).distance(250))

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

var links = linksContainer.selectAll("g")
  .data(data.links)
  .join("g")
  .attr("fill", "transparent")

var linkLine = linksContainer.selectAll(".linkPath")
  .data(data.links)
  .join("path")
  .attr("stroke", "red")
  .attr("fill", "transparent")
  .attr("stroke-width", 3)

nodes = nodesContainer.selectAll(".nodes")
  .data(data.nodes, function(d) {
    return d.id;
  })
  .join("g")
  .attr("class", "nodes")
  .attr("id", function(d) {
    return d.id;
  })
  .call(d3.drag()
    .on("start", dragStarted)
    .on("drag", dragged)
    .on("end", dragEnded)
  )

nodes.selectAll("circle")
  .data(d => [d])
  .join("circle")
  .style("fill", "lightgrey")
  .style("stroke", "blue")
  .attr("r", 40)

var smallCircle = nodes.selectAll("g")
  //.data(d => d.shoes)
  .data(d => [d])
  .enter()
  .filter(function(d) {
    return d.shoes !== 0;
  })
  .append("g")
  .attr("cursor", "pointer")
  .attr("transform", function(d, i) {
    const factor = (i / 40) * (15 / 2) * 5;
    return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
  });

smallCircle.append('circle')
  .attr("class", "circle-small")
  .attr('r', 15)
  .attr("fill", "blue")

smallCircle.append("text")
  .attr("font-size", 15)
  .attr("fill", "white")
  .attr("dominant-baseline", "central")
  .style("text-anchor", "middle")
  .attr("pointer-events", "cursor")
  .text(function(d) {
    return d.shoes;
  })

simulation
  .nodes(data.nodes)
  .on("tick", tick)

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

function tick() {
  linkLine.attr("d", function(d) {
    var dx = (d.target.x - d.source.x),
      dy = (d.target.y - d.source.y),
      dr = Math.sqrt(dx * dx   dy * dy)

    return "M"   d.source.x   ","   d.source.y   "A"   dr   ","   dr   " 0 0,1 "   d.target.x   ","   d.target.y;
  })

  nodes
    .attr("transform", d => `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 = null;
  d.fy = null;
}

buttons.on("click", function(_, d) {
  if (d === "add Shoes") {

    data.nodes.forEach(function(item) {
      item.shoes = item.shoes   1
    })
  } else if (d === "remove Shoes") {
    data.nodes.forEach(function(item) {
      if (!item.shoes == 0) {
        item.shoes = item.shoes - 1
      }
    })
  }

  restart()
})

function restart() {
  // Apply the general update pattern to the nodes.

  let smallCircle = nodes.selectAll("g")
    .data(d => d.shoes ? [d] : []);

  smallCircle.exit().remove();

  const smallCircleEnter = smallCircle.enter()
    .append("g")
    .attr("cursor", "pointer")
    .attr("transform", function(d, i) {
      const factor = (i / 40) * (15 / 2) * 5;
      return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
    });

  smallCircleEnter.append("circle")
    .attr("class", "circle-small")
    .attr('r', 15)
    .attr("fill", "blue")

  smallCircleEnter.append("text")
    .attr("font-size", 15)
    .attr("fill", "white")
    .attr("dominant-baseline", "central")
    .style("text-anchor", "middle")
    .text(function(d) {
      return d.shoes;
    });

  smallCircle = smallCircleEnter.merge(smallCircle);

  smallCircle.select("text")
    .text(function(d) {
      return d.shoes;
    });


  // Update and restart the simulation.
  simulation.nodes(data.nodes);
  simulation.restart()
}
body {
  background: whitesmoke, ´;
  overflow: hidden;
  margin: 0px;
}
<!DOCTYPE html>
<html>

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

  <title>D3v7</title>
  <!-- d3.js framework -->
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>

</body>

</html>

  • Related