Categories
Visx

Add Annotations to Lines and Curves 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 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.

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 *