Search code examples
javascripthtmld3.jsgeojsontopojson

Focusing on geojson in D3.js map


I am trying to view a objects in a topojson file (of buildings in a city) but get the following error:

Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…".

Here is my code:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.land {
  fill: #e5e5e5;
  stroke: #000;   
  stroke-width: 0.2;
  stroke-opacity: 0.8;
}
.states {
  fill: none;
  stroke: #fff;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script>
    var width = 800;
    var height = 600;
var projection = d3.geo.mercator()
    .center([30, 30])
        .scale(500)
    .translate([width / 2, height / 2]);
var path = d3.geo.path().projection(projection);
var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);
queue()
    .defer(d3.json, "cairomonuments.json")
    .await(ready);
function ready(error, cairo) {
  if (error) throw error;

      // Refine projection
      var b, s, t;
      projection.scale(1).translate([0, 0]);
      var b = path.bounds(cairo);
      var s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);
      var t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
      projection.scale(s).translate(t); 

  svg.selectAll("path")
      .data(topojson.feature(cairo, cairo.objects.monuments).features)
        .enter()
        .append('path')
        .attr('class', 'land')
        .attr('d', path);
}
</script>
</body>

I just want to center the map on my geojson file and flip it sideways. What am I missing?

topojson file here


Solution

  • The problem

    The primary issue as far as I can see is this line:

    var b = path.bounds(cairo);
    

    path.bounds won't produce expected results with a collection of features (such as your layer). Instead it:

    Computes the projected bounding box (in pixels) for the specified feature. The bounding box is represented by a two-dimensional array: [[left, top], [right, bottom]] , different from GIS geo.bounds' convention.

    Also, you aren't passing it geojson, you're passing it topojson. If you wanted to use a bounds of a specific feature, your code would look more like:

    var b = path.bounds(topojson.feature(cairo, cairo.objects.monuments).features[0]);
    

    Even if you pass it a singular feature in the right format, it still won't project correctly as your scale was defined as 500 earlier when you defined the projection - this will warp the calculations when dynamically re-calculating the scale.

    Possible Solution (Keeping d3.js v3)

    Topojson generally has a bbox property. You could use this to get your centering coordinate:

    var x = (cairo.bbox[0] + cairo.bbox[2]) / 2; // get middle x coordinate
    var y = (cairo.bbox[1] + cairo.bbox[3]) / 2; // get middle y coordinate
    

    Note that the order of a geojson or topojson bounding box is : left, bottom, right, top.

    So we can easily center the map on the layer center now:

    projection.center([x,y]) or projection.rotate([-x,0]).center([0,y]) or projection.rotate([-x,-y]).

    Now all that is left is to calculate the scale (set it at one to start).

    If path.bounds returns a two coordinate array of the top left and bottom right coordinates ([min x, min y],[max x, max y], in SVG coordinate space), then we can produce an equivalent array using the topojson.bbox:

    var b = [ projection([cairo.bbox[0],cairo.bbox[3]]),projection([cairo.bbox[2],cairo.bbox[1]]) ];
    

    Here it's a little tricky as the SVG coordinate space has y coordinates starting from zero at the top (reversed from the geographic features), and the order of coordinates in the bounds is: left top right bottom (again, different than geographic features).

    That leaves us with the calculation you already had:

    var s = 0.95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);
    

    Which altogether gives us:

    Initial declaration of scale:

    var projection = d3.geo.mercator()
        .scale(1)
        .translate([width / 2, height / 2]);
    

    Refinement of scale and center based on data layer:

    var x = (cairo.bbox[0] + cairo.bbox[2]) / 2;
    var y = (cairo.bbox[1] + cairo.bbox[3]) / 2; 
    projection.rotate([-x,-y]);
    
    var b = [ projection([cairo.bbox[0],cairo.bbox[3]]),projection([cairo.bbox[2],cairo.bbox[1]]) ];
    var s = 0.95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);
    projection.scale(s);
    

    Here's a bl.ock demonstrating it all in action.

    Flipping the map

    There is a seldom used parameter in the projection rotation that allows you to achieve this. In my bl.ock above and in the code block above I used rotate to center the map projection. By adding a third parameter I can rotate the map relative to the viewport:

    projection.rotate([-x,-y,90]);