1

I'm trying to render a map of Switzerland with D3.js and TopoJSON. The underlying JSON can be found here. First I tried to follow this tutorial and after I couldn't render anything looking remotely like a map, I found this question on here with a link to a working example. From where I took this code I'm currently using:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script src="http://d3js.org/topojson.v1.min.js"></script>
    <style>
    path {
        fill: #ccc;
    }
    </style>
</head>

<body>
    <h1>topojson simplified Switzerland</h1>
    <script>
    var width = window.innerWidth,
        height = window.innerHeight;

    var svg = d3.select("body")
        .append("svg")
        .attr("width", width)
        .attr("height", height);

    var projection = d3.geo.mercator()
        .scale(500)
        .translate([-900, 0]);

    var path = d3.geo.path()
        .projection(projection);

    d3.json("ch-cantons.json", function(error, topology) {
        if (error) throw error;

        svg.selectAll("path")
         .data(topojson.feature(topology, topology.objects.cantons).features).enter().append("path").attr("d", path);
    });
    </script>
</body>

</html>

Since the JSON looks ok (checked on some online platforms that renders it properly after copy/pasting) and there is only little code that could go wrong, I assume the error is in the projection parameters. Fiddled around a bit couldn't make it work. So any help would be very much appreciated!

Community
  • 1
  • 1
citivin
  • 626
  • 1
  • 9
  • 24

1 Answers1

2

You are right, the error is in the projection. But, the error depends on if your data is projected or unprojected (lat long pairs).

Unprojected Data

If you have data that is in WGS84 - that is to say lat long pairs, then you have this problem:

Using your projection, but changing only the data source I get something like this (I shaved off the empty ocean on the right):

enter image description here

To center a Mercator properly, you need to know the center coordinate of your area of interest. This generally can be fairly general, for Switzerland I might try 47N 8.25E.

Once you have this coordinate you need to place it in the middle. One way is to rotate on the x axis and center on the y:

var projection = d3.geo.mercator()
    .scale(3500)
    .rotate([-8.25,0])
    .center([0,47])
    .translate([width/2,height/2])

Note that the x rotation is negative, you are spinning the globe underneath the projection.

The other option is to rotate on both x and y, this is likely the preferred option as you approach the poles - as Mercator distortion becomes unworkable at high latitudes. This approach would look like:

var projection = d3.geo.mercator()
    .scale(4000)
    .rotate([-8.25,-47])
    .center([0,0])
    .translate([width/2,height/2])

Note again the negative rotation values. The second option requires a higher scale value as this method essentially treats Switzerland as though it were at zero,zero on a Mercator projection - and along the equator land sizes are minimized.

Using the second of these projections, I get: enter image description here

So you'll have to dial in the scale a bit, but now you should be able to see your data (assuming your data is in proper lat long pairs).

Projected Data

Based on the comment below, which includes a linked json file, we can see that this is your problem.

There are two potential solutions to this:

  1. Convert the data to lat long pairs
  2. Use a geoTransform

Option one is the easiest, you'll unproject the data - which requires knowing the current projection. In GIS software this will generally be projecting it as WGS84, which is arguably not really a projection but a datum. Once you have your lat long pairs, you follow the steps above for unprojected data.

Option two skips a d3.geoProjection altogether. Instead, we'll create a transform function that will convert the projected coordinates to the desired SVG coordinates.

A geo projection looks like:

function scale (scaleFactor) {
    return d3.geo.transform({
        point: function(x, y) {
            this.stream.point(x * scaleFactor, (y * scaleFactor);
        }
    });
}

And is used like a projection:

var path = d3.geo.path().projection(scale(0.1));

It simply takes a stream of x,y coordinates that are already cartesian and transforms them in a specified manner.

To translate the map so it is centered you'll need to know the center coordinate. You can find this with path.bounds:

var bounds = path.bounds(features);
var centerX = (bounds[0][0] + bounds[1][0])/2;
var centerY = (bounds[0][1] + bounds[1][1])/2;

Path.bounds returns the top left corner and bottom right corner of a feature. This is the key part, you can make an autoscaling function, there are plenty of examples out there, but I like manually scaling often. If the map is centered, this is easy. For your map, your geoTransform might look like:

function scale (scaleFactor,cx,cy,width,height) {
    return d3.geo.transform({
        point: function(x, y) {
            this.stream.point((x-cx) * scaleFactor + width/2, (y-cy)  * scaleFactor +height/2);
        }
    });
}

Here cx and cy refer to the middle of your feature and the width and height refer to the width and height of the svg element - we don't want features clustered at SVG point [0,0].

Altogether, that gives us something like (with a scale factor of 0.002):

enter image description here

Here's an updated JSbin: http://jsbin.com/wolamuzeze/edit?html,output

Keep in mind that scale is dependent on window size as your width/height are relative to window size in your case. This might be best addressed with automatically setting the zoom level, though this can create problems if you have labels (for example).

This answer might help as well: Scaling d3 v4 map to fit SVG (or at all)

Graham
  • 7,431
  • 18
  • 59
  • 84
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Thanks for your help. I tried changing the projection as advised, but only get a mess back. Here's a [jsbin](http://jsbin.com/jumurupepu/edit?html,output) with said mess. – citivin Mar 23 '17 at 13:39
  • Ah, this is a different problem. You don't need a projection at all - your data is already projected. If you set the projection to null, you'll see that the coordinates are not lat long pairs - d3 geoPaths take lat long pairs representing positions on a spherical earth and projects them onto a 2d plain. The easiest way to fix this is to (un)project your data so that it uses lat long pairs (using the datum WGS84). I will edit the answer to properly center this file using a geoTransform later tonight. – Andrew Reid Mar 23 '17 at 17:36
  • Thank you very much. Will have a more thorough look at it later on. But now it looks like a map at least. – citivin Mar 24 '17 at 08:51