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.