Search code examples
d3.jstopojsondatamaps

How does d3's path.bounds work?


I have a file world.topo.json in TopoJson format which I took from https://datamaps.github.io/ and use it in a d3 geo chart (using merchant projection).

It works well, but I find quite odd why, when I call path.bounds(<TopoJson File Content>.objects.world.feature) and get these values:

[
    [-25.272818452358365, -114.9648719971861],
    [917.2049776245796, 507.5180814546301]
]

So, why is the botom/left corner pointing to -25 and -114? Shouldn't them be either 0,0 or -917, -507 instead?

Update: I have a zoom behavior object bound to my d3 chart, which works for me exactly as expected. So, I've written a console.log for every zoom/drag even like below:

const topojson = <response of an ajax request>;
const bounds = path.bounds(topojson.objects.world.feature);
console.log(translate, JSON.stringify(path.bounds(feature))); // XXX

So, every single time zoom/drag even is called, this is the type of output I get:

[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"

The first array being the current translate and the second being the bounds.

But, when I drag/pan or zoom, here is the output:

[0.021599999999999998, 0.10368] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[24.88185889212827, 119.4329226822157] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[25, 120] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-15, 119] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-27, 117] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-27.32184332502962, 117.03468139278337] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-125.83796642848066, 127.65064293410353] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-165.15379127139124, 131.88726199045166] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.98081187505056, 132.83844955550114] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.98081187505056, 132.83844955550114] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-173.4557969093005, 132.7818746669505] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-89.06290511198648, 123.68781305086063] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"
[-89.06290511198648, 123.68781305086063] "[[-25.272818452358365,-114.9648719971861],[917.2049776245796,507.5180814546301]]"

As you can see, although the first argument changes constantly according to zoom and pan events, the bounds remain untouched.


Solution

  • The documentation about path.bounds(object)has it covered:

    Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON object. The bounding box is represented by a two-dimensional array: [[x₀, y₀], [x₁, y₁]], where x₀ is the minimum x-coordinate, y₀ is the minimum y-coordinate, x₁ is maximum x-coordinate, and y₁ is the maximum y-coordinate.

    So, -25 and -114 are the minimum x and y values, and refer to the top left corner (in the SVG coordinates system), not the bottom left.

    Have in mind that path.bounds is different from geoBounds, which:

    Returns the spherical bounding box for the specified GeoJSON feature. The bounding box is represented by a two-dimensional array: [[left, bottom], [right, top]], where left is the minimum longitude, bottom is the minimum latitude, right is maximum longitude, and top is the maximum latitude.

    How does it work?

    path.bounds(object) will use your projection to drawn a "rectangle" around your object and will return an array with the four corners of that rectangle, as described above. Let's see how it works in these demos (this code is not mine):

    In this first demo, the map of Japan has an scale of 1000. Check the console to see path.bounds.

    var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
    
    var width = 500,
        height = 500,
        scale = 1;
    
    d3.select("body").append("svg")
    	.attr("width", width)
    	.attr("height", height)
        .append("g").attr("id", "all-g");
    
    var projection = d3.geo.mercator()
    		.center([138, 38])
    		.scale(1000)
    		.translate([width / 2, height / 2]);
        
    d3.json(topoJsonUrl, onLoadMap);
    
    function onLoadMap (error, jpn) {
        var path = d3.geo.path()
            		.projection(projection);
    	var features = topojson.object(jpn, jpn.objects.japan);
      
      var mapJapan = features;
      
     console.log(JSON.stringify(path.bounds(mapJapan)))
      
    
    	d3.select("#all-g")
            .append("g").attr("id", "path-g").selectAll("path")
                .data(features.geometries)
                .enter()
                .append("path")
                .attr("fill", "#f0f0f0")
                .attr("id", function(d,i){ return "path" + i})
                .attr("stroke", "#999")
                .attr("stroke-width", 0.5/scale)
                .attr("d", path);
    
    }
    path {
      stroke: black;
      }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <script src="https://d3js.org/topojson.v0.min.js"></script>

    It logs:

    [[-12.878670523380151,73.71036362631844],[529.0014631418044,535.5463567314675]]
    

    Which are [[x0, y0],[x1, y1]] values.

    Now the same code, but with a scale of 500:

    var topoJsonUrl = "https://dl.dropboxusercontent.com/u/1662536/topojson/japan.topo.json";
    
    var width = 500,
        height = 500,
        scale = 1;
    
    d3.select("body").append("svg")
    	.attr("width", width)
    	.attr("height", height)
        .append("g").attr("id", "all-g");
    
    var projection = d3.geo.mercator()
    		.center([138, 38])
    		.scale(500)
    		.translate([width / 2, height / 2]);
        
    d3.json(topoJsonUrl, onLoadMap);
    
    function onLoadMap (error, jpn) {
        var path = d3.geo.path()
            		.projection(projection);
    	var features = topojson.object(jpn, jpn.objects.japan);
      
      var mapJapan = features;
      
     console.log(JSON.stringify(path.bounds(mapJapan)))
      
    
    	d3.select("#all-g")
            .append("g").attr("id", "path-g").selectAll("path")
                .data(features.geometries)
                .enter()
                .append("path")
                .attr("fill", "#f0f0f0")
                .attr("id", function(d,i){ return "path" + i})
                .attr("stroke", "#999")
                .attr("stroke-width", 0.5/scale)
                .attr("d", path);
    
    }
    path {
      stroke: black;
      }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <script src="https://d3js.org/topojson.v0.min.js"></script>

    It logs different values:

    [[118.56066473830992,161.85518181315928],[389.5007315709022,392.77317836573377]]