Legendary Leaflet Legends
I recently had a request to make some simple interactive web maps to show street lighting faults - columns gone "dark" - and marked parking bays on roads.
Easy.
Then, to make the maps a little nicer, I thought let's style the data using the available attributes. Some minor duckduckgoing and I had bays coloured by type and lighting columns by status.
Nice.
But if you style your data then you need a legend on the map so users know what's going down. Some head scratching and more searching and reading up on Javascript syntax and, voila!, a legend on the map. My work here is done. For now.
So how does it all work?
Styling the data
I am loading in my data direct from PostgreSQL/PostGIS as GeoJSON. One of the attributes pulled in is a status field and I used this to set the colour of the features on the map. In the new GeoJSON object I created a style element that read the status attribute of the feature and then applied a colour based on the value. The feature was then styled as a circle marker and the popup defined for each one.
// Define GeoJSON data
lampfaultsdata = new L.geoJson(null, {
style: function(feature) {
switch (feature.properties.faultstatus) {
case 'Logged': return {color: "#e41a1c"};
case 'Repaired': return {color: "#4daf4a"};
case 'Cancelled': return {color: "#0000ff"};
}},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {radius: 6, fillOpacity: 0.80});
},
onEachFeature: function (feature, layer) {
if (feature.properties) {
var content = '<table border="1" style="border-collapse:collapse;" cellpadding="2">' +
'<tr>' + '<th>Name</th>' + '<td><b>' + feature.properties.assetname + '</b></td>' + '</tr>' +
'<tr>' + '<th>Date reported</th>' + '<td>' + feature.properties.faultdate + '</td>' + '</tr>' +
'<tr>' + '<th>Fault Status</th>' + '<td>' + feature.properties.faultstatus + '</td>' + '</tr>' +
'<tr>' + '<th>View Map</th>' + '<td>' + feature.properties.lamplocation + '</td>' + '</tr>' +
'<table>';
layer.bindPopup(content);
}
}
});
Creating the legend
The Leaflet docs outline the process for creating a legend and this is adapted the choropleth example. For the lighting columns there are only two possible values and so the getColour function is simple. Check the status and if it is 'Logged' then set it to red else it is green. Create the legend control and set the position. Create the legend div and build it then add it to the map.
function getColour(d) {
switch (d) {
case 'Logged': return '#e41a1c';
case 'Repaired': return '#4daf4a';
case 'Cancelled': return '#0000ff';
default: return '#fff';
}
};
var legend = L.control({position: 'bottomright'});
legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'info legend');
faultstatus = ['Logged', 'Repaired', 'Cancelled'];
// loop through the status values and generate a label with a coloured square for each value
for (var i = 0; i < faultstatus.length; i++) {
div.innerHTML +=
'<i class="circle" style="background:' + getColour(faultstatus[i]) + '"></i> ' + (faultstatus[i] ? faultstatus[i] + '<br>' : '+');
}
return div;
};
legend.addTo(map);
Styling the legend
By default the legend is created with square blocks of colour but if you want the symbol on the legend to match the symbol on the map - circles in this case - then you need to edit your CSS file. I added the following to my style.css to create a "circle" class.
.legend {
text-align: left;
line-height: 18px;
color: #555;
}
.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.7;
}
.legend .circle {
border-radius: 50%;
width: 10px;
height: 10px;
margin-top: 8px;
}
To style the marked bays I had the following in my Leaflet map.
Style the GeoJSON:
var disabledbaysdata = new L.geoJson(null, {
style: function(feature) {
switch (feature.properties.status) {
case 1: return {color: "#e41a1c"};
case 2: return {color: "#377eb8"};
case 3: return {color: "#4daf4a"};
case 4: return {color: "#984ea3"};
case 5: return {color: "#ff7f00"};
case 6: return {color: "#ffff33"};
}
},
onEachFeature: function (feature, layer) {
if (feature.properties) {
var content = '<table border="1" style="border-collapse:collapse;" cellpadding="2">' +
'<tr>' + '<th>Bay Ref No</th>' + '<td>' + feature.properties.refno + '</td>' + '</tr>' +
'<tr>' + '<th>Road</th>' + '<td>' + feature.properties.road_name + '</td>' + '</tr>' +
'<tr>' + '<th>Status</th>' + '<td>' + feature.properties.status + '</td>' + '</tr>' +
'<tr>' + '<th>Date</th>' + '<td>' + feature.properties.date_order + '</td>' + '</tr>' +
'<tr>' + '<th>View Photo</th>' + '<td>' + '<a href="'+ feature.properties.photo_link +'"target="_blank">This will open in a new Window</a></td>' + '</tr>' +
'<table>';
layer.bindPopup(content);
}
}
});
To build and style the legend:
function getColor(d) {
switch(d) {
case 1: return "#e41a1c";
case 2: return "#377eb8";
case 3: return "#4daf4a";
case 4: return "#984ea3";
case 5: return "#ff7f00";
case 6: return "#ffff33";
default: return "#ffff33";
}
};
// FIRST LEGEND FOR TESTING
var Legend = L.control({position: 'bottomright'});
Legend.onAdd = function (map) {
var legdiv = L.DomUtil.create('div', 'info legend'),
status = [1, 2, 3, 4, 5, 6],
labels = ['On ground w/o TRO', 'On ground w TRO', 'Not on road', 'To be removed', 'Removed', 'Prospective'];
// loop through our status intervals and generate a label with a coloured square for each interval
for (var i = 0; i < status.length; i++) {
legdiv.innerHTML +=
'<i style="background:' + getColor(status[i]) + '"></i> ' + (status[i] ? labels[i] + '<br>' : '+');
}
return legdiv;
};
Legend.addTo(map);