A Foray into Building Interactive Maps with Leaflet

Evan Sheffield
iVantage Technical Blog
9 min readMay 28, 2019

As a part of iVantage’s Research & Innovation program, we recently completed a project in which we investigated and built a prototype interactive map in Leaflet as a replacement for our existing solution built using the ArcGIS API for Javascript. In this post, we’ll discuss our experience with Leaflet thus far and some of the lessons learned over the course of the project. Overall, we’ve been very pleased with the performance and developer experience that Leaflet provides compared to our previous implementation.

A screenshot of the Leaflet-based proof-of-concept we created.

Background

At iVantage, a lot of the analytics that we build are focused around understanding your market as a healthcare provider. A fundamental first step in doing so is defining a geographic area that your organization serves — a service area. Creating a service area is a nuanced art that involves referencing both volume data (how many of my patients are coming from ZIP Codes in my service area?) and geospatial data (are all the ZIP Codes in my service area contiguous?). As such, being able to visualize and interact with a service area on a map is a valuable tool in crafting service area definitions.

At a basic level, the map we use to facilitate creating service areas needs to be able to visualize ZIP Codes or county geographies and provide a means of adding or removing these geographies to and from the definition. Because we host an ArcGIS Server for all our geospatial needs, our initial implementation was built using the ArcGIS API for Javascript 4.4. Despite a seemingly simple set of functionality requirements, we encountered severe performance issues and a complex implementation in our React-based app fueled by the high barrier of entry to the API and an AMD style module loader that was an awkward fit for a React project with ES6 modules.

And then came Leaflet

After some investigation, Leaflet stood out as an ideal candidate for our situation. Indeed, the mission statement on their site spoke to all of the issues we sought to improve upon in our next iteration:

Leaflet is designed with simplicity, performance and usability in mind.

The core set of capabilities seemed appropriate for a small mapping application like ours and given the pool of open-source plugins available we were confident we could achieve feature parity with our existing map. We embarked on a project to build a proof-of-concept interactive service area map with Leaflet with the following goals in mind:

  1. Loading and interacting with the map should be performant.
  2. The map application should have achieve feature parity with our existing solution (visualizing geographies, adding/removing from definition, plotting point locations, etc.).
  3. The user experience should be intuitive.
  4. The developer experience should be pleasant and the API easy to learn and work with.

Working with Leaflet in a React App

One of the pain points of working with the ArcGIS API for Javascript was the difficulty of integrating it cleanly with our React application. Although Leaflet does not integrate with the React lifecycle on it’s own, the React-Leaflet library provides an abstraction layer that makes it easy to do so. Their website has a good introduction to how this works, but essentially what it does is represent Leaflet elements as React components that tie relevant Leaflet handlers to React’s various lifecyle methods:

  • A top level Map component creates an empty <div> and adds a Leaflet map to it.
  • Leaflet components do not actually render anything to the DOM, but they trigger all of their children to be rendered.
  • After a component mounts, the Leaflet element attached to it is added to the map during componentDidMount.
  • Before a component unmounts, the Leaflet element attached to is removed from the map during componentWillUnmount.

Depending on what actions are available in the Leaflet API for a given element, certain options in React-Leaflet are made dynamic by triggering the appropriate actions to update the element in the componentDidUpdate method.

React-Leaflet provides components that cover all the standard Leaflet functionality, but in case you want to further customize it or consume a plugin it’s very easy to create your own component by extending a few methods that allow you to define how to hook into these lifecycle events. Here is an example of a component we created to wrap a Leaflet element provided as a part of the Esri Leaflet plugin.

import PropTypes from 'prop-types'
import { featureLayer } from 'esri-leaflet'
import { MapLayer, withLeaflet } from 'react-leaflet'
/**
* A MapLayer used to display Esri feature layers
*/
class EsriFeatureLayer extends MapLayer {
createLeafletElement (props) {
return featureLayer({
url: props.url,
...props.options
})
}
updateLeafletElement (fromProps, toProps) {
// ...
}
}
EsriFeatureLayer.propTypes = {
url: PropTypes.string.isRequired,
options: PropTypes.object
}
EsriFeatureLayer.defaultProps = {
options: {}
}
export default withLeaflet(EsriFeatureLayer)

We extend React-Leaflet’s MapLayer since a feature layer is just a map layer that renders geographic data from an ArcGIS server. createLeafletElement creates and returns a Leaflet element (in this case a featureLayer) which is stored in the component and added to the map upon mount. In the initial implementation, our component does not respond to any prop changes. However, we later found that we wanted the map to update when the URL prop changed so we implemented the updateLeafletMethod to handle this case:

updateLeafletElement (fromProps, toProps) {
if (fromProps.url !== toProps.url) {
this.layerContainer.removeLayer(this.leafletElement)
this.leafletElement = featureLayer({
url: toProps.url,
...toProps.options
})
this.layerContainer.addLayer(this.leafletElement)
}
}

In this case, the API of featureLayer did not support dynamically updating the URL of the element. Fortunately, the Leaflet context object we get by wrapping our component in withLeaflet(...) allows us to access the layer’s container directly so we can simply remove the old layer and create a new one with the desired properties.

The Promises and Pitfalls of GeoJSON

One of Leaflet’s most powerful features is it’s ability to easily consume GeoJSON to create interactive geometries on a map. GeoJSON is a format which represents various geometries (such as points and polygons) as a JSON object that encodes the geographic data needed to render the geometry as well as any arbitrary attributes that describe them. In our case, this seemed an ideal fit for representing County and ZIP Code geographies and storing their properties like name and code.

GeoJSON is an appealing format for a variety of reasons:

  • Geometries are encoded in an easy-to-read and modify format. This stands in contrast to proprietary formats like ESRI shapefiles which are binary files.
  • By using a flat file representation we remove the dependency on a geospatial server. Server setup and maintenance can be complex and communication with it introduces latency in the map’s interactions, especially when that communication needs to be proxied (e.g. for authentication purposes).

In the course of building our proof-of-concept map we attempted using GeoJSON as a replacement for our internal ArcGIS services, but ultimately came across several limitations and decided it wouldn’t be appropriate for our application. Here are some of the main hurdles we ran into:

Issue 1: File Size

Not surprisingly, representing detailed geographies like ZIP Codes in a flat file format can lead to some pretty big GeoJSON files. We converted an ESRI shapefile representing all US counties to GeoJSON and came away with a file that was over 200 MB. Wow! That’s to say nothing of representing all >42,000 US ZIP Codes which easily approaches a gigabyte. Needless to say, 200 MB is an awful lot to ask clients to download in order for them to be able to use your map application. Fortunately, there are some measures that you can take to reduce file sizes. For instance, the TopoJSON extension of GeoJSON eliminates some redundancy by using arcs to represent shared line segments (think the border between two counties). TopoJSON can be converted back to GeoJSON to use in a Leaflet app, and there are some plugins out there such as leaflet-omnivore that will do this for you.

Issue 2: Fidelity of Geographies

A geospatial server like ArcGIS is capable of dynamically adjusting the fidelity of polygons based on the zoom level, but with a flat file representation in GeoJSON you’ll only ever have as much precision as you bake into the file. This leads to a balancing act between level of detail and size of the GeoJSON file. You can cut down on file size pretty dramatically by simplifying geographies using a tool like Mapshaper, but these simpler shapes might not look good when zoomed in closely.

Issue 3: Memory

Even after you’ve got the right balance of file size and geography detail, you still need to keep in mind that when you create a GeoJSON layer in Leaflet all of your geographies are going to be loaded at once into memory. In our testing this wasn’t a problem for county boundaries, but even in a simplified format loading all US ZIP Codes into a layer led to severe performance issues and often resulted in the browser crashing. A much more efficient way of handling this is using vector tiles so that only the necessary chunks of data will be loaded into memory as the user interacts with the map. We tried working with the Leaflet.VectorGrid plugin and while it was able to handle our large GeoJSON data with ease, at the time of writing we encountered some issues with mouse interactions for vector grid layers that made us decide not to use it.

Sooo… should I use GeoJSON files?

As with most things, the answer is “it depends”. If your data is relatively small or you don’t need a great level of detail then GeoJSON is a great choice as it is very easy to work with. If you don’t have a geospatial server already then GeoJSON is also great because setting up and maintaining a server like ArcGIS is no small task. Unfortunately, we needed to represent a lot of data and couldn’t accept a large file size or low quality geometries so we found it ill-suited to this particular application. Still, we learned a lot in the process and recognize the promise of the format so we look forward to potentially using it for future projects.

Adding Interaction

So we’ve got our Leaflet map running in a React app and we’re loading data using either GeoJSON or a geospatial service. Now how do we add our interactions? Leaflet makes it very simple to make your maps interactive and respond to events such as clicks and mouse hovers. For example, in our case we wanted the ability to “paint” a service area by clicking geographies on the map. Let’s assume we’re working with a GeoJSON layer. We define a style function which colors features based on a selection maintained in our component’s state. To add interaction we then register event handlers using the onEachFeature property:

const selectedAreas = { this.state }... <GeoJSON data={myData}
style={(feature) => {
return {
fillColor: selectedAreas[feature.id] === 1 ? 'blue' : 'white'
}
}}
onEachFeature={(feature, layer) => {
layer.on({
click: this.onGeographyClick
})
}} />

In our click handler we have access to the target layer and feature that were clicked in order to define our interaction. In this case we want the color of the area we clicked to be updated.

onGeographyClick = (e) => {
const nextSelectedAreas = { ...this.state.selectedAreas }
nextSelectedAreas[e.target.feature.id] = 1
this.setState({ selectedAreas: nextSelectedAreas })
}

We’ve set up our style as a function of our component’s state, and because style is a dynamic property under React-Leaflet the style function will automatically update when our state changes. No need to manually set the style of the feature! Just be mindful of which properties are static vs. dynamic, as some interactions like this will need to be handled by manually manipulating the underlying Leaflet elements.

Looking Forward

At the conclusion of the project, not only did we have a functioning prototype but we also had found in Leaflet a powerful library that was a joy to develop with. The initial load time for our map decreased over our prior implementation using the ArcGIS for Javascript API, and user interactions with the map were much more seamless. Leaflet alone got us most of the way towards feature parity and the vast plugin library helped round it out (e.g. for exporting map images). Leaflet integrates nicely with React and there is a lot of potential behind GeoJSON even though it caused us to stumble a bit given our requirements.

As we transition out of this research project and into our regular development pipeline, we are looking forward to optimizing the performance and user experience of our new Leaflet-based service area creator even further.

--

--