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.