How to implement real-time SVG line graphs in React

Price Charts Are an Important Part of Any Financial Platform

It has become standard practice to include price charts on landing pages and dashboards in financial applications, both in app and on the web. These charts are built as an SVG aimed to show a trend over a period of time, often 24 hours, and are commonly updated every few seconds to every few minutes.

This is image title
A typical example of a price chart, displaying 24-hour price movements of an asset

Charts depicting price movements are often displayed in a list alongside other asset data, such as the price and price change, commonly maintaining a simple aesthetic by only displaying the line itself.

We’ll also cover how to shade the area below the line in a different colour, another common characteristic of these graphs.

Because of the simplicity of these graphs, it’s unnecessary to adopt a more capable graphing solution that’d result in bloating your project size and adding unnecessary complexity. With the knowledge of SVG manipulation used in conjunction with a React state, along with the process of normalising a data set to fit inside our SVG as anchor points, we can create an elegant, fast, and lightweight solution ourselves.

This article walks through this exact process, creating these graphs as a React component that takes a range of points to construct an SVG via inline JSX. We’ll cover:

  • How to construct a line graph with an SVG using a polyline for the line itself and a polygon for an optional shaded area below the line — also very useful to add additional aesthetics to the graph
  • How to build a <PriceChart /> component that accepts an array of coordinates as a prop, leveraging them to construct the shape of the line graph
  • How to take a range of raw prices and normalise them between a range of 0 and 100 to plot onto the SVG

Let’s firstly explore how to construct the SVG itself, before wrapping it in a React component and embedding props for the line coordinates.

Constructing a Price Chart SVG

Constructing an SVG chart component is a three-step process:

  1. The shape and density of the price chart is the first thing to be considered — coinciding with your data set that determines the SVG dimensions to be used.
  2. Once determined, we can go ahead and template out an SVG as a React component, wrapping it in a styled component to cater for its size and colours.
  3. Finally, the price coordinates can be embedded within the SVG via props and plotted as points within the SVG elements.

Determining SVG size

Planning the dimensions of your SVG is very important. Most price charts are plotted within a rectangle-shaped area, which is no coincidence. This is why:

  • The width of the SVG usually coincides with how many prices will be plotted. For example, 1 point per pixel for five-minute intervals give us a total of 288 plots for a 24-hour period. The width of the SVG in this case will be 287px (as the first point will sit at x position 0).
  • The height of the SVG needs to be normalisation friendly. In other words, once we normalise our prices into values between 0 and 1, they need to be multiplied again to fit the dimensions of the SVG (the SVG won’t just be 1px in height). We can add another calculation to multiply these normalised values by 100, for example, that’ll then give us a range between 0 and 100. Now this we can work with.

We’ll discuss this normalisation process in more detail in the second half of the article once our React price chart component is constructed and ready to take coordinates.

With the above in mind, we’re going to display price movements for the past 24 hours within our chart.

For this time frame, plotting points in intervals of five minutes is more than acceptable, providing enough detail to display accurate movements of the prices — a total of 288 price points to play with:

// price points with 5 minute intervals
86,400 (seconds in a day) / 300 (seconds in 5 minutes) = 288

What we end up with is a suitable SVG area of 287px by 100px. This area is great for embedding price charts in lists or in widgets:

This is image title

Some things to consider when determining your price interval: How big are your planned graphs going to be? Could you get away with less points and therefore decrease your app bandwidth requirements? What is the shortest interval your data source allows? You may need to design your UI around this or other constraints.

Our SVG size can now act as a basis for further manipulation depending on your UI goals. So if your price chart needed to stretch across an entire page, you could plot each point every four pixels instead of every pixel, which would be a total width of 1151px for the same five-minute interval — or whichever shape fits your needs.

Now the SVG viewBox dimensions have been determined, we can safely define our svg element (albeit with nothing inside it yet):

// defining the price chart viewBox dimensions
<svg
   xmlns="http://www.w3.org/2000/svg"
   viewBox="0 0 287 100"
>
   ...
</svg>

Now, let’s move onto defining the elements that’ll make the line graph itself and the shaded area below this line.

SVG elements: polyline and polygon

The line of our price chart is a compulsory element and is achieved with the polyline SVG element. A polyline is a basic shape that creates straight lines by connecting several coordinates via a points attribute:

// polyline drawing a zig-zag line
<svg ...>
  <polyline points="0,0 25,50 50,0 75,50 100,0" />
</svg>

The shaded area below the polyline is achieved with the polygon element. A polygon defines a closed shape consisting of a set of connected straight-line segments. Like polyline, polygon also accepts a range of points that define the shape:

// polygon drawing a square
<svg ...>
  <polygon points="0,0 100,0 100,100 100,0" />
</svg>

Combining these shapes together is all that is needed for an aesthetically pleasing price chart. Consider the following illustration to see how polyline and polygon work together to give the price chart some personality:

This is image title
SVG consists of two elements: a

We only need to worry about the stroke colour of the polyline and only a fill colour of the polygon. This can all be achieved with CSS, which will be addressed further down.

You can think of the polygon as a box that has a whole bunch of points lying on top of it ready to conform to the same movements as the line itself:

This is image title
Only the top line of our

With this in mind, we can now update our svg a little from the last section, adding these two elements:

<svg
   xmlns="http://www.w3.org/2000/svg"
   viewBox="0 0 287 100"
>
  <polygon
    points={`${...} 287,100 0,100}
  />
  <polyline
    points={`${...}}
   />
</svg>

Now we’re getting somewhere, but the points props now need to be provided to define the shape.

For now, I’ve added empty string literals, with the addition of the bottom points of the polygon element — remember, we only need to define the top line of that shape to match the polyline movement.

SVG as a React component

The final part of the puzzle is to implement a React component that’ll take a points prop — an array of coordinates — and place them as points in the SVG elements.

Firstly, let’s consider how these coordinates will be formatted. They’ll be arrays, with each point providing an x and y value. Let’s not assume any formatting rules here, like the commas that separate the x and y coordinates of each point in the polyline and polygon elements — we’ll deal with formatting in the component itself.

Let’s just import the plain values with dummy data at this point. This is how that might be done:

import { PriceChart } from './PriceChart';
const MyComponent = () => (
  <PriceChart
    points={[[0,50],[1,51],[2, 50.5],[3,56],[4,50] ... [287,78]]}
  />
);

We’re providing an array of arrays, each with an x and y coordinate. The x value starts at 0 — the left side of the chart — and works its way across point by point. The y values are our normalised prices between 0 and 100, the range of which we ascertained earlier.

At the API level, the x coordinate is very simple to calculate. You could just loop through your y coordinates and increment a counter that initialises at 0, before returning the completed coordinate sequence. Another solution would be to simply return the normalised prices and increment the x coordinate on the front end.

That just leaves our <PriceChart /> component to plot these points. Lets [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) them out and format them in a way our SVG elements understand:

export const PriceChart = (props) => (
  <svg...>
    <polygon
      points={`${props.points.map(p => 
                 ' ' + p[0] + ',' + p[1]
                )} 287,100 0,100`
             }
    />
    <polyline
      points={`${props.points.map(p => 
                 ' ' + p[0] + ',' + p[1]
                )}`
             }
    />
  </svg>
);

Our <PriceChart /> functional component now returns the completed SVG. It maps out the points prop at the JSX level, taking each point as p and returning a formatted coordinate in the format of x,y, while also adding a space between each point. Index 0 of each p point is our x coordinate, whereas index 1 is our y coordinate.

After adding some CSS via styled components, our completed component resembles the following Github Gist. Here’s the full implementation:

import React from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`  
  overflow: hidden;
  width: 100%;
  height: 100%;
  svg  {
    width: 100%;
    polygon {
      fill: #f2f2f2;
    }
    polyline {
      stroke: #777;
      stroke-width: 2.5;
      fill: none;
    }
  }
`;

export const PriceChart = (props) => {

  return (
    <Wrapper>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 287 100">
        <polygon
          points={`${props.points.map((p) => ' ' + p[0] + ',' + p[1])} 287,100 0,100`}
        />
        <polyline
          points={`${props.points.map((p) => ' ' + p[0] + ',' + p[1])}`}
        />
      </svg>
    </Wrapper>
  );
}

export default PriceChart;

pricechart.js

In terms of styling, we have wrapped the SVG itself in a styled div we’ve termed Wrapper.

We’ve ensured the containing svg element maintains Wrapper’s full width and have also defined stroke and fill properties for polyline and polygon, respectively. Wrapper itself adheres to its containing element’s dimensions; it’s likely that we’ll be embedding <PriceChart /> within another containing component, so we’ll want to fall back to those dimensions.

OK, with our component now out of the way, let’s finally explore the price normalisation process in JavaScript.

Normalising a Data Set of Prices

This section is dedicated to how to turn an array of prices, such as the following:

const prices = [
  961.7442,
  8963.1259,
  8961.5466,
  8959.3715,
  8954.2278,
  ...
];

… into normalised values that fit our SVG, which is 100px in height:

const normalised_prices = [
  12.40342549423265,
  12.111408873991664,
  12.445187442672605,
  12.904885894352674,
  13.991985763740644,
  ...
}

The normalisation method we are adopting here is called feature scaling. Feature scaling takes the largest and smallest value in a data set and rescales the entire data set between the values of 0 and 1. The equation, which we will code in JavaScript shortly, is as follows:

This is image title
Feature scaling equation to normalise a data set between 0 and 1

Why do this? Because data, and prices in particular, vary a lot. They are so unpredictable that it’s impossible to predict and therefore base SVG dimensions on. Furthermore, our SVG is designed to handle a wide range of markets, each with varying price ranges.

Normalising a range of values is a great solution to this unpredictability. We already have access to the minimum and maximum prices at hand. If you simply have your prices in an array, you can use JavaScript’s Math.min() and Math.max() functions to get these values:

// getting min and max from an array
const min = Math.min(...prices);
const max = Math.max(...prices);

If you are dealing with complex JSON objects, a more resource-intensive (but syntax-minimal) solution would be to loop through the object and manually populate an array before calculating min and max values:

// getting max and min from some JSON API result
let prices = [];
for (let i = 0; i < json.length; i++) {
   prices.push(parseFloat(json[i].marketdata.ask_price));
}
const min = Math.min(...prices);
const max = Math.max(...prices);

In the above example, we’re fetching the price from an ask_price field from json[i].marketdata.

From here, we can loop through the each price and apply the normalisation equation to return a value between 0 and 1:

// feature scaling equation in Javascript
let normalised_price = 
   (parseFloat(prices[i]) - min) / (min - max);
if (isNaN(normalised_price)) {
  normalised_price = 0;
}

In JavaScript, 0 divided by 0 will result to NaN, which is not a float. Because of this, I have added an additional check with isNan() to override the normalised price to 0 in the event we’re given prices for an inactive market.

This is almost good enough — we now need to increase this range from 0–1 to 0–100 to coincide with our SVG that is 100px in height. To do so, we can multiply normalised_price by 100:

normalised_price = Math.abs(normalised_price * 100);

We’ve utilised Math.abs() here to ensure we are dealing with unsigned values.

Surely all is well now; our values will slot into our SVG with no issues. Well — almost. There is one more issue to contend with: An SVG’s origin (0, 0) is at the top left of the viewBox, not at the bottom left as we expect in standard mathematics.

Because of this, our values are currently inverted. A very high normalised price of 90 will appear near the bottom of the chart, whereas a low price of 5 will appear five pixels from the top. We need to add another calculation to invert our prices:

// inverting our prices to make up for SVG coordinate system
normalised_price = Math.abs(normalised_price - 100);

We have simply deducted 100 from the normalised price and removed the negative sign ensuring the resulting value is unsigned again. With this in place, a high-normalised price of 90 will invert to 10, and a low price of 5 will invert to 95 — all coinciding with our SVG viewBox setup.

To sum up this entire normalisation process, here’s the full solution:

/*
 * normalising a range of prices between 0 - 100 for SVGs
 * assuming raw prices are stored in `raw_prices` array
 */

const min = Math.min(...raw_prices);
const max = Math.max(...raw_prices);

let normalised_prices = [];

for (i = 0; i < raw_prices.length; i++) {
   let new_price = (parseFloat(raw_prices[i]) - min) / (min - max);
   if (isNaN(new_price)) {
      new_price = 0;
   }
   normalised_prices.push(Math.abs((Math.abs(new_price * 100)) - 100));
}

normalisation.js

Summary

This article has explored how SVGs can be utilised with React to create live price charts, whereby polygon and polyline points are fed via a component prop.

This prop passes raw coordinate values, not assuming any formatting rules the SVG expects. Instead, formatting is done within the React component when the points array is mapped.

We also visited how these normalised values are calculated using feature scaling, which ideally will be calculated on the back end and provided via an API or Websocket to your React app.

#reactjs #react #javascript

How to implement real-time SVG line graphs in React
53.05 GEEK