Digital Geography

23. June 2016

Filter Leaflet Maps with a Slider

If you create maps you always need to ask yourself: how can I make it as easy as possible to read and still have anything I need in my map… or in short: reduction and abstraction. There are different approaches out there when it comes to web maps. Let me show you how to reduce the number of map elements with a slider in leaflet to filter your data interactively.

The Initial Data

Let’s start with a very basic map. It consists of a point layer and a clustered layer which is on the same map. A simple example map looks like this:
Here’s the source code I will work with:
<!DOCTYPE html>
<html>
	<head>
		<title>leaflet basic example</title>
		<meta charset="utf-8" />
		<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.css" />
		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.5.0/MarkerCluster.Default.css" />
		<script src="http://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.js"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.5.0/leaflet.markercluster.js"></script>
		<script src="exp_popplaces.js"></script>
		<script src="exp_ne10mparksandprotectedlandspoint.js"></script>
		<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />

<style>
			body {
				padding: 0;
				margin: 0;
			}
			html, body, #map {
				height: 100%;
			}
		</style>

	</head>
	<body>

<div id="map"></div>


		<script>
		var map = L.map('map', {
			zoomControl:true, maxZoom:19
		}).fitBounds([[-22.6665544576,-206.634809944],[100.520856024,-21.8536942211]]);
		var basemap_0 = L.tileLayer('http://{s}.www.toolserver.org/tiles/bw-mapnik/{z}/{x}/{y}.png');
		basemap_0.addTo(map);
		function pop_prot(feature, layer) {
			var popupContent = 'name: ' + String(feature.properties['unit_name']);
			layer.bindPopup(popupContent);
		}
		function protecland_marker(feature, latlng) {
			return L.circleMarker(latlng, {
				radius: 8.0,
				fillColor: '#11fe00',
				color: '#000000',
				weight: 1,
				opacity: 1.0,
				fillOpacity: 0.8
			})
		}
		var protecland = new L.geoJson(exp_ne10mparksandprotectedlandspoint,{
			onEachFeature: pop_prot,
			pointToLayer: protecland_marker
		});
		protecland.addTo(map);
		function pop_popplaces(feature, layer) {
			var popupContent = 'name: ' + String(feature.properties['name']) + '<br>pop_max: ' + String(feature.properties['pop_max']);
			layer.bindPopup(popupContent);
		}
		function popplaces_marker(feature, latlng) {
			return L.circleMarker(latlng, {
				radius: 8.0,
				fillColor: '#ff0000',
				color: '#000000',
				weight: 1,
				opacity: 1.0,
				fillOpacity: 0.8
			})
		}
		var popplaces = new L.geoJson(exp_popplaces,{
			onEachFeature: pop_popplaces,
			pointToLayer: popplaces_marker
		});
		var cluster_popplaces= new L.MarkerClusterGroup({showCoverageOnHover: false});
		cluster_popplaces.addLayer(popplaces);
		cluster_popplaces.addTo(map);
		var baseMaps = {'OSM Black & White': basemap_0};
		controler = L.control.layers(baseMaps,{"places": cluster_popplaces,"protected land points": protecland},{collapsed:false}).addTo(map);
	</script>
</body>
</html>
Used example files can be found here and here. The places have a field called pop_max which I will use to filter! In the second step I will change the filter attribute and show the example for the non clustered protected land points. Overall this will be a 3 step process:
  1. create the slider/input fields in html
  2. connect those with the map
  3. change for non clustered points

The Slider and the Inputs

I am working in this example with the noUiSlider:

noUiSlider example image

So first I need to create the html for the slider and the input fields. For this simple example I will place it in the middle above the map by first creating the div container and the input fields. I am using the minimum and maximum data from the used data.
<div id="slider" style="top: 0px; right: 1px; margin: 10px 25px;"></div>
<div style="margin-right: auto; margin-left: auto; width: 90%; margin-bottom: 10px; text-align: center;">
<input type="number" min='1' max='35675999' id="input-number-min">
<input type="number" min='2' max='35676000' id="input-number-max">
</div>
Make sure, you’ve added the src files for the js and css in your head of the html file:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/8.5.1/nouislider.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/8.5.1/nouislider.min.js"></script>
As I have now all the “magic” available I will create the slider inside our div:
var slidervar = document.getElementById('slider');
noUiSlider.create(slidervar, {
	connect: true,
	start: [ 1, 35676000 ],
	range: {
		min: 1,
		max: 35676000
	}
});
Now there is a nice slider:

slider above our map

Let’s populate the input fields with some data:
document.getElementById('input-number-min').setAttribute("value", 1);
document.getElementById('input-number-max').setAttribute("value", 35676000);
Yet there is something more to do. I also need the possibility to update the slider via the input fields:
var inputNumberMin = document.getElementById('input-number-min');
var inputNumberMax = document.getElementById('input-number-max');
inputNumberMin.addEventListener('change', function(){
	slidervar.noUiSlider.set([this.value, null]);
});
inputNumberMax.addEventListener('change', function(){
	slidervar.noUiSlider.set([null, this.value]);
});

Connect Slider with the Data to Filter

Until now, the slider stands for himself and works with the input fields. Now I will connect the values in th input fields with the slider so the fields are updated with the movement of the slider. Those values will also be used as the min and max values of the filter.
slidervar.noUiSlider.on('update', function( values, handle ) {
	//handle = 0 if min-slider is moved and handle = 1 if max slider is moved
	if (handle==0){
		document.getElementById('input-number-min').value = values[0];
	} else {
		document.getElementById('input-number-max').value =  values[1];
	}
//we will definitely do more here...wait
})
This will show the current values from the slider in the input fields. I need to update the layers and filter the underlying json. For filtering purpose it’s handy to get the min/max value in a distinct variable:
rangeMin = document.getElementById('input-number-min').value;
rangeMax = document.getElementById('input-number-max').value;
I will remove the current filtered layer from the map and reload it after the slider was updated with the filtered layer:
//first let's clear the layer:
cluster_popplaces.clearLayers();
//and repopulate it
popplaces = new L.geoJson(exp_popplaces,{
	onEachFeature: pop_popplaces,
		filter:
			function(feature, layer) {
				 return (feature.properties.pop_max <= rangeMax) && (feature.properties.pop_max >= rangeMin);
			},
	pointToLayer: popplaces_marker
})
//and back again into the cluster group
cluster_popplaces.addLayer(popplaces);
And now it’s done and works like a charm!

Change for non Clustered Points

The adoption for non clustered points is quite easy: Instead of treating the non clustered layer itself I packed it into a featureGroup and this makes it to work like with the cluster by removing the layer from the featureGroup by clearLayers(), creating the filtered version and adding it back to the featureGroup. Here is the example:
Keep in mind that I also changed the input settings to accept integers only as we filter on integer values.
  • LauraLou

    Thank you for the tutorial, that’s exactly what I was looking for!