How to Add Graphs and Charts to a React App

John Au-Yeung - Jan 28 '20 - - Dev Community

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

Many more articles at https://medium.com/@hohanga

Even more articles at http://thewebdev.info/

Business apps often display graphs and charts. The hard work of React developers has resulted in graphing libraries that make it easy to meet the requirements for displaying graphs and charts in apps. One popular graph display library is Chart.js, which can used in any JavaScript application. For React apps, we can use react-chartjs-2, a React wrapper for Chart.js, to easily add graphs and charts to our application.

In this piece, we will build an app to display current and historical exchange rates. The current exchange rates against the Euro will be displayed in a bar graph and historic rates will be displayed in line graphs. The data is obtained from the Foreign Exchange Rates API located at https://exchangeratesapi.io/. It’s free and does not require registration to use. It also supports cross-domain requests, so it can be used by web client-side apps.

To start we run Create React App to create the scaffolding code. Run npx create-react-app exchange-rate-app to create the app. Next, we need to install our libraries: run npm i axios bootstrap chart.js formik react-bootstrap react-chartjs-2 react-router-dom yup to install the libraries. Axios is our HTTP client for making requests to the Exchange Rates API. Bootstrap is for styling, React ChartJS is our graph library. React Router is for routing URLs to our pages. Formik and Yup are for handling form value changes and form validation, respectively.

Now we have all the libraries installed, we can start writing code. Code is located in the src folder unless otherwise stated. In App.js , we replace the existing code with this:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import TopBar from "./TopBar";
import HistoricRatesBetweenCurrenciesPage from "./HistoricRatesBetweenCurrenciesPage";
import HistoricRatesPage from "./HistoricRatesPage";
const history = createHistory();
function App() {
  window.Chart.defaults.global.defaultFontFamily = `
  -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
  "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
  sans-serif`;
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route path="/" exact component={HomePage} />
        <Route path="/historicrates" exact component={HistoricRatesPage} />
        <Route
          path="/historicrates2currencies"
          exact
          component={HistoricRatesBetweenCurrenciesPage}
        />
      </Router>
    </div>
  );
}
export default App;

We defined the routes with React Router here since it’s the entry point of our app. We also set the font for the graph here, so it will now be applied everywhere.

In App.css, we replace the existing code with this:

.center {  
    text-align: center;  
}

This centers the text in our app.

We add a file to add the list of currencies that we will use. Create a file called export.js and add this code:

export const CURRENCIES = [
  "CAD",
  "HKD",
  "ISK",
  "PHP",
  "DKK",
  "HUF",
  "CZK",
  "AUD",
  "RON",
  "SEK",
  "IDR",
  "INR",
  "BRL",
  "RUB",
  "HRK",
  "JPY",
  "THB",
  "CHF",
  "SGD",
  "PLN",
  "BGN",
  "TRY",
  "CNY",
  "NOK",
  "NZD",
  "ZAR",
  "USD",
  "MXN",
  "ILS",
  "GBP",
  "KRW",
  "MYR",
];

Now we can use this in our components.

Next, we create a page to display the historical exchange rates between two currencies. Create a file called HistoricRatesBetweenCurrenciesPage.js and add the following:

import React, { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import {
  getHistoricRates,
  getHistoricRatesBetweenCurrencies,
} from "./requests";
import { Line } from "react-chartjs-2";
import { CURRENCIES } from "./exports";
const schema = yup.object({
  startDate: yup
    .string()
    .required("Start date is required")
    .matches(/([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/),
  endDate: yup
    .string()
    .required("End date is required")
    .matches(/([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/),
  fromCurrency: yup.string().required("From currency is required"),
  toCurrency: yup.string().required("To currency is required"),
});
function HistoricRatesBetweenCurrenciesPage() {
  const [data, setData] = useState({});
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    const params = {
      start_at: evt.startDate,
      end_at: evt.endDate,
      base: evt.fromCurrency,
      symbols: evt.toCurrency,
    };
    const response = await getHistoricRatesBetweenCurrencies(params);
    const rates = response.data.rates;
    const lineGraphData = {
      labels: Object.keys(rates),
      datasets: [
        {
          data: Object.keys(rates).map(key => rates[key][evt.toCurrency]),
          label: `${evt.fromCurrency} to ${evt.toCurrency}`,
          borderColor: "#3e95cd",
          fill: false,
        },
      ],
    };
    setData(lineGraphData);
  };
  return (
    <div className="historic-rates-page">
      <h1 className="center">Historic Rates</h1>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="startDate">
                <Form.Label>Start Date</Form.Label>
                <Form.Control
                  type="text"
                  name="startDate"
                  placeholder="YYYY-MM-DD"
                  value={values.startDate || ""}
                  onChange={handleChange}
                  isInvalid={touched.startDate && errors.startDate}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.startDate}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="endDate">
                <Form.Label>End Date</Form.Label>
                <Form.Control
                  type="text"
                  name="endDate"
                  placeholder="YYYY-MM-DD"
                  value={values.endDate || ""}
                  onChange={handleChange}
                  isInvalid={touched.endDate && errors.endDate}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.endDate}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="fromCurrency">
                <Form.Label>From Currency</Form.Label>
                <Form.Control
                  as="select"
                  placeholder="From Currency"
                  name="fromCurrency"
                  onChange={handleChange}
                  value={values.fromCurrency || ""}
                  isInvalid={touched.fromCurrency && errors.fromCurrency}
                >
                  <option>Select</option>
                  {CURRENCIES.filter(c => c != values.toCurrency).map(c => (
                    <option key={c} value={c}>
                      {c}
                    </option>
                  ))}
                </Form.Control>
                <Form.Control.Feedback type="invalid">
                  {errors.fromCurrency}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="currency">
                <Form.Label>To Currency</Form.Label>
                <Form.Control
                  as="select"
                  placeholder="To Currency"
                  name="toCurrency"
                  onChange={handleChange}
                  value={values.toCurrency || ""}
                  isInvalid={touched.toCurrency && errors.toCurrency}
                >
                  <option>Select</option>
                  {CURRENCIES.filter(c => c != values.fromCurrency).map(c => (
                    <option key={c} value={c}>
                      {c}
                    </option>
                  ))}
                </Form.Control>
                <Form.Control.Feedback type="invalid">
                  {errors.toCurrency}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Search
            </Button>
          </Form>
        )}
      </Formik>
      <br />
      <div style={{ height: "400px", width: "90vw", margin: "0 auto" }}>
        <Line data={data} />
      </div>
    </div>
  );
}
export default HistoricRatesBetweenCurrenciesPage;

The page has a form to let users enter the date range for the historical rates they want and the currency that they are converting. Once the user enters the data, it’s validated against our form validation schema in the schema object, provided by the Yup library. We require the dates to be in YYYY-MM-DD format and all fields are required, so they’re checked against the schema for validity.

We filter out the currency that has been selected for the forCurrency from the choices of the toCurrency and vice versa so we won’t end up with the same currency for both dropdowns.

When the form submission is done we submit the data to the API and get the rates. We have to massage the data into a format that can be used by react-chartjs-2 , so we define the lineGraphData object with a datasets property to be an array of historical exchanges rates. label is the title of the line chart, borderColor is the border color of the line, andfill false means that we do not fill in the line with color. Once we set that with the setData(lineGraphData); function call, the graph is displayed.

Next, we create a page to search for historical exchange rates with the Euro as the base currency. To do this, we add a file called HistoricRatePage.js , and add this:

import React, { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import "./HistoricRatesPage.css";
import { getHistoricRates } from "./requests";
import { Line } from "react-chartjs-2";
import { CURRENCIES } from "./exports";
const schema = yup.object({
  startDate: yup
    .string()
    .required("Start date is required")
    .matches(/([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/),
  endDate: yup
    .string()
    .required("End date is required")
    .matches(/([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/),
  currency: yup.string().required("Currency is required"),
});
function HistoricRatesPage() {
  const [data, setData] = useState({});
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    const params = {
      start_at: evt.startDate,
      end_at: evt.endDate,
    };
    const response = await getHistoricRates(params);
    const rates = response.data.rates;
    const lineGraphData = {
      labels: Object.keys(rates),
      datasets: [
        {
          data: Object.keys(rates).map(key => rates[key][evt.currency]),
          label: `EUR to ${evt.currency}`,
          borderColor: "#3e95cd",
          fill: false,
        },
      ],
    };
    setData(lineGraphData);
  };
  return (
    <div className="historic-rates-page">
      <h1 className="center">Historic Rates</h1>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="startDate">
                <Form.Label>Start Date</Form.Label>
                <Form.Control
                  type="text"
                  name="startDate"
                  placeholder="YYYY-MM-DD"
                  value={values.startDate || ""}
                  onChange={handleChange}
                  isInvalid={touched.startDate && errors.startDate}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.startDate}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="endDate">
                <Form.Label>End Date</Form.Label>
                <Form.Control
                  type="text"
                  name="endDate"
                  placeholder="YYYY-MM-DD"
                  value={values.endDate || ""}
                  onChange={handleChange}
                  isInvalid={touched.endDate && errors.endDate}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.endDate}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="currency">
                <Form.Label>Currency</Form.Label>
                <Form.Control
                  as="select"
                  placeholder="Currency"
                  name="currency"
                  onChange={handleChange}
                  value={values.currency || ""}
                  isInvalid={touched.currency && errors.currency}
                >
                  <option>Select</option>
                  {CURRENCIES.map(c => (
                    <option key={c} value={c}>
                      {c}
                    </option>
                  ))}
                </Form.Control>
                <Form.Control.Feedback type="invalid">
                  {errors.country}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Search
            </Button>
          </Form>
        )}
      </Formik>
      <br />
      <div style={{ height: "400px", width: "90vw", margin: "0 auto" }}>
        <Line data={data} />
      </div>
    </div>
  );
}
export default HistoricRatesPage;

It’s similar to the previous page, except that we only choose the currency to convert to to display since the currency to convert from is always Euro. Once again we have a lineGraphData , with the datasets being an array and within it, data is an array of historical exchange rates. label is the title of the chart. borderColor and fill are the same as the previous graph.

Both forms are created by React Bootstrap form components. The Form components correspond to the regular Bootstrap 4 components.

Then we create HistoricalRatesPage.css and put the following:

.historic-rates-page {  
  margin: 0 auto;  
  width: 90vw;  
}

This adds some margins to our page.

Next, we create our home page. Create a file called HomePage.js and add the following:

import React, { useEffect, useState } from "react";
import Card from "react-bootstrap/Card";
import { getExchangeRate } from "./requests";
import "./HomePage.css";
import { Bar } from "react-chartjs-2";
function HomePage() {
  const [rates, setRates] = useState({});
  const [initialized, setInitialized] = useState(false);
  const [date, setDate] = useState("");
  const [base, setBase] = useState("");
  const [chartData, setChartData] = useState({});
  const getRates = async () => {
    const response = await getExchangeRate();
    const { base, date, rates } = response.data;
    setRates(rates);
    setDate(date);
    setBase(base);
    const filteredRates = Object.keys(rates).filter(key => rates[key] < 50);
    const data = {
      labels: filteredRates,
      datasets: [
        {
          backgroundColor: "green",
          data: filteredRates.map(key => rates[key]),
        },
      ],
    };
    setChartData(data);
    setInitialized(true);
  };
  useEffect(() => {
    if (!initialized) {
      getRates();
    }
  });
  const options = {
    maintainAspectRatio: false,
    legend: { display: false },
    scales: {
      yAxes: [{ ticks: { beginAtZero: true } }],
    },
    title: {
      display: true,
      text: "EUR Exchanges Rates",
    },
  };
  return (
    <div className="home-page">
      <h1 className="center">Rates as of {date}</h1>
      <br />
      <div style={{ height: "400px", width: "90vw", margin: "0 auto" }}>
        <Bar data={chartData} options={options} />
      </div>
      <br />
      {Object.keys(rates).map(key => {
        return (
          <Card style={{ width: "90vw", margin: "0 auto" }}>
            <Card.Body>
              <Card.Title>
                {base} : {key}
              </Card.Title>
              <Card.Text>{rates[key]}</Card.Text>
            </Card.Body>
          </Card>
        );
      })}
    </div>
  );
}
export default HomePage;

In this page, we display the list of current exchange rates from the API. We make a data object, with the currency symbols as the labels, and we also have a datasets property — an array of objects with data in the object being the current exchange rates. Also, we display the exchange rates in Bootstrap cards, provided by React Boostrap.

For styling this page, we create HomePage.css and add the following:

.home-page {  
  margin: 0 auto;  
}

This gives us some margins on our page.

Next, we create a file to let us make the requests to the Foreign Exchange Rates API. Create a file called requests.js and add the following:

const APIURL = "https://api.exchangeratesapi.io";
const axios = require("axios");
const querystring = require("querystring");
export const getExchangeRate = () => {
  return axios.get(`${APIURL}/latest`);
};
export const getRateBetweenCurrencies = data =>
  axios.get(`${APIURL}/history?${querystring.encode(data)}`);
export const getHistoricRates = data =>
  axios.get(`${APIURL}/history?${querystring.encode(data)}`);
export const getHistoricRatesBetweenCurrencies = data =>
  axios.get(`${APIURL}/history?${querystring.encode(data)}`);

This will get the exchange rates the way we want them, with requests to get the latest rates and historical rates, with or without specifying currency symbols for the base currency and currency to convert to.

Next, we create the top bar. Create a file called TopBar.js and add the following code:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
  const { pathname } = location;
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Currenc Converter App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            Home
          </Nav.Link>
          <Nav.Link
            href="/historicrates"
            active={pathname.includes("/historicrates")}
          >
            Historic Rates
          </Nav.Link>
          <Nav.Link
            href="/historicrates2currencies"
            active={pathname.includes("/historicrates2currencies")}
          >
            Historic Rates Between 2 Currencies
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);

This adds the navigation bar provided by Bootstrap to our pages and a link to the pages we created before. It also adds highlights for the link on the currently opened page. We wrap the component with the withRouter function, so we can get the currently opened route to let us highlight the links.

Finally, we replace the code in index.html with this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React Currency App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

This is so we get some Bootstrap styles and can change the title of the app. We replaced the title tag with our own and add the following between the head tags:

<link rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
/>
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .