sirallen
sirallen

Reputation: 1966

Drag-rotate projection in D3 v4

I'm having trouble getting my orthographic projection to rotate via d3.drag(). It is unresponsive:

var projection = d3.geoOrthographic()
  .scale(100)
  .translate([500, 300])
  .rotate([55,-40])
  .center([0,0])
  .clipAngle(90);

var geoPath = d3.geoPath().projection(projection);

svg.call(d3.drag().on('drag', dragged));

function dragged() {
    var transform = d3.event.transform;
    var r = {x: λ(transform.x), y: φ(transform.y)};
    projection.rotate([origin.x + r.x * k, origin.y + r.y]);
    updatePaths(svg, graticule, geoPath);
};

I tried replacing dragged() with a console.log() and nothing happened even in this case (clearly the function is not being called). I implemented d3.zoom() in a similar fashion without any problems. What am I doing wrong?

jsFiddle: https://jsfiddle.net/sirallen/2L4ajskL/

Upvotes: 3

Views: 1944

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102198

The reason why your drag is unresponsive is that you are already calling a zoom on that SVG, and the zoom handles panning. Therefore, for the d3.drag to work, you have to make the drag behaviour take precedence over panning.

Besides that, there is no d3.event.transform for a drag event. According to the API, these are the properties in the event object:

  • target - the associated drag behavior.
  • type - the string “start”, “drag” or “end”; see drag.on.
  • subject - the drag subject, defined by drag.subject.
  • x - the new x-coordinate of the subject; see drag.container.
  • y - the new y-coordinate of the subject; see drag.container.
  • dx - the change in x-coordinate since the previous drag event.
  • dy - the change in y-coordinate since the previous drag event.
  • identifier - the string “mouse”, or a numeric touch identifier.
  • active - the number of currently active drag gestures
  • sourceEvent - the underlying input event, such as mousemove or touchmove.

So, your transform will be undefined... if you can call dragged.

Solution 1

Just drop the d3.drag, using the zoom's click-and-drag (panning) to rotate your globe.

Here is your code with minimal changes (I put everything inside zoomed). Use the mouse wheel for zooming, and click on the globe to drag it:

function createMap() {
  var width = 400,
    height = 300,
    scale = 100,
    lastX = 0,
    lastY = 0,
    origin = {
      x: 55,
      y: -40
    };

  var svg = d3.select('body').append('svg')
    .style('width', 400)
    .style('height', 300)
    .style('border', '1px lightgray solid')

  var projection = d3.geoOrthographic()
    .scale(scale)
    .translate([width / 2, height / 2])
    .rotate([origin.x, origin.y])
    .center([0, 0])
    .clipAngle(90);

  var geoPath = d3.geoPath()
    .projection(projection);

  var graticule = d3.geoGraticule();

  // zoom AND rotate
  svg.call(d3.zoom().on('zoom', zoomed));

  // code snippet from http://stackoverflow.com/questions/36614251
  var λ = d3.scaleLinear()
    .domain([-width, width])
    .range([-180, 180])

  var φ = d3.scaleLinear()
    .domain([-height, height])
    .range([90, -90]);

  svg.append('path')
    .datum(graticule)
    .attr('class', 'graticule')
    .attr('d', geoPath);

  function zoomed() {
    var transform = d3.event.transform;
    var r = {
      x: λ(transform.x),
      y: φ(transform.y)
    };
    var k = Math.sqrt(100 / projection.scale());
    if (d3.event.sourceEvent.wheelDelta) {
      projection.scale(scale * transform.k)
      transform.x = lastX;
      transform.y = lastY;
    } else {
      projection.rotate([origin.x + r.x, origin.y + r.y]);
      lastX = transform.x;
      lastY = transform.y;
    }
    updatePaths(svg, graticule, geoPath);
  };

};

function updatePaths(svg, graticule, geoPath) {
  svg.selectAll('path.graticule').datum(graticule).attr('d', geoPath);
};

createMap();
.graticule {
  fill: none;
  stroke: #777;
  stroke-width: .5px;
  stroke-opacity: .5;
}
<script src="https://d3js.org/d3.v4.min.js"></script>

Solution 2

If you really want to use d3.drag, this is my proposed solution:

First, create a group to hold your globe:

var group = svg.append("g")

Then, call the drag to this group:

group.call(d3.drag().on('drag', dragged));

Finally, in the dragged function, use d3.event.x and d3.event.y:

function dragged(d) {
    var r = {
        x: λ((d.x = d3.event.x)),
        y: φ((d.y = d3.event.y))
    };
    projection.rotate([origin.x + r.x, origin.y + r.y]);
    updatePaths(svg, graticule, geoPath);
};

Also, don't forget to set the pointer-events of the path to all: since its fill is none, it's very hard to click exactly over the thin lines.

Here is the updated code, click on the globe and drag it:

function createMap() {
  var width = 400,
    height = 300,
    scale = 100,
    origin = {
      x: 55,
      y: -40
    };

  var svg = d3.select('body').append('svg')
    .style('width', 400)
    .style('height', 300)
    .style('border', '1px lightgray solid');

  var group = svg.append("g").datum({
    x: 0,
    y: 0
  })

  var projection = d3.geoOrthographic()
    .scale(scale)
    .translate([width / 2, height / 2])
    .rotate([origin.x, origin.y])
    .center([0, 0])
    .clipAngle(90);

  var geoPath = d3.geoPath()
    .projection(projection);

  var graticule = d3.geoGraticule();

  // zoom AND rotate
  svg.call(d3.zoom().on('zoom', zoomed));

  group.call(d3.drag().on('drag', dragged));

  // code snippet from http://stackoverflow.com/questions/36614251
  var λ = d3.scaleLinear()
    .domain([-width, width])
    .range([-180, 180])

  var φ = d3.scaleLinear()
    .domain([-height, height])
    .range([90, -90]);

  group.append('path')
    .datum(graticule)
    .attr('class', 'graticule')
    .attr('d', geoPath);

  function dragged(d) {
    var r = {
      x: λ((d.x = d3.event.x)),
      y: φ((d.y = d3.event.y))
    };
    projection.rotate([origin.x + r.x, origin.y + r.y]);
    updatePaths(svg, graticule, geoPath);
  };

  function zoomed() {
    var transform = d3.event.transform;
    var k = Math.sqrt(100 / projection.scale());
    projection.scale(scale * transform.k)
    updatePaths(svg, graticule, geoPath);
  };

};

function updatePaths(svg, graticule, geoPath) {
  svg.selectAll('path.graticule').datum(graticule).attr('d', geoPath);
};

createMap();
.graticule {
  fill: none;
  stroke: #777;
  stroke-width: .5px;
  stroke-opacity: .5;
  pointer-events: all;
}
<script src="https://d3js.org/d3.v4.min.js"></script>

Upvotes: 8

Related Questions