2

I've read and used Mike Bostock's answer to Center a map in D3 given a geoJSON object, which is a generic way to scale to fit one item in a geojson file based on its bounds in D3 version 3. I've also adapted it to use every item in a GeoJson. Key part of that algorithm is:

// Calculate bounding box transforms for entire collection
var b = path.bounds( geojson ),
s = .95 / Math.max((b[1][0] - b[0][0]) / w, (b[1][1] - b[0][1]) / h),
t = [(w - s * (b[1][0] + b[0][0])) / 2, (h - s * (b[1][1] + b[0][1])) / 2];

// Update the projection, which initially had .translate([0, 0]) .scale(1); 
projection
  .scale(s)
  .translate(t);

Using the V4 D3-geo docs I've crudely adapted it to work in with the new/ammended d3.geoPath() function / object in D3 version 4.0. It seems to work in the demo below (I'm using d3.geoPath().bounds(), not yet tried V4's seemingly new d3.geoBounds()). However, I then get stuck with adapting it to get the bounding box across all layers in a multi-layer topojson file that has been converted using topojson.js (i.e. the bounding box of multiple GeoJSON feature collections). d3.geoPath().bounds() seems to only accept one "layer" and d3.geoBounds() appears even more restricted; to one feature.

I'm also somewhat concerned about performance - all this seems to involve looping over potentially very many shapes in potentially many layers, I feel like maybe there might be a more efficient approach in D3 V4?


Here's a rough demo as a starting point with a very small simple TopoJSON file that should show two islands with a few subregions each (very very simplified versions of Wales and Northern Ireland for a simple demo, each on separate layers). After some trial and error, I've managed to get it to scale and translate the map to centre around the "Wales-like" island, but I can't figure out how to make it centre and scale on all the layers of the TopoJSON.

The standard approach to layered TopoJSON seems to be to turn each layer into what's essentially a seperate GeoJSON, so how do I get the bounds across multiple GeoJSONs while still being able to handle them as separate layers?

//Width and height
var w = 300;
var h = 200;

//Define map projection
var projection = d3.geoEquirectangular()
  .translate([0, 0])
  .scale(1);

//Define path generator
var path = d3.geoPath()
  .projection(projection);

//Create SVG element
var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

//Load in GeoJSON data

var json = someUKJSON();

for (var key in json.objects) {
  if (json.objects.hasOwnProperty(key)) {
    
    // Topojson unpacks one layer at a time
    var layer = json.objects[key];
    var geojson = topojson.feature( json, layer );

    // Calculate bounding box transforms for entire collection
    var b = path.bounds(geojson),
      s = .95 / Math.max((b[1][0] - b[0][0]) / w, (b[1][1] - b[0][1]) / h),
      t = [(w - s * (b[1][0] + b[0][0])) / 2, (h - s * (b[1][1] + b[0][1])) / 2];

    // Update the projection    
    projection
      .scale(s)
      .translate(t);

    // Bind data and create one path per GeoJSON feature
    svg.selectAll("path")
      .data(geojson.features)
      .enter()
      .append("path")
      .attr("d", path)
      .style("fill", "steelblue");
    };
    // ...but each iteration will just zoom/centre on the latest
    // How do we expand the bounding box with each layer?
}

function someUKJSON(){
  return {"type":"Topology","transform":{"scale":[0.0034431267161520807,0.002017902170346754],"translate":[-7.8508544159644345,51.47680014500252]},
  "arcs":[[[1366,700],[-216,-155]],[[1150,545],[-121,275],[292,101],[45,-221]],[[1366,700],[23,-449]],[[1389,251],[-77,-96]],[[1312,155],[-75,-18]],[[1237,137],[-62,17]],[[1175,154],[-35,383]],[[1140,537],[10,8]],[[1175,154],[-71,-37]],[[1104,117],[-314,73],[350,347]],[[1237,137],[27,-119]],
  [[1264,18],[-35,-18]],[[1229,0],[-125,117]],[[1312,155],[28,-118]],[[1340,37],[-76,-19]],[[1385,12],[-45,25]],[[1389,251],[-4,-239]],[[1385,12],[-156,-12]],[[563,1572],[16,-7]],[[579,1565],[-55,-14]],[[524,1551],[39,21]],[[397,1870],[166,-298]],[[524,1551],[-63,-5]],[[461,1546],[-96,-18]],
  [[365,1528],[-109,10]],[[256,1538],[35,290]],[[291,1828],[106,42]],[[574,1342],[-124,192]],[[450,1534],[6,4],[5,8]],[[579,1565],[-5,-223]],[[365,1528],[85,6]],[[574,1342],[-219,-72],[-162,148]],[[193,1418],[63,120]],[[0,1515],[291,313]],[[193,1418],[-193,97]]],
  "objects":{"Wales":{"type":"GeometryCollection","geometries":[{"arcs":[[0,1]],"type":"Polygon","id":"Bedr"},{"arcs":[[2,3,4,5,6,7,-1]],"type":"Polygon","id":"Pong"},{"arcs":[[8,9,-7]],"type":"Polygon","id":"Hyda"},{"arcs":[[-6,10,11,12,-9]],"type":"Polygon","id":"Abwg"},
  {"arcs":[[13,14,-11,-5]],"type":"Polygon","id":"Cwaf"},{"arcs":[[15,-14,-4,16]],"type":"Polygon","id":"Anan"},{"arcs":[[17,-12,-15,-16]],"type":"Polygon","id":"Cave"}]},"nernIrel":{"type":"GeometryCollection","geometries":[{"arcs":[[18,19,20]],"type":"Polygon","id":"Blft"},
  {"arcs":[[21,-21,22,23,24,25,26]],"type":"Polygon","id":"nern"},{"arcs":[[27,28,-23,-20,29]],"type":"Polygon","id":"hern"},{"arcs":[[30,-28,31,32,-25]],"type":"Polygon","id":"sorn"},{"arcs":[[33,-26,-33,34]],"type":"Polygon","id":"wern"}]}}};
};
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
Community
  • 1
  • 1
user56reinstatemonica8
  • 32,576
  • 21
  • 101
  • 125

1 Answers1

2

Here's a very simplistic approach. It's the closest any of my attempts at this have come. Leaving it here while hoping someone will come up with something better.

  • Create a throwaway fake geojson-like object purely for calculating the bounds
  • Merge the features arrays of each feature into it
  • Use it to calculate the bounds and adjust the projections once

This feels very clunky though, and I can't find a way to draw one layer at a time with this, I'm resorting to using the throwaway flattened collection to draw the paths.

Also, strangely d3.merge() doesn't seem to work with geojson.features, which is why I'm using Array.concat() instead. Don't understand that one, but this does get it done.

//Width and height
var w = 300;
var h = 200;

//Define map projection
var projection = d3.geoEquirectangular()
  .translate([0, 0])
  .scale(1);

//Define path generator
var path = d3.geoPath()
  .projection(projection);

//Create SVG element
var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

//Load in GeoJSON data

var json = someUKJSON();

//Create a fake bounds layer
var boundsCollection = {
  type: "FeatureCollection",
  features: []
}

for (var key in json.objects) {
  if (json.objects.hasOwnProperty(key)) {

    // Topojson unpacks one layer at a time
    var layer = json.objects[key];
    var geojson = topojson.feature(json, layer);

  boundsCollection.features = boundsCollection.features.concat( geojson.features );

  };
}


// Calculate bounding box transforms for entire collection
var b = path.bounds(boundsCollection),
  s = .95 / Math.max((b[1][0] - b[0][0]) / w, (b[1][1] - b[0][1]) / h),
  t = [(w - s * (b[1][0] + b[0][0])) / 2, (h - s * (b[1][1] + b[0][1])) / 2];

// Update the projection    
projection
  .scale(s)
  .translate(t);

    // Bind data and create one path per GeoJSON feature
    svg.selectAll("path")
      .data(boundsCollection.features)
      .enter()
      .append("path")
      .attr("d", path)
      .style("fill", "steelblue");


function someUKJSON() {
  return {
    "type": "Topology",
    "transform": {
      "scale": [0.0034431267161520807, 0.002017902170346754],
      "translate": [-7.8508544159644345, 51.47680014500252]
    },
    "arcs": [
      [
        [1366, 700],
        [-216, -155]
      ],
      [
        [1150, 545],
        [-121, 275],
        [292, 101],
        [45, -221]
      ],
      [
        [1366, 700],
        [23, -449]
      ],
      [
        [1389, 251],
        [-77, -96]
      ],
      [
        [1312, 155],
        [-75, -18]
      ],
      [
        [1237, 137],
        [-62, 17]
      ],
      [
        [1175, 154],
        [-35, 383]
      ],
      [
        [1140, 537],
        [10, 8]
      ],
      [
        [1175, 154],
        [-71, -37]
      ],
      [
        [1104, 117],
        [-314, 73],
        [350, 347]
      ],
      [
        [1237, 137],
        [27, -119]
      ],
      [
        [1264, 18],
        [-35, -18]
      ],
      [
        [1229, 0],
        [-125, 117]
      ],
      [
        [1312, 155],
        [28, -118]
      ],
      [
        [1340, 37],
        [-76, -19]
      ],
      [
        [1385, 12],
        [-45, 25]
      ],
      [
        [1389, 251],
        [-4, -239]
      ],
      [
        [1385, 12],
        [-156, -12]
      ],
      [
        [563, 1572],
        [16, -7]
      ],
      [
        [579, 1565],
        [-55, -14]
      ],
      [
        [524, 1551],
        [39, 21]
      ],
      [
        [397, 1870],
        [166, -298]
      ],
      [
        [524, 1551],
        [-63, -5]
      ],
      [
        [461, 1546],
        [-96, -18]
      ],
      [
        [365, 1528],
        [-109, 10]
      ],
      [
        [256, 1538],
        [35, 290]
      ],
      [
        [291, 1828],
        [106, 42]
      ],
      [
        [574, 1342],
        [-124, 192]
      ],
      [
        [450, 1534],
        [6, 4],
        [5, 8]
      ],
      [
        [579, 1565],
        [-5, -223]
      ],
      [
        [365, 1528],
        [85, 6]
      ],
      [
        [574, 1342],
        [-219, -72],
        [-162, 148]
      ],
      [
        [193, 1418],
        [63, 120]
      ],
      [
        [0, 1515],
        [291, 313]
      ],
      [
        [193, 1418],
        [-193, 97]
      ]
    ],
    "objects": {
      "Wales": {
        "type": "GeometryCollection",
        "geometries": [{
          "arcs": [
            [0, 1]
          ],
          "type": "Polygon",
          "id": "Bedr"
        }, {
          "arcs": [
            [2, 3, 4, 5, 6, 7, -1]
          ],
          "type": "Polygon",
          "id": "Pong"
        }, {
          "arcs": [
            [8, 9, -7]
          ],
          "type": "Polygon",
          "id": "Hyda"
        }, {
          "arcs": [
            [-6, 10, 11, 12, -9]
          ],
          "type": "Polygon",
          "id": "Abwg"
        }, {
          "arcs": [
            [13, 14, -11, -5]
          ],
          "type": "Polygon",
          "id": "Cwaf"
        }, {
          "arcs": [
            [15, -14, -4, 16]
          ],
          "type": "Polygon",
          "id": "Anan"
        }, {
          "arcs": [
            [17, -12, -15, -16]
          ],
          "type": "Polygon",
          "id": "Cave"
        }]
      },
      "nernIrel": {
        "type": "GeometryCollection",
        "geometries": [{
          "arcs": [
            [18, 19, 20]
          ],
          "type": "Polygon",
          "id": "Blft"
        }, {
          "arcs": [
            [21, -21, 22, 23, 24, 25, 26]
          ],
          "type": "Polygon",
          "id": "nern"
        }, {
          "arcs": [
            [27, 28, -23, -20, 29]
          ],
          "type": "Polygon",
          "id": "hern"
        }, {
          "arcs": [
            [30, -28, 31, 32, -25]
          ],
          "type": "Polygon",
          "id": "sorn"
        }, {
          "arcs": [
            [33, -26, -33, 34]
          ],
          "type": "Polygon",
          "id": "wern"
        }]
      }
    }
  };
};
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
user56reinstatemonica8
  • 32,576
  • 21
  • 101
  • 125