Crypto Interactive Visualization – React and Chartjs
Data is beautiful
Following on from my previous post introducing React and Chartjs, I want to demonstrate how to make an interactive bar chart using crypto currency data.
This is still based on a software service that I’m currently creating called Coin Profit. This post will continue to use TypeScript and React.js.
Here is the example we will be using in this post.
Here is the repo for further inspection.
Data representation
We will have two components in this post:
- A Higher Order Component that will handle our Axios HTTP requests for coin data.
- This component will populate our dropdown selectors.
- A bar chart component that will receive data from the higher order component and render the bar chart using Chartjs.
Its important to note that the higher order component i.e the parent component will handle all changes of data. When the parent component changes its state, this data will be drilled back down into its child component i.e the BarChart
component
This is a common pattern in a typical React project. Having a component hierarchy like this provides a separation of concerns between a component that deals with logic and a component that receives its data.
Lets create our higher order component called App.tsx:
import * as React from 'react'; import BarChart from './barChart'; import { CoinPrice } from './types/CoinPrice'; import { CoinInfo } from './types/CoinInfo'; import './App.css'; import axios from 'axios'; export type State = { loading: { barChart?: boolean; lineChart?: boolean }; coinTypes: CoinInfo[]; barChartData: CoinPrice[]; barChartFilters: { coinToCompare: CoinInfo[] }; }; export type Props = {}; class App extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { barChartData: [], barChartFilters: { coinToCompare: [] }, coinTypes: [], loading: { barChart: true, lineChart: true }, }; } componentWillMount() { this.getCoinTypes(); this.getCoinCompare(); } private async getCoinCompare(coinType?: string) { if (coinType) this.setState({ loading: { barChart: true } }); let coinToCompare = coinType ? coinType : 'ETH'; const res = axios.get( `https://min-api.cryptocompare.com/data/price?fsym=${coinToCompare}&tsyms=${ coinType ? coinType + ',' : ',' }USD,EUR` ); const response = await res; let coinPrice = response.data; const barChartData = this.state.barChartData; if (coinType) this.setState({ barChartData: barChartData, barChartFilters: { coinToCompare: coinPrice }, loading: { barChart: false }, }); else { this.setState({ barChartData: coinPrice }); } } private getCoinTypes() { let coins: CoinInfo[] = []; axios .get( 'https://rocky-bayou-96357.herokuapp.com/https://www.cryptocompare.com/api/data/coinlist/' ) .then(response => { Object.keys(response.data.Data).forEach(function(key) { coins.push(response.data.Data[key]); }); coins.sort((a, b) => a.CoinName.toUpperCase().localeCompare(b.CoinName.toUpperCase()) ); this.getCoinCompare(coins[0].Symbol); this.setState({ coinTypes: coins, }); }) .catch(function(error) {}); } public render() { return ( <div className="App"> <div className=""> <h2> Compare coin by day </h2> <BarChart isLoading={this.state.loading.barChart} barChartFilters={this.state.barChartFilters} onCoinChange={(coinType: string, chartType: string) => this.getCoinCompare(coinType) } coinTypes={this.state.coinTypes} barChartData={this.state.barChartData} symbol={'ETH'} /> </div> </div> ); } } export default App;
Lets also create a React component called barchart.tsx
with its associated type State and Props.
import * as React from 'react'; import { ClipLoader } from 'react-spinners'; import { Bar } from 'react-chartjs-2'; import './App.css'; import { CoinInfo } from './types/CoinInfo'; import { CoinPrice } from './types/CoinPrice'; export type State = { selectedCoin: string; selectedTime: string; currentFilter: string; selectedCurrency: string; }; export type Props = { symbol: string; isLoading: boolean | undefined; coinTypes: CoinInfo[]; barChartFilters: { coinToCompare: CoinInfo[] }; barChartData: CoinPrice[]; onCoinChange: (coinType: string, chartType: string) => void; }; class BarChart extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { selectedCoin: '', selectedCurrency: 'EUR', currentFilter: 'low', selectedTime: '', }; } private onCurrencyChange(e: React.ChangeEvent<HTMLSelectElement>) { this.setState({ selectedCurrency: e.target.value }); } onSymChange(e: React.ChangeEvent<HTMLSelectElement>) { this.setState({ selectedCoin: e.target.value, }); this.props.onCoinChange(e.target.value, 'barchart'); } render() { const initialCoinType = this.props.coinTypes.length > 0 ? this.props.coinTypes[0].CoinName : ''; const coinTypesExist = this.props.coinTypes.length > 0; let data = { labels: [this.props.symbol, this.state.selectedCoin || initialCoinType], datasets: [ { label: 'Todays price', fillColor: 'white', data: [ this.props.barChartData[this.state.selectedCurrency], this.props.barChartFilters.coinToCompare[ this.state.selectedCurrency ], ], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 99, 132, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(255, 206, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(255, 159, 64, 0.2)', ], borderColor: [ 'rgba(255,99,132,1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)', ], borderWidth: 1, }, ], }; let options = { maintainAspectRatio: false, responsive: true, legend: { labels: { fontColor: 'white', fillStyle: 'white', margin: 5 + 'px', fontFamily: `"Roboto Mono",Helvetica,Arial,sans-serif`, }, }, scales: { yAxes: [ { ticks: { callback: (value: number, index: number) => { return ( `${this.state.selectedCurrency === 'EUR' ? '€' : '$'}` + new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3, }).format(value) ); }, fontColor: 'white', fontSize: 10, fontFamily: 'Roboto Mono', beginAtZero: true, }, }, ], xAxes: [ { ticks: { fontColor: 'white', fontFamily: 'Roboto Mono', fontSize: 12, beginAtZero: true, }, barPercentage: 0.5, }, ], }, }; return ( <div className="barchart panel"> {this.props.isLoading && this.props.coinTypes ? ( <div className="loader"> <ClipLoader color={'white'} loading={this.props.isLoading} /> </div> ) : ( <div> <label className="pt-label .modifier"> Currency <div className="pt-select"> <select defaultValue={'EUR'} onChange={e => this.onCurrencyChange(e)} > <option value="EUR">Eur</option> <option value="USD">USD</option> </select> </div> </label> <label className="pt-label .modifier"> Comparison Coin <div className="pt-select"> <select value={ this.state.selectedCoin ? this.state.selectedCoin : coinTypesExist ? this.props.coinTypes[0].FullName : '' } onChange={e => this.onSymChange(e)} name="coin-type" > {this.props.coinTypes ? this.props.coinTypes.map((coin, index) => { return ( <option value={coin.Symbol} key={index}> {' '} {coin.CoinName}{' '} </option> ); }) : null} </select> </div> </label> <div> <Bar data={data} width={50} height={350} options={options} /> </div> </div> )} </div> ); } } export default BarChart;
TypeScript Definitions
For the purpose of this example, I’ve mapped some of the expected data responses from CryptoCompare to some type definitions. The bar chart component will be mainly using the CoinInfo
data type:
export interface CoinInfo { Id: string; Url: string; ImageUrl: string; Name: string; CoinName: string; FullName: string; Algorithm: string; ProofType: string; TotalCoinSupply?: number; Symbol: string; SortOrder: string; EUR?: number; USD?: number; }
Having interfaces defined like this helps to determine what properties are accessible from a data request at compile time.
Interfaces also enable Intellisense to provide further information about in your coding environment.
Axios
We want to retrieve crypto information using the HTTP library Axios, once this data is retrieved it will flow down into the BarChart
component. Before the App.tsx
higher level component mounts we want to to fire off all the Axios actions.
componentWillMount() { this.getCoinTypes() this.getCoinCompare() }
In getCoinTypes()
we query the coin list endpoints in order to get a comprehensive list coins to populate the comparison dropdown:
private getCoinTypes() { let coins: CoinInfo[] = []; let coinNames: string[] = []; axios .get( 'https://rocky-bayou-96357.herokuapp.com/https://www.cryptocompare.com/api/data/coinlist/' ) .then(response => { Object.keys(response.data.Data).forEach(function(key) { coins.push(response.data.Data[key]); }); coins.forEach(element => { coinNames.push(element.CoinName); }); coinNames.sort(); coins.sort((a, b) => a.CoinName.toUpperCase().localeCompare(b.CoinName.toUpperCase()) ); this.getCoinCompare(coins[0].Symbol); this.setState({ coinNames: coinNames, coinTypes: coins, }); }) .catch(function(error) {}); }
From this request we:
- Retrieve the coins from the coinlist endpoint
- Push the coins into a separate array
- Sort coins alphabetically
- Update the components state with this information
Notice, that I didn’t include async+await here as I wanted to demonstrate HTTP requests with both types of syntax in order to highlight those differences.
Also note that I have prefixed the CryptoCompare endpoint with
https://rocky-bayou-96357.herokuapp.com in order to make a cross origin request.
This API enables cross origin requests to anywhere.
Get coins to compare
This request occurs when the user selects a coin to compare in the dropdown field:
In barchart.tsx
we have a dropdown fields that has a onChange
event:
<label className="pt-label .modifier"> Comparison Coin <div className="pt-select"> <select value={ this.state.selectedCoin ? this.state.selectedCoin : coinTypesExist ? this.props.coinTypes[0].FullName : '' } onChange={e => this.onSymChange(e)} name="coin-type" > {this.props.coinTypes ? this.props.coinTypes.map((coin, index) => { return ( <option value={coin.Symbol} key={index}> {coin.CoinName} </option> ); }) : null} </select> </div> </label>
It calls this.onSymChange(e)
and passes the event:
onSymChange(e: React.ChangeEvent<HTMLSelectElement>) { this.setState({ selectedCoin: e.target.value, }); this.props.onCoinChange(e.target.value, 'barchart'); }
This sends the new chosen value up to App.tsx
via the onCoinChange
props callback.
When App.tsx
receives this callback from it also :
<BarChart isLoading={this.state.loading.barChart} barChartFilters={this.state.barChartFilters} onCoinChange={(coinType: string, chartType: string) => this.getCoinCompare(coinType) } coinTypes={this.state.coinTypes} barChartData={this.state.barChartData} symbol={'ETH'} />
Finally, with the new coin type available to our higher order component we can make a new request for its price information:
private async getCoinCompare(coinType?: string) { if (coinType) this.setState({ loading: { barChart: true } }); let coinToCompare = coinType ? coinType : 'ETH'; const res = axios.get( `https://min-api.cryptocompare.com/data/price?fsym=${coinToCompare}&tsyms=${ coinType ? coinType + ',' : ',' }USD,EUR` ); const response = await res; let coinPrice = response.data; const barChartData = this.state.barChartData; if (coinType) this.setState({ barChartData: barChartData, barChartFilters: { coinToCompare: coinPrice }, loading: { barChart: false }, }); else { this.setState({ barChartData: coinPrice }); } }
Again, we are setting the loading state when the call occurs, retrieving coin data and updating our components state with this new information to filter down to the BarChart
component.
Note, that we are using await
and wrapping the function withasync
. This is ES6 shorthand syntax for handling Promises. This allows us to handle our data asynchronously without the need for invoking .then()
like a regular promise implementation.
In otherwords, any code below the await
will not occur until the request has completed. This shorthand syntax is very handy for asynchronous data calls. If you are interested in finding out more about async+await have a look here.
Chart Options
The Chartjs library contains a wide array of options for customizing a bar chart. These options include styling the chart, changing the axis types, specifying labels and much more. I want to highlight the important options used in this example.
Data
data: [ this.props.barChartData[this.state.selectedCurrency], this.props.barChartFilters.coinToCompare[ this.state.selectedCurrency ],
this.props.barChartData[this.state.selectedCurrency]
represents the default coin selected for this tutorial (Etheruem)this.props.barChartFilters.coinToCompare[ this.state.selectedCurrency ]
represents the coin selected for by the user from the comparison coin dropdown.
Currency Formatting
The ticks on both axis’s of the chart contain values of the retrieved crypto data. Ideally, we want each individual tick to be in the format of the currency shown. This is achieved by using a callback in scales
property.
scales: { yAxes: [{ ticks: { callback: (value, index, values) => { return `${this.state.selectedCurrency === 'EUR' ? '€' : '$'}` + new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(value) }, } }],
Notice we are manipulating the tick value being returned and prefixing it with our desired currency symbol. This ensures our axis value is properly formatted and has a cap on how many digits it can show using maximumSignificantDigits
.
Labels
let data = { labels: [this.props.symbol, this.state.selectedCoin || initialCoinType], datasets: [ { label: 'Todays price', fillColor: 'white', data: [ this.props.barChartData[this.state.selectedCurrency], this.props.barChartFilters.coinToCompare[ this.state.selectedCurrency ], ],
Chartjs labels are defined via an array. Each index of the labels array corresponds to the index of the datasets array.
For this example, we only have two bars on our chart. So two labels are specified with two datasets. This data has been sent down to us via props.
Additionally, we can style the labels with the following legend options:
legend: { labels: { fontColor: 'white', fillStyle: 'white', margin: 5 + 'px', fontFamily: `"Roboto Mono",Helvetica,Arial,sans-serif`, }, },
Responsive
Ideally, we want our graph to be responsive on all screens and to not stretch unnecessarily:
maintainAspectRatio: false, responsive: true,
Once all these options properties are defined they can be applied to the Bar
component like so:
<div> <Bar data={data} width={50} height={350} options={options} /> </div>
BarChart
Inside our render function we have the following:
- A ternary operator that dictates when the loading spinner is shown (this is set by the state the App.tsx component).
-
{this.props.isLoading && this.props.coinTypes ? ( <div className="loader"> <ClipLoader color={'white'} loading={this.props.isLoading} /> </div> ) :
-
- A dropdown where the user can select currency
-
<label className="pt-label .modifier"> Currency <div className="pt-select"> <select defaultValue={'EUR'} onChange={e => this.onCurrencyChange(e)} > <option value="EUR">Eur</option> <option value="USD">USD</option> </select> </div> </label>
-
- A dropdown where the user can select an additional coin to compare on the chart:
-
<label className="pt-label .modifier"> Comparison Coin <div className="pt-select"> <select value={ this.state.selectedCoin ? this.state.selectedCoin : coinTypesExist ? this.props.coinTypes[0].FullName : '' } onChange={e => this.onSymChange(e)} name="coin-type" > {this.props.coinTypes ? this.props.coinTypes.map((coin, index) => { return ( <option value={coin.Symbol} key={index}> {coin.CoinName} </option> ); }) : null} </select> </div> </label>
-
React Modular Components
A key takeaway from this demonstration is that the BarChart
component now exists in its own right. It is modular and recyclable and with a small bit of tweaking, any data can be injected into it. This is quite a powerful concept, as having a component means that it can be reused in many different projects
This is a great benefit of libraries like React and shows how effective it can be for creating modular data visualisation components. The bar chart component embodies its own specific functionality and can be used as part of a bigger data visualisation dashboard.