Recharts is a charting library that provides a set of declarative React components for building charts with D3. Ten highly customizable chart types are available along with helper components. In this tutorial we will build a few AreaCharts to display portfolio value, total coin accumulated, and total invested over a particular historical time period when Dollar Cost Averaging Bitcoin.
Visit the project's Github to learn more.
Sections
- Graphing Dollar Cost Averaging
- Getting Started
- Historical Prices with CoinGecko’s API
- Getting the data
- Calculating Totals
- Building the Chart Array
- Recharts Area Chart
- Recharts Tooltip
- Recharts Dots
- Recharts YAxis and XAxis
- Recharts With Multiple Areas
- Responsive Recharts
- Conclusion
Graphing Dollar Cost Averaging
Dollar Cost Averaging (DCA) is an investment strategy where one buys the same dollar amount of an asset over regular intervals in order to reduce short-term volatility. For example, investing 200 dollars into a specific stock or cryptocurrency every month means that you will buy more stock when the stock price is low and less stock when the price is higher. Read the Investopedia Article on DCA to learn more.
Graphing a Bitcoin DCA account's value over time requires that we calculate the total account value at each interval over a time period. For example, if that interval is a month and the time period is two years, then we need to calculate the total account value 24 times. To calculate the total value at a particular interval we need to multiply the total accumulated coin up to that point by the coin price at the time of purchase. The total accumulated coin up to that point can be calculated by dividing the amount to be invested by the price of the coin at that the time purchase for each interval. Let's illustrate this with an example, say we plan to purchase $200 dollars worth of Bitcoin every month from January 2016 to May 2016.
The Amount of Coin for the first month is easy to calculate, simply take the Amount to Invest (200) divided by the Coin Price ($434.33) on January 1, 2016. Total value is similarly easy, simply take the Amount of Coin so far times the current Coin Price, which for the first month should equal the amount invested (200).
// amountToInvest / coinPrice
200 / 434.33 ~= .46 // Amount of Coin for the first month
// amountOfCoin * coinPrice
.46 * 434.33 ~= 200 // Total Value
Calculating the Amount of Coin for the second month is slightly different. First, similarly to last month, divide the Amount to Invest by the current month's Coin Price (371.04). Then add that value to the previous month's Amount of Coin (.46).
// amountToInvest / coinPrice
200 / 371.04 ~= .54 // Amount of Coin bought in the second month
// amountOfCoin for second month + amountOfCoin for first month
.54 + .46 = 1 // Total Accumulated Amount of Coin so far
To calculate the second month's Total value we take the Total Accumulated Amount of Coin times the current Coin Price.
// Total Accumulated Amount of Coin * coinPrice
1 * 371.04 = 371.04
Extending this process to the rest of the months produces a table like this:
Month | Coin Price | Total Invested | Amount of Coin | Total Value |
---|---|---|---|---|
1 | 434.33 | 200 | .46 | 200 |
2 | 371.04 | 400 | 1 | 371.04 |
3 | 424.49 | 600 | 1.47 | 624.00 |
4 | 416.75 | 800 | 1.95 | 811.20 |
5 | 452.59 | 1000 | 2.39 | 1081.69 |
The code to calculate these values might look something like this.
for (let i = 0; i < numOfDays; i += freqInDays) {
const coinPrice = priceArr[i].price;
coinAmount += amountToInvest / coinPrice;
totalInvested += amountToInvest;
const total = coinAmount * coinPrice;
dataArr.push({
TotalInvested: totalInvested,
CoinAmount: coinAmount,
CoinPrice: coinPrice,
Total: total,
date: priceArr[i].date,
});
}
numOfDays
is the total number of days for the time period. In this case there are 121 days between Jan 2016 to May 2016.
freqInDays
is the time interval of buying, which in this case is 30 days.
priceArr
is an array of objects with historical Bitcoin prices and date.
amountToInvest
is the dollar amount that will invested per time period, in this case it is 200.
coinAmount
is the total amount of coin accumulated up to this point.
totalInvested
is the total amount invested up to this point.
total
is the total value in USD of the portfolio.
These four values, TotalInvested
, CoinAmount
, CoinPrice
, and Total
are what we want to graph over time. freqInDays
, amountToInvest
, and numOfDays
will be provided by the user, while the historical Bitcoin prices, priceArr
, will be provided from CoinGecko's API.
Getting started
Initialize a new Create A React App project.
npx create-react-app bitcoin-dca
cd bitcoin-dca
npm start
Go to src/App.js
and remove the starter code.
import React from "react";
import "./App.css";
function App() {
return (
<div className="App">
<h1 className="title">Bitcoin</h1>
</div>
);
}
export default App;
Finally, go to src/App.css
and update the styling as follows.
body {
background-color: #232323;
color: white;
}
.title {
color: #f7931a;
font-size: 40px;
}
.App {
text-align: center;
}
Historical Prices with CoinGecko's API
CoinGecko's API offers free crypto data without an API key. The /coins/{id}/market_chart/range
endpoint gives historical market data for a specific coin within a specified range and is exactly what we need. The id
parameter refers to the id of the coin, which in this case is just bitcoin
. The vs_currency
param determines what currency the Bitcoin price will be sent as. The from
and to
params indicate the time period of prices to fetch and must be provided as a UNIX time stamp.
For example, https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=usd&from=1392577232&to=1422577232 fetches the price of Bitcoin in USD for each day between 02/16/2014
and 01/30/2015
.
Getting the data
First, let's set the static values, startDate
, endDate
, freqInDays
, and amountToInvest
at the top of App.js
. Ideally we would build a form to capture these values from a user, but now we'll statically define them here.
Next, build a basic async function that passes in startDate
and endDate
, fetches the data from CoinGecko's API, and finally puts that data in state. To hold the data and different states, we'll need to define coinData
, isLoading
, and error
in the component state.
import React, { useEffect, useState } from "react";
import "./App.css";
const APIURL = "https://api.coingecko.com/api/v3/";
function App() {
const startDate = "1/1/2016";
const endDate = "1/1/2020";
const freqInDays = 30;
const amountToInvest = 200;
const [coinData, setCoinData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);
const getCoinData = async (startDate, endDate) => {
setIsLoading(true);
const url = ""; // TODO
try {
const coinResponse = await fetch(url);
const data = await coinResponse.json();
setCoinData(data);
setError(false);
setIsLoading(false);
} catch (e) {
setIsLoading(false);
setError(e);
}
};
return (
<div className="App">
<h1>Bitcoin</h1>
</div>
);
}
export default App;
To pass the startDate
and endDate
parameters as human readable dates, we will use the dayjs library to convert human readable dates to UNIX timestamps. Import dayjs
and apply its advancedformat
extension.
...
import dayjs from "dayjs";
import advancedFormat from "dayjs/plugin/advancedFormat";
dayjs.extend(advancedFormat);
...
Next Use dayjs
's format
method to convert the dates to Unix timestamp from within the getCoinData
function.
...
const getCoinData = async (startDate, endDate) => {
...
const startDateUnix = dayjs(startDate).format("X");
const endDateUnix = dayjs(endDate).format("X");
...
}
...
Next build the URL as described above, fetch the data, and update the component's state with setCoinData
.
...
const getCoinData = async (startDate, endDate) => {
...
const startDateUnix = dayjs(startDate).format("X");
const endDateUnix = dayjs(endDate).format("X");
const range = `range?vs_currency=usd&from=${startDateUnix}&to=${endDateUnix}`;
const url = `${APIURL}/coins/bitcoin/market_chart/${range}`;
try {
const coinResponse = await fetch(url);
const data = await coinResponse.json();
setCoinData(data);
setError(false);
setIsLoading(false);
} catch (e) {
setIsLoading(false);
setError(e);
}
}
...
Now we can call this function in the useEffect
hook with the dates provided at the top of the component.
...
useEffect(() => {
getCoinData(startDate, endDate);
}, []);
...
There are four UI states we need to handle: noData
, loading
, error
, and data
. Add some conditionals below the useEffect
hook as shown below.
...
let content = <div>No Data</div>;
if (coinData && coinData.prices && coinData.prices.length > 0)
content = <div>Data</div>;
if (isLoading) content = <div>Loading</div>;
if (error) content = <div>{error}</div>;
return (
<div className="App">
<h1 className="title">Bitcoin</h1>
{content}
</div>
);
...
The data returned from const data = await coinResponse.json()
should be an array of UNIX timestamps and prices between the two dates we provided. This is exactly what we need to both calculate total values and create the graph.
Calculating Totals
Our goal here is to calculate the following values using the coinData.prices
array:
- Total Amount of Coin in BTC -
totalCoinAmount
- Total Value in USD -
endTotal
- Total Invested in USD -
totalInvested
- Money Gained in USD -
numberGained
- Money Gained in Percent -
percentGained
Much of the logic here should be familiar from the Graphing Dollar Cost Averaging
section above. numberGained
is simply the total value in USD minus the totalInvested
. percentGained
is the percent that the totalInvested
grew to reach the endTotal
. Create a file src/Totals
as shown below.
import React from "react";
export default function Totals({ priceArr, freqInDays, amountToInvest }) {
const numOfDays = priceArr.length;
let coinAmount = 0;
for (let i = 0; i < numOfDays; i += freqInDays) {
const coinValue = priceArr[i][1];
coinAmount += amountToInvest / coinValue;
}
const totalCoinAmount = coinAmount;
const totalInvested = amountToInvest * Math.floor(numOfDays / freqInDays);
const endTotal = totalCoinAmount * priceArr[priceArr.length - 1][1];
const numberGained = endTotal - totalInvested;
const percentGained = ((endTotal - totalInvested) / totalInvested) * 100;
return <div>Totals</div>;
}
To display these values, create another component src/Totaljs
with some simple styling.
import React from "react";
export default function Total({ title, value }) {
return (
<div style={styles.row}>
<h4 style={styles.title}>{title}:</h4>
<h4 style={styles.value}>{value}</h4>
</div>
);
}
const styles = {
row: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
maxWidth: 350,
margin: "10px auto",
},
title: {
fontWeight: 600,
margin: 0,
},
value: {
color: "#f7931a",
fontSize: 24,
margin: 0,
},
};
If you run the calculations above you'll find that most of the values contain many decimal places. Create a utility function, ./src/round.js
, to round the numbers off so they look nicer.
export default function round(num, digit) {
return +(Math.round(num + "e+" + digit) + "e-" + digit);
}
Import both round
and the Total
component into the Totals
component. Next, create a few Total
components while passing in a description into the title
prop, and the actual value into the value
prop. We can also format these values using the round
function.
// ./src/Totals.js
import Total from "./Total";
import round from "./round";
...
return (
<div>
<Total title={"Ending Value (USD)"} value={`$${round(endTotal, 2)}`} />
<Total title={"Amount of Coin (BTC)"} value={round(totalCoinAmount, 5)} />
<Total
title={"Amount Invested (USD)"}
value={`$${round(totalInvested, 2)}`}
/>
<Total title={"Gained (USD)"} value={`$${round(numberGained, 2)}`} />
<Total title={"Gained (%)"} value={`${round(percentGained, 2)}%`} />
</div>
);
...
Finally, import Totals
into App.js
, and replace the "data" state with the Totals
component.
...
import Totals from "./Totals";
...
let content = <div>No Data</div>;
if (coinData && coinData.prices && coinData.prices.length > 0)
content = (
<Totals
priceArr={coinData.prices}
freqInDays={freqInDays}
amountToInvest={amountToInvest}
/>
);
if (isLoading) content = <div>Loading</div>;
if (error) content = <div>{error}</div>;
...
Building the Chart Array
The code below should be very familiar from the Graphing Dollar Cost Averaging section above, please check out that section to learn how this code works. One difference is that we want to store the date in a human readable way using dayjs
again. Create a new file ./src/Graph.js
as below:
import React from "react";
import dayjs from "dayjs";
export default function Graph({ priceArr, freqInDays, amountToInvest }) {
const numOfDays = priceArr.length;
let coinAmount = 0;
let totalInvested = 0;
let dataArr = [];
for (let i = 0; i < numOfDays; i += freqInDays) {
const coinPrice = priceArr[i][1];
coinAmount += amountToInvest / coinPrice;
totalInvested += amountToInvest;
const total = coinAmount * coinPrice;
const date = dayjs(priceArr[i][0]).format("MM/DD/YYYY");
dataArr.push({
TotalInvested: totalInvested,
CoinAmount: coinAmount,
CoinPrice: coinPrice,
Total: total,
date: date,
});
}
return <div style={styles.container}>Chart</div>;
}
const styles = {
container: {
maxWidth: 700,
margin: "0 auto",
},
};
This will create an array of objects, dataArr
, that will look like this:
[
{TotalInvested: 200, CoinAmount: .46, CoinPrice: 460, Total: 200, date: '1/1/2016'},
{TotalInvested: 400, CoinAmount: 1, CoinPrice: 380, Total: 200, date: '1/5/2016'},
...
]
Rechart Area Chart
We're finally ready to start creating our charts. The Recharts <AreaChart>
and <Area>
components can be customized in a myriad of ways, but to start we'll create a very basic chart and build from there.
The <AreaChart>
component is a wrapping component that accepts the chart's data in the data
prop and provides that data to its children. In our case, we need to pass in the dataArr
array we created above into the data
prop. For the chart to display at all we also need to provide a height and width prop, in this case set height to 250 and width to 700.
The <Area>
component is what actually displays the data on the graph. The dataKey
prop will select the key in each object in the dataArr
object to display as data on the graph. Remember from above each object in the dataArr
looks something like this:
{
TotalInvested: 400,
CoinAmount: 1,
CoinPrice: 380,
Total: 200,
date: '1/5/2016'
},
Let's show the Total
value, so set the dataKey
prop to "Total". The <Area>
component accepts many other props for customizing the graph exactly how we want. For now let's just style the stroke
, fillOpacity
, and fill
.
...
import { AreaChart, Area } from "recharts";
...
return (
<div style={styles.container}>
<AreaChart data={dataArr} height={250} width={700}>
<Area
dataKey="Total"
stroke="none"
fillOpacity={1}
fill="#f7931a"
/>
</AreaChart>
</div>
)
...
Add the Graph
component to App.js
to see AreaChart we built above.
...
import Graph from "./Graph";
...
let content = <div>No Data</div>;
if (coinData && coinData.prices && coinData.prices.length > 0)
content = (
<div>
<Totals
priceArr={coinData.prices}
freqInDays={freqInDays}
amountToInvest={amountToInvest}
/>
<Graph
priceArr={coinData.prices}
freqInDays={freqInDays}
amountToInvest={amountToInvest}
/>
</div>
);
if (isLoading) content = <div>Loading</div>;
if (error) content = <div>{error}</div>;
...
The shape of the <Area>
component can also be changed with the type
prop. For example, pass in step
to the type prop.
<Area
type="step"
dataKey="Total"
stroke="none"
fillOpacity={1}
fill="#f7931a"
/>
Now try passing in natural
.
Recharts Tooltip
The above chart is a good start, but there's no way to see the individual values on the chart. We can use Recharts tooltip
to show the total value at each interval on the chart. We can also modify the styles of the tooltip with the contentStyle
and labelStyle
props.
...
import { AreaChart, Tooltip, Area } from "recharts";
...
...
<AreaChart data={dataArr} height={250} width={700}>
<Tooltip
contentStyle={styles.tooltipWrapper}
labelStyle={styles.tooltip}
formatter={value => `${value}`}
/>
<Area
dataKey="Total"
stroke="none"
fillOpacity={1}
fill="#f7931a"
/>
</AreaChart>
...
const styles = {
container: {
maxWidth: 700,
margin: "0 auto"
},
tooltipWrapper: {
background: "#444444",
border: "none"
},
tooltip: {
color: "#ebebeb"
}
};
One problem you'll notice is that the total values on the tooltips have a bunch of digits. We can format this number using the formatter
prop which takes a callback function that returns the data in a format. Pull in the rounding utility function we built above, ./src/round.js
to round the values to two places. Also add a $
character in front of the value to indicate that unit is in USD.
<Tooltip
contentStyle={styles.tooltipWrapper}
labelStyle={styles.tooltip}
formatter={value => `$${round(value, 2)}`}
/>
Recharts Dots
The dot
prop on the <Area>
component will add dots at each individual point on the chart. We can either pass in true
to show the dots with default style, pass in an object of styles to display the dots how we want, or pass in a custom dot element. For now, add a simple style object.
...
<Area
dataKey="Total"
stroke="none"
fillOpacity={1}
fill="#f7931a"
dot={{ fill: "white", strokeWidth: 2 }}
/>
...
We can also edit the dots on hover using the activeDot
prop.
...
<Area
dataKey="Total"
stroke="none"
fillOpacity={1}
fill="#f7931a"
activeDot={{ strokeWidth: 0 }}
/>
...
Recharts YAxis and XAxis
Using the <YAxis>
and <XAxis>
components, we can display both the YAxis and XAxis to give even more information about the scale of values. The <XAxis>
component will default to displaying the number of points in ascending order.
But we want to show the dates themselves on the XAxis. To do this, add the dataKey
prop to the <XAxis>
prop with the string 'date'.
There are a ton of props and customizations for both the XAxis
and YAxis
components, from custom labels, to custom scaling, ticks, and event handlers. We're going to keep it simple for now, however.
...
import {
AreaChart,
XAxis,
YAxis,
Tooltip,
Area,
} from "recharts";
...
<AreaChart data={dataArr} height={250} width={700}>
<XAxis dataKey={"date"} />
<YAxis orientation={"left"} />
...
</AreaChart>
...
Recharts With Multiple Areas
With Recharts we can add multiple Areas within the same chart to display related data along on the same timeline. In our case we want to show CoinAmount
, TotalInvested
, and CoinPrice
along with Total
within the same chart to see how all of the data relates. We'll need to give each new Area
a different color to distinguish them easily, as well as lower the opacity so we can see the charts overlapping. Create the rest of the Area
components within in the AreaChart
in the same way we created the one above using the dataKey
for each set of data.
<AreaChart data={dataArr} height={250} width={700}>
<XAxis dataKey={"date"} />
<YAxis orientation={"left"} />
<Tooltip
contentStyle={styles.tooltipWrapper}
labelStyle={styles.tooltip}
formatter={value => `$${round(value, 2)}`}
/>
<Area
type="linear"
dataKey="CoinAmount"
stroke="none"
fillOpacity={0.4}
fill="#55efc4"
activeDot={{ strokeWidth: 0 }}
/>
<Area
type="linear"
dataKey="Total"
stroke="none"
fillOpacity={0.6}
fill="#f7931a"
activeDot={{ strokeWidth: 0 }}
/>
<Area
type="linear"
dataKey="TotalInvested"
stroke="none"
fillOpacity={0.6}
fill="#3498db"
activeDot={{ strokeWidth: 0 }}
/>
<Area
type="linear"
dataKey="CoinPrice"
stroke="none"
fillOpacity={0.6}
fill="#e84393"
activeDot={{ strokeWidth: 0 }}
/>
</AreaChart>
One problem with this chart is that CoinAmount
is not measured in dollars but in Bitcoins, so displaying the CoinAmount
on the same graph is somewhat misleading. However, we can create two YAxis
components, one on the right and one on the left, to solve this problem. Currently, we already have the YAxis
on the left that's mapped to USD, so what we need is a second YAxis
mapped to BTC on the right side. Add a second YAxis
component with a yAxisId
prop set to "right" and a "orientation" prop set to "right". The yAxisId
prop will allow us to map an Area
to the correct YAxis
scale.
<YAxis yAxisId="right" orientation="right" />
Update each<Area>
to map to the correct yAxisId
value by providing the yAxisId
prop to the <Area>
component.
...
<Area
type="linear"
dataKey="CoinAmount"
stroke="none"
fillOpacity={0.4}
fill="#f7931a"
yAxisId="right"
activeDot={{ strokeWidth: 0 }}
/>
<Area
type="linear"
dataKey="Total"
stroke="none"
fillOpacity={0.6}
fill="#f7931a"
yAxisId="left"
activeDot={{ strokeWidth: 0 }}
/>
<Area
type="linear"
dataKey="TotalInvested"
stroke="none"
fillOpacity={0.6}
fill="#3498db"
yAxisId="left"
activeDot={{ strokeWidth: 0 }}
/>
<Area
type="linear"
dataKey="CoinValue"
stroke="none"
fillOpacity={0.6}
fill="#e84393"
yAxisId="left"
activeDot={{ strokeWidth: 0 }}
/>
...
There are plenty more customizations you can do with Recharts, checkout the Recharts docs to learn more.
Responsive Recharts
The chart will not automatically resize for smaller screens because the chart's height and width are statically defined. Making the chart responsive is surprisingly easy with Recharts, however. Wrap the <AreaChart>
component in a <ResponsiveContainer>
, remove the height and width from the <AreaChart>
, and provide a new height to the <ResponsiveContainer>
component.
...
import {
AreaChart,
XAxis,
YAxis,
Tooltip,
Area,
ResponsiveContainer
} from "recharts";
...
<ResponsiveContainer height={250}>
<AreaChart data={dataArr}>
...
</AreaChart>
</ResponsiveContainer>
...
Conclusion
There are plenty of other things we can do to make this project better. For example adding user input, better loading and error messaging, easy to share buttons, and URLs that are easy to link to a specific graph. If you're interested in how to add any of these extra features, check out the Github repo for crypto-dca.
Recharts makes creating charts extremely easy with React and D3 while at the same time providing a great amount of customization. Although there are more features to Recharts than can be covered in one project, I hope these examples helps you get started.
Top comments (2)
Great work :)
I'm building a simple tool to calculate DCA crypto, support more than 30 coin/token on the top market with combo Nuxtjs & Golang
cryptosaving.app
Some comments may only be visible to logged-in visitors. Sign in to view all comments.