Analyze data with Turf.js

Now that you’ve walked through adding interactivity to your maps with Mapbox.js, let’s take it a step further and learn about how to add some spatial analysis to your maps with Turf.js.

This guide walks through the basics of Turf.js, a JavaScript library used 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:

L.mapbox.accessToken = '<your access token here>';
  • 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.

Adding structure

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

<link href='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css' rel='stylesheet' />
<script src='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js'></script>
<script src='https://api.mapbox.com/mapbox.js/plugins/turf/v2.0.2/turf.min.js'></script>

Now, add your basic 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%;
}

Initializing 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 map ID. Add the following script inside the <body> after the HTML:

L.mapbox.accessToken = '<your access token here>';
var map = L.mapbox.map('map', 'mapbox.light')
  .setView([38.05, -84.5], 12);
map.scrollWheelZoom.disable();

View demo one.

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

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

Loading 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.

L.mapbox.accessToken = '<your access token here>';

// store GeoJSON objects in these variables
var 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] } }
  ]
};
var 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 (var i = 0; i < hospitals.features.length; i++) {
  hospitals.features[i].properties['marker-color'] = '#DC143C';
  hospitals.features[i].properties['marker-symbol'] = 'hospital';
  hospitals.features[i].properties['marker-size'] = 'small';
}

// Add marker color, symbol, and size to library GeoJSON
for (var j = 0; j < libraries.features.length; j++) {
  libraries.features[j].properties['marker-color'] = '#4169E1';
  libraries.features[j].properties['marker-symbol'] = 'library';
  libraries.features[j].properties['marker-size'] = 'small';
}

var map = L.mapbox.map('map', 'mapbox.light')
  .setView([38.05, -84.5], 12);
map.scrollWheelZoom.disable();

var hospitalLayer = L.mapbox.featureLayer(hospitals)
  .addTo(map);
var 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.

View demo two.

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 (http://localhost/turfjs-intro.html) otherwise, you will receive a Cross-origin Resource Sharing (CORS) error.

Adding 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(function(layer) {
    layer.bindPopup('<strong>' + layer.feature.properties.Name + '</strong>', { closeButton: false });
  }).addTo(map);
  libraryLayer.eachLayer(function(layer) {
    layer.bindPopup(layer.feature.properties.Name, { closeButton: false });
  }).addTo(map);

  // Open popups on hover
  libraryLayer.on('mouseover', function(e) {
    e.layer.openPopup();
  });
  hospitalLayer.on('mouseover', function(e) {
    e.layer.openPopup();
  });

View demo three.

Awesome! This is a pretty cool map of the libraries and hospitals in Lexington, KY. Let’s make it even more useful by adding some analysis.

Introducing 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 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. Previously, you created event handlers for hovering over hospital and library markers; now you’re going to make one for clicks.

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

This is the basic structure of an event handler; anything you want to happen on click goes inside of the curly 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', function(e) {
  // Get the GeoJSON from libraryFeatures and hospitalFeatures
  var libraryFeatures = libraryLayer.getGeoJSON();
  var hospitalFeatures = hospitalLayer.getGeoJSON();

  // Using Turf, find the nearest hospital to library clicked
  var 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);
});

View demo four.

Excellent! This is almost ready to go.

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() {
  var hospitalFeatures = hospitalLayer.getGeoJSON();
  for (var i = 0; i < hospitalFeatures.features.length; i++) {
    hospitalFeatures.features[i].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', function(e) {
  // Reset any and all marker sizes to small
  reset();

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

  // Using Turf, find the nearest hospital to library clicked
  var 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(function(layer) {
    layer.bindPopup('<strong>' + layer.feature.properties.Name + '</strong>', { closeButton: false });
    if (layer.feature.properties['marker-size'] === 'large') {
      layer.openPopup();
    }
  });
});

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

map.on('click', function(e) {
  reset();
});

View demo five.

Mission complete!

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

<html>
<head>
<meta charset=utf-8 />
<title>Turf.js Map</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.mapbox.com/mapbox.js/v2.1.5/mapbox.js'></script>
<link href='https://api.mapbox.com/mapbox.js/v2.1.5/mapbox.css' rel='stylesheet' />
<script src='https://api.mapbox.com/mapbox.js/plugins/turf/v1.3.0/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 = 'pk.eyJ1IjoibHl6aWRpYW1vbmQiLCJhIjoiRkh4OW9layJ9.P2o48WlCqjhGmqoFJl3C_A';

    var 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] } }
      ]
    };
    var 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 (var i = 0; i < hospitals.features.length; i++) {
      hospitals.features[i].properties['marker-color'] = '#DC143C';
      hospitals.features[i].properties['marker-symbol'] = 'hospital';
      hospitals.features[i].properties['marker-size'] = 'small';
    }

    // Add marker color, symbol, and size to library GeoJSON
    for (var j = 0; j < libraries.features.length; j++) {
      libraries.features[j].properties['marker-color'] = '#4169E1';
      libraries.features[j].properties['marker-symbol'] = 'library';
      libraries.features[j].properties['marker-size'] = 'small';
    }

    var map = L.mapbox.map('map', 'mapbox.light')
      .setView([38.05, -84.5], 12);
    map.scrollWheelZoom.disable();

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

    map.fitBounds(libraryLayer.getBounds());

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

    // Open popups on hover
    libraryLayer.on('mouseover', function(e) {
      e.layer.openPopup();
    });
    hospitalLayer.on('mouseover', function(e) {
      e.layer.openPopup();
    });

    // Reset marker size to small
    function reset() {
      var hospitalFeatures = hospitalLayer.getGeoJSON();
      for (var k = 0; k < hospitalFeatures.features.length; k++) {
        hospitalFeatures.features[k].properties['marker-size'] = 'small';
      }
      hospitalLayer.setGeoJSON(hospitalFeatures);
    }

    // When a library is clicked, do the following
    libraryLayer.on('click', function(e) {
      // Reset any and all marker sizes to small
      reset();
      // Get the GeoJSON from libraryFeatures and hospitalFeatures
      var libraryFeatures = libraryLayer.getGeoJSON();
      var hospitalFeatures = hospitalLayer.getGeoJSON();
      // Using Turf, find the nearest hospital to library clicked
      var 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(function(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', function(e) {
      reset();
    });

  </script>
</body>
</html>

Turf has dozens of tools that would help extend this map even further. Using turf.distance, you could figure out not only which hospital is closest, but exactly how far away it is. Using turf.filter, you could change the color of hospitals that are closest to any library or turf.remove to remove hospitals from the map that are not closest to any library. The possibilities are virtually endless! Be sure to show us the cool things you make by tweeting @Mapbox.

Additional questions? Ask our support team or learn more about How Mapbox Works.