Categories
Visx

Add Radial Line with the Visx Library

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 radial lines into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/curve @visx/gradient @visx/group @visx/mock-data @visx/responsive @visx/scale @visx/shape

to install the packages.

Create the Radial Line

We can create the chart by adding the items provided by the modules.

We use the data from the @visx/mock-data module.

Then we can create the radial line chart by writing:

import React, { useRef, useState, useEffect } from "react";
import { Group } from "@visx/group";
import { LineRadial } from "@visx/shape";
import { scaleTime, scaleLog } from "@visx/scale";
import { curveBasisOpen } from "@visx/curve";
import appleStock from "@visx/mock-data/lib/mocks/appleStock";
import { LinearGradient } from "@visx/gradient";
import { animated, useSpring } from "react-spring";

const green = "#e5fd3d";
export const blue = "#aeeef8";
const darkgreen = "#dff84d";
export const background = "#744cca";
const darkbackground = "#603FA8";
const springConfig = {
  tension: 20
};

// utils
function extent(data, value) {
  const values = data.map(value);
  return [Math.min(...values), Math.max(...values)];
}
// accessors
const date = (d) => new Date(d.date).valueOf();
const close = (d) => d.close;

// scales
const xScale = scaleTime({
  range: [0, Math.PI * 2],
  domain: extent(appleStock, date)
});
const yScale = scaleLog({
  domain: extent(appleStock, close)
});

const angle = (d) => xScale(date(d)) ?? 0;
const radius = (d) => yScale(close(d)) ?? 0;

const firstPoint = appleStock[0];
const lastPoint = appleStock[appleStock.length - 1];

const Example = ({ width, height, animate = true }) => {
  const lineRef = useRef(null);
  const [lineLength, setLineLength] = useState(0);
  const [shouldAnimate, setShouldAnimate] = useState(false);

  const spring = useSpring({
    frame: shouldAnimate ? 0 : 1,
    config: springConfig,
    onRest: () => setShouldAnimate(false)
  });

  const effectDependency = lineRef.current;
  useEffect(() => {
    if (lineRef.current) {
      setLineLength(lineRef.current.getTotalLength());
    }
  }, [effectDependency]);

if (width < 10) return null;
  yScale.range([0, height / 2 - 20]);

const yScaleTicks = yScale.ticks();
  const handlePress = () => setShouldAnimate(true);

  return (
    <>
      {animate && (
        <>
          <button
            type="button"
            onClick={handlePress}
            onTouchStart={handlePress}
          >
            Animate
          </button>
          <br />
        </>
      )}
      <svg
        width={width}
        height={height}
        onClick={() => setShouldAnimate(!shouldAnimate)}
      >
        <LinearGradient from={green} to={blue} id="line-gradient" />
        <rect width={width} height={height} fill={background} rx={14} />
        <Group top={height / 2} left={width / 2}>
          {yScaleTicks.map((tick, i) => (
            <circle
              key={`radial-grid-${i}`}
              r={yScale(tick)}
              stroke={blue}
              strokeWidth={1}
              fill={blue}
              fillOpacity={1 / (i + 1) - (1 / i) * 0.2}
              strokeOpacity={0.2}
            />
          ))}
          {yScaleTicks.map((tick, i) => (
            <text
              key={`radial-grid-${i}`}
              y={-(yScale(tick) ?? 0)}
              dy="-.33em"
              fontSize={8}
              fill={blue}
              textAnchor="middle"
            >
              {tick}
            </text>
          ))}

          <LineRadial angle={angle} radius={radius} curve={curveBasisOpen}>
            {({ path }) => {
              const d = path(appleStock) || "";
              return (
                <>
                  <animated.path
                    d={d}
                    ref={lineRef}
                    strokeWidth={2}
                    strokeOpacity={0.8}
                    strokeLinecap="round"
                    fill="none"
                    stroke={animate ? darkbackground : "url(#line-gradient)"}
                  />
                  {shouldAnimate && (
                    <animated.path
                      d={d}
                      strokeWidth={2}
                      strokeOpacity={0.8}
                      strokeLinecap="round"
                      fill="none"
                      stroke="url(#line-gradient)"
                      strokeDashoffset={spring.frame.interpolate(
                        (v) => v * lineLength
                      )}
                      strokeDasharray={lineLength}
                    />
                  )}
                </>
              );
            }}
          </LineRadial>
          {[firstPoint, lastPoint].map((d, i) => {
            const cx = ((xScale(date(d)) ?? 0) * Math.PI) / 180;
            const cy = -(yScale(close(d)) ?? 0);
            return (
              <circle
                key={`line-cap-${i}`}
                cx={cx}
                cy={cy}
                fill={darkgreen}
                r={3}
              />
            );
          })}
        </Group>
      </svg>
    </>
  );
};

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

We set the color for the lines and background color with the green , blue and background variables.

darkBackground is used for the dark background.

The extent function returns an array of number with the min and max values of the data.

date and close are data accessors that return the data for the time and value respectively.

The xScale and yScale values are created from the domain and range .

xScale has both the domain and range so that the radial line can be created.

We also need the angle and radius functions to compute the points for the radial line.

We have the shouldAnimate state to set when to animate the filling of the line.

In the Group component, we have the circle element to create the concentric circles.

The text elements have the text with the values for each ring.

The LineRadial component renders the line from the values returned from the angle and radius function which we pass in as props.

The render prop returns the animated.path component, which is created from the d path string.

The 2nd animated.path component has the filled line which is generated by animating the stroke and strokeDashOffset .

The circle below that is added to the start and end of the line respectively as markers for the ends of the line.

Conclusion

We can add a radial line easily into our React app with the Visx library.

Categories
Visx

Add Annotations to Lines and Curves with the Visx Library

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 curves into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/annotation @visx/mock-data @visx/responsive @visx/scale @visx/shape

to install the packages.

Create the Lines/Curves with Annotations

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

import React, { useMemo, useState } from "react";
import { Label, Connector, CircleSubject, LineSubject } from "@visx/annotation";
import { LinePath } from "@visx/shape";
import { bisector, extent } from "d3-array";
import { scaleLinear, scaleTime } from "@visx/scale";
import appleStock from "@visx/mock-data/lib/mocks/appleStock";
import { Annotation } from "@visx/annotation";

export const orange = "#ff7e67";
export const greens = ["#ecf4f3", "#68b0ab", "#006a71"];
const data = appleStock.slice(-100);
const getDate = (d) => new Date(d.date).valueOf();
const getStockValue = (d) => d.close;
const annotateDatum = data[Math.floor(data.length / 2) + 4];
const approxTooltipHeight = 70;

function findNearestDatum({ value, scale, accessor, data }) {
  const bisect = bisector(accessor).left;
  const nearestValue = scale.invert(value);
  const nearestValueIndex = bisect(data, nearestValue, 1);
  const d0 = data[nearestValueIndex - 1];
  const d1 = data[nearestValueIndex];
  let nearestDatum = d0;
  if (d1 && accessor(d1)) {
    nearestDatum =
      nearestValue - accessor(d0) > accessor(d1) - nearestValue ? d1 : d0;
  }
  return nearestDatum;
}

function Example({ width, height, compact = false }) {
  const xScale = useMemo(
    () =>
      scaleTime({
        domain: extent(data, (d) => getDate(d)),
        range: [0, width]
      }),
    [width]
  );
  const yScale = useMemo(
    () =>
      scaleLinear({
        domain: extent(data, (d) => getStockValue(d)),
        range: [height - 100, 100]
      }),
    [height]
  );

  const [editLabelPosition] = useState(false);
  const [editSubjectPosition] = useState(false);
  const [title] = useState("Title");
  const [subtitle] = useState(
    compact ? "Subtitle" : "Long Subtitle"
  );
  const [connectorType] = useState("elbow");
  const [subjectType] = useState("circle");
  const [showAnchorLine] = useState(true);
  const [verticalAnchor] = useState("auto");
  const [horizontalAnchor] = useState("auto");
  const [labelWidth] = useState(compact ? 100 : 175);
  const [annotationPosition, setAnnotationPosition] = useState({
    x: xScale(getDate(annotateDatum)) ?? 0,
    y: yScale(getStockValue(annotateDatum)) ?? 0,
    dx: compact ? -50 : -100,
    dy: compact ? -30 : -50
  });

  return (
    <svg width={width} height={height}>
      <rect width={width} height={height} fill={greens[0]} />
      <LinePath
        stroke={greens[2]}
        strokeWidth={2}
        data={data}
        x={(d) => xScale(getDate(d)) ?? 0}
        y={(d) => yScale(getStockValue(d)) ?? 0}
      />
      <Annotation
        width={width}
        height={height}
        x={annotationPosition.x}
        y={annotationPosition.y}
        dx={annotationPosition.dx}
        dy={annotationPosition.dy}
        canEditLabel={editLabelPosition}
        canEditSubject={editSubjectPosition}
        onDragEnd={({ event, ...nextPosition }) => {
          const nearestDatum = findNearestDatum({
            accessor:
              subjectType === "horizontal-line" ? getStockValue : getDate,
            data,
            scale: subjectType === "horizontal-line" ? yScale : xScale,
            value:
              subjectType === "horizontal-line"
                ? nextPosition.y
                : nextPosition.x
          });
          const x = xScale(getDate(nearestDatum)) ?? 0;
          const y = yScale(getStockValue(nearestDatum)) ?? 0;

          const shouldFlipDx =
            (nextPosition.dx > 0 && x + nextPosition.dx + labelWidth > width) ||
            (nextPosition.dx < 0 && x + nextPosition.dx - labelWidth <= 0);
          const shouldFlipDy =
            (nextPosition.dy > 0 &&
              height - (y + nextPosition.dy) < approxTooltipHeight) ||
            (nextPosition.dy < 0 &&
              y + nextPosition.dy - approxTooltipHeight <= 0);
          setAnnotationPosition({
            x,
            y,
            dx: (shouldFlipDx ? -1 : 1) * nextPosition.dx,
            dy: (shouldFlipDy ? -1 : 1) * nextPosition.dy
          });
        }}
      >
        <Connector stroke={orange} type={connectorType} />
        <Label
          backgroundFill="white"
          showAnchorLine={showAnchorLine}
          anchorLineStroke={greens[2]}
          backgroundProps={{ stroke: greens[1] }}
          fontColor={greens[2]}
          horizontalAnchor={horizontalAnchor}
          subtitle={subtitle}
          title={title}
          verticalAnchor={verticalAnchor}
          width={labelWidth}
        />
        {subjectType === "circle" && <CircleSubject stroke={orange} />}
        {subjectType !== "circle" && (
          <LineSubject
            orientation={
              subjectType === "vertical-line" ? "vertical" : "horizontal"
            }
            stroke={orange}
            min={0}
            max={subjectType === "vertical-line" ? height : width}
          />
        )}
      </Annotation>
    </svg>
  );
}

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

We create the orange and green variable to set the color for the marker and the line respectively.

data has the data for the line.

getDate and getStockValue are getter functions for the x and y-axis data respectively.

The findNearestDatum function lets us find the point to let us drag the annotation box to.

In the Example component, we create the xScale and yScale objects to let us create the scales for the line chart.

The Annotation component has the annotation for the line.

We set the x and y position to place the annotation for the line.

width and height sets the width and height.

dx and dy sets the position offset.

canEditLabel lets us set whether we can edit the label with a boolean.

canEditSubject lets us set whether we can edit the subject with a boolean.

The onDragEnd handler changes the annotation position when we drag the annotation box.

We add the Connection and the Label to add the marker and the label for the annotation box.

Now we should see a box displayed on the line with the text as set by the subtitle prop.

Conclusion

We can add an annotation box into our line with the Visx library.

Categories
Visx

Create Curves with the Visx Library

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 curves into our React app

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/curve @visx/gradient @visx/group @visx/marker @visx/mock-data @visx/responsive @visx/scale @visx/shape

to install the packages.

Create the Curves

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

import React, { useState } from "react";
import { extent, max } from "d3-array";
import * as allCurves from "@visx/curve";
import { Group } from "@visx/group";
import { LinePath } from "@visx/shape";
import { scaleTime, scaleLinear } from "@visx/scale";
import {
  MarkerArrow,
  MarkerCross,
  MarkerX,
  MarkerCircle,
  MarkerLine
} from "@visx/marker";
import generateDateValue from "@visx/mock-data/lib/generators/genDateValue";

const curveTypes = Object.keys(allCurves);
const lineCount = 3;
const series = new Array(lineCount)
  .fill(null)
  .map((_) =>
    generateDateValue(25).sort((a, b) => a.date.getTime() - b.date.getTime())
  );
const allData = series.reduce((rec, d) => rec.concat(d), []);

const getX = (d) => d.date;
const getY = (d) => d.value;

// scales
const xScale = scaleTime({
  domain: extent(allData, getX)
});
const yScale = scaleLinear({
  domain: [0, max(allData, getY)]
});

function Example({ width, height, showControls = true }) {
  const [curveType, setCurveType] = useState("curveNatural");
  const [showPoints, setShowPoints] = useState(true);
  const svgHeight = showControls ? height - 40 : height;
  const lineHeight = svgHeight / lineCount;

  xScale.range([0, width - 50]);
  yScale.range([lineHeight - 2, 0]);

  return (
    <div className="visx-curves-demo">
      {showControls && (
        <>
          <label>
            Curve type &nbsp;
            <select
              onChange={(e) => setCurveType(e.target.value)}
              value={curveType}
            >
              {curveTypes.map((curve) => (
                <option key={curve} value={curve}>
                  {curve}
                </option>
              ))}
            </select>
          </label>
          &nbsp;
          <label>
            Show points&nbsp;
            <input
              type="checkbox"
              checked={showPoints}
              onChange={() => setShowPoints(!showPoints)}
            />
          </label>
          <br />
        </>
      )}
      <svg width={width} height={svgHeight}>
        <MarkerX
          id="marker-x"
          stroke="#333"
          size={22}
          strokeWidth={4}
          markerUnits="userSpaceOnUse"
        />
        <MarkerCross
          id="marker-cross"
          stroke="#333"
          size={22}
          strokeWidth={4}
          strokeOpacity={0.6}
          markerUnits="userSpaceOnUse"
        />
        <MarkerCircle id="marker-circle" fill="#333" size={2} refX={2} />
        <MarkerArrow
          id="marker-arrow-odd"
          stroke="#333"
          size={8}
          strokeWidth={1}
        />
        <MarkerLine id="marker-line" fill="#333" size={16} strokeWidth={1} />
        <MarkerArrow id="marker-arrow" fill="#333" refX={2} size={6} />
        <rect width={width} height={svgHeight} fill="#efefef" rx={14} ry={14} />
        {width > 8 &&
          series.map((lineData, i) => {
            const even = i % 2 === 0;
            let markerStart = even ? "url(#marker-cross)" : "url(#marker-x)";
            if (i === 1) markerStart = "url(#marker-line)";
            const markerEnd = even
              ? "url(#marker-arrow)"
              : "url(#marker-arrow-odd)";
            return (
              <Group key={`lines-${i}`} top={i * lineHeight} left={13}>
                {showPoints &&
                  lineData.map((d, j) => (
                    <circle
                      key={i + j}
                      r={3}
                      cx={xScale(getX(d))}
                      cy={yScale(getY(d))}
                      stroke="rgba(33,33,33,0.5)"
                      fill="transparent"
                    />
                  ))}
                <LinePath
                  curve={allCurves[curveType]}
                  data={lineData}
                  x={(d) => xScale(getX(d)) ?? 0}
                  y={(d) => yScale(getY(d)) ?? 0}
                  stroke="#333"
                  strokeWidth={even ? 2 : 1}
                  strokeOpacity={even ? 0.6 : 1}
                  shapeRendering="geometricPrecision"
                  markerMid="url(#marker-circle)"
                  markerStart={markerStart}
                  markerEnd={markerEnd}
                />
              </Group>
            );
          })}
      </svg>
      <style jsx>{`
        .visx-curves-demo label {
          font-size: 12px;
        }
      `}</style>
    </div>
  );
}

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

We get the curve types from the @visx/curve library.

The curveTypes is generated from the keys of the module object.

The data for the curves are created from the generateDataValue function from the @visx/mock-data module.

The getX and getY functions are the data accessor functions to get the data for the x and y axes respectively.

xScale has the x-axis scale.

yScale has the y-axis scale.

The Example component has the curves.

We have a dropdown to set the curveType state.

And we have a checkbox to set the showPoints state.

We add the markers for the curves with the MarkerX , MarkerCross , MarkerCircle , MarkerLine and MarkerArrow components.

Then to add the curves, we add the LinePath component.

We set the curve prop to the type of curve we want to render.

data has the line data.

x and y have the x and y values to render respectively.

stroke and strokeWidth have the stroke styles.

We set the markers to display in the curve with the markerMid , markerStart and markerEnd props.

markerMid has the markers in the middle of the curve.

And the others are displayed at the ends.

Conclusion

We can add curves easily into our React app with the Visx library.

Categories
Visx

Create Graphics with Voronoi Diagram with the Visx Library

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.

Categories
Visx

Create Graphics with Zoom and Pan with the Visx Library

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.