Visualizing taxi demand over time with Mapbox and React range slider rc slider
How to visualize taxi demand over time using Mapbox and rc slider
Welcome to the third and last of my tutorial series on using the Uber h3-js library and react-map-gl to visualize taxi demand on a map. In the first part, I showed how to use the h3-js APIs to display a simple hexagon on a Mapbox base map. In the second, we extended this idea to draw a heat map of taxi demand in Singapore built from multiple hexagons. In this post, we'll use a React slider component, rc-slider, to show how taxi demand changes over time on a weekday during the morning rush hour in Singapore.
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
Part 3: Visualizing taxi demand over time with Mapbox and React slider rc slider (this article)
As with all my tutorials, you can find working sample code for this project on Github. Unlike a typical tutorial where we work together to build an app from the ground up, the best way to work through this one is to download the repository from Github, run it locally on your machine and refer to this blog post for an explanation of what each section of code does. The project is structured like a typical React single page app, with most of the heavy lifting done in src/MapBoxTimelineSlider.jsx
which is responsible for loading the taxi booking data, drawing the map (react-map-gl) and hexagonal cells (h3-js) and most importantly, using the rc-slider React slider component to animate it.
Taxi booking data
Let's start with our data set. singapore_taxi_hexagons.json
(link) is derived from the same taxi status and position data file used in the last post.
/* singapore_taxi_hexagons.json */
{
"03/08/2016 07:15:00-03/08/2016 07:30:00": {
"876526375ffffff": 43,
"876526acaffffff": 41,
"876526365ffffff": 171,
"876526ac3ffffff": 225,
... 112 more entries
},
"03/08/2016 07:30:00-03/08/2016 07:45:00": {
"876526375ffffff": 63,
"876526acaffffff": 54,
"876526365ffffff": 224,
"876526ac3ffffff": 278,
"876526368ffffff": 18,
... 112 more entries
},
... 14 more time periods
}
The file is structured as a JavaScript object that contains the number of taxi bookings recorded in each h3 cell, sampled at 15 minute intervals from 6 am to 10 am e.g. 06:00 - 06:15, 06:15 - 07:00, ..., 09:45 - 10:00.
The keys of this object are a specially formatted date time string e.g. 03/08/2010 07:15:00-03/08/2010 07:30:00
refers to 7:15 am to 7:30 am (exclusive, i.e. it does not include 7:30 am). The value corresponding to each key is another key-value pair that maps the cell's H3 index to the number of bookings that took place inside it. For example, the JSON extract below means that H3 cell 876526375ffffff
/* singapore_taxi_hexagons.json */
{
"03/08/2016 07:15:00-03/08/2016 07:30:00": {
"876526375ffffff": 43,
... 115 more entries
},
"03/08/2016 07:30:00-03/08/2016 07:45:00": {
"876526375ffffff": 63,
... 115 more entries
},
"03/08/2016 07:45:00-03/08/2016 08:00:00": {
"876526375ffffff": 93,
... 115 more entries
}
}
on 3rd August, 2016 had 43 bookings (taxi pickups) recorded within its boundaries between 07:15 and 07:30, 63 bookings between 07:30 and 07:45 and 93 bookings between 07:45 and 08:00.
The goal of the application we are going to build in this project is to step through time and display an animated heatmap showing how these booking counts change over time. To do this, we are going to use a slider written in React JS, react-component/slider, more commonly known as rc-slider.
Introduction to rc slider
rc-slider is part of Ant Design's collection of high quality React components used to build rich, interactive user interfaces. It is one of the most popular React slider components out there, and with just a few lines of code:
import Slider from "rc-slider";
import 'rc-slider/assets/index.css';
export default () => (
<>
<div className="slider">
<h4 className="slider-title">Time Window: (07:00 - 07:15)</h4>
<Slider/>
</div>
</>
);
You can build a simple slider that looks just like this:
With a bit more styling from MapBoxTimelineSlider.scss
(link), we can add a nice scale and position the slider at the center left of our map.
Integrating rc slider into our app takes a bit more work. Basically, there are two important sources of data in our app:
timePeriods
: an array that contains a list of specially formatted date time strings.
singaporeTaxiHexagons
: the JavaScript object imported from the booking data file described in the previous section.
Using the <Slider/> component, we are going to increment a global counter currentStep
to retrieve the corresponding timePeriod
date time string, which is then used to access the booking counts for singaporeTaxiHexagons
from that time period. For example, if currentStep
= 2, timePeriods[2]
= "03/08/2016 06:30:00-03/08/2016 06:45:00"
and singaporeTaxiHexagons["03/08/2016 06:30:00-03/08/2016 06:45:00"]
=
which is a JavaScript object that maps taxi booking counts to their corresponding H3 cells - exactly what we need!
The React range slider component
Here is what the full rc-slider component looks like:
/* MapboxTimelineSlider.jsx */
<div className="slider">
<h4 className="slider-title">Time Window: ({sliderTitle})</h4>
<Slider
onChange={handleSliderChange}
min={0}
max={timePeriods.length - 1}
defaultValue={currentStep}
value={currentStep}
/>
</div>
onChange
: The onChange
prop lets your provide a callback (in this example, its the handleSliderChange()
function) that is triggered whenever the value of the slider changes.
min
: The minimum value of the slider. Since we are using the slider value to retrive elements from the timePeriods
array, we are setting this to "0" so that we never risk running into an index out of bounds error.
max
: The maximum value of the slider. Like with min
, we set this to timePeriods.length - 1
to avoid index out of bounds errors.
value
: The current value of the slider. We set this to be equal to currentStep
to keep it in sync with the data being displayed.
handleSliderChange() method
When the user moves the slider, the new value is passed on to the handleSliderChange()
method:
/* MapboxTimelineSlider.jsx */
const handleSliderChange = (step) => {
setCurrentStep(step);
setSliderTitle(timePeriods[step].substring(11,16) + " - " + timePeriods[step].substring(31,36))
updateHexagonData(step);
};
Here's a breakdown of what this method does:
- First, we set the
currentStep
to the value ofstep
, which is the new value of the slider. For example, if the user moves the slider one unit to the right, the new slider value of 1 is passed on tostep
which is used to update the value ofcurrentStep
via its correspondinguseState
hook (const [currentStep, setCurrentStep] = useState(0);
). - Next, we update the title of the slider based on the value of
currentStep
e.g. ifcurrentStep
is 1,timePeriods[1]
is "03/08/2010 06:15:05-03/08/2010 06:30:05" so the slider title will be a shortened "06:15 - 06:30". - Finally, a call is made to the
updateHexagonData()
method to load the taxi booking data that matchescurrentStep
.
updateHexagonData() method
The updateHexagonData()
method retrieves taxi booking data as a tuple { hexindex7, bookingCount}
for the 15 minute time period that matches value of step
passed to it. This data is stored in the sgHexagonsArr
array.
/* MapboxTimelineSlider.jsx */
const updateHexagonData = (step) => {
const singaporeHexagonsArr = singaporeTaxiHexagons[timePeriods[step]];
const sgHexagonsArr = [];
singaporeHexagonsArr.forEach(singaporeHexagon => {
sgHexagonsArr.push({
hexindex7: singaporeHexagon.key,
bookingCount: singaporeHexagon.count
});
})
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);
}
After that, we map sgHexagonsArr
to a new array rs
("row style") of Mapbox style objects, and use the getStyle()
method to map booking counts to the corresponding color
and opacity
values (see next section).
Lastly, 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.
/* MapboxTimelineSlider.jsx.js <Source/> component */
<Source
type="geojson"
data={{
type: "FeatureCollection",
features: singaporeHexagonsArr
}}
>
<Layer/>
</Source>
getStyle() method
The final method we'll look at is getStyle()
, which basically maps taxi booking counts to a range of colors and opacity values for the associated H3 hexagon.
/* MapboxTimelineSlider.jsx */
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) < 50) {
return styles[0];
}
if (Number(row.bookingCount) < 100) {
return styles[1];
}
if (Number(row.bookingCount) < 150) {
return styles[2];
}
if (Number(row.bookingCount) < 200) {
return styles[3];
}
return styles[4];
};
These styles are rendered in the <Layer/> component of our map, with the fill-color
and fill-opacity
values retrieved from the parent <Source/> component.
Testing the React range slider component
Clone this project's repository and in your terminal, run npm install
to install dependencies followed by npm start
to run the app.
Spin up localhost:3000 on your favorite browser and if everything worked correctly, you should be able to see a map of Singapore (above). Use the rc-slider range slider on the left to step through each 15 minute time period starting from 6 am. You'll see that taxi demand starts off low in the early morning but quickly picks up with sustained demand coming from the Central Business District, the Airport and a stretch of high density housing across Singapore's East Coast.
And that's the end of this tutorial series! You now have a fully working example of how to build an interactive map of taxi demand across Singapore using h3-js, react-map-gl and rc-slider.
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
Part 3: Visualizing taxi demand over time with Mapbox and React range slider rc slider
👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.