d3.geo: projections, scale and translate methods, and the coordinate system

3,907 views
Skip to first unread message

Iain

unread,
Oct 12, 2011, 11:28:06 AM10/12/11
to d3...@googlegroups.com
Hi,

I was wondering if there was any more information about d3.geo. For example...

There are three documented projections (mercator, albers, azimuthal). Each has scale and translate methods. Some questions...
  • The scale method. What does this refer to? Yes, I know a bigger value results in a bigger map. But it would be nice to know more.
  • The translate method. You pass it an array of two values; these are horizontal and vertical 'offsets' which I guess are measured in pixels. However, where is the origin? It seems to be somewhere towards the top-left, but it's not the top-left of the svg element that contains the map. How and from where is 'projected' space measured?
  • Conversion. Clearly it's possible to go from geographic space (lon/lat) to 'projected' space (x/y). However, is it possible to do the reverse? Having looked at d3.geo.js I see the three documented projections have invert methods that accept x and y coordinates. I guess this answers my question: Yes. But coordinates from the map's origin, or from the svg element that contains the map? (In other words, is invert consistent with translate?)
  • I've combined d3.geo with d3.behavior.zoom to create an map that you can zoom and pan with the mouse. But what if I want to zoom and pan another object, say a rectangle at the same time? The rectangle isn't a geographic object, so I cant update its position with the path generator. I wanted to use d3.event.scale and d3.event.translate in combination with the transform attribute. But without knowing more about the coordinate system, this is hard.
Thanks if you can offer any help,

Iain

Jason Davies

unread,
Oct 12, 2011, 12:06:00 PM10/12/11
to d3...@googlegroups.com
Hi Iain,

On Wed, Oct 12, 2011 at 08:28:06AM -0700, Iain wrote:
> - The scale method. What does this refer to? Yes, I know a bigger value

> results in a bigger map. But it would be nice to know more.

I need to review the other projections to answer this fully, but at
least for d3.geo.mercator, the idea is that scale(1) projects the full
world onto a 1x1 square, with longitude, latitude [0, 0] at the centre
of the square, which is [0, 0].

This doesn't necessarily apply to projections that don't project the
whole world onto a square, but I think the 1x1 idea for mercator
*should* be maintained for the "standard parallels" for each projection.
That is, along the standard parallel for any projection, scale(1) should
result in each degree being projected to a size of 1/360. Notice I said
*should*, as I haven't checked that each projection does this! The
origin should probably always be projected to [0, 0].

We're working on some d3.geo updates at the moment, so there may be some
updates soon to ensure this consistent behaviour for scales (subject to
backwards compatibility constraints).

> - The translate method. You pass it an array of two values; these are

> horizontal and vertical 'offsets' which I guess are measured in pixels.
> However, where is the origin? It seems to be somewhere towards the top-left,
> but it's not the top-left of the svg element that contains the map. How and
> from where is 'projected' space measured?

The translate offset defines where the origin (typically longitude,
latitude of [0, 0]) should be projected to in pixel space. It is
measured in pixels from the top left. The default is [480, 250], which
is the centre of a 960x500 rectangle.

Internally, what happens is the spherical coordinates are projected onto
the 1x1 square, then multiplied by the scale value, and finally
translated by the pixel-based offset.

> - Conversion. Clearly it's possible to go from geographic space (lon/lat)

> to 'projected' space (x/y). However, is it possible to do the reverse?
> Having looked at d3.geo.js I see the three documented projections have
> invert methods that accept x and y coordinates. I guess this answers my
> question: Yes. But coordinates from the map's origin, or from the svg

> element that contains the map? (In other words, is *invert* consistent
> with *translate*?)

Yes, the d3.geo.*.invert functions take into account the translate and
scale values. So projection.invert(projection(x)) === x should hold for
any translate and scale.

> - I've combined d3.geo with d3.behavior.zoom to create an map that you

> can zoom and pan with the mouse. But what if I want to zoom and pan another
> object, say a rectangle at the same time? The rectangle isn't a geographic
> object, so I cant update its position with the path generator. I wanted to
> use d3.event.scale and d3.event.translate in combination with the transform
> attribute. But without knowing more about the coordinate system, this is
> hard.

As translate is specified in pixels, it should be consistent with the
pixel-space translation vector of d3.behavior.zoom. I'm not sure how
easy it is to do zooming as well though, I'll need to get back to you on
that! I imagine you could do this by taking d3.event.scale and
multiplying it by your initial projection scale value to get the new
projection scale value, and everything should stay consistent.

--
Jason Davies, http://www.jasondavies.com/

Mike Bostock

unread,
Oct 12, 2011, 12:19:17 PM10/12/11
to d3...@googlegroups.com
> The scale method. What does this refer to? Yes, I know a bigger value
> results in a bigger map. But it would be nice to know more.

It's mostly arbitrary at the moment. As Jason pointed out, it means
something for d3.geo.mercator, but I'm not sure it means anything for
the other projections. It would be nice to come up with a consistent
meaning, but it's a bit tricky as projections have wildly different
behaviors. One possibility might be a mapping between surface area to
pixels on screen, but since some projections are not equal-area you'd
have to make a restriction, such as (an infinitesimally small) area at
the origin.

> The translate method. You pass it an array of two values; these are
> horizontal and vertical 'offsets' which I guess are measured in pixels.
> However, where is the origin?

The translate method specifies where the projection's origin should
appear on-screen. For example, if you use the default d3.geo.mercator,
the default origin (longitude 0, latitude 0) will be displayed in the
center of a 960x500 rectangle (x 480, y 250).

> Conversion. Clearly it's possible to go from geographic space (lon/lat) to
> 'projected' space (x/y). However, is it possible to do the reverse?

Yep, projection.invert(point), where supported. And yes, it's the
inverse of projection(location), so it is consistent with translate
(and scale).

Mike

Iain

unread,
Oct 13, 2011, 7:32:21 AM10/13/11
to d3...@googlegroups.com
Hi Jason, hi Mike,

Thanks so much for your quick replies and for providing more information about d3.geo. Just to summarise...

d3.geo.mercator.scale(1) projects the earth's surface (WGS84 lon/lat, so measurements from -180 to + 180 and -90 to +90 decimal degrees) onto a 1x1 square. I guess those are pixel measurements. However, I'm unsure about where the centre of the earth's surface, or the geographic origin (lon 0, lat 0), is positioned in this square. Is it at the centre of the square (x 0.5, y 0.5) or at the top-left corner of the square (x 0, y 0)? I assume the former.

d3.geo.mercator.translate() moves the geographic origin (lon 0, lat 0) to a given point in pixel space. By default, this point is (x 480, y 250), measured from the top-left of the parent SVG element.

This might not be the case for the other projections: Understood.

d3.geo.*.invert() converts from pixel space to geographic space, accounting for any translate and scale. This is great.

Zooming and panning, then. I've cooked up an example here. Panning works well, for both geographic and non-geographic objects. Zooming, works less well: Although the code is simple, and as Jason suggested, the problem is that the map doesn't zoom onto the mouse point. Is this because the map has been translated? When the translation is set to (0, 0)...

var proj = d3.geo.mercator().translate([0, 0]);

...then the map zooms as expected. However, this is less flexible. Any suggestions would be greatly appreciated.

Thanks again for your help. I would like to update the API to reflect this information, but feel I don't understand the principles firmly enough just yet.

And Mike, see you at VisWeek!

Iain

Jason Davies

unread,
Oct 13, 2011, 8:01:09 AM10/13/11
to d3...@googlegroups.com
On Thu, Oct 13, 2011 at 04:32:21AM -0700, Iain wrote:
> d3.geo.mercator.scale(1) projects the earth's surface (WGS84 lon/lat, so
> measurements from -180 to + 180 and -90 to +90 decimal degrees) onto a 1x1
> square. I guess those are pixel measurements. However, I'm unsure about
> where the centre of the earth's surface, or the geographic origin (lon 0,
> lat 0), is positioned in this square. Is it at the centre of the square (x
> 0.5, y 0.5) or at the top-left corner of the square (x 0, y 0)? I assume the
> former.

Sorry if I wasn't clear: it's a 1x1 pixel square centred at [0, 0] (so
the top side of the square goes from [-.5, -.5] to [.5, -.5]. The
origin is mapped to [0, 0].

> d3.geo.mercator.translate() moves the geographic origin (lon 0, lat 0) to a
> given point in pixel space. By default, this point is (x 480, y 250),
> measured from the top-left of the parent SVG element.

Yes, typically a projection will map the origin (usually lon 0, lat 0)
to [0, 0] in pixel space (see above), so the translate just offsets this
[0, 0] by some number of pixels to move the origin to a particular point
in screen coordinates. In SVG, this is indeed measured from the
top-left corner.

> This might not be the case for the other projections: Understood.

This translation works in the same way for all projections. However,
I'm not sure the origin is necessarily always projected to [0, 0] by
default for all projections right now, although it should do for
consistency.

> d3.geo.*.invert() converts from pixel space to geographic space, accounting
> for any translate and scale. This is great.

Yes.

> Zooming and panning, then. I've cooked up an example here

> <http://bl.ocks.org/1283960>. Panning works well, for both geographic


> and non-geographic objects. Zooming, works less well: Although the
> code is simple, and as Jason suggested, the problem is that the map
> doesn't zoom onto the mouse point. Is this because the map has been
> translated? When the translation is set to (0, 0)...
>
> var proj = d3.geo.mercator().translate([0, 0]);
>
> ...then the map zooms as expected. However, this is less flexible. Any
> suggestions would be greatly appreciated.

The issue is occurring because the behaviour automatically adjusts its
translation vector to match the current scale. However, when you were
adding this vector to your original offset, you weren't correcting the
original offset to match the scale. So, simply multiplying by
d3.event.scale fixes this issue:

http://bl.ocks.org/1284044

Nice to see a map of the UK for a change. :)

Iain

unread,
Oct 13, 2011, 8:54:19 AM10/13/11
to d3...@googlegroups.com
Doh! So close, but so far! Thanks for that.

Iain
Reply all
Reply to author
Forward
0 new messages