Analyze data with Turf.js and Mapbox.js
Familiarity with front-end development concepts.
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.
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><linkhref="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 GeoJSONfor (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 GeoJSONfor (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 libraryLayerhospitalLayer.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 hoverlibraryLayer.on('mouseover', (event) => event.layer.openPopup());hospitalLayer.on('mouseover', (event) => event.layer.openPopup()); // Reset marker size to smallfunction 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 followinglibraryLayer.on('click', (e) => {// Reset any and all marker sizes to smallreset();const hospitalFeatures = hospitalLayer.getGeoJSON();// Using Turf, find the nearest hospital to library clickedconst nearestHospital = turf.nearest(e.layer.feature, hospitalFeatures);// Change the nearest hospital to a large markernearestHospital.properties['marker-size'] = 'large';// Add the new GeoJSON to hospitalLayerhospitalLayer.setGeoJSON(hospitalFeatures);// Bind popups to new hospitalLayer and open popup// for nearest hospitalhospitalLayer.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 smallmap.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!