Categories
Visx

Create Graphics with Voronoi Diagram with the Visx Library

Spread the love

Visx is a library that lets us add graphics to our React app easily.

A Voronoi diagram is a diagram that’s composed of partitions in a plane.

In this article, we’ll look at how to use it to add Voronoi diagrams into 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/gradient @visx/group @visx/responsive @visx/voronoi

to install the packages.

Create the Diagram

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 Voronoi diagram, we write:

import React, { useState, useMemo, useRef } from "react";
import { Group } from "@visx/group";
import { GradientOrangeRed, GradientPinkRed } from "@visx/gradient";
import { RectClipPath } from "@visx/clip-path";
import { voronoi, VoronoiPolygon } from "@visx/voronoi";
import { localPoint } from "@visx/event";

const data = new Array(150).fill(null).map(() => ({
  x: Math.random(),
  y: Math.random(),
  id: Math.random().toString(36).slice(2)
}));

const neighborRadius = 75;

const defaultMargin = {
  top: 0,
  left: 0,
  right: 0,
  bottom: 76
};

const Example = ({ width, height, margin = defaultMargin }) => {
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;

const voronoiLayout = useMemo(
    () =>
      voronoi({
        x: (d) => d.x * innerWidth,
        y: (d) => d.y * innerHeight,
        width: innerWidth,
        height: innerHeight
      })(data),
    [innerWidth, innerHeight]
  );

const polygons = voronoiLayout.polygons();
  const svgRef = useRef(null);
  const [hoveredId, setHoveredId] = useState(null);
  const [neighborIds, setNeighborIds] = useState(new Set());

  return width < 10 ? null : (
    <svg width={width} height={height} ref={svgRef}>
      <GradientOrangeRed id="voronoi_orange_red" />
      <GradientPinkRed id="voronoi_pink_red" />
      <RectClipPath
        id="voronoi_clip"
        width={innerWidth}
        height={innerHeight}
        rx={14}
      />
      <Group
        top={margin.top}
        left={margin.left}
        clipPath="url(#voronoi_clip)"
        onMouseMove={(event) => {
          if (!svgRef.current) return;

const point = localPoint(svgRef.current, event);
          if (!point) return;

const closest = voronoiLayout.find(point.x, point.y, neighborRadius);
          if (closest && closest.data.id !== hoveredId) {
            const neighbors = new Set();
            const cell = voronoiLayout.cells[closest.index];
            if (!cell) return;

cell.halfedges.forEach((index) => {
              const edge = voronoiLayout.edges[index];
              const { left, right } = edge;
              if (left && left !== closest) neighbors.add(left.data.id);
              else if (right && right !== closest) neighbors.add(right.data.id);
            });

setNeighborIds(neighbors);
            setHoveredId(closest.data.id);
          }
        }}
        onMouseLeave={() => {
          setHoveredId(null);
          setNeighborIds(new Set());
        }}
      >
        {polygons.map((polygon) => (
          <VoronoiPolygon
            key={`polygon-${polygon.data.id}`}
            polygon={polygon}
            fill={
              hoveredId &&
              (polygon.data.id === hoveredId ||
                neighborIds.has(polygon.data.id))
                ? "url(#voronoi_orange_red)"
                : "url(#voronoi_pink_red)"
            }
            stroke="#fff"
            strokeWidth={1}
            fillOpacity={
              hoveredId && neighborIds.has(polygon.data.id) ? 0.5 : 1
            }
          />
        ))}
        {data.map(({ x, y, id }) => (
          <circle
            key={`circle-${id}`}
            r={2}
            cx={x * innerWidth}
            cy={y * innerHeight}
            fill={id === hoveredId ? "fuchsia" : "#fff"}
            fillOpacity={0.8}
          />
        ))}
      </Group>
    </svg>
  );
};

export default function App() {
  return (
    <div className="App">
      <Example width={500} height={300} />
    </div>
  );
}

We create the data for the diagram with the data array.

The x and y properties are for the x and y coordinates of the partition.

And id is a unique ID for each partition.

neighborhoodRadius has the radius for the partitions to highlight when we hover over one partition.

defaultMargin has the default margins for the diagram.

The Example component has the Voronoi diagram.

We compute the innerWidth and innerHeight of the diagram to compute the display area of the diagram.

The voronoiLayout variable computes the partition layout from the dimensions of the diagram.

The polygons variable has the polygons for the partitions.

Next, we add the GradientOrangeRed to highlight the partition that we hovered over.

GradientPinkRed has the normal color gradient for the partitions.

In the Group component, we create the partition polygons with the polygons.map call.

In the onMouseMove handler, we check if there are any neighboring polygons with the closest variable, which is generated from the voronoiLayout.find call.

We use that to generate the cell variable, which we use to add polygons to the neighbord object to highlight the neighboring partitions.

It returns the VoronoiPolygon component with the ID with the fill color.

And we use the x and y coordinates to display a dot in the center of each polygon.

Conclusion

We can create a Voronoi diagram easily 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 *