Search code examples
javascriptd3.jsgeo

d3.js alter projection on each iteration of the data loop


I'm very new to d3.js so my apologies if this is a stupid question.

When iterating over a geojson FeatureCollection list, i would like to change the projection on each item. is this possible?

My code looks something like this:

var width = 200,
var height = 200;
var svg = d3.select('#content g.map')
            .append("svg")
            .attr("width", width)
            .attr("height", height);

var projection = d3.geoAlbers()
                   .fitExtent(
                      [
                      [0, 0],
                      [width, height],
                      ],
                    features
                   );


let geoGenerator = d3.geoPath().projection(projection)

var group = svg.append('g');

group.selectAll('path')
     .data(geojson.features)
     .enter()
     .append('path')
     .attr('d', geoGenerator)
     .attr('stroke-width', '3')
     .attr('stroke', 'black')
     .attr('fill', 'none');

I'm using geoAlbers() with .fitExtend(), where my projection is drawn according to all elements in the geojson file. But i would like to draw it for each element independently.

My goal is to create a plot where each element in the array is the same size. Any help is appreciated!


Solution

  • You can use projection.fitSize or projection.fitExtent to modify a projection so that it will project a geojson feature to fill the specified bounding box. Normally these methods are used for a feature collection or a single feature, but there is no reason that we can't use it for each feature.

    projection.fitSize/fitExtent only modify a projection's translate and scale values: they only zoom and pan the projected features. To make a grid using a conical projection like an Albers, you'll want to recalculate the projection's sequant lines/parallels for each feature or you may risk severe distortion in shapes. The use of a cylindrical projection removes this need (I've used a Mercator to simplify a solution). However, you should, in certain cases, calculate an appropriate anti-meridian for each feature so that no feature would span it, most d3 projections use 180 degrees east west as the default anti-meridian, however, you can change this for each projection by rotating the projection with projection.rotate(). The use of d3.geoBounds or d3.geoCenter could help facilitate this, my solution below does not account for these edge cases.

    The snippet below uses g elements, one per feature, for positioning, then appends a path, and, by using selection.each(), calculates the projection parameters needed using projection.fitSize() so that the features bounding box is of the right size (each bounding box is size pixels square).

    d3.json('https://unpkg.com/world-atlas@1/world/110m.json').then(function(world) {
    
            let width = 960,
                height = 500,
                size = 40;
    
            let projection = d3.geoMercator()
            let path = d3.geoPath(projection);
            
            let svg = d3.select("body").append("svg")
              .attr("width", width)
              .attr("height", height);
            
            let grid = svg.selectAll(null)
              .data(topojson.feature(world, world.objects.countries).features)
              .enter()
              .append("g")
              .attr("transform", function(d,i) {
                return "translate("+[i%16*size,Math.floor(i/16)*size]+")";
              })
     
            let countries = grid.append("path")
              .each(function(feature) {
                projection.fitSize([size,size],feature)
                d3.select(this).attr("d", path);
              })
     })
         
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <script src="https://d3js.org/topojson.v2.min.js"></script>