Home > Software engineering >  Why does D3 circle position virtually reset after drag
Why does D3 circle position virtually reset after drag

Time:07-16

I'm implementing the drag example from https://observablehq.com/@d3/circle-dragging-i using d3v7.

When I try the code it works correctly for the first drag, however from the start of the second drag, the position resets/jumps to the original one provided in the join data.

It appears that the event position is influenced by the data. I have confirmed this behavior using logs that tell me the starting position and the event and comparing it to the one given to me by the browser inspector.

After much toiling I found out that the issue here is in the way that the data has it's variables called x and y:

var circles = d3.range(20).map(i => ({
        x: Math.random() * (width - radius * 2)   radius,
        y: Math.random() * (height - radius * 2)   radius,
    }));

By changing the data:

var circles = d3.range(20).map(i => ({
        posx: Math.random() * (width - radius * 2)   radius,
        posy: Math.random() * (height - radius * 2)   radius,
    }));

I am able to drag the circles as intended.

Interestingly, if I update the position of the variable circles, this updated data is not reflected in the jump behaviour, by that I mean that the jump will happen to the initial value.

Should the name of the data influence the initial position of the items? Why does the drag position resets after the first drag? (I tried changing the x, y properties alongside cx and cy, but the reset still happens).

Full code with problematic behaviour:

<!DOCTYPE html>
<html lang="en"> 

  
<body>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script>
        radius = 32;
        height = 600;
        width = 800;
        
        function dragStart(event,d){
            d3.select(this)
              .style("stroke", "black")
              
              console.log("Start",[event.x,event.y])
              var current = d3.select(this);
              console.log([current.attr("cx"),current.attr("cy")]);
              console.log(current.attr("translate"));             

          }
          
        function dragging(event,d){
            var current = d3.select(this);
            current
                .attr('cx', event.x)
                .attr('cy', event.y);
          }
          
          function dragEnd(event,d){            
            d3.select(this)
              .style("stroke", "")
            console.log("End",[event.x,event.y])
            var current = d3.select(this);
            console.log(d3.select(this));
            
          }
          
        drag = d3.drag()
                  .on("start", dragStart)
                  .on("drag", dragging)
                  .on("end", dragEnd);
                  
        
        
        var svg = d3.select("body")
          .append("svg")
          .attr("viewBox", [0, 0, width, height])
          .attr("stroke-width", 2);
        
        var circles = d3.range(20).map(i => ({
            x: Math.random() * (width - radius * 2)   radius,
            y: Math.random() * (height - radius * 2)   radius,
        }));
        
        console.log(circles);
        console.log(drag);
        
        svg.selectAll("circle")
            .data(circles)
            .join("circle")
              .attr("cx", (d)=>d.x)
              .attr("cy", (d)=>d.y)
              .attr("r", radius)
              .attr("fill", (d, i) => d3.schemeCategory10[i % 10])
              .on("click",e => console.log(d3.pointer(e)))
              .call(drag);
        
    </script>

</body>
  
</html>

CodePudding user response:

Your issue is not in how you set-up circles but in how you have implemented dragging.

In the Observable, the equivalent dragged function is:

function dragged(event, d) {
  d3.select(this).attr("cx", d.x = event.x).attr("cy", d.y = event.y);
}

Note that in thh attrs that cx/ d.x and cy/ d.y are updated per the event values which changes the underlying data. See here.

But, in your implementation:

function dragging(event,d){
  var current = d3.select(this);
  current
    .attr("cx", event.x)
    .attr("cy", event.y);
}

You are not updating the underlying values, you just animate the movement by updating cx and cy. So on the next drag event the circle 'jumps' back to its original position and you get the disconnect between the pointer and the circle.

See the working example below with one circle - the log is now on circles rather than the selection and you can see the underlying data be updated. If you revert to your original dragging definition and keep the log for circles you will see the circle move, but the underlying data unchanged.

<!DOCTYPE html>
<html lang="en"> 

  
<body>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script>
        radius = 32;
        height = 200; // change to suit Stack Overflow snippet
        width = 500; // change to suit Stack Overflow snippet
        
        function dragStart(event,d){
            d3.select(this)
              .style("stroke", "black")
              
              //console.log("Start",[event.x,event.y])
              //var current = d3.select(this);
              //console.log([current.attr("cx"),current.attr("cy")]);
              //console.log(current.attr("translate"));             

          }
          
        function dragging(event,d){
            var current = d3.select(this);
            current
              .attr("cx", d.x = event.x)
              .attr("cy", d.y = event.y);
              console.log(circles)
          }
          
          function dragEnd(event,d){            
            d3.select(this)
              .style("stroke", "")
            //console.log("End",[event.x,event.y])
            //var current = d3.select(this);
            //console.log(d3.select(this));
            
          }
          
        drag = d3.drag()
                  .on("start", dragStart)
                  .on("drag", dragging)
                  .on("end", dragEnd);
                  
        
        
        var svg = d3.select("body")
          .append("svg")
          //.attr("viewBox", [0, 0, width, height])
          .attr("width", width) // change to suit Stack Overflow snippet
          .attr("height", height)
          .attr("stroke-width", 2);
        
        var circles = d3.range(1).map(i => ({
            x: Math.random() * (width - radius * 2)   radius,
            y: Math.random() * (height - radius * 2)   radius,
        }));
        
        //console.log(circles);
        //console.log(drag);
        
        svg.selectAll("circle")
            .data(circles)
            .join("circle")
              .attr("cx", (d)=>d.x)
              .attr("cy", (d)=>d.y)
              .attr("r", radius)
              .attr("fill", (d, i) => d3.schemeCategory10[i % 10])
              //.on("click",e => console.log(d3.pointer(e)))
              .call(drag);
        
    </script>

</body>
  
</html>

  • Related