Leaflet – Dynamically Change Layers on Feature Click

leaflet

I need to dynamically change layers on a leaflet map on click, having two GeoJSON files, one for states and other for cities, like a dashboard.

When clicking a state it should zoom to the clicked state, remove states layer and show only the cities that are within that state, which could be done filtering by a attribute (STATE_NAME in case) in cities geojson that refers to the state it belongs.

I'm starting from the sample code from Leaflet tutorial of choropleth map https://leafletjs.com/examples/choropleth/ which already have the zoom on click function.

Starting from the sample from leaflet website, I managed to add a filter inside the cities geojson and then defined a function that selects only the cities of a specific state, just to know that it works, now I'm struggling to make that filter work dinamically, from the state I clicked. I dont know what should I aim to achieve this, if its using layer groups or something else.

Both cities and states are polygons. Cities geojson have the attributes CITY_NAME, STATE_NAME and VARNAME as a numeric value used by cloropeth map. States geojson have also STATE_NAME attribute.

I uploaded both geojson and the html file that works with them at the following Gist: https://gist.github.com/caduguedess/86caa8f04fe9335e6ef4dc62cc4f6c0b

   var states = L.geoJson(states, {
        style,
        onEachFeature,
    }).addTo(map);

    /* citiesData */
    var geojson = L.geoJson(cities, {
        style,
        onEachFeature,
/*      filter: selection
 */ }).addTo(map);

/*  function selecao(feature) {
        if (feature.properties.CITY_NAME === "Malanje") return true
    } */
    
    function resetHighlight(e) {
        geojson.resetStyle(e.target);
        info.update();
    }

    function zoomToFeature(e) {
        map.fitBounds(e.target.getBounds());
    }

    function onEachFeature(feature, layer) {
        layer.on({
            mouseover: highlightFeature,
            mouseout: resetHighlight,
            click: zoomToFeature
        });
    }

    map.attributionControl.addAttribution('Population data &copy; <a href="http://census.gov/">US Census Bureau</a>');


    const legend = L.control({position: 'bottomright'});

    legend.onAdd = function (map) {

        const div = L.DomUtil.create('div', 'info legend');
        const grades = [0, 10, 20, 50, 100, 200, 500, 1000];
        const labels = [];
        let from, to;

        for (let i = 0; i < grades.length; i++) {
            from = grades[i];
            to = grades[i + 1];

            labels.push(`<i style="background:${getColor(from + 1)}"></i> ${from}${to ? `&ndash;${to}` : '+'}`);
        }

        div.innerHTML = labels.join('<br>');
        return div;
    };

    legend.addTo(map);

Best Answer

There are a few things that have to be taken care of to make it work like you want:

  • Since states and cities layers are different in nature there must be different mouseover, mouseout and click processing function for each of them.
  • There must be also different style for states and cities layers.
  • info.update call result must be different for states in cities layer, so additional parameter is needed to differentiate between those two.
  • Cities layer features should be filtered by currently selected state name. Since filter is static, on each state change layer has to be cleared and data loaded again.

Complete code could then look something like this:

const map = L.map('map').setView([-12, 17], 5);

const tiles = 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);

const info = L.control();

info.onAdd = function (map) {
  this._div = L.DomUtil.create('div', 'info');
  this.update(1);
  return this._div;
};

info.update = function (infoType, props) {
  if (infoType == 1) {
    const contents = props ? 
      'State: <b>' + props.STATE_NAME + '</b><br>Click for state cities' : 'Hover mouse over a state';
    this._div.innerHTML = `<h4>Angola states</h4>${contents}`;
    }
  else {
    const contents = props ? `<b>${props.VARNAME} Value in city <b>${props.CITY_NAME} ` : 'Hover mouse over a city';
    this._div.innerHTML = `<h4>Sample of value in Angola country</h4>${contents}`;
  }
};

info.addTo(map);

function getColor(d) {
  return d > 1000 ? '#800026' :
    d > 500  ? '#BD0026' :
    d > 200  ? '#E31A1C' :
    d > 100  ? '#FC4E2A' :
    d > 50   ? '#FD8D3C' :
    d > 20   ? '#FEB24C' :
    d > 10   ? '#FED976' : '#FFEDA0';
}

function stateStyle(feature) {
  return {
    weight: 2,
    opacity: 1,
    color: 'blue',
    dashArray: '3',
    fillOpacity: 0.1,
    fillColor: 'blue'
  };
}

function highlightState(e) {
  const layer = e.target;

  layer.setStyle({
    weight: 5,
    color: 'blue',
    dashArray: '',
    fillColor: 'blue',
    fillOpacity: 0.3
  });

  layer.bringToFront();

  info.update(1, layer.feature.properties);
}

function resetState(e) {
  statesLayer.resetStyle(e.target);
  info.update(1);
}

var selectedState = '';

function zoomToState(e) {
  var layer = e.target;
  map.fitBounds(layer.getBounds());
  selectedState = layer.feature.properties.STATE_NAME;
  citiesLayer.clearLayers();
  citiesLayer.addData(cities);
}

function onEachState(feature, layer) {
  layer.on({
    mouseover: highlightState,
    mouseout: resetState,
    click: zoomToState
  });
}

var statesLayer = L.geoJson(states, {
  style: stateStyle,
  onEachFeature: onEachState,
}).addTo(map);

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

function cityFilter(feature) {
  return (feature.properties.STATE_NAME === selectedState);
}

function highlightCity(e) {
  const layer = e.target;

  layer.setStyle({
    weight: 5,
    color: 'blue',
    dashArray: '',
    fillOpacity: 0.7
  });

  layer.bringToFront();

  info.update(2, layer.feature.properties);
}

function zoomToCity(e) {
  map.fitBounds(e.target.getBounds());
}

function resetCity(e) {
  citiesLayer.resetStyle(e.target);
  info.update();
}

function onEachCity(feature, layer) {
  layer.on({
    mouseover: highlightCity,
    mouseout: resetCity,
    click: zoomToCity
  });
}

var citiesLayer = L.geoJson(cities, {
  style: cityStyle,
  onEachFeature: onEachCity,
  filter: cityFilter
}).addTo(map);

map.attributionControl.addAttribution('Population data &copy; <a href="http://census.gov/">US Census Bureau</a>');

const legend = L.control({position: 'bottomright'});

legend.onAdd = function (map) {
  const div = L.DomUtil.create('div', 'info legend');
  const grades = [0, 10, 20, 50, 100, 200, 500, 1000];
  const labels = [];
  let from, to;
  for (let i = 0; i < grades.length; i++) {
    from = grades[i];
    to = grades[i + 1];
    labels.push(`<i style="background:${getColor(from + 1)}"></i> ${from}${to ? `&ndash;${to}` : '+'}`);
  }
  div.innerHTML = labels.join('<br>');
  return div;
};

legend.addTo(map);

Here is sample screenshot:

enter image description here