This is Part Three of a four part tutorial on how to make a data-driven map using JavaScript.

The end result is an web map visualising electric vehicle chargepoint locations:

Click map to view live version

In the previous part we loaded a dataset from the UK's National Chargepoint Registry using D3 and displayed each location on the map:

In this part we'll style the markers in a data-driven fashion. Charging points which are in service will be coloured according to the controller (e.g. Charge Your Car, Source London, Chargemaster) while out of service charging points will be displayed in a semi-transparent grey.

Getting started

Open your work from Part Two (or use Part Two's pen).

Outline

The steps you'll take are:

  • refactoring (to make the code more modular)
  • add a colour look-up object
  • style the 'in service' charging points
  • style the 'out of service' charging points
  • adjust the basemap opacity to make the markers stand out

Refactoring

We'll start by refactoring the code to make it more modular. We'll create functions for:

  • initialising the map
  • requesting the data

Add a function for intialising the map

Make the map variable a global by declaring it at the top of the JavaScript:

var data, map;

Add a function called initMap() at the top of the file (after the var data, map; declaration) and move the map initialisation code into this function:

function initMap() {
map = L.map('map');

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

map.setView([55, -2], 6);
}

Make sure you remove the var from before map = L.map('map');

Add a call to initMap() at the bottom of the JavaScript:

initMap();

The map should re-appear when the initMap() call is added.

Add a function for requesting the CSV

Add a function getData() below the initMap function and move the data request code into it:

function getData() {
d3.csv('https://s3-us-west-2.amazonaws.com/s.cdpn.io/2814973/charge-points-slim.csv')
.then(function(csv) {
data = csv;
addMarkers();
});
}

Add a call to getData() at the bottom of the JavaScript:

getData();

Your code should now look like:

var data, map;

function initMap() {
map = L.map('map');

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

map.setView([55, -2], 6);
}

function getData() {
d3.csv('https://s3-us-west-2.amazonaws.com/s.cdpn.io/2814973/charge-points-slim.csv')
.then(function(csv) {
data = csv;
addMarkers();
});
}

function addMarkers() {
data.forEach(function(d) {
var marker = L.circleMarker([+d.latitude, +d.longitude]);
marker.addTo(map);
})
}

initMap();
getData();

Can you see the code has a clearer structure now? There's three functions initMap(), getData() and addMarkers() whose names describe what they do. initMap() and getData() are called at the end while addMarkers() is called when the CSV arrives. Modularising your code becomes more important as features are added and the application grows.

Check that your map is still working - you should be able to see the map with the blue circular markers overlaid.

Add a colour key object

We'll now specify an object that defines colours for the most common controllers (such as Charge Your Car and Source London). Add this near the top of the code, above initMap():

var colorScale = {
'Charge Your Car': '#B6A6CA',
'Source London': '#6D98BA',
'POD Point': '#FC814A',
'Chargemaster (POLAR)': '#D282A6',
'Ecotricity (Electric Highway)': '#64B6AC',
'ChargePoint Services': '#EEC170',
'ChargePlace Scotland': '#6D98BA',
'APT': '#AF4319'
};

The colours were chosen by hand. Feel free to change them!

Style the 'in service' charging points

Towards the bottom of your JavaScript is the addMarkers() function which iterates through the data and adds a circleMarker for each charging station.

The markers can be styled using the .setStyle() function. This function accepts an object containing key value pairs.

Common keys include:

Key Description Example values
radius Circle radius 2, 5, 10
fillColor Circle colour red, #123
color Circle's outline colour red, #123
fillOpacity Circle opacity 0, 0.4, 1
weight Circle's outline thickness 0, 0.1, 5

You can see other options in Leaflet's documentation.

Let's start by changing the radius of the markers. Just before marker.addTo(map) add:

    marker.setStyle({
radius: 1
});

Your addMarkers() function should look like:

function addMarkers() {
data.forEach(function(d) {
var marker = L.circleMarker([+d.latitude, +d.longitude]);

marker.setStyle({
radius: 1
});

marker.addTo(map);
})
}

and the map should now look like:

We actually want our circles to be larger so that they're easier to click on so increase the radius to 8:

    marker.setStyle({
radius: 8
});

Now let's set the colour of the marker using the colorScale object we added earlier. First add a variable color that stores the colour of the current marker:

    ...
var marker = L.circleMarker([+d.latitude, +d.longitude]);
var color = colorScale[d.deviceControllerName] || '#aaa';
...

Notice color fallbacks to #aaa if colorScale doesn't contain the controller we're looking for

Use the variable color to set the fillColor:

    var color = colorScale[d.deviceControllerName] || '#aaa';

marker.setStyle({
radius: 8,
fillColor: color
});

Hmmm... the markers don't look great do they?!

The markers still have a thick blue outline (this is the default) and the default fill opacity is 0.2. Let's change these:

    marker.setStyle({
radius: 8,
fillOpacity: 1,
fillColor: color,
color: '#ddd',
weight: 0.25
});

fillOpacity sets the opacity of the circle, color sets the colour of the circle's outline and weight sets the thickness of the circle's outline

The markers look much better now:

Now add a variable inService that determines whether the current charging point is in service:

    ...
var marker = L.circleMarker([+d.latitude, +d.longitude]);
var inService = d.chargeDeviceStatus === 'In service';
var color = colorScale[d.deviceControllerName] || '#aaa';
...

and wrap the styling code in an if statement so that only the 'In service' markers are styled:

    if (inService) {
marker.setStyle({
radius: 8,
fillColor: color,
fillOpacity: 1,
color: '#ddd',
weight: 0.25
});
}

Now the 'In service' markers are coloured and the 'Out of service' ones revert to the default styling. Here's how London looks:

Style the 'Out of service' charging points

We'll style the 'out of service' charging points in a similar manner. First add an else block:

    if (inService) {
...
} else {
}

and add an empty .setStyle call:

    if (inService) {
...
} else {
marker.setStyle({
});
}

If a charging point is 'out of service' we'll colour the circle light grey, make it semi-transparent, make its stroke medium grey and it stroke a bit thicker:

    if (inService) {
...
} else {
marker.setStyle({
radius: 8,
fillColor: '#eee',
fillOpacity: 0.5,
color: '#777',
weight: 1
});
}

Now the map should look like:

Adjust basemap opacity

Finally we'll make the basemap a bit lighter to help the circles stand out.

In the CSS panel add an opacity rule to the map's filter:

.leaflet-container .leaflet-tile-pane img {
filter: grayscale(1)opacity(0.3);
}

This is a subtle change so you'll need to look carefully to see the difference! Hopefully you'll agree it helps the circles stand out.

Before lightening the basemap:

And after:

The code

See the Pen UK charging points map (Step 3) by Peter Cook (@createwithdata) on CodePen.

Wrapping up

In this part you styled the charging points in a data-driven manner. The colour of each 'in service' charging point was set according to its controller and 'out of service' points were set to a semi-transparent grey.

In the next part you'll add a pop-up which shows information on a hovered charging point:

a legend showing the colour of each controller:

and a title and link to the data:

Hope to see you in the next part!