Home > Back-end >  d3js v7, the circle moves in the opposite direction in y coordinate when I drag the circle by the cu
d3js v7, the circle moves in the opposite direction in y coordinate when I drag the circle by the cu

Time:04-07

I am trying to move a circle by dragging it in d3js, but I can't get it to move in the y-axis direction.

I am using v7 of d3js and from several attempts I have found that the event.x and event.y in the drag function are the coordinates on the graph of the cursor being dragged. Therefore, I tried to transform these two coordinates with the scale transformation function to obtain the coordinates of the display on the svg.

There are two problems here.
The first is that the y-coordinates obtained by event.y are the opposite of the actual vertical movement. For example, if I actually run the code and drag the circle to the upper right, the circle will move to the lower right. This is a line symmetry-like movement in the y-coordinate of the circle's initial position; the positive and negative values of the y-coordinate movement are reversed. This is also true for event.dy. The x-coordinates in event.x and event.dx are working fine. Also, d3.pointer(event) does not work as intended.

The second problem is that the circle jumps at the beginning of the drag. I found the cause of this by looking within the function that is executed during the drag. The initial values of event.x and event.y are the initial values of the circle's coordinates, even after multiple drags, which causes the circle to always start moving from the initial value of the circle when it is dragged.

I have a program that reproduces the problem.
How can these problems be resolved?

Thanks.
Here is the code.

  var dataset = [
      {x: 5, y: 20, name: "one"},
      {x: 100, y: 33, name: "two"},
      {x: 250, y: 50, name: "three"},
      {x: 480, y: 90, name: "four"},
    ];

  const width = 400;
  const height = 300;
  const margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };
  const svgWidth = width   margin.left   margin.right;
  const svgHeight = height   margin.top   margin.bottom;

  var svg = d3.select("#graph-area").append("svg")
      .attr("width", svgWidth)
      .attr("height", svgHeight)
      .style("border-radius", "20px 20px 20px 20px")
      .append("g")
      .attr("transform", `translate(${margin.left}, ${margin.top})`);

  var xScale = d3.scaleLinear()
    .domain([0, d3.max(dataset, function(d) { return d.x; })   10])
    .range([0, width]);

  var yScale = d3.scaleLinear()
    .domain([0, d3.max(dataset, function(d) { return d.y; })   10])
    .range([height, 0]);

  var axisx = d3.axisBottom(xScale).ticks(5);
  var axisy = d3.axisLeft(yScale).ticks(5);

  var x = svg.append("g")
    .attr("transform", "translate("   0   ","   height   ")")
    .call(axisx)
    .attr("class", "x_axis")

   x.append("text")
        .attr("x", (width - margin.left - margin.right) / 2   margin.left)
        .attr("y", 35)
        .attr("text-anchor", "middle")
        .attr("font-size", "10pt")
        .attr("font-weight", "bold")
        .text("X Label")
        .attr("class", "x_axis")

  var y = svg.append("g")
    .call(axisy)
    .attr("class", "y_axis")

  y.append("text")
    .attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
    .attr("y", -35)
    .attr("transform", "rotate(-90)")
    .attr("text-anchor", "middle")
    .attr("font-weight", "bold")
    .attr("font-size", "10pt")
    .text("Y Label")
    .attr("class", "y_axis")

  var clip = svg.append("defs").append("svg:clipPath")
            .attr("id", "clip")
            .append("svg:rect")
            .attr("width", width )
            .attr("height", height )
            .attr("x", 0) 
            .attr("y", 0);

 
  var circle = svg.append("g")
        .attr("id", "scatterplot")
        .attr("clip-path", "url(#clip)");
  
  circle.selectAll("circle")
    .data(dataset)
    .enter()
    .append("circle")
    .attr("cx", function(d) { return xScale(d.x); })
    .attr("cy", function(d) { return yScale(d.y); })
    .attr("fill", "steelblue")
    .attr("r", 20)
    .on('mouseover',function(elem, data) {
        //console.log("over data name: "   data.name);
    })
    .on('mouseout',function (elem, data) {
        //console.log("out data name: "   data.name);
    })
    .on("mousedown", function(elem, data){
        console.log("down data name: "   data.name);
    })
    .on("mouseup", function(elem, data){
        console.log("up data name: "   data.name);
    })
    .call(d3.drag().on("drag", circleDragged).on("start", circleStartDrag))

    function circleStartDrag(event){
        console.log("start drag");
    }

    function circleDragged(event){
        d3.select(this).attr('cx', xScale(event.x)).attr('cy', yScale(event.y));
    }
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 Scatter Plot</title>
  <script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
  <script src="https://d3js.org/d3.v7.min.js"></script>
</head>

<body>
  
  <div id="graph-area" stype="position: relative;"></div
  
</body>

</html>

Edit (Perception of coordinate calculation in the drag function)

The drag function refers to the values set in the data (cx and cy) and indicates the new coordinates of the cursor reflecting dy and dx from those values. So in my case I have the unscaled data values set as event.subject, so when I drag directly above, it will be calculated as (cx=250, cy=-40) based on (cx=250, cy=-50) and dy=-1. This is then reflected in event.y. This is the reason why the y-coordinates of the circle I was talking about during the drag were upside down.

The correct value is set as (cx=204, cy=150) as a scaled value of the data at the time of enter() execution. When the circle is dragged directly up, event.y is set to cy=140 based on dy=-1. In pixel coordinates in svg, a decrease in the y coordinate value means an upward movement, so the circle moves up. At this point I need to update d.x=event.y to take into account the reference to the value of event.subject in the next drag.

CodePudding user response:

This is the expected result given your code.

You initially position the data points with scale(d.y) which converts your data vales to pixel values. When you drag you take the pixel value (event.y) and apply the scale again - scaling pixel values as though they were data.

The data value of d.y = 0 is mapped to height by the scale. If you drag a circle that is at the top of the SVG (y=0) then the scale will return height which is at the bottom of the SVG - hence your inversion.

The solution is to just use the event.y / event.x values as these values represent pixel values already - there is no need to scale them. If you want to convert the pixel values to data values, then you can use scale.invert() - but this is only useful in updating the data, not the pixel coordinates of each circle.

Removing the scaling functions from the drag reveals one last issue - while the drag moves along with the mouse, you still have a jump on drag start: the drag subject (reference coordinates of the thing being dragged) is by default the x,y properties of your datum. As this represents the data values in your datum, rather than scaled pixel values, you get a jump. The quickest solution would be to redefine the x,y properties of the datum as the scaled pixel values and update these on drag.

var dataset = [
      {x: 5, y: 20, name: "one"},
      {x: 100, y: 33, name: "two"},
      {x: 250, y: 50, name: "three"},
      {x: 480, y: 90, name: "four"},
    ];

  const width = 400;
  const height = 300;
  const margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };
  const svgWidth = width   margin.left   margin.right;
  const svgHeight = height   margin.top   margin.bottom;

  var svg = d3.select("#graph-area").append("svg")
      .attr("width", svgWidth)
      .attr("height", svgHeight)
      .style("border-radius", "20px 20px 20px 20px")
      .append("g")
      .attr("transform", `translate(${margin.left}, ${margin.top})`);

  var xScale = d3.scaleLinear()
    .domain([0, d3.max(dataset, function(d) { return d.x; })   10])
    .range([0, width]);

  var yScale = d3.scaleLinear()
    .domain([0, d3.max(dataset, function(d) { return d.y; })   10])
    .range([height, 0]);

  var axisx = d3.axisBottom(xScale).ticks(5);
  var axisy = d3.axisLeft(yScale).ticks(5);

  var x = svg.append("g")
    .attr("transform", "translate("   0   ","   height   ")")
    .call(axisx)
    .attr("class", "x_axis")

   x.append("text")
        .attr("x", (width - margin.left - margin.right) / 2   margin.left)
        .attr("y", 35)
        .attr("text-anchor", "middle")
        .attr("font-size", "10pt")
        .attr("font-weight", "bold")
        .text("X Label")
        .attr("class", "x_axis")

  var y = svg.append("g")
    .call(axisy)
    .attr("class", "y_axis")

  y.append("text")
    .attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
    .attr("y", -35)
    .attr("transform", "rotate(-90)")
    .attr("text-anchor", "middle")
    .attr("font-weight", "bold")
    .attr("font-size", "10pt")
    .text("Y Label")
    .attr("class", "y_axis")

  var clip = svg.append("defs").append("svg:clipPath")
            .attr("id", "clip")
            .append("svg:rect")
            .attr("width", width )
            .attr("height", height )
            .attr("x", 0) 
            .attr("y", 0);

 
  var circle = svg.append("g")
        .attr("id", "scatterplot")
        .attr("clip-path", "url(#clip)");
  
  circle.selectAll("circle")
    .data(dataset)
    .enter()
    .append("circle")
    .attr("cx", function(d) { return d.x = xScale(d.x); })
    .attr("cy", function(d) { return d.y = yScale(d.y); })
    .attr("fill", "steelblue")
    .attr("r", 20)
    .on('mouseover',function(elem, data) {
        //console.log("over data name: "   data.name);
    })
    .on('mouseout',function (elem, data) {
        //console.log("out data name: "   data.name);
    })
    .on("mousedown", function(elem, data){
        console.log("down data name: "   data.name);
    })
    .on("mouseup", function(elem, data){
        console.log("up data name: "   data.name);
    })
    .call(d3.drag().on("drag", circleDragged).on("start", circleStartDrag))

    function circleStartDrag(event){
        console.log("start drag");
    }

    function circleDragged(event){
        d3.select(this).attr('cx', d=>d.x=event.x).attr('cy', d=>d.y=event.y);
    }
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 Scatter Plot</title>
  <script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
  <script src="https://d3js.org/d3.v7.min.js"></script>
</head>

<body>
  
  <div id="graph-area" stype="position: relative;"></div
  
</body>

</html>

  • Related