How to keep projections in sync during zoom scaling/transforming?

428 views
Skip to first unread message

Jason Yergeau

unread,
Nov 19, 2014, 4:13:05 AM11/19/14
to d3...@googlegroups.com
Hello,

My first post, so first a quick thank you to everyone contributing. 

I'm a bit confused about how scaling works in geo projections vs d3.behavior.zoom. In an example in this thread, when configuring the projection, the scale is set to (1 << 19):

projection
    .scale(1 << 19)
    .center([-122.4183, 37.7750]) // temporarily set center
    .translate([width / 2, height / 2])
    .translate(projection([0, 0])) // compute appropriate translate
    .center([0, 0]); // reset
zoom
    .scale(projection.scale())
    .translate(projection.translate());

The zoom then receives the same scale and translate as the projection. But typically the zoom is applied by transforming an svg grouping element on zoom events, like this:

g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");

If the zoom scale were to be set to the same as the projection's scale, then the element would be scaled twice (first through drawing a path via a projection, then again by scaling the element that contains it). I don't want to scale by 1 << 19. I just want want my projection to initially be "bigger", then use that as my base point.

Perhaps I am using the zoom.behavior incorrectly. How do you typically keep your zoom/transform state in sync with your projection? "Let the zoom behavior drive the projection", but the snippet above does the opposite if you call it during a zoom event.

Yours,
Jason

Max Goldstein

unread,
Nov 19, 2014, 9:55:10 PM11/19/14
to d3...@googlegroups.com
Hi Jason,

As usual in JS, there is more than one way to do it. I'd take a look at Map Pan & Zoom I (there are three more linked from here). (Also note that Mike has said that the server may be down tonight.) Basically, you establish a canonical projection and path, render geometry once (on file load), and then let svg transforms and the zoom behavior do the rest. Don't touch the geography in the zoom event handler. Again, that's the simplest way, not the only way.

Jason Yergeau

unread,
Nov 21, 2014, 10:27:13 AM11/21/14
to d3...@googlegroups.com
Hi Max,

Thanks for the response. The problem I was facing was in keeping the zoom behavior in sync with my projection. In the example you linked to (Map Pan & Zoom I), they are not kept in sync. The pan/zoom works by transforming an SVG group: this is the fastest way to handle panning and zooming because D3 doesn't need to re-draw the map. (Like you said, "don't touch the geography in the zoom event handler.") Instead the browser can scale/translate and take advantage of the GPU.
var zoom = d3.behavior.zoom()
    .scaleExtent([1, 8])
    .on("zoom", zoomed);
function zoomed() {

  g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
The problem here is that the projection is not aware of the translations done on the svg group, so you can't use the projection to calculate for example the longitude and latitude of where a user clicks after the user has zoomed/panned. The projection is useful for more than just drawing the original paths.
It's possible to update the projection whenever the on("zoom") event is fired, but you have to be aware of where you're applying the scaling, and why. The scaling on the projection in the example you linked to is relative to the container size:
var projection = d3.geo.mercator()

    .translate([width / 2, height / 2])

    .scale((width - 1) / 2 / Math.PI);
Therefore it's not possible to pass the scale passed by the zoom event directly to the projection. The original scale of the projection must be preserved, and multipled by the zoom's updated scale. 
Then there's the pixel offset: if the projection doesn't have a center or translate, then you can directly pass the event's translate into the projection. If not, a bit more calculation is needed to sync the zoom and projection back together. This is the beset way to calculate that.
I've got it working now -- thanks again for the feedback.

On Thu, Nov 20, 2014 at 3:55 AM, Max Goldstein <maxgol...@gmail.com> wrote:
Hi Jason,

As usual in JS, there is more than one way to do it. I'd take a look at Map Pan & Zoom I (there are three more linked from here). (Also note that Mike has said that the server may be down tonight.) Basically, you establish a canonical projection and path, render geometry once (on file load), and then let svg transforms and the zoom behavior do the rest. Don't touch the geography in the zoom event handler. Again, that's the simplest way, not the only way.

--
You received this message because you are subscribed to a topic in the Google Groups "d3-js" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/d3-js/BdHSfdkNVfA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to d3-js+un...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Max Goldstein

unread,
Nov 21, 2014, 7:59:43 PM11/21/14
to d3...@googlegroups.com
I'm glad you got it working. Thanks for the detailed reply.
 
The original scale of the projection must be preserved, and multipled by the zoom's updated scale.
You ought to be able to access these with projection.scale() and d3.event.scale. You still have to do all the math, but at least the values are on hand. 

martinwi

unread,
Feb 29, 2016, 5:11:12 PM2/29/16
to d3-js
Jason,

do you, by any chance, still have your code to hand?  

I believe I'm facing the same issue, in that I have sites marked on a map, where the projection.center is offset and scaling the sites is working to an extent, but they are moving as I zoom in/out. I'm struggling to work it all out.

Thanks
  Martin...
Reply all
Reply to author
Forward
0 new messages