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 radar charts into our React app.
Install Required Packages
We have to install a few modules.
To get started, we run:
npm i @visx/responsive @visx/network
to install the packages for the network diagram.
And we run:
npm i @visx/responsive @visx/pattern @visx/scale @visx/shape
to install the packages for the streamgraph.
Network Diagram
We can add a network diagram by writing:
import React from "react";
import { Graph } from "@visx/network";
const nodes = [
{ x: 50, y: 20 },
{ x: 200, y: 300 },
{ x: 300, y: 40 }
];
const links = [
{ source: nodes[0], target: nodes[1] },
{ source: nodes[1], target: nodes[2] },
{ source: nodes[2], target: nodes[0] }
];
const graph = {
nodes,
links
};
const background = "#272b4d";
function Example({ width, height }) {
return width < 10 ? null : (
<svg width={width} height={height}>
<rect width={width} height={height} rx={14} fill={background} />
<Graph graph={graph} />
</svg>
);
}
export default function App() {
return (
<div className="App">
<Example width={500} height={300} />
</div>
);
}
All we have to do is pass the nodes
into the Graph
component to create the network diagram.
Each nodes
entry has the x
and y
properties to set the coordinates for each node.
Streamgraph
We can create a streamgraph by writing:
import React, { useState } from "react";
import { Stack } from "[@visx/shape](https://medium.com/r/?url=http%3A%2F%2Ftwitter.com%2Fvisx%2Fshape "Twitter profile for @visx/shape")";
import { PatternCircles, PatternWaves } from "[@visx/pattern](https://medium.com/r/?url=http%3A%2F%2Ftwitter.com%2Fvisx%2Fpattern "Twitter profile for @visx/pattern")";
import { scaleLinear, scaleOrdinal } from "[@visx/scale](https://medium.com/r/?url=http%3A%2F%2Ftwitter.com%2Fvisx%2Fscale "Twitter profile for @visx/scale")";
import { transpose } from "d3-array";
import { animated, useSpring } from "react-spring";
function useForceUpdate() {
const [, setValue] = useState(0);
return () => setValue((value) => value + 1); // update state to force render
}
const getPoints = (array, pointCount) => {
const x = 1 / (0.1 + Math.random());
const y = 2 * Math.random() - 0.5;
const z = 10 / (0.1 + Math.random());
for (let i = 0; i < pointCount; i += 1) {
const w = (i / pointCount - y) * z;
array[i] += x * Math.exp(-w * w);
}
};
const generateData = (pointCount, bumpCount) => {
const arr = [];
let i;
for (i = 0; i < pointCount; i += 1) arr[i] = 0;
for (i = 0; i < bumpCount; i += 1) getPoints(arr, pointCount);
return arr;
};
const NUM_LAYERS = 20;
const SAMPLES_PER_LAYER = 200;
const BUMPS_PER_LAYER = 10;
const BACKGROUND = "#ffdede";
const range = (n) => Array.from(new Array(n), (_, i) => i);
const keys = range(NUM_LAYERS);
const xScale = scaleLinear({
domain: [0, SAMPLES_PER_LAYER - 1]
});
const yScale = scaleLinear({
domain: [-30, 50]
});
const colorScale = scaleOrdinal({
domain: keys,
range: [
"#ffc409",
"#f14702",
"#262d97",
"white",
"#036ecd",
"#9ecadd",
"#51666e"
]
});
const patternScale = scaleOrdinal({
domain: keys,
range: [
"mustard",
"cherry",
"navy",
"circles",
"circles",
"circles",
"circles"
]
});
const getY0 = (d) => yScale(d[0]) ?? 0;
const getY1 = (d) => yScale(d[1]) ?? 0;
function Example({ width, height, animate = true }) {
const forceUpdate = useForceUpdate();
const handlePress = () => forceUpdate();
if (width < 10) return null;
xScale.range([0, width]);
yScale.range([height, 0]);
const layers = transpose(
keys.map(() => generateData(SAMPLES_PER_LAYER, BUMPS_PER_LAYER))
);
return (
<svg width={width} height={height}>
<PatternCircles
id="mustard"
height={40}
width={40}
radius={5}
fill="#036ecf"
complement
/>
<PatternWaves
id="cherry"
height={12}
width={12}
fill="transparent"
stroke="#232493"
strokeWidth={1}
/>
<PatternCircles
id="navy"
height={60}
width={60}
radius={10}
fill="white"
complement
/>
<PatternCircles
complement
id="circles"
height={60}
width={60}
radius={10}
fill="transparent"
/>
<g onClick={handlePress} onTouchStart={handlePress}>
<rect
x={0}
y={0}
width={width}
height={height}
fill={BACKGROUND}
rx={14}
/>
<Stack
data={layers}
keys={keys}
offset="wiggle"
color={colorScale}
x={(_, i) => xScale(i) ?? 0}
y0={getY0}
y1={getY1}
>
{({ stacks, path }) =>
stacks.map((stack) => {
// Alternatively use renderprops <Spring to={{ d }}>{tweened => ...}</Spring>
const tweened = animate
? useSpring({ d: path(stack) })
: { d: path(stack) };
const color = colorScale(stack.key);
const pattern = patternScale(stack.key);
return (
<g key={`series-${stack.key}`}>
<animated.path d={tweened.d || ""} fill={color} />
<animated.path
d={tweened.d || ""}
fill={`url(#${pattern})`}
/>
</g>
);
})
}
</Stack>
</g>
</svg>
);
}
export default function App() {
return (
<div className="App">
<Example width={500} height={300} />
</div>
);
}
We have the useForceUpdate
hook to update the streamgraph when we click it.
getPoints
manipulate the points for the streamgraph.
The data is generated with the genereateData
function.
The constants are used to control what kind of data we generate.
xScale
and yScale
have the values in between the extreme values for the x and y dimensions.
colorScale
have the color values generated within the range of colors to fill the streamgraph partitions.
patternScale
have the patterns for the stream graph partitions,.
getY0
and getY1
lets us get the y dimension values we need for the streamgraph.
To render the graph, we use the PatternCircles
component to render the circles in the streamgraph.
To render the other shapes, we use the Stack
component to return the animated.path
component to render the paths for the partition shapes.
When we click on the shapes, forceUpdate
is run to re-render the streamgraph with new data.
Conclusion
We can add network diagrams and streamgraphs in our React app with the Visx library.