1

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);
7
  • Starting from the sample from leaflet website, I did add the filter inside the cities geojson, which is named geojson also, and then defined the function that selects only the cities of Florida state, just to know that it works, now I'm struggling to make that filter dinamically, from the state I clicked
    – CaD
    Commented Jun 3, 2023 at 21:47
  • I added your comment to the body of the question.
    – TomazicM
    Commented Jun 4, 2023 at 7:17
  • Please edit your question and the following info: (1) Are cities points or polygons? (2) Do states and cities both have properties VARNAME_3 and NAME_3?
    – TomazicM
    Commented Jun 4, 2023 at 19:00
  • done it @TomazicM
    – CaD
    Commented Jun 4, 2023 at 20:07
  • Is it possible to get hold of those two GeoJSONs somewhere?
    – TomazicM
    Commented Jun 4, 2023 at 20:58

1 Answer 1

3

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

1
  • Worked perfectly, thank you, I'll read the code to understand it.
    – CaD
    Commented Jun 5, 2023 at 18:42

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.