4

I want to plot multiple countries for which the data comes from individual topoJSON files. The map with these countries should be in the middle of the screen (centered) and the appropriate scale should be set that the map fills up the whole window.

What I've already tried:

  • To get the bounding box with path.bounds(d) for every country and use for every side (top, left, bottm, right) the max and min values respectively and provide the mean center from path.centroid(d) to projection.center(). This didn't work.

  • To apply projection.fitExtent([[x0,y0],[x1,y1]],geojsonObject) or projection.fitSize([width,height],geojsonObject) as proposed in this solution. Here I was not able to create the featureCollection as described and use it to create the map.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title></title>
        <script src="https://d3js.org/d3.v5.min.js"></script>
        <script src="https://d3js.org/topojson.v2.min.js"></script>
        <style type="text/css">

    .svg-container {
        display: inline-block;
        position: relative;
        width: 100%;
        padding-bottom: 100%;
        vertical-align: top;
        overflow: hidden;
    }
    .svg-content {
        display: inline-block;
        position: absolute;
        margin: 0;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }

        </style>
    </head>
    <body>
        <div id="container" class="svg-container"> </div>

        <script type="text/javascript">
                var width = 600; //for simplicity set to fixed value
                var height = 600; //for simplicity set to fixed value

                var projection = d3.geoMercator();

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

                var svg = d3.select("#container").append("svg")
                      .classed("svg-content", true)
                      .attr("width", width)
                      .attr("height", height);

            //Example data
            var countries = ['germany', 'italy', 'switzerland', 'france']; 

            function loadJsonFiles() {

                  var files = ["https://api.myjson.com/bins/1b0ddz", 
                       "https://api.myjson.com/bins/11jkvb",
                       "https://api.myjson.com/bins/96x1j",
                       "https://api.myjson.com/bins/sspzr"];

                  var promises = [];
                  var allCountryData = [];

                  files.forEach(function(url) {
                          promises.push(d3.json(url))
                  });

                  return Promise.all(promises)
                      .then(function(countryData) {

                      for (var i = 0; i < countryData.length; i++) {

                        allCountryData.push(countryData[i]);

                      }
                    return allCountryData;
                  });
            }

            loadJsonFiles().then(function(allCountryData) {

                  var allBounds = [];
                  var objName; 
                  var countryData;

                  for (var i = 0; i < allCountryData.length; i++) {

                      objName = allCountryData[i].objects[countries[i]];
                      countryData = topojson.feature(allCountryData[i], objName);

                      //How can I use the right projection parameters for all country data loaded from topojson (outside the loop)?
                      projection
                          .scale(1000) //How to set this programmatically?
                          .center([8,47]) //How to set this programmatically?
                          .translate([width / 2, height / 2]);
                      //How can I append all the country data to the svg (outside the loop)?
                      svg.append("path")
                          .datum(countryData)
                          .attr("d", path);
                  }

            });

        </script>
    </body>
</html>   

I think it should be possible to solve this problem with the suggestion provided by Andrew Reid in this post with fitSize or fitExtent. But I was no able to apply this to my problem. This would also be my prefered way to solve my problem.

Alternatively, is it possible to solve this problem with center() and scale()? How would I have to determine which coordinates I have to provide to center() and the value to scale().? Thanks very much for the help, I tried for days but did not succeed.

elrey
  • 167
  • 2
  • 8

1 Answers1

3

projection.fitSize and projection.fitExtent both take a single geojson object, not an array of geojson objects. Luckily geojson has an object type that can contain as many child objects as we need: a featureCollection.

I see that you are using topojson, topojson.feature returns a geojson object. In your case you return a feature collection each time you use topojson.

In order to use fitSize/fitExtent we need to create a geojson object that contains all the individual features, a feature collection does this, the structure is as follows:

featureCollection = {
  "type":"FeatureCollection",
  "features": [ ... ]
}

We could build a feature collection with something like:

var featureCollection = {type:"FeatureCollection","features":[]}

for (var i = 0; i < allCountryData.length; i++) {

     objName = allCountryData[i].objects[countries[i]];
     countryData = topojson.feature(allCountryData[i], objName);

     featureCollection.features.push(...countryData.features)

}

Now we can pass this feature collection to fitSize/fitExtent with

projection.fitSize([width,height],featureCollection);

Which should look like this:

                var width = 400; //for simplicity set to fixed value
                var height = 200; //for simplicity set to fixed value

                var projection = d3.geoMercator();

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

                var svg = d3.select("#container").append("svg")
                      .classed("svg-content", true)
                      .attr("width", width)
                      .attr("height", height);

            //Example data
            var countries = ['germany', 'italy', 'switzerland', 'france']; 

            function loadJsonFiles() {

                  var files = ["https://api.myjson.com/bins/1b0ddz", 
                       "https://api.myjson.com/bins/11jkvb",
                       "https://api.myjson.com/bins/96x1j",
                       "https://api.myjson.com/bins/sspzr"];

                  var promises = [];
                  var allCountryData = [];

                  files.forEach(function(url) {
                          promises.push(d3.json(url))
                  });

                  return Promise.all(promises)
                      .then(function(countryData) {

                      for (var i = 0; i < countryData.length; i++) {

                        allCountryData.push(countryData[i]);

                      }
                    return allCountryData;
                  });
            }

            loadJsonFiles().then(function(allCountryData) {

                var allBounds = [];
                var objName; 
                var countryData;


    var featureCollection = {type:"FeatureCollection","features":[]}
    for (var i = 0; i < allCountryData.length; i++) {

      objName = allCountryData[i].objects[countries[i]];
      countryData = topojson.feature(allCountryData[i], objName);

      featureCollection.features.push(...countryData.features)

    }
    
    projection.fitSize([400,200],featureCollection)
    
    svg.selectAll("path")
      .data(featureCollection.features)
      .enter()
      .append("path")
      .attr("d",path);

            });
svg {
   border: 1px solid black;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<div id="container" class="svg-container"> </div>

I've kept your code except that I've used a different for loop (shown above) to create the feature collection, and instead of looping through to append each path I've used a enter selection. Lastly, I changed the size and positioning just so it fits in snippet view better.

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83