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 create graphics with zoom and pan in our React app.
Install Required Packages
We have to install a few modules.
To get started, we run:
npm i @visx/clip-path @visx/event @visx/mock-data @visx/responsive @visx/scale @visx/zoom d3-scale-chromatic
to install the packages.
Create the Graphics
We can create the chart by adding the items provided by the modules.
We use the data from the @visx/mock-data
module.
To create the chord diagram, we write:
import React, { useState } from "react";
import { interpolateRainbow } from "d3-scale-chromatic";
import { Zoom } from "@visx/zoom";
import { localPoint } from "@visx/event";
import { RectClipPath } from "@visx/clip-path";
import genPhyllotaxis from "@visx/mock-data/lib/generators/genPhyllotaxis";
import { scaleLinear } from "@visx/scale";
const bg = "#0a0a0a";
const points = [...new Array(1000)];
const colorScale = scaleLinear({ range: [0, 1], domain: [0, 1000] });
const sizeScale = scaleLinear({ domain: [0, 600], range: [0.5, 8] });
const initialTransform = {
scaleX: 1.27,
scaleY: 1.27,
translateX: -211.62,
translateY: 162.59,
skewX: 0,
skewY: 0
};
function Example({ width, height }) {
const [showMiniMap, setShowMiniMap] = useState(true);
const genenerator = genPhyllotaxis({
radius: 5,
width,
height
});
const phyllotaxis = points.map((d, i) => genenerator(i));
return (
<>
<Zoom
width={width}
height={height}
scaleXMin={1 / 2}
scaleXMax={4}
scaleYMin={1 / 2}
scaleYMax={4}
transformMatrix={initialTransform}
>
{(zoom) => (
<div className="relative">
<svg
width={width}
height={height}
style={{ cursor: zoom.isDragging ? "grabbing" : "grab" }}
>
<RectClipPath id="zoom-clip" width={width} height={height} />
<rect width={width} height={height} rx={14} fill={bg} />
<g transform={zoom.toString()}>
{phyllotaxis.map(({ x, y }, i) => (
<React.Fragment key={`dot-${i}`}>
<circle
cx={x}
cy={y}
r={i > 500 ? sizeScale(1000 - i) : sizeScale(i)}
fill={interpolateRainbow(colorScale(i) ?? 0)}
/>
</React.Fragment>
))}
</g>
<rect
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();
}}
onDoubleClick={(event) => {
const point = localPoint(event) || { x: 0, y: 0 };
zoom.scale({ scaleX: 1.1, scaleY: 1.1, point });
}}
/>
{showMiniMap && (
<g
clipPath="url(#zoom-clip)"
transform={`
scale(0.25)
translate(${width * 4 - width - 60}, ${
height * 4 - height - 60
})
`}
>
<rect width={width} height={height} fill="#1a1a1a" />
{phyllotaxis.map(({ x, y }, i) => (
<React.Fragment key={`dot-sm-${i}`}>
<circle
cx={x}
cy={y}
r={i > 500 ? sizeScale(1000 - i) : sizeScale(i)}
fill={interpolateRainbow(colorScale(i) ?? 0)}
/>
</React.Fragment>
))}
<rect
width={width}
height={height}
fill="white"
fillOpacity={0.2}
stroke="white"
strokeWidth={4}
transform={zoom.toStringInvert()}
/>
</g>
)}
</svg>
<div className="controls">
<button
type="button"
className="btn btn-zoom"
onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}
>
+
</button>
<button
type="button"
className="btn btn-zoom btn-bottom"
onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}
>
-
</button>
<button
type="button"
className="btn btn-lg"
onClick={zoom.center}
>
Center
</button>
<button type="button" className="btn btn-lg" onClick={zoom.reset}>
Reset
</button>
<button type="button" className="btn btn-lg" onClick={zoom.clear}>
Clear
</button>
</div>
<div className="mini-map">
<button
type="button"
className="btn btn-lg"
onClick={() => setShowMiniMap(!showMiniMap)}
>
{showMiniMap ? "Hide" : "Show"} Mini Map
</button>
</div>
</div>
)}
</Zoom>
<style jsx>{`
.btn {
margin: 0;
text-align: center;
border: none;
background: #2f2f2f;
color: #888;
padding: 0 4px;
border-top: 1px solid #0a0a0a;
}
.btn-lg {
font-size: 12px;
line-height: 1;
padding: 4px;
}
.btn-zoom {
width: 26px;
font-size: 22px;
}
.btn-bottom {
margin-bottom: 1rem;
}
.description {
font-size: 12px;
margin-right: 0.25rem;
}
.controls {
position: absolute;
top: 15px;
right: 15px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.mini-map {
position: absolute;
bottom: 25px;
right: 15px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.relative {
position: relative;
}
`}</style>
</>
);
}
export default function App() {
return (
<div className="App">
<Example width={500} height={300} />
</div>
);
}
We add the background color with the bg
variable.
points
have an array that will be filled with the points for the phyllotaxis spiral graphic which we can zoom and pan.
initialTransform
has the initial zoom and pan values.
In the Example
component, we have the showMiniMap
state to toggle the mini-map.
The mini-map lets us see where we are in the graphic.
To generate the graphic, we call the genPhyllotaxis
to generate the phyllotaxis spiral graphic.
Then we create the phyllotaxis
variable to generate the points for the ring.
Next, we return the Zoom
component to let us zoom in and out of the graphic.
We set the scales and the initial transformation to display the initial graphic.
Then to create the phyllotaxis graphic, we call phylloTaxis.map
to render circles for the spiral.
We create the fill color for the phyllotaxis with the interpolateRainbow
method.
This will create a rainbow color effect for it.
Below that, we create the mini-map with the g
element.
Inside it, we create a fragment with the mini version of the phyllotaxis that’s shown in the mini-map.
The rect
element below that draws a rectangle around the area that’s displayed in the mini-map.
Below that, we create the buttons to let us zoom in and out, center the graphic, and reset the graphic to its initial position.
And we add some styles the position the graphic display with the style
element.
Conclusion
We can create graphics and that we can zoom and pan in our React app with the libraries modules provided by Visx.