Create & Code
Published in

Create & Code

Build a radar diagram with d3.js

In this tutorial I am going to show you how to create a radar diagram with random dots within 4 segments. Each dot will be assigned to a quadrant of the radar.

mediaman’s Smart Data Radar

Step 1: Install dependencies

npm install — save d3
npm i 
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"@types/d3": "^6.2.0",
"autoprefixer": "^9.1.3",
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^6.4.0",
"core-js": "^3.1.4",
"css-loader": "^3.0.0",
"extract-loader": "^2.0.1",
"file-loader": "^2.0.0",
"node-sass": "^4.9.3",
"postcss-cli": "^6.0.0",
"postcss-loader": "^3.0.0",
"regenerator-runtime": "^0.13.2",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"ts-loader": "^8.0.4",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^4.35.0",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2"
},

Step 2: Setup your project and implement webpack and TypeScript

<div class=”techradar__chart”>
<div class=”chart”>
<svg id=”radar”></svg>
</div>
</div>

Step 3: Extend the SVG with some basic stuff

const svgOuter = d3.select('#radar'),
margin = {top: 20, right: 20, bottom: 20, left: 20},
width = 900,
height = 900,
domainwidth = width - margin.left - margin.right,
domainheight = height - margin.top - margin.bottom,
tooltip = d3.select('.tooltip');

svgOuter.attr('viewBox', '0 0 ' + (width + margin.left + margin.right) + ' ' + (height + margin.top + margin.bottom));

const svg = svgOuter.append('g').style('transform', 'translate(' + margin.left + 'px, ' + margin.top + 'px)')

const x = d3.scaleLinear()
.domain([-4, 4])
.range([0, domainwidth]);
const y = d3.scaleLinear()
.domain([-4, 4])
.range([domainheight, 0]);

const xAxis = svg.append('g')
.attr('class', 'x-axis')
.attr('transform', 'translate(0,' + y.range()[0] / 2 + ')');

xAxis.call(d3.axisBottom(x).ticks(6))
.call(g => g.selectAll('.tick text').remove())
.call(g => g.selectAll('.tick line').remove())
.call(g => g.selectAll('.domain').remove());

svg.append('g')
.attr('class', 'y-axis')
.attr('transform', 'translate(' + x.range()[1] / 2 + ', 0)')
.call(d3.axisLeft(y).ticks(6))
.call(g => g.selectAll('.tick text').remove())
.call(g => g.selectAll('.tick line').remove())
.call(g => g.selectAll('.domain').remove());

Excursion: Some theoretical maths stuff

Step 4: Creating the radar circles

const arcGen = (innerRadius, outerRadius, startAngle, endAngle) => d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(startAngle)
.endAngle(endAngle);
const innerFirstCircle = (startAngle: number, endAngle: number) => {
return arcGen((domainheight / 8) + 8, (domainheight / 8) - 8, startAngle, endAngle);
};
const innerSecondCircle = (startAngle: number, endAngle: number) => {
return arcGen((domainheight / 4) + 6, (domainheight / 4) - 6, startAngle, endAngle);
};
const innerThirdCircle = (startAngle: number, endAngle: number) => {
return arcGen((domainheight / 8 * 3) + 4, (domainheight / 8 * 3) - 4, startAngle, endAngle);
};
const innerFourthCircle = (startAngle: number, endAngle: number) => {
return arcGen((domainheight / 2) + 2, (domainheight / 2) - 2, startAngle, endAngle);
};
const circleTransform = 'translate(' + (domainwidth / 2) + 'px, ' + (domainheight / 2) + 'px)';
const GREEN = '#84BFA4';
const green = svg.append('g')
.attr('class', 'green');

// green background gradient area
green.append('rect')
.attr('width', domainwidth / 2)
.attr('height', domainheight / 2)
.attr('x', 0)
.attr('y', 0)
.attr('fill', 'url(#green-radial-gradient)')
.style('opacity', .25);

// green circle borders
green.append('path')
.attr('fill', GREEN)
.attr('d', innerFirstCircle(3 * Math.PI / 2, 4 * Math.PI / 2))
.style('transform', circleTransform);
green.append('path')
.attr('fill', GREEN)
.attr('d', innerSecondCircle(3 * Math.PI / 2, 4 * Math.PI / 2))
.style('transform', circleTransform);
green.append('path')
.attr('fill', GREEN)
.attr('d', innerThirdCircle(3 * Math.PI / 2, 4 * Math.PI / 2))
.style('transform', circleTransform);
green.append('path')
.attr('fill', GREEN)
.attr('d', innerFourthCircle(3 * Math.PI / 2, 4 * Math.PI / 2))
.style('transform', circleTransform);

Step 5: Placing the dots inside the circles.

{
"text": "Dot-Name",
"quadrant": 1,
"circle": 2
}
const values: Dots[] = (data as Data[]).reduce((list: Dots[], element: Data) => {
const entry: Dots = {
x: null,
y: null,
text: element.text,
quadrant: element.quadrant,
fillColor: ['green', 'blue', 'red', 'orange'][element.quadrant - 1]
};

let dot;
do {
dot = calculateDot(element, x, y);
// calculate this exact dot new until it has a distance to every other dot that is longer than 28 (2x dot radius)
} while (list.some(item => checkDistanceBetweenDots(dot.xValue, item.x, dot.yValue, item.y) < 20));

entry.x = dot.xValue;
entry.y = dot.yValue;

list.push(entry);

return list;
}, []);
const pi = Math.PI;
export const calculateDot = (element: Data, x: d3.ScaleLinear<number, number>, y: d3.ScaleLinear<number, number>) => {
// radian between 5 and 85
const randomDegree = ((Math.random() * 80 + 5) * pi) / 180;
const circle = element.circle - 0.2;
const r = Math.random() * 0.6 + (circle - 0.6);
// multiples of PI/2
// loops through every quadrant starting from top left, bottom left, bottom right, top right
const shift = pi * [1,4,3,2][element.quadrant - 1] / 2;

return {
xValue: x(Math.cos(randomDegree + shift) * r),
yValue: y(Math.sin(randomDegree + shift) * r)
};
};
export const checkDistanceBetweenDots = (x1: number, x2: number, y1: number, y2: number) => {
const a = x2 - x1;
const b = y2 - y1;
return Math.sqrt((a * a) + (b * b));
};
g.append('g')
.attr('class', 'circles')
.selectAll('circle')
.data(values)
.enter().append('circle')
.attr('class', d => 'dot is-' + d.fillColor)
.attr('r', 5)
.attr('data-value', d => d.text)
.attr('cx', d => d.x)
.attr('cy', d => d.y)

🥳

Summary

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store