Uber h3 js tutorial: How to draw hexagons on a map

I thought it would be fun to go outside of my comfort zone and write about something completely different and new. So today, I'm going to show you how to use the h3-js library to draw hexagons on a map! Here's what we are going to build by the end of this series - an interactive map of taxi demand across Singapore.

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 (this article)
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

Recently, I had the opportunity to use h3-js in a data visualization project for a client, and I wanted to share what I've learned. For those of you not familiar with h3-js, it's the JavaScript port of H3, Uber's hexagon-based geospatial indexing system. Hexagons (unlike say, squares or triangles) approximate various shapes like neighborhoods and city blocks more accurately. This flexibility makes H3 perfect for diverse geospatial use cases, including transportation, urban planning, logistics, and location-based services (also - Hexagons are the Bestagons).

Afi Labs delivers custom software to meet your logistics needs. From vehicle routing, live tracking to proof of delivery, we've got you covered!
👋 Say Hello!

I've created a new Github repository with the examples from this post, all based on h3-js. To try them out, clone the repository, run npm install followed by npm start .

To make the most of this tutorial, you need to have basic experience writing applications in JavaScript in general and React, specifically. This is because we'll be using the amazing react-map-gl library to render our Mapbox base map and programmatically add data to it, so in some ways this post doubles up as a React and react-map-gl tutorial as well. Let's get started.

Introduction to the h3-js API

The core of the h3-js library are the functions that provide the H3 index for geographic coordinates and vice versa. A H3 index e.g. "876520c86ffffff" is the unique "id" of the hexagon, which is defined by two pieces of information: a center coordinate and the "size" or resolution. The higher the resolution of the grid, the smaller the hexagons, from res 0 (continental) to res 15 (1 square meter). Res 9 is roughly a city block. For this tutorial, we'll be using res 7 which roughly corresponds to a large neighborhood.

First, import the required methods from h3-js (note: all the demo code listed here can be found in h3ExampleMethods.js in the Github repository:

/* h3ExampleMethods.js */
const { cellToLatLng, latLngToCell, cellToBoundary } = require('h3-js');

cellToLatLng

To convert a H3 index to a latitude and longitude, we use the cellToLatLng() method (API reference) to obtain the center of the hexagon:

cellToLatLng(h3Index) -> CoordPair

Input

For a given h3Index H3IndexInput string:

/* h3ExampleMethods.js */
function cellToLatLngExample() {
  const cell = '876520d95ffffff';
  return cellToLatLng(cell);
}

Output

Returns the center coordinate as a CoordPair [lat, lng] pair:

[ 1.3049570274662716, 103.84333471371895 ]

latLngToCell

The inverse of cellLatToLng() is latLngToCell() (API reference) which takes in a latitude and longitude pair together with a specified resolution and returns the H3 index of the hexagon that it belongs to.

latLngToCell(lat, lng, res) -> H3Index

Input

For a lat number (latitude), lng number (longitude) and res number (resolution):

/* h3ExampleMethods.js */
function latLngToCellExample() {
    // Newton Hawker Center
    const lat = 1.3119888;
    const lng = 103.8369993;
    const res = 7;

    return latLngToCell(lat, lng, res);
}

Output

Returns the H3Index string of the containing hexagon.

876520d95ffffff

cellToBoundary

To draw the H3 cell on a map, you need the coordinates of each vertex. The cellToBoundary() method (API reference) accepts a H3Index string and returns and array of [lat, lng] pairs corresponding to the cell's vertices.

cellToBoundary(h3Index) -> Array.<CoordPair>

Input

For a given h3Index H3IndexInput string:

/* h3ExampleMethods.js */
function cellToBoundaryExample() {
    const cell = '876520d95ffffff';

    return cellToBoundary(cell);
}

Output

[
  [ 1.3013837085594995, 103.85630417742213 ],
  [ 1.314435751149279, 103.85314992083924 ],
  [ 1.3180097264283908, 103.84017971194217 ],
  [ 1.3085305652330135, 103.83036417616988 ],
  [ 1.295478307425089, 103.8335196239885 ],
  [ 1.2919054258381997, 103.84648941655875 ]
]

Returns the vertices of the H3 cell as an Array.<CoordPair> i.e. an array of [lat, lng] pairs.

To run these examples, clone the Github repository and in your terminal, navigate to the /src/tasks folder and run npx run-func h3ExampleMethods.js <method_name> e.g. npx run-func h3ExampleMethods.js cellToBoundaryExample.

With these building blocks in place - cellToLatLng(), latLngToCell() and cellToBoundary(), we can start drawing hexagons on a map.

How our code is organized

Like most projects on this blog, we scaffolded this one using the Create React App with code that follows this standard folder structure:

my-app/
  README.md
  node_modules/
  package.json
  public/
  src/
    components/
       MapBoxSingleHex.jsx
    tasks/
       h3ExampleMethods.js
    App.css
    App.js
    App.test.js
    index.css
    index.jsx
    logo.svg

The new files in the /src folder are tasks/h3ExampleMethods.js which we used to test the H3 indexing methods described earlier and components/MapBoxSingleHex.jsx which will be our main focus going forward.

MapBoxSingleHex.jsx

/components/MapBoxSingleHex.jsx is made available to the user from index.jsx, the entry point to our React app by importing the file:

/* index.jsx */
import MapBoxSvg from './components/MapBoxSingleHex';

and rendering it:

/* index.jsx */
root.render(
  <React.StrictMode>
    <MapBoxSvg />
  </React.StrictMode>
);

The MapBoxSvg component, which implements the react-map-gl map layer to draw the hexagon, is shown below (you can download and run this code by visiting the h3js_hexagons Github repository).

/* mapBoxSingleHex.jsx */
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 hexindex7Id = "876520d95ffffff";

  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",
            }}
          >
            <Source
              type="geojson"
              data= {{
                "type": "Feature",
                "geometry": {
                  "type": "Polygon",
                  "coordinates": [cellToBoundary(hexindex7Id, true)]
                },
                "id": "abc123"
              }}
            >
              <Layer
                {...{
                  type: "fill",
                  paint: {
                    'fill-outline-color': 'white',
                    "fill-color": "#E14C48",
                    "fill-opacity": 0.7,
                  },
                }}
              />
              
            </Source>
          </Map>
        </div> 
      </div>
  );

};

export default MapBox;

Let's work through the code to figure out what's going on.

/* mapBoxSingleHex.jsx */
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";

Read from top to bottom, this imports:

  • The default Mapbox GL stylesheet mapbox-gl/dist/mapbox-gl.css.
  • The Map, { Layer, Source, MapRef, Marker } components from react-map-gl.
  • Two specific hooks ( useRef and useState) used by React to create and manage state, as well as reference DOM elements.
  • The cellToBoundary() method from h3-js that returns the H3 cell's vertex coordinates.

The heavy lifting is done in the MapBox() function:

/* mapBoxSingleHex.jsx */
function MapBox() {

  const hexindex7Id = "876520d95ffffff";

  return (
      <div>
        <div className="map">
          <Map
            // Map component code
          >
            <Source
              // Source component code
            >
              <Layer
                // Layer component code
              />
            </Source>
          </Map>
        </div> 
      </div>
  );
};

First, we store the H3 index of the cell we want to display:

/* mapBoxSingleHex.jsx */
function MapBox() {

  const hexindex7Id = "876520d95ffffff";

  // Rest of MapBox() code
};

Next, we return a JSX component (a virtual representation of HTML that can be easily combined with JavaScript expressions) that contains the map layer.

/* mapBoxSingleHex.jsx */
function MapBox() {

  // Earlier code

  return (
      <div>
        <div className="map">
          <Map
            // Map component code
          >
            <Source
              // Source component code
            >
              <Layer
                // Layer component code
              />
            </Source>
          </Map>
        </div> 
      </div>
  );
};

This React style code is very different from your typical vanilla JavaScript implementation of Mapbox GL (e.g. add a polygon to a map using a GeoJSON source) which requires you to use the addSource() function to add GeoJSON data to a polygon before calling addLayer() to create a new fill layer and applies paint properties to style the polygon's appearance.

Instead, because react-map-gl gives us a ready made collection of React components that play well with Mapbox, we can inject data and style our map using Source and Layer components like this:

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

I'll walk through each component individually and explain what each does.

<Map/>

/* mapBoxSingleHex.jsx */

          <Map
            initialViewState={{
              latitude: 1.290270,
              longitude: 103.851959,
              zoom: 10,
              bearing: 0,
              pitch: 0,
            }}
            mapStyle="mapbox://styles/mapbox/light-v9"
            mapboxAccessToken="MAPBOX_ACCESS_TOKEN"
            style={{
              height: "100vh",
              width: "100vw",
            }}
          >
            <Source>
              <Layer/>
            </Source>
          </Map>
 

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

<Source/>

/* mapBoxSingleHex.jsx */

          <Source
              type="geojson"
              data= {{
                "type": "Feature",
                "geometry": {
                  "type": "Polygon",
                  "coordinates": [cellToBoundary(hexindex7Id, true)]
                },
                "id": "abc123"
              }}
            >
              <Layer/>
            </Source>
            

The <Source/> component stores data that we can display on the map using <Layer/>. In this example, we specify that the data type is "geojson" (a format for encoding a variety of geographic data structures such as points, lines, polygons etc), with geometry "Polygon" and vertices (coordinates) set to cellToBoundary("876520d95ffffff") which returns an array of latitude and longitude points.

<Layer/>

/* mapBoxSingleHex.jsx */
              <Layer
                {...{
                  type: "fill",
                  paint: {
                    'fill-outline-color': 'white',
                    "fill-color": "#E14C48",
                    "fill-opacity": 0.7,
                  },
                }}
              />
            

Now we can begin styling the <Source/> data set with the <Layer/> component. Here, we use ES6 destructuring ("...") to unpack the style object and inject it into <Layer/>. We instruct Mapbox to draw the polygon with fill-color "#E14C48" (dark red) and give it a fill-opacity of 0.7 (semi transparent) and fill-outline-color "white" (white border).

Here's the final result:

So that's how you draw a hexagon on a map in React using the Uber h3-js library and react-map-gl. This isn't too useful right now, but in the next section I'll show you how to use what we learned in this post to build a heatmap of taxi demand in Singapore.

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

Next: Part 2: Mapping taxi demand with Uber h3 js and react map gl