How to Build a Bitcoin DCA Chart with React and Recharts

Cover image

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.

Checkout www.cryptodca.org to see an interactive example of the type of chart we will be building. 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).

js
1.// amountToInvest / coinPrice
2.200 / 434.33 ~= .46 // Amount of Coin for the first month
3.
4.// amountOfCoin * coinPrice
5..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).

js
1.// amountToInvest / coinPrice
2.200 / 371.04 ~= .54 // Amount of Coin bought in the second month
3.
4.// amountOfCoin for second month + amountOfCoin for first month
5..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.

js
1.// Total Accumulated Amount of Coin * coinPrice
2.1 * 371.04 = 371.04;

Extending this process to the rest of the months produces a table like this:

MonthCoin PriceTotal InvestedAmount of CoinTotal Value
1434.33200.46200
2371.044001371.04
3424.496001.47624.00
4416.758001.95811.20
5452.5910002.391081.69

The code to calculate these values might look something like this.

jsx
1.for (let i = 0; i < numOfDays; i += freqInDays) {
2. const coinPrice = priceArr[i].price;
3. coinAmount += amountToInvest / coinPrice;
4. totalInvested += amountToInvest;
5. const total = coinAmount * coinPrice;
6.
7. dataArr.push({
8. TotalInvested: totalInvested,
9. CoinAmount: coinAmount,
10. CoinPrice: coinPrice,
11. Total: total,
12. date: priceArr[i].date,
13. });
14.}

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.

shell
1.npx create-react-app bitcoin-dca
2.cd bitcoin-dca
3.npm start

Go to src/App.js and remove the starter code.

jsx
1.import React from "react";
2.import "./App.css";
3.
4.function App() {
5. return (
6. <div className="App">
7. <h1 className="title">Bitcoin</h1>
8. </div>
9. );
10.}
11.
12.export default App;

Finally, go to src/App.css and update the styling as follows.

css
1.body {
2. background-color: #232323;
3. color: white;
4.}
5..title {
6. color: #f7931a;
7. font-size: 40px;
8.}
9..App {
10. text-align: center;
11.}

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.

jsx
1.import React, { useEffect, useState } from "react";
2.import "./App.css";
3.
4.const APIURL = "https://api.coingecko.com/api/v3/";
5.
6.function App() {
7. const startDate = "1/1/2016";
8. const endDate = "1/1/2020";
9. const freqInDays = 30;
10. const amountToInvest = 200;
11.
12. const [coinData, setCoinData] = useState([]);
13. const [isLoading, setIsLoading] = useState(false);
14. const [error, setError] = useState(false);
15.
16. const getCoinData = async (startDate, endDate) => {
17. setIsLoading(true);
18.
19. const url = ""; // TODO
20.
21. try {
22. const coinResponse = await fetch(url);
23. const data = await coinResponse.json();
24.
25. setCoinData(data);
26. setError(false);
27. setIsLoading(false);
28. } catch (e) {
29. setIsLoading(false);
30. setError(e);
31. }
32. };
33.
34. return (
35. <div className="App">
36. <h1>Bitcoin</h1>
37. </div>
38. );
39.}
40.
41.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.

jsx
1....
2.import dayjs from "dayjs";
3.import advancedFormat from "dayjs/plugin/advancedFormat";
4.dayjs.extend(advancedFormat);
5....

Next Use dayjs’s format method to convert the dates to Unix timestamp from within the getCoinData function.

jsx
1....
2.const getCoinData = async (startDate, endDate) => {
3. ...
4. const startDateUnix = dayjs(startDate).format("X");
5. const endDateUnix = dayjs(endDate).format("X");
6. ...
7.}
8....

Next build the URL as described above, fetch the data, and update the component’s state with setCoinData.

jsx
1....
2. const getCoinData = async (startDate, endDate) => {
3. ...
4. const startDateUnix = dayjs(startDate).format("X");
5. const endDateUnix = dayjs(endDate).format("X");
6. const range = `range?vs_currency=usd&from=${startDateUnix}&to=${endDateUnix}`;
7.
8. const url = `${APIURL}/coins/bitcoin/market_chart/${range}`;
9. try {
10. const coinResponse = await fetch(url);
11. const data = await coinResponse.json();
12.
13. setCoinData(data);
14. setError(false);
15. setIsLoading(false);
16. } catch (e) {
17. setIsLoading(false);
18. setError(e);
19. }
20. }
21....

Now we can call this function in the useEffect hook with the dates provided at the top of the component.

jsx
1....
2.useEffect(() => {
3. getCoinData(startDate, endDate);
4.}, []);
5....

There are four UI states we need to handle: noData, loading, error, and data. Add some conditionals below the useEffect hook as shown below.

jsx
1....
2.let content = <div>No Data</div>;
3.if (coinData && coinData.prices && coinData.prices.length > 0)
4. content = <div>Data</div>;
5.if (isLoading) content = <div>Loading</div>;
6.if (error) content = <div>{error}</div>;
7.
8.return (
9. <div className="App">
10. <h1 className="title">Bitcoin</h1>
11. {content}
12. </div>
13.);
14....

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.

jsx
1.import React from "react";
2.
3.export default function Totals({ priceArr, freqInDays, amountToInvest }) {
4. const numOfDays = priceArr.length;
5. let coinAmount = 0;
6. for (let i = 0; i < numOfDays; i += freqInDays) {
7. const coinValue = priceArr[i][1];
8. coinAmount += amountToInvest / coinValue;
9. }
10.
11. const totalCoinAmount = coinAmount;
12. const totalInvested = amountToInvest * Math.floor(numOfDays / freqInDays);
13. const endTotal = totalCoinAmount * priceArr[priceArr.length - 1][1];
14. const numberGained = endTotal - totalInvested;
15. const percentGained = ((endTotal - totalInvested) / totalInvested) * 100;
16.
17. return <div>Totals</div>;
18.}

To display these values, create another component src/Totaljs with some simple styling.

jsx
1.import React from "react";
2.
3.export default function Total({ title, value }) {
4. return (
5. <div style={styles.row}>
6. <h4 style={styles.title}>{title}:</h4>
7. <h4 style={styles.value}>{value}</h4>
8. </div>
9. );
10.}
11.
12.const styles = {
13. row: {
14. display: "flex",
15. flexDirection: "row",
16. justifyContent: "space-between",
17. alignItems: "center",
18. maxWidth: 350,
19. margin: "10px auto",
20. },
21. title: {
22. fontWeight: 600,
23. margin: 0,
24. },
25. value: {
26. color: "#f7931a",
27. fontSize: 24,
28. margin: 0,
29. },
30.};

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.

jsx
1.export default function round(num, digit) {
2. return +(Math.round(num + "e+" + digit) + "e-" + digit);
3.}

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.

jsx
1.// ./src/Totals.js
2.
3.import Total from "./Total";
4.import round from "./round";
5....
6.return (
7. <div>
8. <Total title={"Ending Value (USD)"} value={`$${round(endTotal, 2)}`} />
9. <Total title={"Amount of Coin (BTC)"} value={round(totalCoinAmount, 5)} />
10. <Total
11. title={"Amount Invested (USD)"}
12. value={`$${round(totalInvested, 2)}`}
13. />
14. <Total title={"Gained (USD)"} value={`$${round(numberGained, 2)}`} />
15. <Total title={"Gained (%)"} value={`${round(percentGained, 2)}%`} />
16. </div>
17. );
18....

Finally, import Totals into App.js, and replace the “data” state with the Totals component.

jsx
1....
2.import Totals from "./Totals";
3....
4.let content = <div>No Data</div>;
5.if (coinData && coinData.prices && coinData.prices.length > 0)
6. content = (
7. <Totals
8. priceArr={coinData.prices}
9. freqInDays={freqInDays}
10. amountToInvest={amountToInvest}
11. />
12. );
13.if (isLoading) content = <div>Loading</div>;
14.if (error) content = <div>{error}</div>;
15....

Totals

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:

jsx
1.import React from "react";
2.import dayjs from "dayjs";
3.
4.export default function Graph({ priceArr, freqInDays, amountToInvest }) {
5. const numOfDays = priceArr.length;
6. let coinAmount = 0;
7. let totalInvested = 0;
8. let dataArr = [];
9.
10. for (let i = 0; i < numOfDays; i += freqInDays) {
11. const coinPrice = priceArr[i][1];
12. coinAmount += amountToInvest / coinPrice;
13. totalInvested += amountToInvest;
14. const total = coinAmount * coinPrice;
15. const date = dayjs(priceArr[i][0]).format("MM/DD/YYYY");
16.
17. dataArr.push({
18. TotalInvested: totalInvested,
19. CoinAmount: coinAmount,
20. CoinPrice: coinPrice,
21. Total: total,
22. date: date,
23. });
24. }
25.
26. return <div style={styles.container}>Chart</div>;
27.}
28.
29.const styles = {
30. container: {
31. maxWidth: 700,
32. margin: "0 auto",
33. },
34.};

This will create an array of objects, dataArr, that will look like this:

js
1.[
2. {TotalInvested: 200, CoinAmount: .46, CoinPrice: 460, Total: 200, date: '1/1/2016'},
3. {TotalInvested: 400, CoinAmount: 1, CoinPrice: 380, Total: 200, date: '1/5/2016'},
4. ...
5.]

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:

js
1.{
2. TotalInvested: 400,
3. CoinAmount: 1,
4. CoinPrice: 380,
5. Total: 200,
6. date: '1/5/2016'
7.},

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.

jsx
1....
2.import { AreaChart, Area } from "recharts";
3.
4....
5.return (
6. <div style={styles.container}>
7. <AreaChart data={dataArr} height={250} width={700}>
8. <Area
9. dataKey="Total"
10. stroke="none"
11. fillOpacity={1}
12. fill="#f7931a"
13. />
14. </AreaChart>
15. </div>
16.)
17....

Add the Graph component to App.js to see AreaChart we built above.

jsx
1....
2.import Graph from "./Graph";
3....
4.let content = <div>No Data</div>;
5.if (coinData && coinData.prices && coinData.prices.length > 0)
6. content = (
7. <div>
8. <Totals
9. priceArr={coinData.prices}
10. freqInDays={freqInDays}
11. amountToInvest={amountToInvest}
12. />
13. <Graph
14. priceArr={coinData.prices}
15. freqInDays={freqInDays}
16. amountToInvest={amountToInvest}
17. />
18. </div>
19. );
20.if (isLoading) content = <div>Loading</div>;
21.if (error) content = <div>{error}</div>;
22....

First graph

The shape of the <Area> component can also be changed with the type prop. For example, pass in step to the `type prop.

jsx
1.<Area
2. type="step"
3. dataKey="Total"
4. stroke="none"
5. fillOpacity={1}
6. fill="#f7931a"
7./>

Step graph

Now try passing in natural.

Natural graph

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.

jsx
1....
2.import { AreaChart, Tooltip, Area } from "recharts";
3....
4.
5....
6.<AreaChart data={dataArr} height={250} width={700}>
7. <Tooltip
8. contentStyle={styles.tooltipWrapper}
9. labelStyle={styles.tooltip}
10. formatter={value => `${value}`}
11. />
12. <Area
13. dataKey="Total"
14. stroke="none"
15. fillOpacity={1}
16. fill="#f7931a"
17. />
18.</AreaChart>
19....
20.
21.const styles = {
22. container: {
23. maxWidth: 700,
24. margin: "0 auto"
25. },
26. tooltipWrapper: {
27. background: "#444444",
28. border: "none"
29. },
30. tooltip: {
31. color: "#ebebeb"
32. }
33.};

tooltip graph

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.

jsx
1.<Tooltip
2. contentStyle={styles.tooltipWrapper}
3. labelStyle={styles.tooltip}
4. formatter={value => `$${round(value, 2)}`}
5./>

tooltip fixed graph

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.

jsx
1....
2.<Area
3. dataKey="Total"
4. stroke="none"
5. fillOpacity={1}
6. fill="#f7931a"
7. dot={{ fill: "white", strokeWidth: 2 }}
8./>
9....

dots graph

We can also edit the dots on hover using the activeDot prop.

jsx
1....
2.<Area
3. dataKey="Total"
4. stroke="none"
5. fillOpacity={1}
6. fill="#f7931a"
7. activeDot={{ strokeWidth: 0 }}
8./>
9....

dots hover graph

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.

xaxis graph

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.

jsx
1....
2.import {
3. AreaChart,
4. XAxis,
5. YAxis,
6. Tooltip,
7. Area,
8.} from "recharts";
9....
10.<AreaChart data={dataArr} height={250} width={700}>
11. <XAxis dataKey={"date"} />
12. <YAxis orientation={"left"} />
13. ...
14.</AreaChart>
15....

labels graph

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.

jsx
1.<AreaChart data={dataArr} height={250} width={700}>
2. <XAxis dataKey={"date"} />
3. <YAxis orientation={"left"} />
4. <Tooltip
5. contentStyle={styles.tooltipWrapper}
6. labelStyle={styles.tooltip}
7. formatter={value => `$${round(value, 2)}`}
8. />
9. <Area
10. type="linear"
11. dataKey="CoinAmount"
12. stroke="none"
13. fillOpacity={0.4}
14. fill="#55efc4"
15. activeDot={{ strokeWidth: 0 }}
16. />
17. <Area
18. type="linear"
19. dataKey="Total"
20. stroke="none"
21. fillOpacity={0.6}
22. fill="#f7931a"
23. activeDot={{ strokeWidth: 0 }}
24. />
25. <Area
26. type="linear"
27. dataKey="TotalInvested"
28. stroke="none"
29. fillOpacity={0.6}
30. fill="#3498db"
31. activeDot={{ strokeWidth: 0 }}
32. />
33. <Area
34. type="linear"
35. dataKey="CoinPrice"
36. stroke="none"
37. fillOpacity={0.6}
38. fill="#e84393"
39. activeDot={{ strokeWidth: 0 }}
40. />
41.</AreaChart>

multi graph

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.

jsx
1.<YAxis yAxisId="right" orientation="right" />

Update each<Area> to map to the correct yAxisId value by providing the yAxisId prop to the <Area> component.

jsx
1....
2. <Area
3. type="linear"
4. dataKey="CoinAmount"
5. stroke="none"
6. fillOpacity={0.4}
7. fill="#f7931a"
8. yAxisId="right"
9. activeDot={{ strokeWidth: 0 }}
10./>
11.<Area
12. type="linear"
13. dataKey="Total"
14. stroke="none"
15. fillOpacity={0.6}
16. fill="#f7931a"
17. yAxisId="left"
18. activeDot={{ strokeWidth: 0 }}
19./>
20.<Area
21. type="linear"
22. dataKey="TotalInvested"
23. stroke="none"
24. fillOpacity={0.6}
25. fill="#3498db"
26. yAxisId="left"
27. activeDot={{ strokeWidth: 0 }}
28./>
29.<Area
30. type="linear"
31. dataKey="CoinValue"
32. stroke="none"
33. fillOpacity={0.6}
34. fill="#e84393"
35. yAxisId="left"
36. activeDot={{ strokeWidth: 0 }}
37./>
38....

scale fixed graph

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.

jsx
1....
2.import {
3. AreaChart,
4. XAxis,
5. YAxis,
6. Tooltip,
7. Area,
8. ResponsiveContainer
9.} from "recharts";
10....
11.<ResponsiveContainer height={250}>
12. <AreaChart data={dataArr}>
13. ...
14. </AreaChart>
15.</ResponsiveContainer>
16....

Responsive

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.