Supercluster with @react-native-mapbox-gl/maps
Tags: mapbox, reactnative, supercluster
Published: 2019-09-06 12:42:33

---
title: Supercluster with @react-native-mapbox-gl/maps
published: true
tags: mapbox,react native,supercluster
canonical_url: https://sethaalexander.com/supercluster-with-react-native-mapbox-gl-maps/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/b76v9736pikjoj7hvflg.gif
---

During a recent project in my work at [Airship](https://teamairship.com) I had to stop using the built in cluster functionality that `@react-native-mapbox-gl/maps` provides and utilize [Supercluster](https://github.com/mapbox/supercluster) instead. The reason is we need access to the points that make up the clusters. We had some items that never broke out of their clusters because they had the same exact longitude & latitude combination. As well as wanting to show a slide up view of those locations in a list view. What started me down this path was an issue on the deprecated `react-native-mapbox-gl` library which shares a lot of functionality with the new library. You can view that issue [here](https://github.com/nitaliano/react-native-mapbox-gl/issues/1122). I’m honestly surprised that this functionality isn’t available in the library since it _is_ supported in the Mapbox JS SDK as documented [here](https://docs.mapbox.com/mapbox-gl-js/api/#geojsonsource) with the `getClusterLeaves()` function. I noticed people asking how to do this so when I nailed it down I knew a how-to was coming.

## Without Supercluster

I setup a code example [here](https://snack.expo.io/@sethalexander/before-supercluster) to show a contrived starting state utilizing the built in clustering functionality. This is essentially where I started with the core Mapbox functionality. Let’s walk through this code some.

```jsx
render() {
  return (
    <View style={{ flex: 1 }}>
      <MapboxGL.MapView
        ref={c => (this._map = c)}
        zoomEnabled
        style={[{ flex: 1 }]}
      >
        {this.renderPoints()}
      </MapboxGL.MapView>
    </View>
  );
}
```

Here’s our MapboxGL container to setup our map. We then need to render all our points on the map and we handle that in a helper function `renderPoints()`.

```jsx
renderPoints = () => {
  const { points } = this.state;

  return (
    <MapboxGL.ShapeSource
      id="symbolLocationSource"
      hitbox={{ width: 18, height: 18 }}
      onPress={this.onMarkerSelected}
      shape={points}
      cluster
    >
      <MapboxGL.SymbolLayer
        minZoomLevel={6}
        id="pointCount"
        style={mapStyles.clusterCount}
      />

      <MapboxGL.CircleLayer
        id="clusteredPoints"
        minZoomLevel={6}
        belowLayerID="pointCount"
        filter={["has", "point_count"]}
        style={mapStyles.clusteredPoints}
      />

      <MapboxGL.SymbolLayer
        id="symbolLocationSymbols"
        minZoomLevel={6}
        filter={["!has", "point_count"]}
        style={mapStyles.icon}
      />
    </MapboxGL.ShapeSource>
  );
};
```

So here we’re getting the list of groups from state and passing them into `MapboxGL.ShapeSource` and toggling the `cluster` functionality on. We have three layers inside there which I’ll refer to by ID values.  
– `pointCount` is the actual numerical value of the number of items that make up the cluster.  
– `clusteredPoints` is the cluster circle which we see is set to be below the `pointCount` layer.  
– `symbolLocationSymbols` is the map marker for a single location on the map that isn’t being clustered.

When we click a marker, whether it’s a cluster or single point on the map we call `onMarkerSelected` which currently only has functionality implemented for non-clusters like so:

```js
onMarkerSelected = event => {
  const point = event.nativeEvent.payload;
  const { name, cluster } = point.properties;
  const coordinates = point.geometry.coordinates;

  if (cluster) {
    console.log(cluster);
  } else {
    this.setState(
      {
        selectedPointName: name,
        selectedPointLat: coordinates[1],
        selectedPointLng: coordinates[0],
      },
      () => {
        this.map.flyTo(point.geometry.coordinates, 500);
      }
    );
  }
};
```

The `if/else` statement is just logging the cluster if there is one otherwise it’s setting the state to the selected point and centering the map on that point. The idea of adding the point info to state is to do something with that info.

We decide if we’re going to render the circle or marker based on the `filter` criteria being passed into the `CircleLayer` and `SymbolLayer`.

```js
const mapStyles = MapboxGL.StyleSheet.create({
  icon: {
    iconAllowOverlap: true,
    iconSize: 0.35
  },
  clusteredPoints: {
    circleColor: "#004466",
    circleRadius: [
      "interpolate",
      ["exponential", 1.5],
      ["get", "point_count"],
      15,
      15,
      20,
      30
    ],
    circleOpacity: 0.84
  },
  clusterCount: {
    textField: "{point_count}",
    textSize: 12,
    textColor: "#ffffff"
  }
});
```

This last piece provides some of the styling and actually won’t change when we swap out the clustering functionality.

## Implementing Supercluster

So the idea of switching to Supercluster to to replace the built in clustering of the raw `FeatureCollection` data. Supercluster is not going to do anything with the actual rendering of that data. We need to initialize a cluster using Supercluster then update that cluster based on the bounds of the map and zoom level. I’m going to walk through the guts of this conversion.

Firstly, you’ll need the cluster which I decided to store in state. I think this makes the most sense and works well for me.

```js
const collection = MapboxGL.geoUtils.makeFeatureCollection(groupFeatures);
const cluster = new Supercluster({ radius: 40, maxZoom: 16 });
cluster.load(collection.features);

this.setState({
  point: collection,
  loading: false,
  selectedPoints: [],
  superCluster: cluster,
  userFound: false
});
```

So now I have the `FeatureCollection` as `state.groups` and the Supercluster cluster as `state.superCluster`. However, we will not be able to pass this `superCluster` into our `MapboxGL.ShapeSource` just yet. This cluster is immutable and essentially what we will now use to create the shape object we will pass into `ShapeSource`. Next let’s update our `MapView` like so:

```jsx
<MapboxGL.MapView
  ref={c => (this._map = c)}
  onRegionDidChange={this.updateClusters}
  zoomEnabled
  style={[{ flex: 1 }]}
>
  {this.renderPoints()}
</MapboxGL.MapView>
```

Notice the added `onRegionDidChange` which takes a callback function. This prop I found works the best for me, however, as the `react-native-mapbox-gl/maps` library continues to evolve there may be a better solution. This calls `updateClusters` after the map has been moved. Now lets take a look at the `updateClusters` function:

```js
updateClusters = async () => {
  const sc = this.state.superCluster;
  if (sc) {
    const bounds = await this._map.getVisibleBounds();
    const westLng = bounds[1][0];
    const southLat = bounds[1][1];
    const eastLng = bounds[0][0];
    const northLat = bounds[0][1];
    const zoom = Math.round(await this._map.getZoom());
    this.setState({
      superClusterClusters: sc.getClusters(
        [westLng, southLat, eastLng, northLat],
        zoom
      )
    });
  }
};
```

So, I take the cluster that was created in my `componentDidMount()` and set that to a local variable. Just in case I check to ensure it exists before doing anything else. Next, I get the visible bounds from the `_map` ref that is setup on the `MapView`. I extract the four bounds into their own variables mostly for ease of knowing what they are. I then get the zoom and round it to a whole number (I found decimal zooms gave Supercluster issues). Finally, I take all that information and create the appropriate clusters for those bounds and zoom level and save them in state to `superClusterClusters`.

This `superClusterClusters` is what gets fed into the `ShapeSource` in `renderPoints` like so:

```jsx
renderPoints = () => {
  const { superClusterClusters } = this.state;

  return (
    <MapboxGL.ShapeSource
      id="symbolLocationSource"
      hitbox={{ width: 18, height: 18 }}
      onPress={this.onMarkerSelected}
      shape={{ type: "FeatureCollection", features: superClusterClusters }}
    >
      <MapboxGL.SymbolLayer
        id="pointCount"
        minZoomLevel={6}
        style={mapStyles.clusterCount}
      />

      <MapboxGL.CircleLayer
        id="clusteredPoints"
        minZoomLevel={6}
        belowLayerID="pointCount"
        filter={[">", "point_count", 1]}
        style={mapStyles.clusteredPoints}
      />

      <MapboxGL.SymbolLayer
        id="symbolLocationSymbols"
        minZoomLevel={6}
        filter={["!", ["has", "point_count"]]}
        style={mapStyles.icon}
      />
    </MapboxGL.ShapeSource>
  );
};
```

Notice that the `shape` prop requires the creation of an object and I’m not passing in the `superClusterClusters` directly from the state. Also notice that the `cluster` prop is no longer included on `ShapeSource`. This is something I forgot about and caused me a lot of grief. The built in clustering was conflicting with my clustering.

Lastly we add in functionality for getting the info about each point out of the cluster when we touch it on the phone in our `onMarkerSelected()` like so:

```js
onMarkerSelected = event => {
  const point = event.nativeEvent.payload;
  const { name, cluster } = point.properties;
  const coordinates = point.geometry.coordinates;

  if (cluster) {
    const sc = this.state.superCluster;
    if (sc) {
      const points = sc
        .getLeaves(point.properties.cluster_id, Infinity)
        .map(leaf => ({
          selectedPointName: leaf.properties.name,
          selectedPointLat: leaf.geometry.coordinates[1],
          selectedPointLng: leaf.geometry.coordinates[0],
        }));
      this.setState({ selectedPoints: points });
      console.log(points);
    } else {
      this.setState(
        {
          selectedPoints: [
            {
              selectedPointName: name,
              selectedPointLat: coordinates[1],
              selectedPointLng: coordinates[0],
            },
          ],
        },
        () => {
          this.camera.flyTo(point.geometry.coordinates, 500);
        }
      );
    }
  }
};
```

By using Superclusters `getLeaves()` we map those to a new array and set our `selectedPoints` state to it. We can now use this new data in state to render something in the UI.

### Final Thoughts

While there might seem like there are many steps involved in adding Supercluster to a `react-native-mapbox-gl/maps` map to access the underlying points of a cluster most of the code that I have shared can be reused as is to ease the transition.

For reference, the exact versions I’m using in this example are:  
`react-native-mapbox-gl/maps`: 7.0.1  
`supercluster`: 6.0.2  
A final code sample is available [here](https://snack.expo.io/@sethalexander/after-supercluster).

**NOTE:** The code samples WILL NOT WORK. Although, if you’re reading this article I’m making the assumption you already have Mapbox implemented. With that I also assume you have the library initialized with your access token.

I hope this saves people some headaches along the way and you build amazing things having access to the underlying data points that make up your clusters.

The post [Supercluster with @react-native-mapbox-gl/maps](https://sethaalexander.com/supercluster-with-react-native-mapbox-gl-maps/) appeared first on [Seth Alexander](https://sethaalexander.com).