Categories
Visx

Add a Map View into Our React App with the Visx Library

Spread the love

Visx is a library that lets us add graphics to our React app easily.

In this article, we’ll look at how to use it to add a map view into our React app

Install Required Packages

We have to install a few modules to create the map.

To get started, we run:

npm i @visx/geo @visx/responsive @visx/scale @visx/zoom

to install the packages.

Add the Map

We can add the map by writing:

import React, { useState } from "react";
import * as topojson from "topojson-client";
import { scaleQuantize } from "@visx/scale";
import { CustomProjection, Graticule } from "@visx/geo";
import { Projection } from "@visx/geo/lib/types";
import { Zoom } from "@visx/zoom";
import {
  geoConicConformal,
  geoTransverseMercator,
  geoNaturalEarth1,
  geoConicEquidistant,
  geoOrthographic,
  geoStereographic
} from "d3-geo";
import topology from "./world-topo.json";

export const background = "#252b7e";
const purple = "#201c4e";
const PROJECTIONS = {
  geoConicConformal,
  geoTransverseMercator,
  geoNaturalEarth1,
  geoConicEquidistant,
  geoOrthographic,
  geoStereographic
};

const world = topojson.feature(topology, topology.objects.units);
const color = scaleQuantize({
  domain: [
    Math.min(...world.features.map((f) => f.geometry.coordinates.length)),
    Math.max(...world.features.map((f) => f.geometry.coordinates.length))
  ],
  range: [
    "#019ece",
    "#f4448b",
    "#fccf35",
    "#82b75d",
    "#b33c88",
    "#fc5e2f",
    "#f94b3a",
    "#f63a48",
    "#dde1fe",
    "#8993f9",
    "#b6c8fb",
    "#65fe8d"
  ]
});

function Example({ width, height, events = true }: GeoCustomProps) {
  const [projection, setProjection] = useState("geoConicConformal");

  const centerX = width / 2;
  const centerY = height / 2;
  const initialScale = (width / 630) * 100;

  return width < 10 ? null : (
    <>
      <Zoom
        width={width}
        height={height}
        scaleXMin={100}
        scaleXMax={1000}
        scaleYMin={100}
        scaleYMax={1000}
        transformMatrix={{
          scaleX: initialScale,
          scaleY: initialScale,
          translateX: centerX,
          translateY: centerY,
          skewX: 0,
          skewY: 0
        }}
      >
        {(zoom) => (
          <div className="container">
            <svg
              width={width}
              height={height}
              className={zoom.isDragging ? "dragging" : undefined}
            >
              <rect
                x={0}
                y={0}
                width={width}
                height={height}
                fill={background}
                rx={14}
              />
              <CustomProjection
                projection={PROJECTIONS[projection]}
                data={world.features}
                scale={zoom.transformMatrix.scaleX}
                translate={[
                  zoom.transformMatrix.translateX,
                  zoom.transformMatrix.translateY
                ]}
              >
                {(customProjection) => (
                  <g>
                    <Graticule
                      graticule={(g) => customProjection.path(g) || ""}
                      stroke={purple}
                    />
                    {customProjection.features.map(({ feature, path }, i) => (
                      <path
                        key={`map-feature-${i}`}
                        d={path || ""}
                        fill={color(feature.geometry.coordinates.length)}
                        stroke={background}
                        strokeWidth={0.5}
                        onClick={() => {
                          if (events)
                            alert(
                              `Clicked: ${feature.properties.name} (${feature.id})`
                            );
                        }}
                      />
                    ))}
                  </g>
                )}
              </CustomProjection>
              <rect
                x={0}
                y={0}
                width={width}
                height={height}
                rx={14}
                fill="transparent"
                onTouchStart={zoom.dragStart}
                onTouchMove={zoom.dragMove}
                onTouchEnd={zoom.dragEnd}
                onMouseDown={zoom.dragStart}
                onMouseMove={zoom.dragMove}
                onMouseUp={zoom.dragEnd}
                onMouseLeave={() => {
                  if (zoom.isDragging) zoom.dragEnd();
                }}
              />
            </svg>
            {events && (
              <div className="controls">
                <button
                  className="btn btn-zoom"
                  onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}
                >
                  +
                </button>
                <button
                  className="btn btn-zoom btn-bottom"
                  onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}
                >
                  -
                </button>
                <button className="btn btn-lg" onClick={zoom.reset}>
                  Reset
                </button>
              </div>
            )}
          </div>
        )}
      </Zoom>
      <label>
        projection:{" "}
        <select onChange={(event) => setProjection(event.target.value)}>
          {Object.keys(PROJECTIONS).map((projectionName) => (
            <option key={projectionName} value={projectionName}>
              {projectionName}
            </option>
          ))}
        </select>
      </label>
      <style jsx>{`
        .container {
          position: relative;
        }
        svg {
          cursor: grab;
        }
        svg.dragging {
          cursor: grabbing;
        }
        .btn {
          margin: 0;
          text-align: center;
          border: none;
          background: #dde1fe;
          color: #222;
          padding: 0 4px;
          border-top: 1px solid #8993f9;
        }
        .btn-lg {
          font-size: 12px;
          line-height: 1;
          padding: 4px;
        }
        .btn-zoom {
          width: 26px;
          font-size: 22px;
        }
        .btn-bottom {
          margin-bottom: 1rem;
        }
        .controls {
          position: absolute;
          bottom: 20px;
          right: 15px;
          display: flex;
          flex-direction: column;
          align-items: flex-end;
        }
        label {
          font-size: 12px;
        }
      `}</style>
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <Example width={500} height={300} />
    </div>
  );
}

world-topo.json can be found at https://codesandbox.io/s/github/airbnb/visx/tree/master/packages/visx-demo/src/sandboxes/visx-geo-custom?file=/world-topo.json

We add the background and purple to add the colors for the map.

PROJECTIONS have the types of map views that are available.

We transform the data into something that we can display by calling the topojson.feature method with the JSON.

The scaleQuantize method lets us add the colors to the countries in the map.

In the Example component, we have the projection state to display the map with the given projection type.

We set the center coordinatesd with the centerX and centerY variables.

And we set the initialScale variable to the initial zoom level.

To display the map, we add the Zoom component to add the zoom.

Then in its render prop, we add the CustomProjection component to add the map.

And the Graticule component lets us add the grid lines on the map.

Below that, we add the rectangle to let us drag to pan the map with the touch and mouse handlers.

And below that, we add the buttons to add zoom and reset zoom capabilities.

Then we add the select dropdown to let us select the project by changing the projection state.

We used that in the CustomProjection component.

Finally, we add the styles for the map.

Conclusion

We can add a map view with the components that are available with the Visx library.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *