React SVG radar chart

Lorenzo Spyna
ITNEXT
Published in
8 min readJul 23, 2018

--

Beautiful SVG radar chart made with React

There are a lot of libraries around, to create charts, but many of them are very heavy. Let’s see how we can build a radar chart with React.

The image format I choose is the SVG, the main reasons are:

  1. Resolution: SVG images are vectors, made by shapes and fills, you can zoom them all you want, and print at high resolution.
  2. Speed: SVG are weighed less than other image formats, the browser will download them faster. (In our case they will not be downloaded, but generated)
  3. Animations: we can add any kind of animations and CSS style.
  4. Accessibility and SEO: Google indexes SVG images.

Let’s code

Let’s start coding with React.

To generate the SVG radar chart, we’ll start creating a React Component, that will render an empty (by now) SVG.

import React from 'react';const RadarChart = props => {
return (<svg version="1" xmlns="http://www.w3.org/2000/svg" width="450" height="450"></svg>);
};
export default RadarChart;

Now we are going to fill the empty SVG with the radar chart.

Before adding any object to our chart we have to define the data structure of the data we want to display. Those data will be an array of objects, and will have this shape:

const data = [
{battery: 0.7, design: 1, useful: 0.9, speed: 0.67, weight: 0.8 },
{battery: 0.6, design: 0.9, useful: 0.8, speed: 0.7, weight: 0.6 }
];

Each object of the array is composed by fixed properties, whose value is a number between 0 and 1. In the above example, we have two data series, but we can add some other, or maybe leave only one. The advice I may give you is to use at most 3/4 series, or the data visualization will be too much confused.

Draw the scales

Let’s go on with the chart, drawing the scales, the classic radar concentric circles. We’ll create the scale function, to do this we need to know:

  • the size of the chart, to calculate the circle’s radius.
  • the number of circles, how many circles we want.
const chartSize = 450;
const numberOfScales = 4;

And this is the function.

const scale = value => (
<circle
key={`scale-${value}`}
cx={0}
cy={0}
r={(value / numberOfScales * chartSize) / 2}
fill="#FAFAFA"
stroke="#999"
strokeWidth="0.2"
/>
);

To calculate the circle radius ( r property), we use the value parameter and its value is: (value / numberOfScales * chartSize) / 2. What’s remarkable is that we put the center of the circle the position x = 0, y = 0.

We’ll call the scale function as many times as the circles we want:

for (let i = numberOfScales; i > 0; i--) {
scales.push(scale(i));
}

Putting all together the component will be:

import React from 'react';const data = [
{ battery: 0.7, design: 1, useful: 0.9, speed: 0.67, weight: 0.8 },
{ battery: 0.6, design: 0.9, useful: 0.8, speed: 0.7, weight: 0.6 }
];
const chartSize = 450;
const numberOfScales = 4;
const scale = value => (
<circle
key={`scale-${value}`}
cx={0}
cy={0}
r={((value / numberOfScales) * chartSize) / 2}
fill="#FAFAFA"
stroke="#999"
strokeWidth="0.2"
/>
);
const RadarChart = props => {
const groups = [];
const scales = [];
for (let i = numberOfScales; i > 0; i--) {
scales.push(scale(i));
}
groups.push(<g key={`scales`}>{scales}</g>);
return (
<svg
version="1"
xmlns="http://www.w3.org/2000/svg"
width={chartSize}
height={chartSize}
viewBox={`0 0 ${chartSize} ${chartSize}`}
>
<g>{groups}</g>
</svg>
);
};
export default RadarChart;

We set the center in 0,0 position, so the result will be:

The scales of the radar chart, in position 0,0

We have to apply a transformation to the groups, translating them in the middle of the SVG, with the property transform:

const middleOfChart = (chartSize / 2).toFixed(4);
...
<g transform={`translate(${middleOfChart},${middleOfChart})`}>{groups}</g>

And this is the result:

The circles of the radar chart are in the right position now.

The shapes

Let’s go on, drawing the actual shape of the chart.

We need to set an angle for each piece of data, to know where to draw the lines. Next, we’ll add a group g to put the shapes in.

const captions = Object.keys(data[0]);
const columns = captions.map((key, i, all) => {
return {
key,
angle: (Math.PI * 2 * i) / all.length
};
});
groups.push(<g key={`groups}`}>{data.map(shape(columns))}</g>);

We create the shape function, to draw the shapes. It is a little complicated, but the basic concept is: for each series in the data, we add a group g, where we will put a path.

The d property of the path is the path definition, inside this, we can use some functions, we are going to use:

  • moveTo (M): move the cursor to this position.
  • lineTo (L): draw a line from here to there.
  • closePath (z): close the path.

To know where to draw the lines we need to use some trigonometry, so I created the functions polarToX and polarToY, that use cosine and the angle we calculated before.

const polarToX = (angle, distance) => Math.cos(angle - Math.PI / 2) * distance * chartSize;
const polarToY = (angle, distance) => Math.sin(angle - Math.PI / 2) * distance * chartSize;
const pathDefinition = points => {
let d = 'M' + points[0][0].toFixed(4) + ',' + points[0][1].toFixed(4);
for (let i = 1; i < points.length; i++) {
d += 'L' + points[i][0].toFixed(4) + ',' + points[i][1].toFixed(4);
}
return d + 'z';
};
const shape = (columns) => (chartData, i) => {
const data = chartData;
return (
<path
key={`shape-${i}`}
d={pathDefinition(
columns.map(col => {
const value = data[col.key];
return [
polarToX(col.angle, (value) / 2),
polarToY(col.angle, (value) / 2)
];
})
)}
stroke={`#edc951`}
fill={`#edc951`}
fillOpacity=".5"
/>
);
};

Putting, again, all together our code will be:

import React from 'react';const data = [
{ battery: 0.7, design: 1, useful: 0.9, speed: 0.67, weight: 0.8 },
{ battery: 0.6, design: 0.9, useful: 0.8, speed: 0.7, weight: 0.6 }
];
const chartSize = 450;
const numberOfScales = 4;
const scale = value => (
<circle
key={`scale-${value}`}
cx={0}
cy={0}
r={((value / numberOfScales) * chartSize) / 2}
fill="#FAFAFA"
stroke="#999"
strokeWidth="0.2"
/>
);
const polarToX = (angle, distance) => Math.cos(angle - Math.PI / 2) * distance;
const polarToY = (angle, distance) => Math.sin(angle - Math.PI / 2) * distance;
const pathDefinition = points => {
let d = 'M' + points[0][0].toFixed(4) + ',' + points[0][1].toFixed(4);
for (let i = 1; i < points.length; i++) {
d += 'L' + points[i][0].toFixed(4) + ',' + points[i][1].toFixed(4);
}
return d + 'z';
};
const shape = columns => (chartData, i) => {
const data = chartData;
return (
<path
key={`shape-${i}`}
d={pathDefinition(
columns.map(col => {
const value = data[col.key];
return [
polarToX(col.angle, (value * chartSize) / 2),
polarToY(col.angle, (value * chartSize) / 2)
];
})
)}
stroke={`#edc951`}
fill={`#edc951`}
fillOpacity=".5"
/>
);
};
const RadarChart = props => {
const groups = [];
const scales = [];
for (let i = numberOfScales; i > 0; i--) {
scales.push(scale(i));
}
groups.push(<g key={`scales`}>{scales}</g>);
const middleOfChart = (chartSize / 2).toFixed(4);const captions = Object.keys(data[0]);
const columns = captions.map((key, i, all) => {
return {
key,
angle: (Math.PI * 2 * i) / all.length
};
});
groups.push(<g key={`groups}`}>{data.map(shape(columns))}</g>);
return (
<svg
version="1"
xmlns="http://www.w3.org/2000/svg"
width={chartSize}
height={chartSize}
viewBox={`0 0 ${chartSize} ${chartSize}`}
>
<g transform={`translate(${middleOfChart},${middleOfChart})`}>{groups}</g>
</svg>
);
};
export default RadarChart;

And the partial result will become:

The radar chart starts to have its final shape, we added the shapes.

The Axis

Let’s go on adding the axis at the vertexes of the shape.

We’ll create a function to draw the axis, the number of them will change based on how many data we have. In this case, too, we use the function polarToX andpolarToY, to position them.

const points = points => {
return points
.map(point => point[0].toFixed(4) + ',' + point[1].toFixed(4))
.join(' ');
};
const axis = () => (col, i) => (
<polyline
key={`poly-axis-${i}`}
points={points([
[0, 0],
[polarToX(col.angle, chartSize / 2), polarToY(col.angle, chartSize / 2)]
])}
stroke="#555"
strokeWidth=".2"
/>
);

We will add another group, and put it just before the shape group, in order to have a better aesthetic effect.

groups.push(<g key={`group-axes`}>{columns.map(axis())}</g>);

Our radar chart is almost complete, this is the code so far:

import React from 'react';const data = [
{ battery: 0.7, design: 1, useful: 0.9, speed: 0.67, weight: 0.8 },
{ battery: 0.6, design: 0.9, useful: 0.8, speed: 0.7, weight: 0.6 }
];
const chartSize = 450;
const numberOfScales = 4;
const scale = value => (
<circle
key={`scale-${value}`}
cx={0}
cy={0}
r={((value / numberOfScales) * chartSize) / 2}
fill="#FAFAFA"
stroke="#999"
strokeWidth="0.2"
/>
);
const polarToX = (angle, distance) => Math.cos(angle - Math.PI / 2) * distance;
const polarToY = (angle, distance) => Math.sin(angle - Math.PI / 2) * distance;
const pathDefinition = points => {
let d = 'M' + points[0][0].toFixed(4) + ',' + points[0][1].toFixed(4);
for (let i = 1; i < points.length; i++) {
d += 'L' + points[i][0].toFixed(4) + ',' + points[i][1].toFixed(4);
}
return d + 'z';
};
const shape = columns => (chartData, i) => {
const data = chartData;
return (
<path
key={`shape-${i}`}
d={pathDefinition(
columns.map(col => {
const value = data[col.key];
return [
polarToX(col.angle, (value * chartSize) / 2),
polarToY(col.angle, (value * chartSize) / 2)
];
})
)}
stroke={`#edc951`}
fill={`#edc951`}
fillOpacity=".5"
/>
);
};
const points = points => {
return points
.map(point => point[0].toFixed(4) + ',' + point[1].toFixed(4))
.join(' ');
};
const axis = () => (col, i) => (
<polyline
key={`poly-axis-${i}`}
points={points([
[0, 0],
[polarToX(col.angle, chartSize / 2), polarToY(col.angle, chartSize / 2)]
])}
stroke="#555"
strokeWidth=".2"
/>
);
const RadarChart = props => {
const groups = [];
const scales = [];
for (let i = numberOfScales; i > 0; i--) {
scales.push(scale(i));
}
groups.push(<g key={`scales`}>{scales}</g>);
const middleOfChart = (chartSize / 2).toFixed(4);const captions = Object.keys(data[0]);
const columns = captions.map((key, i, all) => {
return {
key,
angle: (Math.PI * 2 * i) / all.length
};
});
groups.push(<g key={`group-axes`}>{columns.map(axis())}</g>);
groups.push(<g key={`groups}`}>{data.map(shape(columns))}</g>);
return (
<svg
version="1"
xmlns="http://www.w3.org/2000/svg"
width={chartSize}
height={chartSize}
viewBox={`0 0 ${chartSize} ${chartSize}`}
>
<g transform={`translate(${middleOfChart},${middleOfChart})`}>{groups}</g>
</svg>
);
};
export default RadarChart;

and this the result:

The radar chart is almost finished.

Captions

Any self-respecting chart has its captions. Let’s create the function caption.

const caption = () => col => (
<text
key={`caption-of-${col.key}`}
x={polarToX(col.angle, (chartSize / 2) * 0.95).toFixed(4)}
y={polarToY(col.angle, (chartSize / 2) * 0.95).toFixed(4)}
dy={10 / 2}
fill="#444"
fontWeight="400"
textShadow="1px 1px 0 #fff"
>
{col.key}
</text>
);

And the relative group:

groups.push(<g key={`group-captions`}>{columns.map(caption())}</g>);

and this is the result:

We are almost done, we only need to add some style.

Finishing with style

We could be satisfied, but there’s something missing:

  • Each shape has the same color, and this is no good.
  • We could add an :hover effect on the shape.
  • The captions are not real text, but just the data object keys.
  • In some cases, the captions don’t fit the SVG.
  • There are no formal or runtime validation on data

I could write the code to add all those improvements in this story, but I preferred to create and publish an npm library where all these things are already done. This is the final result.

This is the final result of the SVG React radar chart

I finish up leaving you all the information to use it:

Demo app: https://spyna.github.io/react-svg-radar-chart/

Npm page: https://www.npmjs.com/package/react-svg-radar-chart

GitHub page: https://github.com/Spyna/react-svg-radar-chart

--

--