Some modifications (adaptating it for d3 v4, adding support for legend) around the code from "A different look for the D3 radar chart".
Last active
June 1, 2017 11:04
-
-
Save mthh/7e17b680b35b83b49f1c22a3613bd89f to your computer and use it in GitHub Desktop.
Radar Chart d3 v4
This file contains 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
height: 600 | |
license: mit |
This file contains 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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/ > | |
<title>Smoothed Radar Chart</title> | |
<!-- Google fonts --> | |
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,300' rel='stylesheet' type='text/css'> | |
<link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'> | |
<!-- D3.js --> | |
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script> | |
<script src="https://d3js.org/d3-path.v1.min.js" charset="utf-8"></script> | |
<script src="radarChart.js" charset="utf-8"></script> | |
<style> | |
body { | |
font-family: 'Open Sans', sans-serif; | |
font-size: 11px; | |
font-weight: 300; | |
fill: #242424; | |
text-align: center; | |
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff; | |
cursor: default; | |
} | |
.legend { | |
font-family: 'Raleway', sans-serif; | |
fill: #333333; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="radarChart" style="display: inline-flex;"></div> | |
<div class="radarChart2" style="display: inline-flex;"></div> | |
<script> | |
////////////////////////////////////////////////////////////// | |
//////////////////////// Set-Up ////////////////////////////// | |
////////////////////////////////////////////////////////////// | |
var margin = { top: 50, right: 80, bottom: 50, left: 80 }, | |
width = Math.min(700, window.innerWidth / 4) - margin.left - margin.right, | |
height = Math.min(width, window.innerHeight - margin.top - margin.bottom); | |
////////////////////////////////////////////////////////////// | |
////////////////////////// Data ////////////////////////////// | |
////////////////////////////////////////////////////////////// | |
var data = [ | |
{ name: 'Allocated budget', | |
axes: [ | |
{axis: 'Sales', value: 42}, | |
{axis: 'Marketing', value: 20}, | |
{axis: 'Development', value: 60}, | |
{axis: 'Customer Support', value: 26}, | |
{axis: 'Information Technology', value: 35}, | |
{axis: 'Administration', value: 20} | |
] | |
}, | |
{ name: 'Actual Spending', | |
axes: [ | |
{axis: 'Sales', value: 50}, | |
{axis: 'Marketing', value: 45}, | |
{axis: 'Development', value: 20}, | |
{axis: 'Customer Support', value: 20}, | |
{axis: 'Information Technology', value: 25}, | |
{axis: 'Administration', value: 23} | |
] | |
} | |
]; | |
////////////////////////////////////////////////////////////// | |
////// First example ///////////////////////////////////////// | |
///// (not so much options) ////////////////////////////////// | |
////////////////////////////////////////////////////////////// | |
var radarChartOptions = { | |
w: 290, | |
h: 350, | |
margin: margin, | |
levels: 5, | |
roundStrokes: true, | |
color: d3.scaleOrdinal().range(["#26AF32", "#762712"]), | |
format: '.0f' | |
}; | |
// Draw the chart, get a reference the created svg element : | |
let svg_radar1 = RadarChart(".radarChart", data, radarChartOptions); | |
////////////////////////////////////////////////////////////// | |
///// Second example ///////////////////////////////////////// | |
///// Chart legend, custom color, custom unit, etc. ////////// | |
////////////////////////////////////////////////////////////// | |
var radarChartOptions2 = { | |
w: 290, | |
h: 350, | |
margin: margin, | |
maxValue: 60, | |
levels: 6, | |
roundStrokes: false, | |
color: d3.scaleOrdinal().range(["#AFC52F", "#ff6600"]), | |
format: '.0f', | |
legend: { title: 'Organization XYZ', translateX: 100, translateY: 40 }, | |
unit: '$' | |
}; | |
// Draw the chart, get a reference the created svg element : | |
let svg_radar2 = RadarChart(".radarChart2", data, radarChartOptions2); | |
</script> | |
</body> | |
</html> |
This file contains 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
///////////////////////////////////////////////////////// | |
/////////////// The Radar Chart Function //////////////// | |
/// mthh - 2017 ///////////////////////////////////////// | |
// Inspired by the code of alangrafu and Nadieh Bremer // | |
// (VisualCinnamon.com) and modified for d3 v4 ////////// | |
///////////////////////////////////////////////////////// | |
const max = Math.max; | |
const sin = Math.sin; | |
const cos = Math.cos; | |
const HALF_PI = Math.PI / 2; | |
const RadarChart = function RadarChart(parent_selector, data, options) { | |
//Wraps SVG text - Taken from http://bl.ocks.org/mbostock/7555321 | |
const wrap = (text, width) => { | |
text.each(function() { | |
var 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 | |
const cfg = { | |
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.scaleOrdinal(d3.schemeCategory10), //Color function, | |
format: '.2%', | |
unit: '', | |
legend: false | |
}; | |
//Put all of the options into a variable called cfg | |
if('undefined' !== typeof options){ | |
for(var i in options){ | |
if('undefined' !== typeof options[i]){ cfg[i] = options[i]; } | |
}//for i | |
}//if | |
//If the supplied maxValue is smaller than the actual one, replace by the max in the data | |
// var maxValue = max(cfg.maxValue, d3.max(data, function(i){return d3.max(i.map(function(o){return o.value;}))})); | |
let maxValue = 0; | |
for (let j=0; j < data.length; j++) { | |
for (let i = 0; i < data[j].axes.length; i++) { | |
data[j].axes[i]['id'] = data[j].name; | |
if (data[j].axes[i]['value'] > maxValue) { | |
maxValue = data[j].axes[i]['value']; | |
} | |
} | |
} | |
maxValue = max(cfg.maxValue, maxValue); | |
const allAxis = data[0].axes.map((i, j) => 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(cfg.format), //Formatting | |
angleSlice = Math.PI * 2 / total; //The width in radians of each "slice" | |
//Scale for the radius | |
const rScale = d3.scaleLinear() | |
.range([0, radius]) | |
.domain([0, maxValue]); | |
///////////////////////////////////////////////////////// | |
//////////// Create the container SVG and g ///////////// | |
///////////////////////////////////////////////////////// | |
const parent = d3.select(parent_selector); | |
//Remove whatever chart with the same id/class was present before | |
parent.select("svg").remove(); | |
//Initiate the radar chart SVG | |
let svg = parent.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"); | |
//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'), | |
feGaussianBlur = filter.append('feGaussianBlur').attr('stdDeviation','2.5').attr('result','coloredBlur'), | |
feMerge = filter.append('feMerge'), | |
feMergeNode_1 = feMerge.append('feMergeNode').attr('in','coloredBlur'), | |
feMergeNode_2 = 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) + cfg.unit); | |
///////////////////////////////////////////////////////// | |
//////////////////// Draw the axes ////////////////////// | |
///////////////////////////////////////////////////////// | |
//Create the straight lines radiating outward from the center | |
var 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) * cos(angleSlice * i - HALF_PI)) | |
.attr("y2", (d, i) => rScale(maxValue* 1.1) * sin(angleSlice * i - HALF_PI)) | |
.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) * cos(angleSlice * i - HALF_PI)) | |
.attr("y", (d,i) => rScale(maxValue * cfg.labelFactor) * sin(angleSlice * i - HALF_PI)) | |
.text(d => d) | |
.call(wrap, cfg.wrapWidth); | |
///////////////////////////////////////////////////////// | |
///////////// Draw the radar chart blobs //////////////// | |
///////////////////////////////////////////////////////// | |
//The radial line function | |
const radarLine = d3.radialLine() | |
.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 | |
const blobWrapper = g.selectAll(".radarWrapper") | |
.data(data) | |
.enter().append("g") | |
.attr("class", "radarWrapper"); | |
//Append the backgrounds | |
blobWrapper | |
.append("path") | |
.attr("class", "radarArea") | |
.attr("d", d => radarLine(d.axes)) | |
.style("fill", (d,i) => cfg.color(i)) | |
.style("fill-opacity", cfg.opacityArea) | |
.on('mouseover', function(d, i) { | |
//Dim all blobs | |
parent.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 | |
parent.selectAll(".radarArea") | |
.transition().duration(200) | |
.style("fill-opacity", cfg.opacityArea); | |
}); | |
//Create the outlines | |
blobWrapper.append("path") | |
.attr("class", "radarStroke") | |
.attr("d", function(d,i) { return radarLine(d.axes); }) | |
.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.axes) | |
.enter() | |
.append("circle") | |
.attr("class", "radarCircle") | |
.attr("r", cfg.dotRadius) | |
.attr("cx", (d,i) => rScale(d.value) * cos(angleSlice * i - HALF_PI)) | |
.attr("cy", (d,i) => rScale(d.value) * sin(angleSlice * i - HALF_PI)) | |
.style("fill", (d) => cfg.color(d.id)) | |
.style("fill-opacity", 0.8); | |
///////////////////////////////////////////////////////// | |
//////// Append invisible circles for tooltip /////////// | |
///////////////////////////////////////////////////////// | |
//Wrapper for the invisible circles on top | |
const blobCircleWrapper = g.selectAll(".radarCircleWrapper") | |
.data(data) | |
.enter().append("g") | |
.attr("class", "radarCircleWrapper"); | |
//Append a set of invisible circles on top for the mouseover pop-up | |
blobCircleWrapper.selectAll(".radarInvisibleCircle") | |
.data(d => d.axes) | |
.enter().append("circle") | |
.attr("class", "radarInvisibleCircle") | |
.attr("r", cfg.dotRadius * 1.5) | |
.attr("cx", (d,i) => rScale(d.value) * cos(angleSlice*i - HALF_PI)) | |
.attr("cy", (d,i) => rScale(d.value) * sin(angleSlice*i - HALF_PI)) | |
.style("fill", "none") | |
.style("pointer-events", "all") | |
.on("mouseover", function(d,i) { | |
tooltip | |
.attr('x', this.cx.baseVal.value - 10) | |
.attr('y', this.cy.baseVal.value - 10) | |
.transition() | |
.style('display', 'block') | |
.text(Format(d.value) + cfg.unit); | |
}) | |
.on("mouseout", function(){ | |
tooltip.transition() | |
.style('display', 'none').text(''); | |
}); | |
const tooltip = g.append("text") | |
.attr("class", "tooltip") | |
.attr('x', 0) | |
.attr('y', 0) | |
.style("font-size", "12px") | |
.style('display', 'none') | |
.attr("text-anchor", "middle") | |
.attr("dy", "0.35em"); | |
if (cfg.legend !== false && typeof cfg.legend === "object") { | |
let legendZone = svg.append('g'); | |
let names = data.map(el => el.name); | |
if (cfg.legend.title) { | |
let title = legendZone.append("text") | |
.attr("class", "title") | |
.attr('transform', `translate(${cfg.legend.translateX},${cfg.legend.translateY})`) | |
.attr("x", cfg.w - 70) | |
.attr("y", 10) | |
.attr("font-size", "12px") | |
.attr("fill", "#404040") | |
.text(cfg.legend.title); | |
} | |
let legend = legendZone.append("g") | |
.attr("class", "legend") | |
.attr("height", 100) | |
.attr("width", 200) | |
.attr('transform', `translate(${cfg.legend.translateX},${cfg.legend.translateY + 20})`); | |
// Create rectangles markers | |
legend.selectAll('rect') | |
.data(names) | |
.enter() | |
.append("rect") | |
.attr("x", cfg.w - 65) | |
.attr("y", (d,i) => i * 20) | |
.attr("width", 10) | |
.attr("height", 10) | |
.style("fill", (d,i) => cfg.color(i)); | |
// Create labels | |
legend.selectAll('text') | |
.data(names) | |
.enter() | |
.append("text") | |
.attr("x", cfg.w - 52) | |
.attr("y", (d,i) => i * 20 + 9) | |
.attr("font-size", "11px") | |
.attr("fill", "#737373") | |
.text(d => d); | |
} | |
return svg; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment