Why d3.event in dragging callback is somehow related to data coordinate

Viewed 392

I'm playing with d3.js and I noticed a weird behavior with dragging. If I add a circle using data and scaleX/scaleY, when I drag it the d3.event.y value is relative to the xy coordinates inside the "data" array, instead of the top left corner...

Here is the code and a jsFiddle: if you open the console, if you drag the first circle the reference is the top left corner, if you drag the second circle the reference is something related to {x: 3, y: 2}

var circle2 = svgGraph
  .selectAll('.circle2')
  .data([{ x: 3, y: 2 }])
  .enter()
  .append('circle') // Uses the enter().append() method
  .attr('cx', scaleX(100))
  .attr('cy', scaleY(100))
  .attr('r', 15)
  .call(d3.drag()
    .on('start', (d) => { })
    .on('drag', onHandlerDrag())
    .on('end', (d) => { }));

function onHandlerDrag() {
  return (d) => {
    console.log(d3.event.y);
  }
}

Is the behavior intended? Do you have any reference about it?

Thanks

1 Answers

Yes, this is the intended behaviour. In fact, we normally use those x and y properties in a drag event to avoid the element jumping around the container (when one is lazy enough to specify the correct subject).

Since you asked for a reference, have a look at drag.subject:

The subject of a drag gesture represents the thing being dragged. It is computed when an initiating input event is received, such as a mousedown or touchstart, immediately before the drag gesture starts.

Then, in the next paragraph, the most important information to you:

The default subject is the datum of the element in the originating selection (see drag) that received the initiating input event; if this datum is undefined, an object representing the coordinates of the pointer is created. When dragging circle elements in SVG, the default subject is thus the datum of the circle being dragged. (emphasis mine)

And finally:

The returned subject should be an object that exposes x and y properties, so that the relative position of the subject and the pointer can be preserved during the drag gesture.

So, if you don't want the x and y in the datum being used by drag, just set a different object in the subject:

.subject(function() {
    return {
        foo: d3.event.x,
        bar: d3.event.y
    };
})

Here is your code with that change:

function onHandlerDrag() {
  return (d) => {
    console.log(d3.event.y);
  }
}

var scaleX = d3
  .scaleLinear()
  .domain([0, 500])
  .range([0, 500]);

var scaleY = d3
  .scaleLinear()
  .domain([0, 500])
  .range([0, 500]);

var svgGraph = d3
  .select('.area')
  .attr('width', 500)
  .attr('height', 500);

var circle1 = svgGraph
  .append('circle')
  .attr('cx', scaleX(20))
  .attr('cy', scaleY(20))
  .attr('r', 15)
  .call(d3.drag()
    .on('start', (d) => {})
    .on('drag', onHandlerDrag())
    .on('end', (d) => {}));

var circle2 = svgGraph
  .selectAll('.circle2')
  .data([{
    x: 3,
    y: 2
  }])
  .enter()
  .append('circle') // Uses the enter().append() method
  .attr('cx', scaleX(100))
  .attr('cy', scaleY(100))
  .attr('r', 15)
  .call(d3.drag()
    .subject(function() {
      return {
        foo: d3.event.x,
        bar: d3.event.y
      };
    })
    .on('start', (d) => {})
    .on('drag', onHandlerDrag())
    .on('end', (d) => {}));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg class='area'></svg>

Related