d3 / d3-drag

Drag and drop SVG, HTML or Canvas using mouse or touch input.

Home Page:https://d3js.org/d3-drag

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Drag events force you to use inverted y-coordinate system

j2kun opened this issue · comments

In a d3 application I have functions that manually convert to and from the rendering coordinate system to my preferred (cartesian) coordinate system:

let width = 800;
let height = 600;
let svg = d3.select("body").append("svg")
                           .attr("width", width)
                           .attr("height", height);

let originX = width / 2;
let originY = height / 2;
function fromCartesianX(x) { return originX + x; }
function fromCartesianY(y) { return originY - y; }
function toCartesianX(x) { return x - originX; }
function toCartesianY(y) { return -y + originY; }

This was working fine for rendering points and lines:

let circles = svg.selectAll("circle").data(data).enter().append("circle");
circles.attr("cx", function (d) { return fromCartesianX(d.x); })  // d has cartesian coordinates
       .attr("cy", function (d) { return fromCartesianY(d.y); })
       .attr("r", 20);

However, when I introduce a drag event handler, the drag event coordinates are computed relative to the input point, and assumes that data point uses the "y-inverted" coordinate system!

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

When dragging, this causes the y value to move inverted to how it should.

How can I tell d3 to compute the event x,y absolutely, in the containing svg coordinate system? Then I could replace the above with the following, which makes much more sense.

function dragged(d) {
  d.x = toCartesian(d3.event.x);
  d.y = toCartesian(d3.event.y);
  ...
  d3.select(this).attr("cx", fromCartesianX(d.x))
                          .attr("cx", fromCartesianY(d.y));

Looking into the code, I noticed the event object also exposes dx and dy. So I modified my example to do d.x += d3.event.dx and dy += d3.event.dy.

d3.drag uses the coordinate system of whatever element you apply it to.

For SVG elements, the coordinate system is obtained using element.getScreenCTM; for other elements, the coordinate system is defined by event.clientX and event.clientY, offset by the subject element’s bounds. See d3-selection’s point.js for the implementation.

Unless you’ve applied an SVG transform, the default coordinate system has [0, 0] in the top-left corner, with +x going right and +y going down. So, one option would be to change the coordinate system of your SVG using the transform attribute.

But, really the problem here is that you are relying on the default drag.subject:

function subject(d) {
  return d == null ? {x: d3.event.x, y: d3.event.y} : d;
}

Thus, d3.drag is using your bound datum as the drag subject, and using d.x and d.y as the starting position of the thing being dragged. Except d3.drag operates in SVG coordinates, and your data is in a different coordinate system. So, the right thing to do is to implement drag.subject and tell the drag behavior the actual starting position of your circle in SVG coordinates:

d3.drag()
    .subject(function(d) { return {x: fromCartesianX(d.x), y: fromCartesianY(d.y)}; })
    .on("drag", dragged)

Then on drag, you set the new position of the circle in SVG coordinates (event.x and event.y), and update the data in data coordinates:

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

Live example:

https://bl.ocks.org/mbostock/519d494035dd642e19eee4e242570114