Making an interactive map with React and Leaflet

Create your interactive map in React using OpenStreetMap and Leaflet. This post shows you how online maps work and how to create your own choropleth map.

Making an interactive map with React and Leaflet

Intro

One of my recent side projects is about visualizing data on a map. I'm a backend developer, but I got the task to create a web app for this gig. I have some minimal experience with Angular and web development, but not much and no active knowledge. I've heard good things about React, Wise also uses React for its web app development, so I decided to give it a try and make this small map visualization PoC in it. This is the story of how it React app turned out.

🤔
I have to let all of you reading this know in advance that I'm no JavaScript, Typescript, or React expert, and just started to experiment with these technologies. So bad practices are likely to be present in this article amongst the code examples. I will update this post (with a changelog) if I improve anything as I get better with time.
🆕
Update 1: You can learn things pretty fast if you are just starting out. I converted everything to function-based components and managed to solve the map coloring changing issues. I have now updated the text of the post and the code snippets to reflect the changes. The code on GitHub is also updated.
🆕
Update 2: I've added a couple of new features and fixed things about this map. My findings and the more refined version can be found in the 2nd part of this post. It is still worth reading this since the 2nd one is a direct continuation.

The goal

The project itself is not public yet so without revealing all the details about it, one of the main aspects is a world map, where countries are represented with distinct polygons, that can be colored according to a numeric value and selected by mouseclick or hover. This kind of visualization is called a choropleth map.

Choropleth map example from Wikipedia

Choropleth maps can be done good and bad. Often it is considered to be a bad choice for displaying data because with not enough granularity it shows averaged values for large areas, hiding the smaller regional differences. Consider a huge country like the USA. A single color for the whole country can display the average population density for example, but it is more likely that in reality, any place in the US will be either a lot more or less dense, a single color can't show you the distribution. Using higher and higher resolution, states or counties even can help with this but more regions means more data, bigger map, slower speeds, etc. This is something that must be considered.

How maps work

Displaying and using maps online is not trivial technically all the big players like Google Maps or OpenStreetMap use similar techniques. In this section, I'm going to review the basics of maps on the internet.

Maps, tiles

OpenStreetMap is an open and free-to-use project providing various mapping-related services. One service I have used during this work is a tile server. In online map services, tiles are rectangular, pre-rendered parts of the whole map.

Tiles for different zoom levels as described on Bing Maps

There are new sets of tiles for each zoom level with more and more detail as we zoom in. The map rendering library in any client application knows which tiles are visible and requests those from the tile server. This way only the visible parts of the map must be downloaded at once, and for just one single zoom level saving a lot of bandwidth and time.

There are many companies providing map tile services with different visualizations and styles, most of them are not free, OpenStreetMap is.

The map rendering library handling all the logic around getting the tiles and showing those, providing zoom and other controls in my case was Leaflet.js. This handy little open source lib is very easy to use and has a lot of features out of the box, but can be extended with custom components too. Leaflet uses OpenStreetMap by default but the tile provider can be easily changed. The documentation is also great! They actually have a choropleth map example on their page, which served as the basis for my implementation. You can check out further leaflet map examples on their website.

GeoJSON

Ok so we know how we can display a map in the browser, but how can we overlay polygons representing the countries on it? There is a JSON format called GeoJSON to make the storage of geographic features standard.

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [125.6, 10.1]
  },
  "properties": {
    "name": "Dinagat Islands"
  }
}
GeoJSON example

A GeoJSON feature contains

  • properties - data describing the feature like name or anything you want
  • geometry - a specific type of geometry as a point, a polygon, or even multiple polygons (imagine a country with a lot of islands)

With this format, we can represent lines rectangles, or any arbitrarily shaped object, like a country. Luckily for us, there are open databases on the internet where we can download complete GeoJSON representations of the world's countries. This source is the one I found most useful. It enables us to select the resolution and the regions to be included.

Important to note is that the file size is very different for the 3 resolutions. The uncompressed versions including all regions are about 600KB, 3,7MB, and 20MB. Obviously, with gzip, these are better, but that only affects the download speed. The browser will still have to render much more detailed polygons with the high res versions, slowing your app down. The lowest resolution is fast but not too accurate, could be good for some use cases, but it was not what I had in mind. The most detailed with its size is no option for a web application in my opinion. The middle one could be a good choice. The gallery below shows example borders for all three resolutions, so you can compare them.

Besides the geometries, the JSON downloaded from the linked source also contains some data on the countries like their name, 3 letter ISO code, population, and a few others. Excellent for my PoC.

Leaflet has a specific GeoJSON layer component that you can put on the map and it will display the contents of the provided JSON.

Getting into React

With all the knowledge about map technology (at least all the knowledge I need at the moment). The first step for me was to learn what React is about. I had never used it before.

I started with the React step-by-step tutorial and also completed the tic-tac-toe game after that. I think these two together provided sufficient intro so I could start to develop my project.

JSX - but why?

Turns out in React you can create similar components than in Angular, but the HTML-like code defining the structure called JSX and business logic is in the same file mixed together.

Someone like me, who has witnessed the reign of JSP and the abuse of the technology where a thousand lines long file was 80% business logic code and minimal markup may ask why do we want to go back there by mixing the logic and structure?

React has a reputation for being simple, quick, and easy to develop in and I also had this feeling while learning the basics. I think this mixing of markup and logic can be fine. With JSPs you usually defined a whole page including a ton of code for the logic for the various functions there and also a lot of HTML describing the complex structures. In React the components you create are much much smaller preventing you from making the same mistakes.

I liked Angular's approach better to separate structure and logic, but as I worked with React I got used to its approach pretty fast.

To create a starter project I used create-react-app.dev by Facebook. By default, it creates a JavaScript-based template but can create a TypeScript based one too with an additional input param on the generate command. For the first version of my proof of concept, I used the JavaScript-based one, so the examples in this post will be in JS too.

In React, there are 2 ways to define a component. You can create a class extending React.Component or a never method by defining a function. As I have learned from a React expert friend I reached out to for some help the class-based method is not really used anymore, however, the tutorial pages I linked above still use that. There is a new React documentation in the making available at beta.reactjs.org where the updated examples use the newer function-based component definition.

I also learned that for many javascript libraries there is a React version that is either the original lib wrapped into React components for our convenience or completely rewritten as React components from scratch. There is one for bootstrap for example, and also for Leaflet called react-leaflet that I chose to work with instead of the original Leaflet lib. With these 3rd party unofficial React wrappers and rewrites the issue of reliability, updates, coverage of the original features, etc are always present. I do not know how big of an issue this is yet. React-leaflet is a wrapper and seems to work pretty well, but on the other hand, the "Reactified" bootstrap lib seems to lack a lot of features.

Creating the components

On the screenshot below you can see the result, an interactive map in react. It has 3 main components:

  • The map itself with its GeoJSON layers and colors
  • The little info box in the top right corner - <InfoBox>
  • The data selector in the bottom left corner - <DataScopeSeelctor>
The result

The map and GeoJSON layers are created using the built-in Leaflet components as I've mentioned before. The infobox and selectors are separate components created by me. I tried to follow the Leaflet tutorial as close as I could but with the React version, things were a bit different. and I also introduced a few bells and whistles here and there.  The structure of my main component looks like this:

<div className='mapContainer' >
	<MapContainer center={[51.505, -0.09]} zoom={3}>
		<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
		<GeoJSON data={countries} style={style} onEachFeature={onEachFeature} />
		<InfoBox data={selectedCountry} scope={dataScope} />
	</MapContainer>
<DataScopeSelector options={dataScopes} value={dataScope} changeHandler={handleDataScopeChange} />
</div>
Component structure

The MapContainer is the leaflet main react component, TileLayer from Leaflet provides the OSM tile layer, you can see the URL that it uses to download the tiles. Changing to another tile server is super easy! GeoJSON is still a built-in Leaflet component. In its data property, it receives the GeoJSON itself, in the style property you can pass a function that calculates the style for the visualization of the features, border color, style, fill colors, etc. You can do this on a feature (country in this case) basis, this is the way we can color the map according to some data.

const style = (feature) => {
	let mapStyle = {
    	fillColor: getColor(feature.properties[dataScope.key]),
        weight: 1,
        opacity: 1,
        color: '#888',
        dashArray: '3',
        fillOpacity: 0.7
	};

	if (hoveredCountry && feature.properties.iso_a3 === hoveredCountry.iso_a3) {
            mapStyle.color = '#444';
            mapStyle.weight = 2;
	}

	return mapStyle;
}
A possible style function

You can also pass an onEachFeature property that you can use to put event handler to each feature. This way you can make countries change color on hover or anything you want.

const onEachFeature = (feature, layer) => {
	layer.on({
		mouseover: highlightFeature,
        mouseout: resetHighlight,
        click: () => setSelectedCountry(feature.properties)
	});
}

InfoBox is a custom component of mine. It is a regular react component however I think this is not the right solution here, but the only one that worked for me. So this is something I will improve on. I believe it should be a custom LeafletControl and there are ways you can make those, but the documentation was a bit sparse on the topic if you use react-leaflet. So for the time, I went with a regular component. The issue I'm having is that I can't interact with it, can't click it, but as a display, it works fine.

function numberWithCommas(x) {
  if (x != null) {
    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  } else {
    return '';
  }
}

// TODO link is not clickable, infobox seems to be "transparent"
export function InfoBox({ data, scope }) {
  let infoBox;
  if (data != null) {
    infoBox = <div><h4>{data.name}</h4>
    <br></br>
    <b>{scope.name}:</b> {numberWithCommas(data[scope.key].value)} {scope.unit}
    <br></br>
    <b>Description:</b> {scope.description}</div>;
  } else {
    infoBox =  <h4><i>select a country</i></h4>;
  }

  return (
    <div className="info leaflet-top leaflet-right">
    { infoBox }
    </div>
  )
}
The InfoBox component - should be a Leaflet specific custom control

The data selector component ( DataScopeSelector ) is a simple controlled input. All state is stored and handled in the main component, this way I can notify all child components about changes.

export function DataScopeSelector({ options, value, changeHandler }) {
    return (
        <select value={value.key} onChange={changeHandler}>
            {options.map((e) => {
                return <option key={e.key} value={e.key}>{e.name}</option>
            })}
        </select>
    );
}

Whenever I change the value of the DataScopeSelector dropdown, the InfoBox changes from GDP to population or vice-versa and displays different information about the selected country. The currently selected country and data are stored in the main component's state.

Still to do and final thoughts

The project is available on my GitHub account here. The code you find there uses 2 data sources, the GeoJSON for geometry and fact data and a constant JavaScript object for "metadata" describing the data scopes available to select (population, GDP - GDP seems to be off btw, no idea what it actually is). I may improve the code in the future so it is subject to change. The improvements will probably be best practices, formalities, and the following main missing things:

  • Fix the InfoBox so it can be clicked as a proper map control

Jumping into React was a lot easier than expected. The tutorial I linked is not that long itself and covers a lot of the basics that one needs to start a little project like this. From the React, Leaflet, and Leaflet-React tutorials with some basic knowledge about web development it was quite easy to put together this little Reactjs leaflet interactive map app. I will continue playing around with React in the future and I think it will be a recurring topic on this blog.