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.