DEV Community

Cover image for How to Build a Bitcoin DCA Chart with React and Recharts
Cody Pearce
Cody Pearce

Posted on • Edited on • Originally published at codinhood.com

How to Build a Bitcoin DCA Chart with React and Recharts

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
...
Enter fullscreen mode Exit fullscreen mode

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");
  ...
}
...
Enter fullscreen mode Exit fullscreen mode

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);
    }
 }
...
Enter fullscreen mode Exit fullscreen mode

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

...
useEffect(() => {
  getCoinData(startDate, endDate);
}, []);
...
Enter fullscreen mode Exit fullscreen mode

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>
);
...
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
...
Enter fullscreen mode Exit fullscreen mode

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>;
...
Enter fullscreen mode Exit fullscreen mode

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:

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",
  },
};
Enter fullscreen mode Exit fullscreen mode

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'},
  ...
]
Enter fullscreen mode Exit fullscreen mode

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'
},
Enter fullscreen mode Exit fullscreen mode

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>
)
...
Enter fullscreen mode Exit fullscreen mode

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>;
...
Enter fullscreen mode Exit fullscreen mode

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.

<Area
  type="step"
  dataKey="Total"
  stroke="none"
  fillOpacity={1}
  fill="#f7931a"
/>
Enter fullscreen mode Exit fullscreen mode

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.

...
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"
  }
};
Enter fullscreen mode Exit fullscreen mode

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.

<Tooltip
  contentStyle={styles.tooltipWrapper}
  labelStyle={styles.tooltip}
  formatter={value => `$${round(value, 2)}`}
/>
Enter fullscreen mode Exit fullscreen mode

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.

...
<Area
  dataKey="Total"
  stroke="none"
  fillOpacity={1}
  fill="#f7931a"
  dot={{ fill: "white", strokeWidth: 2 }}
/>
...
Enter fullscreen mode Exit fullscreen mode

dots graph

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

...
<Area
  dataKey="Total"
  stroke="none"
  fillOpacity={1}
  fill="#f7931a"
  activeDot={{ strokeWidth: 0 }}
/>
...
Enter fullscreen mode Exit fullscreen mode

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.

...
import {
  AreaChart,
  XAxis,
  YAxis,
  Tooltip,
  Area,
} from "recharts";
...
<AreaChart data={dataArr} height={250} width={700}>
  <XAxis dataKey={"date"} />
  <YAxis orientation={"left"}  />
  ...
</AreaChart>
...
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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.

<YAxis yAxisId="right" orientation="right" />
Enter fullscreen mode Exit fullscreen mode

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 }}
/>
...
Enter fullscreen mode Exit fullscreen mode

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.

...
import {
  AreaChart,
  XAxis,
  YAxis,
  Tooltip,
  Area,
  ResponsiveContainer
} from "recharts";
...
<ResponsiveContainer height={250}>
  <AreaChart data={dataArr}>
    ...
  </AreaChart>
</ResponsiveContainer>
...
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
chuongtrh profile image
Chuongtran

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.