Map styles

Styles and patterns used in the maps

Data-driven styles

To modify the width of circles and lines based on the zoom level, Mapbox data-driven styles were used to set stops set with pairs of zoom level and matching pixel width. These pairs are stops and are set as show in the following code for styling lines in a vector tile layer.

return {
    id: this.layerIdName,
    type: "line",
    layout: {
        "line-join": "round",
        "line-cap": "round",
    },
    paint: {
        "line-color": "green",
    "line-width": {
        base: 1,
        stops: [
          [10, 1], // [zoom, width in px]
          [13, 2],
          [16, 10],
          [18, 25],
        ],
      },              
    },
}

The stops key above contain a 2D array which holds pairs of zoom levels and widths in pixels. Out-of-the-box, Mapbox will use calculate the values between these stops to adjust the circle radius linearly between stops levels.

react-map-gl-draw does not support data-driven styles out-of-the-box so a couple of functions were written to calculate the width of circles in the drawing UI when the zoom level updates. These two functions are called getCircleRadiusByZoom and linearInterpolation.

The first step is to traverse the first level of the stops array and find where the currentZoom level fits into the brackets made up by pairs in the second depth of the array. If the currentZoom value falls below the first set of level of zoom, the smallest pixel width is return. Similarly, if the currentZoom is above the highest level of zoom, the highest pixel width is returned. If the currentZoom is somewhere in between the minimum and maximum, the value is compared to two elements of the first level of the array which creates a bracket of zooms and widths. This provides four of the known values described next.

Next, the calculation come down to a bit of Algebra to find the unknown, currentWidth, from the known values - minZoom, currentZoom, maxZoom, minWidth, and maxWidth. This was all stitched together by translating the formula for linear interpolation into code. More here.

Note that the base key in the above sample code is set to 1. If any other value was used, the linearInterpolation function would need to be updated to account for the different rate of change in the output of the function. Luckily, the base of 1 makes it simple and looks nice.

Map control styles

One challenge with adding custom controls to the map is mouse events propagating through the control and interacting with the map below them. This can lead to users accidentally selecting features or interacting with the map when they were really trying to zoom, use a geocoder, or select a layer to show on the map. This was managed in a few ways depending on the control and whether it is a control native to react-map-gl or not.

Native controls

For native controls, like the NavigationControl component, react-map-gl adds props that can be adjusted to control the behavior of mouse events. One example is captureClick which can be set to false to stop propagation of a click to the map below. See a simplified version of this from the app below.

<Box className={classes.mapBox}>
  <ReactMapGL>
    <div className={classes.navStyle}>
      {/* captureClick prop set to false */}
      <NavigationControl captureClick={false} />
    </div>
    {/* Map sources and layers here */}
  </ReactMapGL>
</Box>

Custom controls

Custom controls built with JSX can also be added to the map, but they also present the same challenge of mouse event propagation. For these controls, the solution is to render them outside of the open and close component tags of the map instance. Using CSS, you can position the rendered controls over the map and tune where they are placed in relation to the other controls. See a simplified version of this from the app below.

<Box className={classes.mapBox}>
  {/* Render custom control outside ReactMapGL component */}
  {renderLayerSelect()}
  <ReactMapGL>
    {/* Map sources and layers here */}
  </ReactMapGL>
</Box>

Geocoder controls

When using react-map-gl-geocoder, the Geocoder component contains a prop called containerRef where you can pass a ref that is tied to an external JSX element to tell the library where to place the geocoder input box. The styles of the geocoder input can also be customized using CSS to set where it is positioned on top of the map. This is show in a simplified version below and also in an example in the documentation.

<Box className={classes.mapBox}>
  {/* Render custom control outside ReactMapGL component */}
  {/* and add a ref created with useRef */}
  <div
    ref={mapControlContainerRef}
    style={{
      display: "flex",
      height: 50,
      position: "absolute",
      alignItems: "center",
      right: 32,
    }}
  />
  <ReactMapGL>
    <Geocoder
      mapRef={mapRef}
      onViewportChange={handleGeocoderViewportChange}
      mapboxApiAccessToken={MAPBOX_TOKEN}
      {/* The ref created for the div above is passed here */}
      containerRef={mapControlContainerRef}
    />
  </ReactMapGL>
</Box>

Last updated