Skip to content

Instantly share code, notes, and snippets.

@SevenOutman
Created May 17, 2018 03:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SevenOutman/377f9739703c5c10cb5a39718c255a33 to your computer and use it in GitHub Desktop.
Save SevenOutman/377f9739703c5c10cb5a39718c255a33 to your computer and use it in GitHub Desktop.
D3-based Radar Chart, origins from the work by Nadieh Bremer, made d3@5 compatible
/* eslint-disable no-tabs,no-mixed-operators */
import * as d3 from 'd3';
/**
* @see http://bl.ocks.org/nbremer/21746a9668ffdf6d8242#radarChart.js
*/
function createRadarChart(selector, data, options) {
const defaultOptions = {
w: 600, // Width of the circle
h: 600, // Height of the circle
margin: { top: 20, right: 20, bottom: 20, left: 20 }, // The margins of the SVG
levels: 3, // How many levels or inner circles should there be drawn
maxValue: 0, // What is the value that the biggest circle will represent
labelFactor: 1.25, // How much farther than the radius of the outer circle should the labels be placed
wrapWidth: 60, // The number of pixels after which a label needs to be given a new line
opacityArea: 0.35, // The opacity of the area of the blob
dotRadius: 4, // The size of the colored circles of each blog
opacityCircles: 0.1, // The opacity of the circles of each blob
strokeWidth: 2, // The width of the stroke around each blob
roundStrokes: false, // If true the area and stroke will follow a round path (cardinal-closed)
color: d3.schemeCategory10 // Color function
};
// Put all of the options into a variable called cfg
const cfg = {
...defaultOptions,
...options
};
// If the supplied maxValue is smaller than the actual one, replace by the max in the data
let maxValue = Math.max(cfg.maxValue, d3.max(data, i => d3.max(i.map(o => o.value))));
let allAxis = (data[0].map(i => i.axis)), // Names of each axis
total = allAxis.length, // The number of different axes
radius = Math.min(cfg.w / 2, cfg.h / 2), // Radius of the outermost circle
Format = d3.format('d'), // Percentage formatting
angleSlice = Math.PI * 2 / total; // The width in radians of each "slice"
// Scale for the radius
let rScale = d3.scaleLinear()
.range([0, radius])
.domain([0, maxValue]);
// ///////////////////////////////////////////////////////
// ////////// Create the container SVG and g /////////////
// ///////////////////////////////////////////////////////
// Remove whatever chart with the same id/class was present before
d3.select(selector).select('svg').remove();
// Initiate the radar chart SVG
let svg = d3.select(selector).append('svg')
.attr('width', cfg.w + cfg.margin.left + cfg.margin.right)
.attr('height', cfg.h + cfg.margin.top + cfg.margin.bottom)
.attr('class', `radar${selector}`);
// Append a g element
let g = svg.append('g')
.attr('transform', `translate(${cfg.w / 2 + cfg.margin.left},${cfg.h / 2 + cfg.margin.top})`);
// ///////////////////////////////////////////////////////
// //////// Glow filter for some extra pizzazz ///////////
// ///////////////////////////////////////////////////////
// Filter for the outside glow
let filter = g.append('defs').append('filter').attr('id', 'glow');
filter.append('feGaussianBlur').attr('stdDeviation', '2.5').attr('result', 'coloredBlur');
let feMerge = filter.append('feMerge');
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
// ///////////////////////////////////////////////////////
// ///////////// Draw the Circular grid //////////////////
// ///////////////////////////////////////////////////////
// Wrapper for the grid & axes
let axisGrid = g.append('g').attr('class', 'axisWrapper');
// Draw the background circles
axisGrid.selectAll('.levels')
.data(d3.range(1, (cfg.levels + 1)).reverse())
.enter()
.append('circle')
.attr('class', 'gridCircle')
.attr('r', d => radius / cfg.levels * d)
.style('fill', '#CDCDCD')
.style('stroke', '#CDCDCD')
.style('fill-opacity', cfg.opacityCircles)
.style('filter', 'url(#glow)');
// Text indicating at what % each level is
axisGrid.selectAll('.axisLabel')
.data(d3.range(1, (cfg.levels + 1)).reverse())
.enter().append('text')
.attr('class', 'axisLabel')
.attr('x', 4)
.attr('y', d => -d * radius / cfg.levels)
.attr('dy', '0.4em')
.style('font-size', '10px')
.attr('fill', '#737373')
.text(d => Format(maxValue * d / cfg.levels));
// ///////////////////////////////////////////////////////
// ////////////////// Draw the axes //////////////////////
// ///////////////////////////////////////////////////////
// Create the straight lines radiating outward from the center
let axis = axisGrid.selectAll('.axis')
.data(allAxis)
.enter()
.append('g')
.attr('class', 'axis');
// Append the lines
axis.append('line')
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', (d, i) => rScale(maxValue * 1.1) * Math.cos(angleSlice * i - Math.PI / 2))
.attr('y2', (d, i) => rScale(maxValue * 1.1) * Math.sin(angleSlice * i - Math.PI / 2))
.attr('class', 'line')
.style('stroke', 'white')
.style('stroke-width', '2px');
// Append the labels at each axis
axis.append('text')
.attr('class', 'legend')
.style('font-size', '11px')
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.attr('x', (d, i) => rScale(maxValue * cfg.labelFactor) * Math.cos(angleSlice * i - Math.PI / 2))
.attr('y', (d, i) => rScale(maxValue * cfg.labelFactor) * Math.sin(angleSlice * i - Math.PI / 2))
.text(d => d)
.call(wrap, cfg.wrapWidth);
// ///////////////////////////////////////////////////////
// /////////// Draw the radar chart blobs ////////////////
// ///////////////////////////////////////////////////////
// The radial line function
let radarLine = d3.lineRadial()
.curve(d3.curveLinearClosed)
.radius(d => rScale(d.value))
.angle((d, i) => i * angleSlice);
if (cfg.roundStrokes) {
radarLine.curve(d3.curveCardinalClosed);
}
// Create a wrapper for the blobs
let blobWrapper = g.selectAll('.radarWrapper')
.data(data)
.enter().append('g')
.attr('class', 'radarWrapper');
// Append the backgrounds
blobWrapper
.append('path')
.attr('class', 'radarArea')
.attr('d', radarLine)
.style('fill', (d, i) => cfg.color(i))
.style('fill-opacity', cfg.opacityArea)
.on('mouseover', function(d, i) {
// Dim all blobs
d3.selectAll('.radarArea')
.transition().duration(200)
.style('fill-opacity', 0.1);
// Bring back the hovered over blob
d3.select(this)
.transition().duration(200)
.style('fill-opacity', 0.7);
})
.on('mouseout', () => {
// Bring back all blobs
d3.selectAll('.radarArea')
.transition().duration(200)
.style('fill-opacity', cfg.opacityArea);
});
// Create the outlines
blobWrapper.append('path')
.attr('class', 'radarStroke')
.attr('d', d => radarLine(d))
.style('stroke-width', `${cfg.strokeWidth}px`)
.style('stroke', (d, i) => cfg.color(i))
.style('fill', 'none')
.style('filter', 'url(#glow)');
// Append the circles
blobWrapper.selectAll('.radarCircle')
.data(d => d)
.enter().append('circle')
.attr('class', 'radarCircle')
.attr('r', cfg.dotRadius)
.attr('cx', (d, i) => rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2))
.attr('cy', (d, i) => rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2))
.style('fill', (d, i, j) => cfg.color(j))
.style('fill-opacity', 0.8);
// ///////////////////////////////////////////////////////
// ////// Append invisible circles for tooltip ///////////
// ///////////////////////////////////////////////////////
// Wrapper for the invisible circles on top
let blobCircleWrapper = g.selectAll('.radarCircleWrapper')
.data(data)
.enter().append('g')
.attr('class', 'radarCircleWrapper');
// Set up the small tooltip for when you hover over a circle
let tooltip = g.append('text')
.attr('class', 'tooltip')
.style('opacity', 0);
// Append a set of invisible circles on top for the mouseover pop-up
blobCircleWrapper.selectAll('.radarInvisibleCircle')
.data(d => d)
.enter().append('circle')
.attr('class', 'radarInvisibleCircle')
.attr('r', cfg.dotRadius * 1.5)
.attr('cx', (d, i) => rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2))
.attr('cy', (d, i) => rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2))
.style('fill', 'none')
.style('pointer-events', 'all')
.on('mouseover', function(d, i) {
let newX = parseFloat(d3.select(this).attr('cx')) - 10;
let newY = parseFloat(d3.select(this).attr('cy')) - 10;
tooltip
.attr('x', newX)
.attr('y', newY)
.text(Format(d.value))
.transition()
.duration(200)
.style('opacity', 1);
})
.on('mouseout', () => {
tooltip.transition().duration(200)
.style('opacity', 0);
});
// ///////////////////////////////////////////////////////
// ///////////////// Helper Function /////////////////////
// ///////////////////////////////////////////////////////
// Taken from http://bl.ocks.org/mbostock/7555321
// Wraps SVG text
function wrap(text, width) {
text.each(function() {
let text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.4, // ems
y = text.attr('y'),
x = text.attr('x'),
dy = parseFloat(text.attr('dy')),
tspan = text.text(null).append('tspan').attr('x', x).attr('y', y).attr('dy', `${dy}em`);
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(' '));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(' '));
line = [word];
tspan = text.append('tspan').attr('x', x).attr('y', y).attr('dy', `${++lineNumber * lineHeight + dy}em`).text(word);
}
}
});
}// wrap
}// RadarChart
export default createRadarChart;
import * as d3 from 'd3';
//////////////////////////////////////////////////////////////
//////////////////////// Set-Up //////////////////////////////
//////////////////////////////////////////////////////////////
var margin = {top: 100, right: 100, bottom: 100, left: 100},
width = Math.min(700, window.innerWidth - 10) - margin.left - margin.right,
height = Math.min(width, window.innerHeight - margin.top - margin.bottom - 20);
//////////////////////////////////////////////////////////////
////////////////////////// Data //////////////////////////////
//////////////////////////////////////////////////////////////
var data = [
[//iPhone
{axis:"Battery Life",value:0.22},
{axis:"Brand",value:0.28},
{axis:"Contract Cost",value:0.29},
{axis:"Design And Quality",value:0.17},
{axis:"Have Internet Connectivity",value:0.22},
{axis:"Large Screen",value:0.02},
{axis:"Price Of Device",value:0.21},
{axis:"To Be A Smartphone",value:0.50}
],[//Samsung
{axis:"Battery Life",value:0.27},
{axis:"Brand",value:0.16},
{axis:"Contract Cost",value:0.35},
{axis:"Design And Quality",value:0.13},
{axis:"Have Internet Connectivity",value:0.20},
{axis:"Large Screen",value:0.13},
{axis:"Price Of Device",value:0.35},
{axis:"To Be A Smartphone",value:0.38}
],[//Nokia Smartphone
{axis:"Battery Life",value:0.26},
{axis:"Brand",value:0.10},
{axis:"Contract Cost",value:0.30},
{axis:"Design And Quality",value:0.14},
{axis:"Have Internet Connectivity",value:0.22},
{axis:"Large Screen",value:0.04},
{axis:"Price Of Device",value:0.41},
{axis:"To Be A Smartphone",value:0.30}
]
];
//////////////////////////////////////////////////////////////
//////////////////// Draw the Chart //////////////////////////
//////////////////////////////////////////////////////////////
var color = d3.scaleOrdinal()
.range(["#EDC951","#CC333F","#00A0B0"]);
var radarChartOptions = {
w: width,
h: height,
margin: margin,
maxValue: 0.5,
levels: 5,
roundStrokes: true,
color: color
};
//Call function to draw the Radar chart
RadarChart(".radarChart", data, radarChartOptions);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment