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.