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 filled line charts with navigation into our React app.
Install Required Packages
We have to install a few modules to create the grouped bar chart.
To get started, we run:
npm i @visx/axis @visx/brush @visx/curve @visx/gradient @visx/group @visx/mock-data @visx/pattern @visx/responsive @visx/scale @visx/shape
to install the packages.
Create the Chart
We can create the chart by adding the items provided by the modules.
We use the data from the @visx/mock-data
module.
Then to create the filled line chart with a chart for navigation at the bottom, we write:
import React, { useRef, useState, useMemo } from "react";
import { scaleTime, scaleLinear } from "@visx/scale";
import appleStock from "@visx/mock-data/lib/mocks/appleStock";
import { Brush } from "@visx/brush";
import { PatternLines } from "@visx/pattern";
import { LinearGradient } from "@visx/gradient";
import { max, extent } from "d3-array";
import { AxisBottom, AxisLeft } from "@visx/axis";
import { AreaClosed } from "@visx/shape";
import { Group } from "@visx/group";
import { curveMonotoneX } from "@visx/curve";
const stock = appleStock.slice(1000);
const brushMargin = { top: 10, bottom: 15, left: 50, right: 20 };
const chartSeparation = 30;
const PATTERN_ID = "brush_pattern";
const GRADIENT_ID = "brush_gradient";
export const accentColor = "#f6acc8";
export const background = "#584153";
export const background2 = "#af8baf";
const selectedBrushStyle = {
fill: `url(#${PATTERN_ID})`,
stroke: "white"
};
const getDate = (d) => new Date(d.date);
const getStockValue = (d) => d.close;
const axisColor = "#fff";
const axisBottomTickLabelProps = {
textAnchor: "middle",
fontFamily: "Arial",
fontSize: 10,
fill: axisColor
};
const axisLeftTickLabelProps = {
dx: "-0.25em",
dy: "0.25em",
fontFamily: "Arial",
fontSize: 10,
textAnchor: "end",
fill: axisColor
};
function AreaChart({
data,
gradientColor,
width,
yMax,
margin,
xScale,
yScale,
hideBottomAxis = false,
hideLeftAxis = false,
top,
left,
children
}) {
if (width < 10) return null;
return (
<Group left={left || margin.left} top={top || margin.top}>
<LinearGradient
id="gradient"
from={gradientColor}
fromOpacity={1}
to={gradientColor}
toOpacity={0.2}
/>
<AreaClosed
data={data}
x={(d) => xScale(getDate(d)) || 0}
y={(d) => yScale(getStockValue(d)) || 0}
yScale={yScale}
strokeWidth={1}
stroke="url(#gradient)"
fill="url(#gradient)"
curve={curveMonotoneX}
/>
{!hideBottomAxis && (
<AxisBottom
top={yMax}
scale={xScale}
numTicks={width > 520 ? 10 : 5}
stroke={axisColor}
tickStroke={axisColor}
tickLabelProps={() => axisBottomTickLabelProps}
/>
)}
{!hideLeftAxis && (
<AxisLeft
scale={yScale}
numTicks={5}
stroke={axisColor}
tickStroke={axisColor}
tickLabelProps={() => axisLeftTickLabelProps}
/>
)}
{children}
</Group>
);
}
function BrushChart({
compact = false,
width,
height,
margin = {
top: 20,
left: 50,
bottom: 20,
right: 20
}
}) {
const brushRef = useRef(null);
const [filteredStock, setFilteredStock] = useState(stock);
const onBrushChange = (domain) => {
if (!domain) return;
const { x0, x1, y0, y1 } = domain;
const stockCopy = stock.filter((s) => {
const x = getDate(s).getTime();
const y = getStockValue(s);
return x > x0 && x < x1 && y > y0 && y < y1;
});
setFilteredStock(stockCopy);
};
const innerHeight = height - margin.top - margin.bottom;
const topChartBottomMargin = compact
? chartSeparation / 2
: chartSeparation + 10;
const topChartHeight = 0.8 * innerHeight - topChartBottomMargin;
const bottomChartHeight = innerHeight - topChartHeight - chartSeparation;
const xMax = Math.max(width - margin.left - margin.right, 0);
const yMax = Math.max(topChartHeight, 0);
const xBrushMax = Math.max(width - brushMargin.left - brushMargin.right, 0);
const yBrushMax = Math.max(
bottomChartHeight - brushMargin.top - brushMargin.bottom,
0
);
const dateScale = useMemo(
() =>
scaleTime({
range: [0, xMax],
domain: extent(filteredStock, getDate)
}),
[xMax, filteredStock]
);
const stockScale = useMemo(
() =>
scaleLinear({
range: [yMax, 0],
domain: [0, max(filteredStock, getStockValue) || 0],
nice: true
}),
[yMax, filteredStock]
);
const brushDateScale = useMemo(
() =>
scaleTime({
range: [0, xBrushMax],
domain: extent(stock, getDate)
}),
[xBrushMax]
);
const brushStockScale = useMemo(
() =>
scaleLinear({
range: [yBrushMax, 0],
domain: [0, max(stock, getStockValue) || 0],
nice: true
}),
[yBrushMax]
);
const initialBrushPosition = useMemo(
() => ({
start: { x: brushDateScale(getDate(stock[50])) },
end: { x: brushDateScale(getDate(stock[100])) }
}),
[brushDateScale]
);
const handleClearClick = () => {
if (brushRef?.current) {
setFilteredStock(stock);
brushRef.current.reset();
}
};
const handleResetClick = () => {
if (brushRef?.current) {
const updater = (prevBrush) => {
const newExtent = brushRef.current.getExtent(
initialBrushPosition.start,
initialBrushPosition.end
);
const newState = {
...prevBrush,
start: { y: newExtent.y0, x: newExtent.x0 },
end: { y: newExtent.y1, x: newExtent.x1 },
extent: newExtent
};
return newState;
};
brushRef.current.updateBrush(updater);
}
};
return (
<div>
<svg width={width} height={height}>
<LinearGradient
id={GRADIENT_ID}
from={background}
to={background2}
rotate={45}
/>
<rect
x={0}
y={0}
width={width}
height={height}
fill={`url(#${GRADIENT_ID})`}
rx={14}
/>
<AreaChart
hideBottomAxis={compact}
data={filteredStock}
width={width}
margin={{ ...margin, bottom: topChartBottomMargin }}
yMax={yMax}
xScale={dateScale}
yScale={stockScale}
gradientColor={background2}
/>
<AreaChart
hideBottomAxis
hideLeftAxis
data={stock}
width={width}
yMax={yBrushMax}
xScale={brushDateScale}
yScale={brushStockScale}
margin={brushMargin}
top={topChartHeight + topChartBottomMargin + margin.top}
gradientColor={background2}
>
<PatternLines
id={PATTERN_ID}
height={8}
width={8}
stroke={accentColor}
strokeWidth={1}
orientation={["diagonal"]}
/>
<Brush
xScale={brushDateScale}
yScale={brushStockScale}
width={xBrushMax}
height={yBrushMax}
margin={brushMargin}
handleSize={8}
innerRef={brushRef}
resizeTriggerAreas={["left", "right"]}
brushDirection="horizontal"
initialBrushPosition={initialBrushPosition}
onChange={onBrushChange}
onClick={() => setFilteredStock(stock)}
selectedBoxStyle={selectedBrushStyle}
/>
</AreaChart>
</svg>
<button onClick={handleClearClick}>Clear</button>
<button onClick={handleResetClick}>Reset</button>
</div>
);
}
export default function App() {
return (
<div className="App">
<BrushChart width="500" height="300" />
</div>
);
}
We use the appleStock
mock data to create our chart.
We just get the first 1000 entries from the appleStock
array and render them.
brushMargin
is the margins for the chart.
We declare other variables for the colors like accentColor
for the fill color.
background
and background2
for the gradient colors.
getDate
lets us get the date from the data for the x-axis.
getStockValue
returns the value for the y-axis.
Then we create the AreaChart
component with the filled line chart.
We use the axisBottomTickLabelProps
object to style the x-axis.
And we do the same with the y-axis with the axisLeftTickLabelProps
.
In the AreaChart
component, we add the LinearGradient
component with the gradient for the fill of the line chart.
AreaClosed
has the fill between the line and the x-axis.
AxisBottom
has the x-axis. scale
has the x-axis scale. top
sets the x-axis location.
AxisLeft
has the y-axis.
Next, we create the BrushChart
component to create a chart that lets us navigate the filled line chart by dragging it.
We have th onBrushChange
function to change the location of the filled line chart.
Then we set the innerHeight
, topChartBottomMargin
, etc. to set the margins, heights, and the max values for the x and y axes.
We set the scales with the dateScale
, stockScale
, brushDateScale
, and brushStockScale
to set the scales for the brush chart.
And we set the initialBrushPosition
to a value to set the initial position for the brush chart.
We also have the handleClearClick
to clear the brush when we click the Clear button.
handleResetClick
resets the brush position.
Finally, we put the filled line chart above the brush chart by putting the 2 AreaCharts
together.
The first is for the main line chart and the 2nd is for navigation.
The Brush
component lets us drag the 2nd line chart to navigate the first line chart.
Conclusion
We can create a filled line chart with navigation by adding multiple line charts with a brush in our React app.