Recharts: Intro to JavaScript charting

A good charting library makes all the difference when adding visual data to your JavaScript apps. Recharts is one of the better ones, built just for React.

The JavaScript ecosystem includes many charting libraries, but it’s tough to create a full-featured charting API that performs well in real-world usage. This article is a hands-on introduction to one of the good ones. Recharts is a React-specific incarnation of D3, a well-vetted framework. To test it out, we’ll use price and volume data for cryptocurrencies.

Set up the project

To set up our project, we’ll use create-react-app. You should already have Node/NPM installed. Once you do, you can type: npx create-react-app crypto-charts. Once that is finished, you can move into the /crypto-charts directory and add the required dependency with: npm install recharts.

Next, we'll use fetch to load the data from the CoinGecko API and then display it. We’ll get a screen like this one:

IDG

Figure 1. A simple display of cryptocurrency data.

To load and display the data, just put the following code into the project's src/App.js file and run the application with npm start.

Listing 1. First version of the cryptocurrency data-display app


import React, { useState, useEffect } from 'react';

function App() {
  const [cryptoData, setCryptoData] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(
          `https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=30`,
          {
            method: 'GET',
            headers: {
              'Content-Type': 'application/json',
            },
          }
        );

        const data = await response.json();
        setCryptoData(data);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []);

  return (
    <div className="App">
      <h1 className="chart-title">Crypto Currency Price Chart</h1>
      <div className="data-container">
        <h2>Price Data</h2>
        <pre>{JSON.stringify(cryptoData, null, 2)}</pre>
      </div>
    </div>
  );
}

export default App;

This code is intentionally very simple so we can make sure the API is loading the data properly. We just output it to the screen with JSON.stringify(). Notice that we're using the bitcoin endpoint.

Add a line chart

Next, let’s plug the price data from the response into a simple line chart, as showin in Listing 2. I'm only showing the parts of the code that are different from Listing 1. 

Listing 2. Displaying the data in a line chart


import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';

function App() {
  const [cryptoData, setCryptoData] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(
          `https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=30`,
          {
            method: 'GET',
            headers: {
              'Content-Type': 'application/json',
            },
          }
        );

        const data = await response.json();
        setCryptoData(data.prices);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }); // Run effect whenever timeframe changes

  return (
    <div className="App">
      <h1 className="chart-title">Crypto Currency Price Chart</h1>
      {/* LineChart with Recharts */}
      <LineChart width={800} height={400} data={cryptoData}>
        <CartesianGrid stroke="#f5f5f5" />
        <XAxis dataKey="0" tickFormatter={(timestamp) => new Date(timestamp).toLocaleDateString()} />
        <YAxis />
        <Tooltip labelFormatter={(value) => new Date(value).toLocaleDateString()} />
        <Legend verticalAlign="top" height={36} />
        <Line type="monotone" dataKey="1" stroke="#8884d8" dot={false} />
      </LineChart>
    </div>
  );
}

export default App;

The key updates here are importing the Recharts components, creating the cryptoData state variable hook, defining the <LineChart> markup, and plugging the cryptoData state into it. In just seven lines of markup, we’ve got a pretty nice looking and functional chart. Besides the LineChart component itself, which consumes the cryptoData variable as its data prop, we have several nested children that help define the chart layout, including XAxis, YAxis, Tooltip, Legend, and Line.

The XAxis and YAxis components tell the chart how to handle the axes. In this case, we handle the formatting of the “ticks” with a simple time formatter, since the X axis is the timestamps. The dataKey property tells the axis how to find its value, which is the first element of the array, 0. The YAxis has no properties and uses the defaults. The Tooltip formatter is what drive the data you’ll see when you mouse over the line. Legend sets the styling for the legend and the Line component sets the styling on the line itself.

Running the code from Listing 2 should result in a screen like the one in Figure 2.

IDG

Figure 2. Adding a basic line chart

Add a timescale component

Now let’s add the ability to scale the timeframe. Recharts makes this very easy with the Brush component. In Listing 3, you can see the changes necessary for this update.

Listing 3. Adding a timescale component


   import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Brush } from 'recharts';
// … remains the same

        <Line type="monotone" dataKey="1" stroke="#8884d8" dot={false} />
        {/* Add Brush component configuration */}
        <Brush dataKey="0" height={30} stroke="#8884d8" />
      </LineChart>

// … remains the same

To get the slider to adjust the timeframe, we just need to import the Brush component, then add it to the chart with styling instructions. This makes it very easy to add an otherwise tricky feature. Notice that not only can you slide the ends of timeframe, you can drag the timeframe bar itself to scroll across the data horizontally. Figure 3 is the resulting screen.

IDG

Figure 3. The timeframe slider can be scrolled, dragged, and resized.

Now let’s add a simple control to change the range of dates that we pull down from the CoinGecko API, as shown in Listing 4. This change provides a simple drop-down menu with date-range options (1 month, 2 months, etc.). When you change the date range, the number of days displayed in the chart will be updated.

Listing 4. Adding a date-range component


 // ...same imports...
function App() {
  const [cryptoData, setCryptoData] = useState([]);
  const [timeframe, setTimeframe] = useState(30); // Default timeframe: 30 days

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(
          `https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=${timeframe}`,
          {
            method: 'GET',
            headers: {
              'Content-Type': 'application/json',
            },
          }
        );

        const data = await response.json();
        setCryptoData(data.prices);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  },[timeframe]); // Run effect whenever timeframe changes

  const handleTimeframeChange = (event) => {
    const newTimeframe = event.target.value;
          alert(newTimeframe);
    setTimeframe(newTimeframe);
  };

  return (
    <div className="App">
      <h1 className="chart-title">Crypto Currency Price Chart</h1>
      <div className="controls">
        <div className="timeframe-selector">
          <label>Select Timeframe: </label>
          <select value={timeframe} onChange={handleTimeframeChange}>
            <option value={7}>1 Week</option>
            <option value={30}>1 Month</option>
            <option value={90}>3 Months</option>
            <option value={180}>6 Months</option>
          </select>
        </div>
      </div>
      {/* LineChart with Recharts */}
      // … same …
      );
}

export default App;

The main point of Listing 4 is changing the URL of the endpoint that drives the chart, and causing the chart to reflect that change. Here are the steps to make this update:

  • Create a timeframe state variable.
  • Use the timeframe variable in the Fetch URL: days=${timeframe}.
  • Pass the timeframe variable into the useEffect hook as a dependency variable, so when timeframe changes, the hook will run. When the fetch() returns with new data, the cryptoData state changes. Now the chart itself is reactive to that variable as its data property. In short: the chart redraws with the new data.
  • Make a simple drop-down menu that allows changing the timeframe.
  • Add a handleTimeFrameChange handler to apply the new value to timeframe.

Add a volume bar chart

Now let’s add a different kind of chart. The CoinGecko endpoint is also returning a data.total_volumes field with an array of timestamps and volumes (i.e., the amount of assets being transferred at the time). We can use this to create a bar chart on the same time series as the price chart. This will give us a UI that looks like the screen in Figure 4.

IDG

Figure 4. Adding a bar chart

To do this, we add the new bar chart component to the markup and connect it to the same data source, as shown in Listing 5.

Listing 5. Adding the bar chart for volumes


import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Brush, BarChart, Bar } from 'recharts';

function App() {
  // … same ...

  return (
    // … same ...
        <div className="chart">
          <h2>Volume Chart</h2>
            <BarChart width={800} height={400} data={cryptoData.total_volumes}>
              <CartesianGrid stroke="#f5f5f5" />
              <XAxis dataKey="0" tickFormatter={(timestamp) => new Date(timestamp).toLocaleDateString()} />
              <YAxis />
              <Tooltip labelFormatter={(value) => new Date(value).toLocaleDateString()} />
              <Legend verticalAlign="top" height={36} />
              <Bar dataKey="1" fill="#82ca9d" />
            </BarChart>
        </div>
      </div>
    </div>
  );
}

export default App;

The bar chart has the same kind of configuration as the line chart. But instead of connecting it to cryptoData.prices, we use cryptoData.total_volumes. Recharts has made it simple to use a different kind of chart. Another nice thing here is that none of the data wiring has changed, and even the timeframe dropdown works on both charts (since they are using the same root state variable). 

Custom vertical zoom

We’ve seen how to add a zoom on the timescale with the brush component, but what if we want to zoom in on the vertical scale—for example, on the prices in the price chart? 

Recharts doesn't have a built-in component for vertical zooming, but we can still do it with a bit of finagling. The trick is to use the YAxis domain property, which defines the minimum and maximum values on the vertical axis. This property will also accept a function. We can hijack the function to use a reactive variable and dynamically change the min and max values to achieve a vertical zoom, as shown in Figure 5.

IDG

Figure 5. Vertical zoom with a slider

As a first step, we need to add a third-party slider component that supports a range: npm i rc-slider.

Then we can use it, as shown in Listing 6.

Listing 6. Implementing a vertical zoom


// ... same ...
import Slider from 'rc-slider'; // Import the Slider component from rc-slider
import 'rc-slider/assets/index.css';

function App() {
  // ... same ...
  const [sliderValues, setSliderValues] = useState([0, 100]); 
  const [allowableRange, setAllowableRange] = useState([0, 100]);
  const [yAxisDomain, setYAxisDomain] = useState([0, 100]);

  // ... same ...

  useEffect(() => {
    if (cryptoData.prices && cryptoData.prices.length > 0) {
      const minY = Math.min(...cryptoData.prices.map(entry => entry[1]));
      const maxY = Math.max(...cryptoData.prices.map(entry => entry[1]));
      setSliderValues([minY, maxY]);
      setAllowableRange([minY, maxY]);
      setYAxisDomain([minY, maxY]); 
    }
  }, [cryptoData]);

  // Update the yAxisDomain when the slider values change
  useEffect(() => {
    setYAxisDomain(sliderValues);
  }, [sliderValues]);

  // ... same ...

  return (
    // ... same ...
<label>Price Range: </label>
        <Slider
          range
          value={sliderValues}
          onChange={(newValues) => setSliderValues(newValues)}
          min={allowableRange[0]}
          max={allowableRange[1]}
        />

          <LineChart width={800} height={400} data={cryptoData.prices}>
            <CartesianGrid stroke="#f5f5f5" />
            <XAxis dataKey="0" tickFormatter={(timestamp) => new Date(timestamp).toLocaleDateString()} />
            <YAxis domain={([dataMin, dataMax]) => { return [yAxisDomain[0], yAxisDomain[1]]; }} />
            <Tooltip labelFormatter={(value) => new Date(value).toLocaleDateString()} />
            <Legend verticalAlign="top" height={36} />
            <Line type="monotone" dataKey="1" stroke="#8884d8" dot={false} />
            <Brush dataKey="0" height={30} stroke="#8884d8" />
          </LineChart>
        </div>
        // ... same ...
      </div>
    </div>
  );
}

export default App;

The clever part of Listing 6 is


<YAxis domain={([dataMin, dataMax]) => { return [yAxisDomain[0], yAxisDomain[1]]; }} />

The YAxis component accepts a function (as well as hard-coded values), which takes two arguments: dataMin and dataMax. These two are built-in values that express the minimum and maximum values of the given data. In our case, we ignore them and use our own values: the yAxisDomain array, which we calculate ourselves.

To set up the custom slider and use it to drive the vertical zoom,  we need three state variables: sliderValues, allowableRange, and yAxisDomain. These will be the slider’s output, the slider’s range values, and the variable used on the YAxis domain, as we’ve described.

To set them up, we use an effect hook that watches the cryptoData variable. When it changes, we find the minimum and maximum for the price data, set the slider’s allowable range, and default the current value to that range (note that the slider begins all the way zoomed out). 

To keep the yAxisDomain in sync with sliderValues, we use another effect hook that watches sliderValues.

Configuring the slider itself with these values is straightforward:


value={sliderValues}         onChange={(newValues) => setSliderValues(newValues)}         min={allowableRange[0]} max={allowableRange[1]}

The net result is that when the slider is moved, the chart updates its Y Axis range, giving us the zoom effect.

Conclusion

Recharts is a full-featured, reliable charting solution for JavaScript. Here, we’ve just covered some basics of what you can do with Recharts. I also showed you how to rig up a vertical zoom using the Y axis domain functional prop. In general, Recharts is a great option for charting in React, with a wide range of chart styles and options that you can play with.