4

Imagine you have a GeoJSON feature collection of adjacent colored polygons presented in a Leaflet map; in the present case the parishes of the city of Lisbon

enter image description here

fetch('https://geoapi.pt/municipio/lisboa/freguesias?json=1').then(r => r.json()).then(data => {
  const geojsons = data.geojsons
  
  var map = L.map('map')

  const bbox = geojsons.municipio.bbox
  const corner1 = L.latLng(bbox[1], bbox[0])
  const corner2 = L.latLng(bbox[3], bbox[2])
  const bounds = L.latLngBounds(corner1, corner2)
  map.fitBounds(bounds)
  
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
  }).addTo(map)

  // feature collection
  const parishesGeoJsonFeatureCollection = {
    type: 'FeatureCollection',
    features: geojsons.freguesias
  }

  // need this for color pallete
  const numberOfParishes = parishesGeoJsonFeatureCollection.features.length
  parishesGeoJsonFeatureCollection.features.forEach((parish, index) => {
    parish.properties.index = index
  })

  L.geoJson(parishesGeoJsonFeatureCollection, {
    style
  }).addTo(map)

  function style (feature) {
    return {
      weight: 2,
      opacity: 1,
      color: 'white',
      dashArray: '3',
      fillOpacity: 0.7,
      fillColor: getColor(feature.properties.index, numberOfParishes)
    }
  }

})

// get random color
function getColor (index, size) {
  const colors = {
    3: ['#8dd3c7', '#ffffb3', '#bebada'],
    4: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072'],
    5: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3'],
    6: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462'],
    7: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69'],
    8: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5'],
    9: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9'],
    10: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd'],
    11: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd', '#ccebc5'],
    12: ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd', '#ccebc5', '#ffed6f']
  }

  if (size < 3) {
    return colors[3][index]
  } else if (size <= 12) {
    return colors[size][index]
  } else {
    return colors[12][index % 12]
  }
}

How would you apply the Four Colour theorem or any suitable algorithm to avoid adjacent polygons to have the same color?

4
  • There's a qgis plugin that will generate a new attribute that you can style on.
    – Ian Turton
    Apr 22, 2023 at 14:52
  • @IanTurton thank you, but I need to Leaflet/Javascript Apr 22, 2023 at 15:28
  • Good luck with that then. This may help gis.stackexchange.com/a/298105/79
    – Ian Turton
    Apr 22, 2023 at 16:55
  • Search for graph coloring algorithm
    – FelixIP
    Apr 22, 2023 at 19:35

1 Answer 1

5

One possible way to color choropleth map so that no two adjacent features have the same color is to use topojson library (see https://github.com/topojson/topojson) to convert GeoJSON to TopoJSON and then use topojson.neighbors method to find neighbors for each feature and use this info when coloring features.

One simple way of implementing this could be the following code (which can be embellished to use more colors, but this is just proof of concept):

const colors = ['#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd', '#ccebc5', '#ffed6f']

function style (feature) {
  return {
    weight: 2,
    opacity: 1,
    color: 'white',
    dashArray: '3',
    fillOpacity: 0.7,
    fillColor: colors[feature.properties.colorIndex]
  }
}  

var map = L.map('map');
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  maxZoom: 19,
  attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);


fetch('https://geoapi.pt/municipio/lisboa/freguesias?json=1').then(r => r.json()).then(data => {
  const geojsons = data.geojsons;

  const bbox = geojsons.municipio.bbox;
  const corner1 = L.latLng(bbox[1], bbox[0]);
  const corner2 = L.latLng(bbox[3], bbox[2]);
  const bounds = L.latLngBounds(corner1, corner2);
  map.fitBounds(bounds);
  
  const parishesGeoJsonFeatureCollection = {
    type: 'FeatureCollection',
    features: geojsons.freguesias
  };

  var topoJSON = topojson.topology([parishesGeoJsonFeatureCollection], 1e4);
  var neighbors = topojson.neighbors(topoJSON.objects[0].geometries);
  
  var featureColors = [];
  parishesGeoJsonFeatureCollection.features.forEach((parish, index) => {
    for (var i = 0; i < colors.length; i++) {
      var found = false;
      for (var j = 0; j < neighbors[index].length; j++) {
        if (featureColors[neighbors[index][j]] == i) {
          found = true;
          break;
        }
      }
      if (!found) break;
    }
    featureColors[index] = i;
    parish.properties.colorIndex = i;
  });

  L.geoJson(parishesGeoJsonFeatureCollection, {
    style
  }).addTo(map);
})

This is then the result:

enter image description here

3
  • What a fantastic posting this is. Working code example for tricky problem. I salute you sir. Jun 2, 2023 at 6:58
  • Hmm... as much as I appreciate this code, it doesn't implement the 4 color theorum. If you reduce the number of colors to 4, it doesn't work. You have 13 colors, but logically, that might not be enough for certain topographies. Jun 4, 2023 at 7:30
  • @xpusostomos Of course you are right, it's just practical solution to given practical problem.
    – TomazicM
    Jun 4, 2023 at 8:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.