Drag-rotate projection in D3 v4

d3.js

Solution

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>

Problem

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/

Original source