Categories
Visx

Add Network Diagrams and Streamgraphs into Our React App 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 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.

Categories
Visx

Add Radar Charts into Our React App 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 radar charts into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

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

to install the packages.

Radar Chart

To create a radar chart, we write:

import React from "react";
import { Group } from "@visx/group";
import letterFrequency from "@visx/mock-data/lib/mocks/letterFrequency";
import { scaleLinear } from "@visx/scale";
import { Point } from "@visx/point";
import { Line, LineRadial } from "@visx/shape";

const orange = "#ff9933";
const pumpkin = "#f5810c";
const silver = "#d9d9d9";
const background = "#FAF7E9";

const degrees = 360;
const data = letterFrequency.slice(2, 12);

const y = (d) => d.frequency;

const genAngles = (length) =>
  [...new Array(length + 1)].map((_, i) => ({
    angle: i * (degrees / length)
  }));

const genPoints = (length, radius) => {
  const step = (Math.PI * 2) / length;
  return [...new Array(length)].map((_, i) => ({
    x: radius * Math.sin(i * step),
    y: radius * Math.cos(i * step)
  }));
};

function genPolygonPoints(dataArray, scale, getValue) {
  const step = (Math.PI * 2) / dataArray.length;
  const points = new Array(dataArray.length).fill({
    x: 0,
    y: 0
  });
  const pointString = new Array(dataArray.length + 1)
    .fill("")
    .reduce((res, _, i) => {
      if (i > dataArray.length) return res;
      const xVal = scale(getValue(dataArray[i - 1])) * Math.sin(i * step);
      const yVal = scale(getValue(dataArray[i - 1])) * Math.cos(i * step);
      points[i - 1] = { x: xVal, y: yVal };
      res += `${xVal},${yVal} `;
      return res;
    });

  return { points, pointString };
}

const defaultMargin = { top: 40, left: 80, right: 80, bottom: 80 };

function Example({ width, height, levels = 5, margin = defaultMargin }) {
  const xMax = width - margin.left - margin.right;
  const yMax = height - margin.top - margin.bottom;
  const radius = Math.min(xMax, yMax) / 2;

const radialScale = scaleLinear({
    range: [0, Math.PI * 2],
    domain: [degrees, 0]
  });

const yScale = scaleLinear({
    range: [0, radius],
    domain: [0, Math.max(...data.map(y))]
  });

const webs = genAngles(data.length);
  const points = genPoints(data.length, radius);
  const polygonPoints = genPolygonPoints(data, (d) => yScale(d) ?? 0, y);
  const zeroPoint = new Point({ x: 0, y: 0 });

  return width < 10 ? null : (
    <svg width={width} height={height}>
      <rect fill={background} width={width} height={height} rx={14} />
      <Group top={height / 2 - margin.top} left={width / 2}>
        {[...new Array(levels)].map((_, i) => (
          <LineRadial
            key={`web-${i}`}
            data={webs}
            angle={(d) => radialScale(d.angle) ?? 0}
            radius={((i + 1) * radius) / levels}
            fill="none"
            stroke={silver}
            strokeWidth={2}
            strokeOpacity={0.8}
            strokeLinecap="round"
          />
        ))}
        {[...new Array(data.length)].map((_, i) => (
          <Line
            key={`radar-line-${i}`}
            from={zeroPoint}
            to={points[i]}
            stroke={silver}
          />
        ))}
        <polygon
          points={polygonPoints.pointString}
          fill={orange}
          fillOpacity={0.3}
          stroke={orange}
          strokeWidth={1}
        />
        {polygonPoints.points.map((point, i) => (
          <circle
            key={`radar-point-${i}`}
            cx={point.x}
            cy={point.y}
            r={4}
            fill={pumpkin}
          />
        ))}
      </Group>
    </svg>
  );
}

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

We define the variables with the colors for the chart as the values.

degrees has the degrees for the radar chart.

data has the data for the chart.

y is a getter to let us get the y-axis values.

genAngles lets us create the angles for the chart and genPoints create the points.

genPolygonPoints generate the points for the radar chart.

Then to create the radar chart, we add the LineRadial compoennt to add the web for the radar chart.

We get the angles from the radialScale function.

The radius is computed from the radius prop.

The line segments for the graph is created with the Line component.

polygon lets us create the polygon fill for the radar chart.

The circle in the polygonPoints.map callback creates the points that are located at the angles.

Conclusion

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

Categories
Visx

Add Heat Maps into Our React App 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 heat maps into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/group @visx/heatmap @visx/mock-data @visx/responsive @visx/scale

to install the packages.

Add the Heatmaps

We can add the heatmaps by writing:

import React from "react";
import { Group } from "@visx/group";
import genBins from "@visx/mock-data/lib/generators/genBins";
import { scaleLinear } from "@visx/scale";
import { HeatmapCircle, HeatmapRect } from "@visx/heatmap";

const hot1 = "#77312f";
const hot2 = "#f33d15";
const cool1 = "#122549";
const cool2 = "#b4fbde";
const background = "#28272c";

const binData = genBins(/* length = */ 16, /* height = */ 16);

function max(data, value) {
  return Math.max(...data.map(value));
}

function min(data, value) {
  return Math.min(...data.map(value));
}
const bins = (d) => d.bins;
const count = (d) => d.count;

const colorMax = max(binData, (d) => max(bins(d), count));
const bucketSizeMax = max(binData, (d) => bins(d).length);

// scales
const xScale = scaleLinear({
  domain: [0, binData.length]
});
const yScale = scaleLinear({
  domain: [0, bucketSizeMax]
});
const circleColorScale = scaleLinear({
  range: [hot1, hot2],
  domain: [0, colorMax]
});
const rectColorScale = scaleLinear({
  range: [cool1, cool2],
  domain: [0, colorMax]
});
const opacityScale = scaleLinear({
  range: [0.1, 1],
  domain: [0, colorMax]
});
const defaultMargin = { top: 10, left: 20, right: 20, bottom: 110 };

const Example = ({
  width,
  height,
  events = false,
  margin = defaultMargin,
  separation = 20
}) => {
  const size =
    width > margin.left + margin.right
      ? width - margin.left - margin.right - separation
      : width;
  const xMax = size / 2;
  const yMax = height - margin.bottom - margin.top;
  const binWidth = xMax / binData.length;
  const binHeight = yMax / bucketSizeMax;
  const radius = min([binWidth, binHeight], (d) => d) / 2;

xScale.range([0, xMax]);
  yScale.range([yMax, 0]);

return width < 10 ? null : (
    <svg width={width} height={height}>
      <rect
        x={0}
        y={0}
        width={width}
        height={height}
        rx={14}
        fill={background}
      />
      <Group top={margin.top} left={margin.left}>
        <HeatmapCircle
          data={binData}
          xScale={(d) => xScale(d) ?? 0}
          yScale={(d) => yScale(d) ?? 0}
          colorScale={circleColorScale}
          opacityScale={opacityScale}
          radius={radius}
          gap={2}
        >
          {(heatmap) =>
            heatmap.map((heatmapBins) =>
              heatmapBins.map((bin) => (
                <circle
                  key={`heatmap-circle-${bin.row}-${bin.column}`}
                  className="visx-heatmap-circle"
                  cx={bin.cx}
                  cy={bin.cy}
                  r={bin.r}
                  fill={bin.color}
                  fillOpacity={bin.opacity}
                  onClick={() => {
                    if (!events) return;
                    const { row, column } = bin;
                    alert(JSON.stringify({ row, column, bin: bin.bin }));
                  }}
                />
              ))
            )
          }
        </HeatmapCircle>
      </Group>
      <Group top={margin.top} left={xMax + margin.left + separation}>
        <HeatmapRect
          data={binData}
          xScale={(d) => xScale(d) ?? 0}
          yScale={(d) => yScale(d) ?? 0}
          colorScale={rectColorScale}
          opacityScale={opacityScale}
          binWidth={binWidth}
          binHeight={binWidth}
          gap={2}
        >
          {(heatmap) =>
            heatmap.map((heatmapBins) =>
              heatmapBins.map((bin) => (
                <rect
                  key={`heatmap-rect-${bin.row}-${bin.column}`}
                  className="visx-heatmap-rect"
                  width={bin.width}
                  height={bin.height}
                  x={bin.x}
                  y={bin.y}
                  fill={bin.color}
                  fillOpacity={bin.opacity}
                  onClick={() => {
                    if (!events) return;
                    const { row, column } = bin;
                    alert(JSON.stringify({ row, column, bin: bin.bin }));
                  }}
                />
              ))
            )
          }
        </HeatmapRect>
      </Group>
    </svg>
  );
};

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

We define the color codes for the heatmap at the top of the code.

binData has the data for the heatmap.

max and min returns the max and min values from the data.

bins and count are the getter methods for the data.

We get the max values for the color and bucket size with the colorMax and bucketSizeMax functions.

Next, we computed the xScale and yScale for the heatmaps to get the range of the values for the heatmap.

circleColorScale and rectColorScale are the color scales for the heatmap’s circles or rectangles.

opacityStyle have the opacity scale for the circles or rectangles.

In the Example component we get the width of the heatmap.

binWidth and binHeight have the width and height of the bins in the heatmap.

radius have the radius for the circles in the heatmap.

We pass all the variables into the HeatmapCircle component to create a heatmap with circles.

And the HeatmapRect component does the same to create a heatmap with rectangles.

The tiles are rendered in the render prop of each. We render circle or rect depending on the heatmap type.

Conclusion

We can create a heatmap with different kinds of tiles in a React app with the Visx library.

Categories
Visx

Add TreeMaps into Our React App 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 treemaps into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/group @visx/hierarchy @visx/mock-data @visx/responsive @visx/scale

to install the packages.

Add the TreeMap

We can add the treemap into our React app by writing:

import React, { useState } from "react";
import { Group } from "@visx/group";
import {
  Treemap,
  hierarchy,
  stratify,
  treemapSquarify,
  treemapBinary,
  treemapDice,
  treemapResquarify,
  treemapSlice,
  treemapSliceDice
} from "@visx/hierarchy";
import shakespeare from "@visx/mock-data/lib/mocks/shakespeare";
import { scaleLinear } from "@visx/scale";

const color1 = "#f3e9d2";
const color2 = "#4281a4";
const background = "#114b5f";

const colorScale = scaleLinear({
  domain: [0, Math.max(...shakespeare.map((d) => d.size || 0))],
  range: [color2, color1]
});

const data = stratify()
  .id((d) => d.id)
  .parentId((d) => d.parent)(shakespeare)
  .sum((d) => d.size || 0);

const tileMethods = {
  treemapSquarify,
  treemapBinary,
  treemapDice,
  treemapResquarify,
  treemapSlice,
  treemapSliceDice
};

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

function Example({ width, height, margin = defaultMargin }) {
  const [tileMethod, setTileMethod] = useState("treemapSquarify");
  const xMax = width - margin.left - margin.right;
  const yMax = height - margin.top - margin.bottom;
  const root = hierarchy(data).sort((a, b) => (b.value || 0) - (a.value || 0));

  return width < 10 ? null : (
    <div>
      <label>tile method</label>{" "}
      <select
        onClick={(e) => e.stopPropagation()}
        onChange={(e) => setTileMethod(e.target.value)}
        value={tileMethod}
      >
        {Object.keys(tileMethods).map((tile) => (
          <option key={tile} value={tile}>
            {tile}
          </option>
        ))}
      </select>
      <div>
        <svg width={width} height={height}>
          <rect width={width} height={height} rx={14} fill={background} />
          <Treemap
            top={margin.top}
            root={root}
            size={[xMax, yMax]}
            tile={tileMethods[tileMethod]}
            round
          >
            {(treemap) => (
              <Group>
                {treemap
                  .descendants()
                  .reverse()
                  .map((node, i) => {
                    const nodeWidth = node.x1 - node.x0;
                    const nodeHeight = node.y1 - node.y0;
                    return (
                      <Group
                        key={`node-${i}`}
                        top={node.y0 + margin.top}
                        left={node.x0 + margin.left}
                      >
                        {node.depth === 1 && (
                          <rect
                            width={nodeWidth}
                            height={nodeHeight}
                            stroke={background}
                            strokeWidth={4}
                            fill="transparent"
                          />
                        )}
                        {node.depth > 2 && (
                          <rect
                            width={nodeWidth}
                            height={nodeHeight}
                            stroke={background}
                            fill={colorScale(node.value || 0)}
                          />
                        )}
                      </Group>
                    );
                  })}
              </Group>
            )}
          </Treemap>
        </svg>
      </div>
    </div>
  );
}

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

We add the colors for the treemap partitions with the color1 and color2 variables.

background has the background color.

We create the colorScale variable to create color variants for the partitions.

Then we convert the data into a structure that can be used with the treemap with the stratify method.

Next, in the Example component, we add a dropdown to display the tile method choices.

This lets us set the tile format for the treemap.

After this, we add the Treemap component to add the treemap.

In its render prop, we call descendants to get the descendants.

Then we call reverse to reverse the data.

map maps them to rect s so that we render the partitions.

We render them differently depending on the depth level.

Conclusion

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

Categories
Visx

Add Box Plots into Our React App 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 box plots into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/gradient @visx/group @visx/mock-data @visx/pattern @visx/responsive @visx/scale @visx/stats @visx/tooltip

to install the packages.

Add Box Plots

Now we can add the box plots with:

import React from "react";
import { Group } from "@visx/group";
import { ViolinPlot, BoxPlot } from "@visx/stats";
import { LinearGradient } from "@visx/gradient";
import { scaleBand, scaleLinear } from "@visx/scale";
import genStats from "@visx/mock-data/lib/generators/genStats";
import {
  Tooltip,
  defaultStyles as defaultTooltipStyles,
  useTooltip
} from "@visx/tooltip";
import { PatternLines } from "@visx/pattern";

const data = genStats(5);
const x = (d) => d.boxPlot.x;
const min = (d) => d.boxPlot.min;
const max = (d) => d.boxPlot.max;
const median = (d) => d.boxPlot.median;
const firstQuartile = (d) => d.boxPlot.firstQuartile;
const thirdQuartile = (d) => d.boxPlot.thirdQuartile;
const outliers = (d) => d.boxPlot.outliers;

const Example = ({ width, height }) => {
  const {
    tooltipOpen,
    tooltipLeft,
    tooltipTop,
    tooltipData,
    hideTooltip,
    showTooltip
  } = useTooltip();

  const xMax = width;
  const yMax = height - 120;
  const xScale = scaleBand({
    range: [0, xMax],
    round: true,
    domain: data.map(x),
    padding: 0.4
  });

  const values = data.reduce((allValues, { boxPlot }) => {
    allValues.push(boxPlot.min, boxPlot.max);
    return allValues;
  }, []);
  const minYValue = Math.min(...values);
  const maxYValue = Math.max(...values);

  const yScale = scaleLinear({
    range: [yMax, 0],
    round: true,
    domain: [minYValue, maxYValue]
  });

  const boxWidth = xScale.bandwidth();
  const constrainedWidth = Math.min(40, boxWidth);

  return width < 10 ? null : (
    <div style={{ position: "relative" }}>
      <svg width={width} height={height}>
        <LinearGradient id="statsplot" to="#8b6ce7" from="#87f2d4" />
        <rect
          x={0}
          y={0}
          width={width}
          height={height}
          fill="url(#statsplot)"
          rx={14}
        />
        <PatternLines
          id="hViolinLines"
          height={3}
          width={3}
          stroke="#ced4da"
          strokeWidth={1}
          orientation={["horizontal"]}
        />
        <Group top={40}>
          {data.map((d, i) => (
            <g key={i}>
              <ViolinPlot
                data={d.binData}
                stroke="#dee2e6"
                left={xScale(x(d))}
                width={constrainedWidth}
                valueScale={yScale}
                fill="url(#hViolinLines)"
              />
              <BoxPlot
                min={min(d)}
                max={max(d)}
                left={xScale(x(d)) + 0.3 * constrainedWidth}
                firstQuartile={firstQuartile(d)}
                thirdQuartile={thirdQuartile(d)}
                median={median(d)}
                boxWidth={constrainedWidth * 0.4}
                fill="#FFFFFF"
                fillOpacity={0.3}
                stroke="#FFFFFF"
                strokeWidth={2}
                valueScale={yScale}
                outliers={outliers(d)}
                minProps={{
                  onMouseOver: () => {
                    showTooltip({
                      tooltipTop: yScale(min(d)) ?? 0 + 40,
                      tooltipLeft: xScale(x(d)) + constrainedWidth + 5,
                      tooltipData: {
                        min: min(d),
                        name: x(d)
                      }
                    });
                  },
                  onMouseLeave: () => {
                    hideTooltip();
                  }
                }}
                maxProps={{
                  onMouseOver: () => {
                    showTooltip({
                      tooltipTop: yScale(max(d)) ?? 0 + 40,
                      tooltipLeft: xScale(x(d)) + constrainedWidth + 5,
                      tooltipData: {
                        max: max(d),
                        name: x(d)
                      }
                    });
                  },
                  onMouseLeave: () => {
                    hideTooltip();
                  }
                }}
                boxProps={{
                  onMouseOver: () => {
                    showTooltip({
                      tooltipTop: yScale(median(d)) ?? 0 + 40,
                      tooltipLeft: xScale(x(d)) + constrainedWidth + 5,
                      tooltipData: {
                        ...d.boxPlot,
                        name: x(d)
                      }
                    });
                  },
                  onMouseLeave: () => {
                    hideTooltip();
                  }
                }}
                medianProps={{
                  style: {
                    stroke: "white"
                  },
                  onMouseOver: () => {
                    showTooltip({
                      tooltipTop: yScale(median(d)) ?? 0 + 40,
                      tooltipLeft: xScale(x(d)) + constrainedWidth + 5,
                      tooltipData: {
                        median: median(d),
                        name: x(d)
                      }
                    });
                  },
                  onMouseLeave: () => {
                    hideTooltip();
                  }
                }}
              />
            </g>
          ))}
        </Group>
      </svg>

      {tooltipOpen && tooltipData && (
        <Tooltip
          top={tooltipTop}
          left={tooltipLeft}
          style={{
            ...defaultTooltipStyles,
            backgroundColor: "#283238",
            color: "white"
          }}
        >
          <div>
            <strong>{tooltipData.name}</strong>
          </div>
          <div style={{ marginTop: "5px", fontSize: "12px" }}>
            {tooltipData.max && <div>max: {tooltipData.max}</div>}
            {tooltipData.thirdQuartile && (
              <div>third quartile: {tooltipData.thirdQuartile}</div>
            )}
            {tooltipData.median && <div>median: {tooltipData.median}</div>}
            {tooltipData.firstQuartile && (
              <div>first quartile: {tooltipData.firstQuartile}</div>
            )}
            {tooltipData.min && <div>min: {tooltipData.min}</div>}
          </div>
        </Tooltip>
      )}
    </div>
  );
};

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

The data variable has the data for the boxplot.

Then we add the getter methods below it.

In the Example component, we call the useTooltip hook to show tooltips when we hover over the boxplots.

showTooltip shows the tooltip. hideTooltip hides the tooltip.

We add the values variable to gather the values needed for the boxplots.

yScale has the scale for the y-axis.

To add the boxplots, we call data.map to map the data values box plot components.

We use the ViolinPlot component to render the violin plot surrounding the boxplot.

Then we use the BoxPlot component to render the boxplot.

We pass in all the getter methods as props into ViolinPlot and BoxPlot .

We call showTooltip in the onMouseOver method to show a tooltip when we hover over the boxplot.

And in the onMouseLeave handler, we call hideTooltip to hide the tooltips when we move away from the boxplot.

minProps takes handlers that runs when we hover over the bottom of the boxplot.

maxProps takes handlers that runs when we hover over the top of the boxplot.

boxProps takes handlers that runs when we hover over the boxes of the boxplot.

medianProps takes handlers that runs when we hover over the median point of the boxplot.

Finally, we add the tooltip content with the Tooltip component.

Now we should see a box and violin plot. And when we hover over the whiskers or the boxes, we see a tooltip with the values.

Conclusion

We can add box and violin plots into our React app with the Visx library.