Skip to main content

Analyze data with Turf.js and Mapbox GL JS

Prerequisite
Familiarity with front-end development concepts.

This guide walks through the basics of Turf.js, a JavaScript library for spatial analysis and statistics, and how to use it to add spatial analysis to your Mapbox GL JS maps.

Let's say you are part of a team that manages health and safety for the libraries in Lexington, KY. One important part of your preparedness mandate is to know which hospital is closest to each library in case there's an emergency at one of your facilities. This example will walk you through making a map of libraries and hospitals; when a user clicks on a library, the map will show which hospital is nearest.

Getting started

There are a few resources you'll need to get started:

mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN ';
  • Mapbox GL JS. A Mapbox JavaScript API for building maps.
  • Turf.js. Turf is the JavaScript library you'll be using today to add analysis to your map.
  • Data. This example uses two data files: hospitals in Lexington, KY and libraries in Lexington, KY.
  • A text editor. You'll be writing HTML, CSS, and JavaScript.

Add structure

For this guide, include the latest versions of Mapbox GL JS and Turf.js. Add these libraries to your HTML file by copying the snippet below.

<link
href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<script src="https://unpkg.com/@turf/turf@^7/turf.min.js"></script>

Now, add your map element. First, in the body, create an empty div for your map.

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

Next, add some CSS to a style element in the head so your map takes up the width of the page.

body {
margin: 0;
padding: 0;
}

#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}

Initialize a map

Now that your page has some nice structure to it, go ahead and get a map on the page using Mapbox GL JS. This is where you'll need to use your access token. Add the following script inside the <body> after the HTML. Create a new mapboxgl.Map object called map. In this example, you'll be using the Mapbox Light template style, but you could also use a custom style made with Mapbox Studio. Finally, use center and zoom to set the view on Lexington, KY.

mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN ';
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v11', // stylesheet location
center: [-84.5, 38.05], // starting position
zoom: 12 // starting zoom
});

Sweet! Now your page has a map centered on Lexington, KY.

Load data

As mentioned above, this example uses two data files: libraries and hospitals in Lexington, KY, each of them is a GeoJSON FeatureCollection. In the next step, you'll add them to the style as layers and add code to make sure they're styled differently from each other.

First, store the GeoJSON objects as constants:

const hospitals = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties':
{
'Name': 'VA Medical Center -- Leestown Division',
'Address': '2250 Leestown Rd'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.539487, 38.072916]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'St. Joseph East',
'Address': '150 N Eagle Creek Dr'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.440434, 37.998757]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Central Baptist Hospital',
'Address': '1740 Nicholasville Rd'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.512283, 38.018918]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'VA Medical Center -- Cooper Dr Division',
'Address': '1101 Veterans Dr'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.506483, 38.02972]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Shriners Hospital for Children',
'Address': '1900 Richmond Rd'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.472941, 38.022564]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Eastern State Hospital',
'Address': '627 W Fourth St'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.498816, 38.060791]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Cardinal Hill Rehabilitation Hospital',
'Address': '2050 Versailles Rd'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.54212, 38.046568]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'St. Joseph Hospital',
'ADDRESS': '1 St Joseph Dr'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.523636, 38.032475]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'UK Healthcare Good Samaritan Hospital',
'Address': '310 S Limestone'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.501222, 38.042123]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'UK Medical Center',
'Address': '800 Rose St'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.508205, 38.031254]
}
}]
};
const libraries = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties':
{
'Name': 'Village Branch',
'Address': '2185 Versailles Rd'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.548369, 38.047876]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Northside Branch',
'ADDRESS': '1733 Russell Cave Rd'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.47135, 38.079734]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Central Library',
'ADDRESS': '140 E Main St'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.496894, 38.045459]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Beaumont Branch',
'Address': '3080 Fieldstone Way'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.557948, 38.012502]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Tates Creek Branch',
'Address': '3628 Walden Dr'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.498679, 37.979598]
}
},
{
'type': 'Feature',
'properties':
{
'Name': 'Eagle Creek Branch',
'Address': '101 N Eagle Creek Dr'
},
'geometry':
{
'type': 'Point',
'coordinates': [-84.442219, 37.999437]
}
}]
};

Next, load the GeoJSON objects as layers on the map:

map.on('load', () => {
map.addLayer({
id: 'hospitals',
type: 'symbol',
source: {
type: 'geojson',
data: hospitals
},
layout: {
'icon-image': 'hospital',
'icon-allow-overlap': true
},
paint: {}
});
map.addLayer({
id: 'libraries',
type: 'symbol',
source: {
type: 'geojson',
data: libraries
},
layout: {
'icon-image': 'library'
},
paint: {}
});
});

Note that hospitals layer and libraries layer are added to the map after the map by wrapping them in map.on('load', () => {});

Add interactivity

Your map users will want to know the names of the libraries and hospitals displayed on the map, so next you'll add some popups. For this map, add some popups to these features that appear when the user hovers over the markers. Insert this inside map.on('load', () => {});, after the hospitals and libraries layers.

const popup = new mapboxgl.Popup();

map.on('mousemove', (event) => {
const features = map.queryRenderedFeatures(event.point, {
layers: ['hospitals', 'libraries']
});
if (!features.length) {
popup.remove();
return;
}
const feature = features[0];

popup
.setLngLat(feature.geometry.coordinates)
.setHTML(feature.properties.Name)
.addTo(map);

map.getCanvas().style.cursor = features.length ? 'pointer' : '';
});

Next you'll make your map of the libraries and hospitals in Lexington even more useful by adding some analysis.

Add Turf

Turf is a JavaScript library for adding spatial and statistical analysis to your web maps. It contains many commonly-used GIS tools -- like buffer, union, and merge -- as well as statistical analysis functions -- like sum, median, and average.

Fortunately, Turf has some functions that will help you out here! You're going to update your map so that clicking on a library will show users which hospital is closest to that library.

As a first step, you'll need to add a new source with the id 'nearest-hospital' when the map is loaded.

map.addSource('nearest-hospital', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});

You’ll make an “event handler” for when someone clicks on a library marker. When an event occurs, like a click on a marker, the event handler tells the map what to do in response. Before, you created event handlers for hovering over hospital and library markers; now you’re going to make one for clicks.

This is the structure of an event handler; anything you want to happen on click goes inside of the braces {}. In this case, you want to use Turf to identify the nearest hospital to the clicked library and make that marker larger to identify it.

map.on('click', (event) => {
// Return any features from the 'libraries' layer whenever the map is clicked
const libraryFeatures = map.queryRenderedFeatures(event.point, {
layers: ['libraries']
});
if (!libraryFeatures.length) {
return;
}
const libraryFeature = libraryFeatures[0];

// Using Turf, find the nearest hospital to library clicked
const nearestHospital = turf.nearest(libraryFeature, hospitals);

// If a nearest library is not found, return early
if (nearestHospital === null) return;
// Update the 'nearest-library' data source to include
// the nearest library
map.getSource('nearest-hospital').setData({
type: 'FeatureCollection',
features: [nearestHospital]
});

// Create a new circle layer from the 'nearest-library' data source
if (map.getLayer('nearest-hospital')) {
map.removeLayer('nearest-hospital');
}
map.addLayer(
{
id: 'nearest-hospital',
type: 'circle',
source: 'nearest-hospital',
paint: {
'circle-radius': 12,
'circle-color': '#486DE0'
}
},
'hospitals'
);
});

Finished product

Nicely done! You have successfully created a map that calculates which hospital is closest to each library dynamically. Your finished HTML file should look like this:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Analyze data with Turf.js and Mapbox GL JS</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<script src="https://unpkg.com/@turf/turf@^7/turf.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
}

#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = '{{MAPBOX_ACCESS_TOKEN}}';
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v11', // stylesheet location
center: [-84.5, 38.05], // starting position
zoom: 12 // starting zoom
});

const hospitals = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {
'Name': 'VA Medical Center -- Leestown Division',
'Address': '2250 Leestown Rd'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.539487, 38.072916]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'St. Joseph East',
'Address': '150 N Eagle Creek Dr'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.440434, 37.998757]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Central Baptist Hospital',
'Address': '1740 Nicholasville Rd'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.512283, 38.018918]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'VA Medical Center -- Cooper Dr Division',
'Address': '1101 Veterans Dr'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.506483, 38.02972]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Shriners Hospital for Children',
'Address': '1900 Richmond Rd'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.472941, 38.022564]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Eastern State Hospital',
'Address': '627 W Fourth St'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.498816, 38.060791]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Cardinal Hill Rehabilitation Hospital',
'Address': '2050 Versailles Rd'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.54212, 38.046568]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'St. Joseph Hospital',
'ADDRESS': '1 St Joseph Dr'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.523636, 38.032475]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'UK Healthcare Good Samaritan Hospital',
'Address': '310 S Limestone'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.501222, 38.042123]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'UK Medical Center',
'Address': '800 Rose St'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.508205, 38.031254]
}
}
]
};
const libraries = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {
'Name': 'Village Branch',
'Address': '2185 Versailles Rd'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.548369, 38.047876]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Northside Branch',
'ADDRESS': '1733 Russell Cave Rd'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.47135, 38.079734]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Central Library',
'ADDRESS': '140 E Main St'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.496894, 38.045459]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Beaumont Branch',
'Address': '3080 Fieldstone Way'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.557948, 38.012502]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Tates Creek Branch',
'Address': '3628 Walden Dr'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.498679, 37.979598]
}
},
{
'type': 'Feature',
'properties': {
'Name': 'Eagle Creek Branch',
'Address': '101 N Eagle Creek Dr'
},
'geometry': {
'type': 'Point',
'coordinates': [-84.442219, 37.999437]
}
}
]
};

map.on('load', () => {
map.addLayer({
id: 'hospitals',
type: 'symbol',
source: {
type: 'geojson',
data: hospitals
},
layout: {
'icon-image': 'hospital',
'icon-allow-overlap': true
},
paint: {}
});

map.addLayer({
id: 'libraries',
type: 'symbol',
source: {
type: 'geojson',
data: libraries
},
layout: {
'icon-image': 'library',
'icon-allow-overlap': true
},
paint: {}
});

map.addSource('nearest-hospital', {
type: 'geojson',
data: {
'type': 'FeatureCollection',
'features': []
}
});

const popup = new mapboxgl.Popup();

map.on('mousemove', (event) => {
const features = map.queryRenderedFeatures(event.point, {
layers: ['hospitals', 'libraries']
});
if (!features.length) {
popup.remove();
return;
}

const feature = features[0];

popup
.setLngLat(feature.geometry.coordinates)
.setHTML(feature.properties.Name)
.addTo(map);

map.getCanvas().style.cursor = features.length ? 'pointer' : '';
});

map.on('click', (event) => {
const libraryFeatures = map.queryRenderedFeatures(event.point, {
layers: ['libraries']
});
if (!libraryFeatures.length) {
return;
}

const libraryFeature = libraryFeatures[0];

const nearestHospital = turf.nearest(libraryFeature, hospitals);

if (nearestHospital === null) return;
map.getSource('nearest-hospital').setData({
'type': 'FeatureCollection',
'features': [nearestHospital]
});

if (map.getLayer('nearest-hospital')) {
map.removeLayer('nearest-hospital');
}

map.addLayer(
{
id: 'nearest-hospital',
type: 'circle',
source: 'nearest-hospital',
paint: {
'circle-radius': 12,
'circle-color': '#486DE0'
}
},
'hospitals'
);
});
});
</script>
</body>
</html>

Next steps

Turf has dozens of tools that would help extend this map even further. For example, you could also use turf.distance to determine not only which hospital is closest, but exactly how far away it is. The possibilities are virtually endless with Turf.js!

Was this page helpful?