Create a Single Line Chart in React with @vx
This is part 2 of the Data Visualization with React and @vx series
- 1) Bar Chart in React with @vx
- 2) Create a Single Line Chart in React with @vx
Update: October 2020 - @vx is now @visx and is at 1.0. You can read about the update from Airbnb Engineering. As of this update, the api is all the same, and it has TypeScript support 👍.
We’ve built a bar chart in react using the data vis library @vx. The next chart that we can build to help tell our data’s story is a line chart. Line charts are great for showing trends over time and that’s what we’ll build today.
What We're Building TL;DR
Requirements
- Display time along the x-axis
- Display metrics along the y-axis
- Show each data point on line
Packages
Let's start by getting the packages we need from @vx. We'll need shapes, scale, axis, gradient (easy background color), and some mock data to get started.
1yarn add @vx/shapes @vx/group @vx/scale @vx/axis @vx/curve @vx/gradient @vx/text
Or
1npm install @vx/shapes @vx/group @vx/scale @vx/axis @vx/curve @vx/gradient @vx/text --save
Data
Now that we have our packages we can start stubbing out our data. We're going to use some mock data to get started so feel free to create your own or use this data set.
1const defaultData1 = [2 {3 miles: 5.6,4 date: 1595228400000,5 },6 {7 miles: 3.2,8 date: 1595314800000,9 },10 {11 miles: 7.9,12 date: 1595401200000,13 },14 {15 miles: 4.1,16 date: 1595487600000,17 },18 {19 miles: 9.3,20 date: 1595574000000,21 },22];
Now that we have the shape of our data we can add some helper functions that will access those items. This will help us add the date across the x-axis and the miles along the y-axis. We'll see how these come into play a little later.
1// accessors return the miles and date of that data item2const x = (d) => d.miles;3const y = (d) => d.date;
Scales
We can now define the max height and max width that we would like our chart to be. Our component will take height and width as props and then we can add a little padding. This will help us as we define our scales for this chart.
1// bounds2const xMax = width - 120;3const yMax = height - 80;
The scales are where the magic really happen. It all comes down to domain
and range
. The general rule of thumb based on my understanding is that domain
is the lowest and highest data points. The range
is the pixel range we would like to plot these data points on.
In our scales below we can see that range
(rangeRound
) is from 0
to xMax
which is the height bound of our chart. @vx gives us a helper, rangeRound
, that prettifies the numbers.
The domain
is an array of all data points which resolves to lowest (4.1) and highest (9.3) of the data set.
1const xScale = scaleTime({2 rangeRound: [0, xMax],3 domain: [Math.min(...data.map(x)), Math.max(...data.map(x))],4});56const yScale = scaleLinear({7 rangeRound: [0, yMax],8 domain: [Math.max(...data.map(y)), 0],9});
Building our Line Chart
Now we can start building the component. Let's start by setting up the svg
that will hold our line and axes.
1import React from "react";2import { Group } from "@vx/group";3import { scaleTime, scaleLinear } from "@vx/scale";45// dimensions6const height = 500;7const width = 800;89// accessors10const x = (d) => new Date(d.date).valueOf();11const y = (d) => d.miles;1213const LineChart = ({ data = [] }) => {14 // bounds15 const xMax = width - 120;16 const yMax = height - 80;1718 const xScale = scaleTime({19 rangeRound: [0, xMax],20 domain: [Math.min(...data.map(x)), Math.max(...data.map(x))],21 });2223 const yScale = scaleLinear({24 rangeRound: [0, yMax],25 domain: [Math.max(...data.map(y)), 0],26 });2728 return (29 <svg width={width} height={height}>30 <Group top={25} left={65}></Group>31 </svg>32 );33};3435export default LineChart;
Looks good. The first thing we'll add is the y-axis. To do this we use AxisLeft
from @vx. We need to pass it our yScale and we'll give it a few other props for styling. The numTicks
limits the number of values shown on the y-axis and label
is what will display along the axis.
Then we'll add the AxisBottom
that has similar props to to AxisLeft
. The top
is where it should start vertically from the top, which is the chart height in this case. The labelOffset
prop dictates how much space is in between the ticks and the axis label. It should look like this:
1import React from "react"2import { Group } from "@vx/group"3import { scaleTime, scaleLinear } from "@vx/scale"4import { AxisLeft, AxisBottom } from "@vx/axis"56...78<Group top={25} left={65}>9 <AxisLeft scale={yScale} numTicks={4} label="Miles" />10 <AxisBottom scale={xScale} label="Day" labelOffset={15} numTicks={5} top={yMax} />11</Group>
- Display time along the x-axis
- Display metrics along the y-axis
Now we can add the line to the chart using LinePath
from @vx/shapes
and we'll pass it curveLinear
from @vx/curve
to dictate its shape.
1import React from "react"2import { Group } from "@vx/group"3import { scaleTime, scaleLinear } from "@vx/scale"4import { AxisLeft, AxisBottom } from "@vx/axis"5import { LinePath } from "@vx/shape"6import { curveLinear } from "@vx/curve"78...910<Group top={25} left={65}>11 <AxisLeft scale={yScale} numTicks={4} label="Miles" />12 <AxisBottom scale={xScale} label="Day" labelOffset={15} numTicks={5} top={yMax} />13 <LinePath14 data={data}15 curve={curveLinear}16 x={(d) => xScale(x(d))}17 y={(d) => yScale(y(d))}18 stroke="#222222"19 strokeWidth={1.5}20 />21</Group>
It's looking like a nice, one-line chart now. We might want to add some dots to represent the data points. To do that we'll map over the data items and use the circle
element positioned using each item's points.
1<Group top={25} left={65}>2 <AxisLeft scale={yScale} numTicks={4} label="Miles" />3 <AxisBottom scale={xScale} label="Day" labelOffset={15} numTicks={5} top={yMax} />4 {data.map((point, pointIndex) => (5 <circle6 key={pointIndex}7 r={5}8 cx={xScale(x(point))}9 cy={yScale(y(point))}10 stroke="#222222"11 fill="#222222"12 fillOpacity={0.5}13 />14 ))}15 <LinePath16 data={data}17 curve={curveLinear}18 x={(d) => xScale(x(d))}19 y={(d) => yScale(y(d))}20 stroke="#222222"21 strokeWidth={1.5}22 />23</Group>
- Show each data point on line
Awesome, we fulfilled all of our requirements for this one-line chart. Here is all the code together.
1import React from "react";2import { Group } from "@vx/group";3import { scaleTime, scaleLinear } from "@vx/scale";4import { AxisLeft, AxisBottom } from "@vx/axis";5import { LinePath } from "@vx/shape";6import { curveLinear } from "@vx/curve";78// dimensions9const height = 500;10const width = 800;1112// accessors13const x = (d) => new Date(d.date).valueOf();14const y = (d) => d.miles;1516const LineChart = ({ data = [] }) => {17 // bounds18 const xMax = width - 120;19 const yMax = height - 80;2021 const xScale = scaleTime({22 rangeRound: [0, xMax],23 domain: [Math.min(...data.map(x)), Math.max(...data.map(x))],24 });2526 const yScale = scaleLinear({27 rangeRound: [0, yMax],28 domain: [Math.max(...data.map(y)), 0],29 });3031 return (32 <svg width={width} height={height}>33 <Group top={25} left={65}>34 <AxisLeft scale={yScale} numTicks={4} label="Miles" />35 <AxisBottom scale={xScale} label="Day" labelOffset={15} numTicks={5} top={yMax} />36 {data.map((point, pointIndex) => (37 <circle38 key={pointIndex}39 r={5}40 cx={xScale(x(point))}41 cy={yScale(y(point))}42 stroke="#222222"43 fill="#222222"44 fillOpacity={0.5}45 />46 ))}47 <LinePath48 data={data}49 curve={curveLinear}50 x={(d) => xScale(x(d))}51 y={(d) => yScale(y(d))}52 stroke="#222222"53 strokeWidth={1.5}54 />55 </Group>56 </svg>57 );58};5960export default LineChart;
Bonus
For better sizing/resizing we can use a resize observer hook in our component. I like to use the package use-resize-observer
for this. Let's see how we can use it in our component.
1import React from "react";2import { Group } from "@vx/group";3import { scaleTime, scaleLinear } from "@vx/scale";4import { AxisLeft, AxisBottom } from "@vx/axis";5import { LinePath } from "@vx/shape";6import { curveLinear } from "@vx/curve";7import useResizeObserver from "use-resize-observer";89// dimensions10const height = 500;11const width = 800;1213// accessors14const x = (d) => new Date(d.date).valueOf();15const y = (d) => d.miles;1617const LineChart = ({ data = [] }) => {18 const { ref, width = 1, height = 1 } = useResizeObserver();1920 // bounds21 const xMax = width - 120;22 const yMax = height - 80;2324 const xScale = scaleTime({25 rangeRound: [0, xMax],26 domain: [Math.min(...data.map(x)), Math.max(...data.map(x))],27 });2829 const yScale = scaleLinear({30 rangeRound: [0, yMax],31 domain: [Math.max(...data.map(y)), 0],32 });3334 return (35 <div style={{ width: "100%", height: "100%" }} ref={ref}>36 <svg width={width} height={height}>37 <Group top={25} left={65}>38 <AxisLeft scale={yScale} numTicks={4} label="Miles" />39 <AxisBottom scale={xScale} label="Day" labelOffset={15} numTicks={5} top={yMax} />40 {data.map((point, pointIndex) => (41 <circle42 key={pointIndex}43 r={5}44 cx={xScale(x(point))}45 cy={yScale(y(point))}46 stroke="#222222"47 fill="#222222"48 fillOpacity={0.5}49 />50 ))}51 <LinePath52 data={data}53 curve={curveLinear}54 x={(d) => xScale(x(d))}55 y={(d) => yScale(y(d))}56 stroke="#222222"57 strokeWidth={1.5}58 />59 </Group>60 </svg>61 </div>62 );63};6465export default LineChart;