All docschevron-rightHelpchevron-rightarrow-leftTutorialschevron-rightAnalyze data with Turf.js and Mapbox.js

Analyze data with Turf.js and Mapbox.js

Intermediate
JavaScript
Prerequisite

Familiarity with front-end development concepts.

alert
LEGACY

Mapbox.js is no longer in active development. To learn more about our newer mapping tools see Analyze data with Turf.js and Mapbox GL JS.

This guide walks you through how to use Turf.js, a JavaScript library for spatial analysis and statistics.

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:

  • A tileset ID. An ID points to a unique map you have created on Mapbox.
  • An access token. The token is used to associate a map with your account:
L.mapbox.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
  • Mapbox.js. The 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, you will include the latest versions of Mapbox.js and Turf.js. Create a new HTML file in your text editor, and add these libraries to the head by copying the snippet below:

<link href='https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.css' rel='stylesheet' />
<script src='https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.js'></script>
<script src='https://api.mapbox.com/mapbox.js/plugins/turf/v3.0.11/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.js.

You'll create a L.mapbox.map object called map and use setView to center the map on Lexington, KY. This is where you'll need to use your access token and tileset ID. Add the following script inside the <body> after the HTML:

L.mapbox.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const map = L.mapbox
  .map('map')
  .setView([38.05, -84.5], 12)
  .addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/light-v11'));
map.scrollWheelZoom.disable();

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

book
NOTE

There is an additional line in the code above that turns off scroll zoom on the map. This is optional.

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 map as L.mapbox.featureLayer objects and add a little code to make sure they're styled differently from each other. Also, you'll make sure that your map view contains all the points by fitting the map bounds to your features.

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:

// Add marker color, symbol, and size to hospital GeoJSON
for (const hospital of hospitals.features) {
  hospital.properties['marker-color'] = '#DC143C';
  hospital.properties['marker-symbol'] = 'hospital';
  hospital.properties['marker-size'] = 'small';
}

// Add marker color, symbol, and size to library GeoJSON
for (const library of libraries.features) {
  library.properties['marker-color'] = '#4169E1';
  library.properties['marker-symbol'] = 'library';
  library.properties['marker-size'] = 'small';
}

const map = L.mapbox
  .map('map')
  .setView([38.05, -84.5], 12)
  .addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/light-v11'));
map.scrollWheelZoom.disable();

const hospitalLayer = L.mapbox.featureLayer(hospitals).addTo(map);
const libraryLayer = L.mapbox.featureLayer(libraries).addTo(map);

// When map loads, zoom to libraryLayer features
map.fitBounds(libraryLayer.getBounds());

Note that hospitalLayer and libraryLayer are defined after you create your map object; you must define them in this order to make sure they can be added to the map.

Alternatively, you can save the GeoJSON as one or two .geojson files and load the files on to the map. If you do this, you will need to run this application from a local web server otherwise, you will receive a Cross-origin Resource Sharing (CORS) error.

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 into your script after you've created hospitalLayer and libraryLayer:

// Bind a popup to each feature in hospitalLayer and libraryLayer
hospitalLayer
  .eachLayer((layer) => {
    layer.bindPopup(`<strong>${layer.feature.properties.Name}</strong>`, {
      closeButton: false
    });
  })
  .addTo(map);

libraryLayer
  .eachLayer((layer) => {
    layer.bindPopup(layer.feature.properties.Name, { closeButton: false });
  })
  .addTo(map);

// Open popups on hover
libraryLayer.on('mouseover', (event) => event.layer.openPopup());
hospitalLayer.on('mouseover', (event) => event.layer.openPopup());

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

Use 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, 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.

libraryLayer.on('click', (e) => {});

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.

libraryLayer.on('click', (e) => {
  // Get the GeoJSON from libraryFeatures and hospitalFeatures
  const libraryFeatures = libraryLayer.getGeoJSON();
  const hospitalFeatures = hospitalLayer.getGeoJSON();

  // Using Turf, find the nearest hospital to library clicked
  const nearestHospital = turf.nearest(e.layer.feature, hospitalFeatures);

  // Change the nearest hospital to a large marker
  nearestHospital.properties['marker-size'] = 'large';

  // Add the new GeoJSON to hospitalLayer
  hospitalLayer.setGeoJSON(hospitalFeatures);
});

Excellent! This is almost ready to go.

Add finishing touches

When a user clicks on a library, the nearest hospital gets larger. But when the user click on a different library, or on the map, the previously-nearest hospital doesn't go back to a small marker again. To address this, add some code to make the popup for the nearest hospital open up when it gets larger.

Add the following function before the click event handler.

// reset marker size to small
function reset() {
  const hospitalFeatures = hospitalLayer.getGeoJSON();
  for (const hospital of hospitalFeatures.features) {
    hospital.properties['marker-size'] = 'small';
  }
  hospitalLayer.setGeoJSON(hospitalFeatures);
}

Then, inside of the click handler, add a function call to reset() and some code for making sure the nearest hospital opens a popup when it gets larger.

libraryLayer.on('click', (e) => {
  // Reset any and all marker sizes to small
  reset();

  // Get the GeoJSON from libraryFeatures and hospitalFeatures
  const libraryFeatures = libraryLayer.getGeoJSON();
  const hospitalFeatures = hospitalLayer.getGeoJSON();

  // Using Turf, find the nearest hospital to library clicked
  const nearestHospital = turf.nearest(e.layer.feature, hospitalFeatures);

  // Change the nearest hospital to a large marker
  nearestHospital.properties['marker-size'] = 'large';

  // Add the new GeoJSON to hospitalLayer
  hospitalLayer.setGeoJSON(hospitalFeatures);

  // Bind popups to new hospitalLayer and open popup
  // for nearest hospital
  hospitalLayer.eachLayer((layer) => {
    layer.bindPopup(`<strong>${layer.feature.properties.Name}</strong>`, {
      closeButton: false
    });
    if (layer.feature.properties['marker-size'] === 'large') {
      layer.openPopup();
    }
  });
});

Lastly, add a little bit of code at the end to reset all the markers to small when anywhere on the map is clicked (besides on a library).

map.on('click', () => reset());

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.js</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.js"></script>
<link
href="https://api.mapbox.com/mapbox.js/v3.2.1/mapbox.css"
rel="stylesheet"
/>
<script src="https://api.mapbox.com/mapbox.js/plugins/turf/v3.0.11/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>
L.mapbox.accessToken = '<your access token here>';
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]
}
}
]
};
// Add marker color, symbol, and size to hospital GeoJSON
for (const hospital of hospitals.features) {
hospital.properties['marker-color'] = '#DC143C';
hospital.properties['marker-symbol'] = 'hospital';
hospital.properties['marker-size'] = 'small';
}
// Add marker color, symbol, and size to library GeoJSON
for (const library of libraries.features) {
library.properties['marker-color'] = '#4169E1';
library.properties['marker-symbol'] = 'library';
library.properties['marker-size'] = 'small';
}
const map = L.mapbox
.map('map')
.setView([38.05, -84.5], 12)
.addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/light-v11'));
map.scrollWheelZoom.disable();
const hospitalLayer = L.mapbox.featureLayer(hospitals).addTo(map);
const libraryLayer = L.mapbox.featureLayer(libraries).addTo(map);
map.fitBounds(libraryLayer.getBounds());
// Bind a popup to each feature in hospitalLayer and libraryLayer
hospitalLayer
.eachLayer((layer) => {
layer.bindPopup(`<strong>${layer.feature.properties.Name}</strong>`, {
closeButton: false
});
})
.addTo(map);
libraryLayer
.eachLayer((layer) => {
layer.bindPopup(layer.feature.properties.Name, {
closeButton: false
});
})
.addTo(map);
// Open popups on hover
libraryLayer.on('mouseover', (event) => event.layer.openPopup());
hospitalLayer.on('mouseover', (event) => event.layer.openPopup());
// Reset marker size to small
function reset() {
const hospitalFeatures = hospitalLayer.getGeoJSON();
for (const hospital of hospitalFeatures.features) {
hospital.properties['marker-size'] = 'small';
}
hospitalLayer.setGeoJSON(hospitalFeatures);
}
// When a library is clicked, do the following
libraryLayer.on('click', (e) => {
// Reset any and all marker sizes to small
reset();
const hospitalFeatures = hospitalLayer.getGeoJSON();
// Using Turf, find the nearest hospital to library clicked
const nearestHospital = turf.nearest(e.layer.feature, hospitalFeatures);
// Change the nearest hospital to a large marker
nearestHospital.properties['marker-size'] = 'large';
// Add the new GeoJSON to hospitalLayer
hospitalLayer.setGeoJSON(hospitalFeatures);
// Bind popups to new hospitalLayer and open popup
// for nearest hospital
hospitalLayer.eachLayer((layer) => {
layer.bindPopup(`<strong>${layer.feature.properties.Name}</strong>`, {
closeButton: false
});
if (layer.feature.properties['marker-size'] === 'large') {
layer.openPopup();
}
});
});
// When the map is clicked on anywhere, reset all
// hospital markers to small
map.on('click', () => reset());
</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!