Merging topojson using topomerge messes up winding order

308 views Asked by At

I'm trying to create a custom world map where countries are merged into regions instead of having individual countries. Unfortunately for some reason something seems to get messed up with the winding order along the process.

As base data I'm using the natural earth 10m_admin_0_countries shape files available here. As criteria for merging countries I have a lookup map that looks like this:

const countryGroups = {
  "EUR": ["ALA", "AUT", "BEL"...],
  "AFR": ["AGO", "BDI", "BEN"...],
  ...
}

To merge the shapes I'm using topojson-client. Since I want to have a higher level of control than the CLI commands offer, I wrote a script. It goes through the lookup map and picks out all the topojson features that belong to a group and merges them into one shape and places the resulting merged features into a geojson frame:

const topojsonClient = require("topojson-client");
const topojsonServer = require("topojson-server");

const worldTopo = topojsonServer.topology({
  countries: JSON.parse(fs.readFileSync("./world.geojson", "utf-8")),
});

const geoJson = {
  type: "FeatureCollection",
  features: Object.entries(countryGroups).map(([region, ids]) => {
    const relevantCountries = worldTopo.objects.countries.geometries.filter(
      (country, i) =>
        ids.indexOf(country.properties.ISO_A3) >= 0
    );

    return {
      type: "Feature",
      properties: { region, countries: ids },
      geometry: topojsonClient.merge(worldTopo, relevantCountries),
    };
  }),
};

So far everything works well (allegedly). When I try to visualise the map using github gist (or any other visualisation tool like vega lite) the shapes seem to be all messed up. I'm suspecting that I'm doing something wrong during the merging of the features but I can't figure out what it is.

enter image description here

When I try to do the same using the CLI it seems to work fine. But since I need more control over the merging, using just the CLI is not really an option.

1

There are 1 answers

3
Ruben Helsloot On

The last feature, called "World", should contain all remaining countries, but instead, it contains all countries, period. You can see this in the following showcase.

var w = 900,
  h = 300;

var projection = d3.geoMercator().translate([w / 2, h / 2]).scale(100);
var path = d3.geoPath().projection(projection);
var color = d3.scaleOrdinal(d3.schemeCategory10);

var svg = d3.select('svg')
  .attr('width', w)
  .attr('height', h);

var url = "https://gist.githubusercontent.com/Flave/832ebba5726aeca3518b1356d9d726cb/raw/5957dca433cbf50fe4dea0c3fa94bb4f91c754b7/world-regions-wrong.topojson";
d3.json(url)
  .then(data => {
    var geojson = topojson.feature(data, data.objects.regions);
    geojson.features.forEach(f => {
      console.log(f.properties.region, f.properties.countries);
    });

    svg.selectAll('path')
      // Reverse because it's the last feature that is the problem
      .data(geojson.features.reverse())
      .enter()
      .append('path')
      .attr('d', path)
      .attr('fill', d => color(d.properties.region))
      .attr('stroke', d => color(d.properties.region))
      .on('mouseenter', function() {
        d3.select(this).style('fill-opacity', 1);
      })
      .on('mouseleave', function() {
        d3.select(this).style('fill-opacity', null);
      });
  });
path {
  fill-opacity: 0.3;
  stroke-width: 2px;
  stroke-opacity: 0.4;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js"></script>
<script src="https://d3js.org/topojson.v3.js"></script>
<svg></svg>

To fix this, I'd make sure to always remove all assigned countries from the list. From your data, I can't see where "World" is defined, and if it contains all countries on earth, or if it's a wildcard assignment.

In any case, you should be able to fix it by removing all matches from worldTopo:

const topojsonClient = require("topojson-client");
const topojsonServer = require("topojson-server");

const worldTopo = topojsonServer.topology({
  countries: JSON.parse(fs.readFileSync("./world.geojson", "utf-8")),
});

const geoJson = {
  type: "FeatureCollection",
  features: Object.entries(countryGroups).map(([region, ids]) => {
    const relevantCountries = worldTopo.objects.countries.geometries.filter(
      (country, i) =>
        ids.indexOf(country.properties.ISO_A3) >= 0
    );

    relevantCountries.forEach(c => {
      const index = worldTopo.indexOf(c);
      if (index === -1) throw Error(`Expected to find country ${c.properties.ISO_A3} in worldTopo`);
      worldTopo.splice(index, 1);
    });

    return {
      type: "Feature",
      properties: { region, countries: ids },
      geometry: topojsonClient.merge(worldTopo, relevantCountries),
    };
  }),
};