Mapping taxi demand with Uber h3 js and react map gl

How to build a taxi demand heat map using Uber h3 js and Mapbox react map gl.

Mapping taxi demand with Uber h3 js and react map gl

In the previous article, we used the Uber h3-js library to draw a single hexagon on a map. In this article I'm going to show you how to draw a group of hexagons to visualize taxi demand on a Mapbox map with the React Mapbox library, react-map-gl.

Taxi demand in Singapore visualized using h3 js and react map gl (react mapbox gl)
Taxi demand across Singapore from 6 am to 10 am on a weekday

Part 1: Uber h3 js tutorial: How to draw hexagons on a map
Part 2: Mapping taxi demand with Uber h3 js and react map gl (this article)
Part 3: Visualizing taxi demand over time with Mapbox and React range slider rc slider

The solution I'm going to present to you uses h3-js to define the geographic boundaries of each hexagonal cell. We then run a script to detect if a pickup took place within the cell and if so, we increment a counter. These pickup "booking" counts are saved as a key-value pair:

{
	"876526375ffffff": 158,
	"876526acaffffff": 152,
	"876526365ffffff": 634,
	"876526ac3ffffff": 885,
	"876526368ffffff": 70,
	"876520cb0ffffff": 201,
	... 110 more entries
}

that maps the cell's H3 index to the number of bookings that took place inside it. The last step is to load this data into react-map-gl (a React wrapper for Mapbox GL) and color code each hexagon based on the booking count. If you are interested in implementing this solution in one of your own projects, I recommend that you clone the Github repository and study it carefully to understand the details of my implementation.

Processing Taxi Demand Data

The data we are using in this project comes from the Singapore Government's open data repository data.gov.sg (note: this data set has since been removed, please reach out to me if you'd like a personal copy). It consists of taxi status and position data from a weekday between 6 am and 10 am saved as a CSV file:

date_time,taxi_id,ignore,longitude,latitude,speed,status
03/08/2016 06:00:00, SH6006C, 0110617,103.83272, 1.30534, 0, FREE
03/08/2016 06:00:00, SH6061S, 0227879,103.75754, 1.31769, 81, POB
03/08/2016 06:00:00, SH6220A, ,103.87265, 1.35343, 0, OFFLINE
03/08/2016 06:00:00, SH6395D, 1528378,103.93975, 1.3277, 0, BREAK
03/08/2016 06:00:00, SH6419U, ,103.88155, 1.32256, 0, OFFLINE
...
03/08/2016 06:06:49, SHC0091B, 1645455,103.98955, 1.3579, 0, FREE
03/08/2016 06:06:49, SHC0338R, ,103.94007, 1.34925, 0, OFFLINE
03/08/2016 06:06:49, SHC0647Z, 1741721,103.8199, 1.30721, 0, BUSY

Here's what each column represents:

date_time : the date and time in DD/MM/YYYY hh:mm:ss format

taxi_id: the unique license plate number of the taxi

longitude: the longitude coordinate of the taxi

latitude: the latitude coordinate of the taxi

speed: the spot speed of the taxi in km per hour

status: the status of the taxi (FREE - empty and looking for passengers), (POB - passenger on board)

If we monitor a specific taxi by its taxi_id and observe a change in its status from "FREE" to "POB," it indicates that a passenger pickup, or a booking, has occurred. The latitude and longitude values will give us the precise location of the booking, while the date_time will provide the exact time when the booking took place.

/* taxiDataParser.js (not included in repo) */
if (records[i].status === 'FREE' && records[i + 1].status === 'POB') {
   taxiData.push({
      date_time: records[i + 1].date_time,
      taxi_id: records[i + 1].taxi_id,
      latitude: records[i + 1].latitude,
      longitude: records[i + 1].longitude
   })
}

With the latitude and longitude saved, we can use the latLngToCell() method run a quick check to see which H3 cell the booking belongs to and increment a counter.

/* taxiDataParser.js (not included in repo) */
const bookingHexindex7 = latLngToCell(parseFloat(element.latitude), parseFloat(element.longitude), 7);

// If the booking falls in a specific hexagon, increment the booking count
if (singaporeHexagonsObj.hasOwnProperty(bookingHexindex7)) {
   singaporeHexagonsObj[bookingHexindex7]++;
}

The final result is singapore_hexagons_count.js, a JavaScript object that pairs the cell's H3 index (we are using resolution level 7, which roughly corresponds to a large neighborhood) to the number of bookings inside it. With this done, we can now write code to visualize our booking data on a map.

Drawing multiple hexagons with react-map-gl

In this tutorial, I will give a detailed explanation of how to add hexagons to a map and assign them colors based on a predefined value so that by the end of this blog post, you should have a broad based understanding of how to use h3-js and react-map-gl to create a heat map of taxi demand.

Using MapBoxSingleHex.jsx as a template (you can download the full source code for afilabs/h3js_hexagons_taxi_demand on GitHub and run it with npm install and npm start), create a new file MapBoxMultipleHex.jsx in the /src/components folder and copy/paste the following code outline:

/* MapBoxMultipleHex.js */
import "mapbox-gl/dist/mapbox-gl.css";
import Map, { Layer, Source, MapRef, Marker } from "react-map-gl";
import React, {useRef, useState } from "react";
import { cellToBoundary } from "h3-js";

function MapBox() {

    const singaporeHexagonsObj = require('../data/singapore_hexagons_count.json');
    const [singaporeHexagonsArr, setSingaporeHexagonsArr] = useState([]);

    const onLoad = () => {
        // onLoad() code here   
    };

    const getRandomStyle = (row) => {
        // getRandomStyle() code here
    }

    const getStyle = (row) => {
        // getStyle() code here
    };
  
    return (
        // JSX code here
    );
  
};
  
export default MapBox;

Initial setup

Next, add a /data folder to /src and save singapore_hexagons_count.js to it. This is what your /src folder should look like now:

h3js_hexagons_taxi_demand/
  src/
    components/
       MapBoxSingleHex.jsx
       MapBoxMultipleHex.jsx
    data/
       singapore_hexagons_count.js

In MapBoxMultipleHex.js we use the require statement to load singapore_hexagons_count.js and make it available via singaporeHexagonsObj .

/* MapBoxMultipleHex.js */
const singaporeHexagonsObj = require('../data/singapore_hexagons_count.json');

This is followed by a useState hook that allows us to access and change the value of a newly declared variable, singaporeHexagonsArr (an array that contains hexagon H3 index ids and booking counts). singaporeHexagonArr is initially set to an empty array ([]) but its value can be updated by calling the setSingaporeHexagonsArr() method.

/* MapBoxMultipleHex.js */
const [singaporeHexagonsArr, setSingaporeHexagonsArr] = useState([]);

Return statement

In a JSX file, the return statement is used to define what should be rendered or displayed on the screen when the component is rendered. For MapBoxMultipleHex.js, this includes the react-map-gl map layer, <Map/>.

<Map/>

The <Map/> component initializes our map with the light-v9 style and centers it at (1.290270, 103.851959) in downtown Singapore. The height and width parameters "`100vh", "100vw" stretch the map to occupy the entire page.

/* MapBoxMultipleHex.js */
return (
      <div>
          <div className="map">
            <Map
              initialViewState={{
                latitude: 1.290270,
                longitude: 103.851959,
                zoom: 10,
                bearing: 0,
                pitch: 0,
              }}
              mapStyle="mapbox://styles/mapbox/light-v9"
              mapboxAccessToken="YOUR_MAPBOX_ACCESS_TOKEN"
              style={{
                height: "100vh",
                width: "100vw",
              }}
              onLoad={onLoad}
            >
              <Source
                type="geojson"
                data={{
                  type: "FeatureCollection",
                  features: singaporeHexagonsArr
                }}
              >
                <Layer
                  {...{
                    id: "polyline-layer",
                    type: "fill",
                    paint: {
                      'fill-outline-color': 'white',
                      "fill-color": ["get", "color"],
                      "fill-opacity": ["get", "opacity"],
                    },
                  }}
                />
              </Source>
            </Map>
          </div> 
        </div>
);

Recall from the last post that react-map-gl lets you structure your map layer using Source and Layer components like this:

<Map>
  <Source>
    <Layer/>
  </Source>
</Map>

In that post, I used a geojson source of "type": "Feature" to load a single H3 cell hexagon onto the map. This time, we'll use "type": "FeatureCollection" in <Source/> to load a collection of H3 cells.

<Source/>

/* MapBoxMultipleHex.js */

              <Source
                type="geojson"
                data={{
                  type: "FeatureCollection",
                  features: singaporeHexagonsArr
                }}
              >
                <Layer/>
              </Source>

The data for the FeatureCollection comes from singaporeHexagonsArr, an array that stores the H3 index hexindex7 of the cell and the associated booking count bookingCount. We'll talk about this in detail when we look at the onLoad() function.

array of h3 js h3 index cells and associated booking counts
singaporeHexagonArr

<Layer/>

The <Layer/> component is used to style the hexagon. Just like in the previous post, we use ES6 destructuring ("...") to unpack the style object and inject it into <Layer/>.

/* MapBoxMultipleHex.js */
              <Source>
                <Layer
                  {...{
                    id: "polyline-layer",
                    type: "fill",
                    paint: {
                      'fill-outline-color': 'white',
                      "fill-color": ["get", "color"],
                      "fill-opacity": ["get", "opacity"],
                    },
                  }}
                />
              </Source>

The hexagon is given a fill-outline-color of "white" and uses the 'get' data expression to retrieve the feature's fill-color and fill-opacity values from the parent <Source/> component. Data expressions allow a feature's properties to determine its appearance, and can be used to differentiate features within the same layer to create data visualizations. In this example, we use the bookingCount value to apply different styles to our hexagons, with higher booking counts resulting in darker colors.

Taxi demand heatmap built with Uber h3 js and Mapbox react map gl (react mapbox gl)

These properties are injected into singaporeHexagonsArr in the onLoad() method, which we'll get to next.

Mapbox style objects that use h3 index level 7 ids

onLoad() method

The onLoad() method is called when we set the prop onLoad={onLoad} in the <Map/> component.

/* MapBoxMultipleHex.js */
const onLoad = () => {
	const sgHexagonsArr = [];

	for (const hexagon in singaporeHexagonsObj) {
		sgHexagonsArr.push({
			hexindex7: hexagon,
			bookingCount: singaporeHexagonsObj[hexagon]
		});
	}

	const rs = sgHexagonsArr.map((row) => {
		const style = getStyle(row);
		return {
			type: "Feature",
			properties: {
				color: style.color,
				opacity: style.opacity,
				id: row.hexindex7,
			},
			geometry: {
				type: "Polygon",
				coordinates: [cellToBoundary(row.hexindex7, true)],
			},
		};
	});
	setSingaporeHexagonsArr(rs);
};

First, we we initialize an empty array sgHexagonsArr and populate it with hexindex7 and bookingCount data from singaporeHexagonsObj.

/* MapBoxMultipleHex.js onLoad() */
	const sgHexagonsArr = [];

	for (const hexagon in singaporeHexagonsObj) {
		sgHexagonsArr.push({
			hexindex7: hexagon,
			bookingCount: singaporeHexagonsObj[hexagon]
		});
	}

Second, we map sgHexagonsArr to an array rs ("row style") of Mapbox style objects, and use the getStyle() method to map booking counts to the corresponding color and opacity values (we'll talk about getStyle() in the next section).

/* MapBoxMultipleHex.js onLoad() */
	const rs = sgHexagonsArr.map((row) => {
		const style = getStyle(row);
		return {
			type: "Feature",
			properties: {
				color: style.color,
				opacity: style.opacity,
				id: row.hexindex7,
			},
			geometry: {
				type: "Polygon",
				coordinates: [cellToBoundary(row.hexindex7, true)],
			},
		};
	});
	setSingaporeHexagonsArr(rs);

Third, we copy over rs to singaporeHexagonsArr by using the setSingaporeHexagonsArr() function (part of React's useState hook). This way, the new singaporeHexagonsArr array and associated data is available to the react-map-gl <Source/> and <Layer/> components for rendering.

getStyle() method

The last method we'll look at is getStyle(), which is responsible for mapping bookingCount to a range of colors that we'll use in our taxi demand heat map.

/* MapBoxMultipleHex.js getStyle() */
 const getStyle = (row) => {

 	const styles = [{
 			color: '#FEDD87',
 			opacity: 0.2
 		},
 		{
 			color: '#FED976',
 			opacity: 0.4
 		},
 		{
 			color: "#FC9653",
 			opacity: 0.6,
 		},
 		{
 			color: "#F77645",
 			opacity: 0.7
 		},
 		{
 			color: "#E14C48",
 			opacity: 0.8
 		}
 	];


 	if (Number(row.bookingCount) === 0) {
 		return {
 			opacity: 0
 		};
 	}

 	if (Number(row.bookingCount) < 250) {
 		return styles[0];
 	}
 	if (Number(row.bookingCount) < 500) {
 		return styles[1];
 	}
 	if (Number(row.bookingCount) < 1000) {
 		return styles[2];
 	}
 	if (Number(row.bookingCount) < 1500) {
 		return styles[3];
 	}
 	return styles[4];
 };

Choosing an appropriate color scheme for our heat map can help users quickly interpret the data and patterns represented in the heat map - in this case, we want to distinguish high taxi demand areas from low taxi demand areas quickly, at a glance. Looking at the data when binned into intervals of 250, we see that the distribution of bookings is "positively skewed", with the majority of bookings concentrated on the left side, towards lower values e.g. there are 68 H3 cells with less than 250 bookings.

Distribution of taxi demand per h3 cell at level 7
Distribution of taxi demand binned at intervals of 250 bookings

With this in mind, here's one way to map booking_count to color:

booking_count color
less than 250 #FEDD87
250 - 499 #FED976
500 - 999 #FC9653
1000 - 1499 #F77645
1500 or more #E14C48

If there are no bookings detected, we give the hexagon an opacity of 0 so that it appears transparent.

Putting it all together

In your terminal, run npm install to install dependencies followed by npm start to run the app.

Taxi demand visualization built with react mapbox gl and uber h3 js
Taxi demand across Singapore from 6 am to 10 am on a weekday

Point your browser to localhost:3000 and if everything worked correctly, you should be able to see the map above. As expected, taxi demand can clearly be seen concentrated in the Central Business District and in dense neighborhoods to the East and West of Singapore. In the next post, we'll make this data visualization even more useful by animating it so that you can see how taxi demand changes over time.

👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.

Next: Part 3: Visualizing taxi demand over time with Mapbox and React range slider rc slider