Created
May 17, 2018 03:19
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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