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/