Categories
Visx

Add Network Diagrams and Streamgraphs 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 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.

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 *