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.
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:
<PriceChart />
component that accepts an array of coordinates as a prop, leveraging them to construct the shape of the line graphLet’s firstly explore how to construct the SVG itself, before wrapping it in a React component and embedding props for the line coordinates.
Constructing an SVG chart component is a three-step process:
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:
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:
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.
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:
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:
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.
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.
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:
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
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