Scroll to see all the examples. Blablabla bla bla. Credits to visual cinnamon.
Last active
February 4, 2021 13:41
-
-
Save makmanalp/9cb99571ee74b169dc109ec3fc3d4920 to your computer and use it in GitHub Desktop.
Multivariate radar charts with different axes
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
license: mit | |
scrolling: yes |
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/ > | |
<title>Smoothed D3.js Radar Chart</title> | |
<!-- Google fonts --> | |
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,300' rel='stylesheet' type='text/css'> | |
<!-- D3.js --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.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: 'Open Sans', sans-serif; | |
fill: #333333; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: slategray; | |
stroke-opacity: 0.6; | |
shape-rendering: crispEdges; | |
} | |
.tooltip { | |
fill: #333333; | |
} | |
.smallMultiples > div { | |
display: inline-block; | |
width: 240px; | |
height: 185px; | |
} | |
</style> | |
</head> | |
<body> | |
<div> | |
<h1> Default chart - Dataset 1</h1> | |
<div class="radarChart1"></div> | |
</div> | |
<div> | |
<h1> Default chart - Dataset 2</h1> | |
<div class="radarChart2"></div> | |
</div> | |
<div> | |
<h1> Dataset 1 - aesthetic options set, hover disabled</h1> | |
<div class="radarChart3"></div> | |
</div> | |
<div> | |
<h1> Dataset 2 - axes ordered and filtered</h1> | |
<div class="radarChart4"></div> | |
</div> | |
<div> | |
<h1> Dataset 1 - custom scales</h1> | |
<div class="radarChart5"></div> | |
</div> | |
<div> | |
<h1> Dataset 2 - small multiples with fixed scales</h1> | |
<div class="smallMultiples"></div> | |
</div> | |
<script src="radarChart.js"></script> | |
<script> | |
////////////////////////////////////////////////////////////// | |
//////////////////////// Set-Up ////////////////////////////// | |
////////////////////////////////////////////////////////////// | |
var margin = {top: 10, right: 10, bottom: 10, left: 10}, | |
width = Math.min(700, window.innerWidth - 10) - margin.left - margin.right, | |
height = Math.min(width, window.innerHeight - margin.top - margin.bottom - 20); | |
////////////////////////////////////////////////////////////// | |
////////////////////////// Data ////////////////////////////// | |
////////////////////////////////////////////////////////////// | |
var data1 = [ | |
{"Battery Life": 0.22, "Brand": 0.28, "Contract Cost": 0.29, "Design And Quality": 0.17, "Have Internet Connectivity": 0.22, "Large Screen": 0.02, "Price Of Device": 0.21, "To Be A Smartphone": 0.5}, | |
{"Battery Life": 0.27, "Brand": 0.16, "Contract Cost": 0.35, "Design And Quality": 0.13, "Have Internet Connectivity": 0.2, "Large Screen": 0.13, "Price Of Device": 0.35, "To Be A Smartphone": 0.38}, | |
{"Battery Life": 0.26, "Brand": 0.1, "Contract Cost": 0.3, "Design And Quality": 0.14, "Have Internet Connectivity": 0.22, "Large Screen": 0.04, "Price Of Device": 0.41, "To Be A Smartphone": 0.3} | |
]; | |
var data2 = [ | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":256,"intra-block.capacity.type":"variable","intra-block.capacity.value":0,"intra-block.utilization.constraint":"none"}, | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":256,"intra-block.capacity.type":"fixed","intra-block.capacity.value":1,"intra-block.utilization.constraint":"leq_capacity"}, | |
{"inter-block.fanout.type":"variable","inter-block.fanout.fixedValue":0,"intra-block.capacity.type":"fixed","intra-block.capacity.value":256,"intra-block.utilization.constraint":"leq_capacity"}, | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":100,"intra-block.capacity.type":"variable","intra-block.capacity.value":0,"intra-block.utilization.constraint":"none"}, | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":100,"intra-block.capacity.type":"variable","intra-block.capacity.value":0,"intra-block.utilization.constraint":"none"}, | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":256,"intra-block.capacity.type":"fixed","intra-block.capacity.value":64,"intra-block.utilization.constraint":"leq_capacity"}, | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":256,"intra-block.capacity.type":"fixed","intra-block.capacity.value":1,"intra-block.utilization.constraint":"leq_capacity"}, | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":20,"intra-block.capacity.type":"balanced","intra-block.capacity.value":0,"intra-block.utilization.constraint":"none"}, | |
{"inter-block.fanout.type":"fixed","inter-block.fanout.fixedValue":20,"intra-block.capacity.type":"balanced","intra-block.capacity.value":0,"intra-block.utilization.constraint":"none"}, | |
{"inter-block.fanout.type":"variable","inter-block.fanout.fixedValue":0,"intra-block.capacity.type":"fixed","intra-block.capacity.value":256,"intra-block.utilization.constraint":"leq_capacity"} | |
]; | |
////////////////////////////////////////////////////////////// | |
//////////////////// Draw the Chart ////////////////////////// | |
////////////////////////////////////////////////////////////// | |
// Defaults | |
RadarChart(".radarChart1", data1); | |
RadarChart(".radarChart2", data2); | |
// Aesthetic options | |
var radarChartOptions = { | |
w: 200, | |
h: 200, | |
roundStrokes: true, | |
axisLabels: false, | |
tickLabels: false, | |
hover: false, | |
}; | |
RadarChart(".radarChart3", data2, radarChartOptions); | |
// Custom fields | |
var radarChartOptions = { | |
fields: [ | |
"intra-block.capacity.type", | |
"intra-block.utilization.constraint", | |
"inter-block.fanout.type", "inter-block.fanout.fixedValue", | |
] | |
}; | |
RadarChart(".radarChart4", data2, radarChartOptions); | |
// Custom fields and axes | |
var fields = [ | |
"Price Of Device", | |
"Contract Cost", | |
"Large Screen", | |
"Design And Quality", | |
]; | |
var scalesAndAxes = {}; | |
fields.forEach(function (field){ | |
var o = {}; | |
o.scale = d3.scale.linear().domain([0, 0.5]); | |
o.axis = d3.svg.axis() | |
.scale(o.scale) | |
.tickFormat(function(d, i){ if(i != 0){return d + "";} else {return "";} }) | |
.orient("bottom"); | |
scalesAndAxes[field] = o; | |
}); | |
var radarChartOptions = { | |
fields: fields, | |
scalesAndAxes: scalesAndAxes, | |
}; | |
RadarChart(".radarChart5", data1.slice(0,2), radarChartOptions); | |
// Small multiples | |
var scalesAndAxes = autoScalesAxes(data2); | |
var radarChartOptions = { | |
w: 150, | |
h: 150, | |
axisLabels: false, | |
tickLabels: false, | |
hover: true, | |
scalesAndAxes: scalesAndAxes, | |
}; | |
var colors = d3.scale.category20(); | |
// Create sub-divs for each small multiple | |
var singleMultiple = d3.select(".smallMultiples") | |
.selectAll("div") | |
.data(data2) | |
.enter() | |
.append("div") | |
.attr("id", function(d, i){ return "multiple-" + i; }); | |
// Add graph | |
singleMultiple | |
.each(function(d, i){ | |
//radarChartOptions.color = function(){ return colors(i); } | |
RadarChart("#multiple-" + i, data2.slice(i, i+1), radarChartOptions); | |
}); | |
// Add text | |
singleMultiple | |
.append("div") | |
.text(function(d){ return JSON.stringify(d); }); | |
</script> | |
</body> | |
</html> |
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
/* | |
* Configurable-axis radar chart that supports different scales per axis | |
* By Mali Akmanalp | |
* | |
* Read more here: http://medium.com/@makmanalp/ | |
* | |
* Heavily modified but based on from Nadieh Bremer's original radar chart: | |
* http://bl.ocks.org/nbremer/21746a9668ffdf6d8242 | |
* | |
* Released under the MIT license. | |
*/ | |
function RadarChart(id, data, options) { | |
var 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 | |
labelFactor: 0.85, //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: 0.7, //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.scale.category10(), //Color function | |
hover: true, | |
axisLabels: true, | |
tickLabels: true, | |
fields: false, | |
scalesAndAxes: 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]; } | |
} | |
} | |
// If fields specified, filter and sort data to fields | |
if (cfg.fields != false){ | |
data = subsetAndSortData(data, cfg.fields); | |
} else { | |
cfg.fields = Object.keys(data[0]); | |
} | |
// Auto-generate scales and axes from given data extents or use given ones. | |
var autos; | |
if (cfg.scalesAndAxes === false){ | |
autos = autoScalesAxes(data); | |
} else { | |
autos = cfg.scalesAndAxes; | |
} | |
var scales = cfg.fields.map(function(k){ return autos[k].scale; }); | |
var axes = cfg.fields.map(function(k){ return autos[k].axis; }); | |
// Rearrange data to an array of arrays | |
data = data.map(function(row){ | |
var newRow = cfg.fields.map(function(key) { | |
return {"axis": key, "value": row[key]}; | |
}); | |
return newRow; | |
}); | |
var total = cfg.fields.length, //The number of different axes | |
radius = Math.min(cfg.w/2, cfg.h/2), //Radius of the outermost circle | |
angleSlice = Math.PI * 2 / total; //The width in radians of each "slice" | |
// Update ranges of scales to match radius. | |
scales = scales.map(function(i){ | |
// This is gross - no other way to get ordinal scales to behave correctly. | |
if (typeof i.rangePoints !== 'undefined'){ | |
return i.rangePoints([0, radius]); | |
} else { | |
return i.range([0, radius]); | |
} | |
}); | |
///////////////////////////////////////////////////////// | |
//////////// Create the container SVG and g ///////////// | |
///////////////////////////////////////////////////////// | |
//Remove whatever chart with the same id/class was present before | |
d3.select(id).select("svg").remove(); | |
//Initiate the radar chart SVG | |
var svg = d3.select(id).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"+id); | |
//Append a g element | |
var g = svg.append("g") | |
.attr("transform", "translate(" + (cfg.w/2 + cfg.margin.left) + "," + (cfg.h/2 + cfg.margin.top) + ")"); | |
///////////////////////////////////////////////////////// | |
//////////////////// Draw the axes ////////////////////// | |
///////////////////////////////////////////////////////// | |
//Wrapper for the grid & axes | |
var axisGrid = g.append("g").attr("class", "axisWrapper"); | |
//Create the straight lines radiating outward from the center | |
var axis = axisGrid.selectAll(".axis") | |
.data(cfg.fields) | |
.enter() | |
.append("g") | |
.attr("class", "axis"); | |
//Append the axes | |
var axisGroup = axis.append("g") | |
.attr("transform", function(d, i){ return "rotate(" + (180 / Math.PI * (i * angleSlice) + 270) + ")"; }) | |
.each(function(d, i){ | |
var ax = axes[i]; | |
if (cfg.tickLabels !== true){ | |
ax = ax.tickFormat(function(d){ return ""; }); | |
} | |
ax(d3.select(this)); | |
}); | |
//Append axis category labels | |
if (cfg.axisLabels === true){ | |
axisGroup.append("text") | |
.attr("class", "legend") | |
.style("font-size", "11px") | |
.attr("text-anchor", "middle") | |
.attr("transform", "translate(" + radius * cfg.labelFactor + ", 20)") | |
.attr("dy", "0.35em") | |
.text(function(d){return d;}) | |
.call(wrap, cfg.wrapWidth); | |
} | |
///////////////////////////////////////////////////////// | |
///////////// Draw the radar chart blobs //////////////// | |
///////////////////////////////////////////////////////// | |
//The radial line function | |
var radarLine = d3.svg.line.radial() | |
.interpolate("linear-closed") | |
.radius(function(d, i) { return scales[i](d.value); }) | |
.angle(function(d,i) { return i*angleSlice; }); | |
if(cfg.roundStrokes) { | |
radarLine.interpolate("cardinal-closed"); | |
} | |
//Create a wrapper for the blobs | |
var blobWrapper = g.selectAll(".radarWrapper") | |
.data(data) | |
.enter().append("g") | |
.attr("class", "radarWrapper"); | |
//Append the backgrounds | |
blobWrapper | |
.append("path") | |
.attr("class", "radarArea") | |
.attr("d", function(d,i) { return radarLine(d); }) | |
.style("fill", function(d,i) { return cfg.color(i); }) | |
.style("fill-opacity", cfg.opacityArea) | |
.on('mouseover', function (d,i){ | |
if (cfg.hover === true){ | |
//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', function(){ | |
if (cfg.hover === true){ | |
//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", function(d,i) { return radarLine(d); }) | |
.style("stroke-width", cfg.strokeWidth + "px") | |
.style("stroke", function(d,i) { return cfg.color(i); }) | |
.style("fill", "none"); | |
//Append the circles | |
blobWrapper.selectAll(".radarCircle") | |
.data(function(d,i) { return d; }) | |
.enter().append("circle") | |
.attr("class", "radarCircle") | |
.attr("r", cfg.dotRadius) | |
.attr("cx", function(d,i){ return scales[i](d.value) * Math.cos(angleSlice*i - Math.PI/2); }) | |
.attr("cy", function(d,i){ return scales[i](d.value) * Math.sin(angleSlice*i - Math.PI/2); }) | |
.style("fill", function(d,i,j) { return cfg.color(j); }) | |
.style("fill-opacity", 0.8); | |
///////////////////////////////////////////////////////// | |
//////// Append invisible circles for tooltip /////////// | |
///////////////////////////////////////////////////////// | |
if (cfg.hover === true){ | |
//Wrapper for the invisible circles on top | |
var 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(function(d,i) { return d; }) | |
.enter().append("circle") | |
.attr("class", "radarInvisibleCircle") | |
.attr("r", cfg.dotRadius*1.5) | |
.attr("cx", function(d,i){ return scales[i](d.value) * Math.cos(angleSlice*i - Math.PI/2); }) | |
.attr("cy", function(d,i){ return scales[i](d.value) * Math.sin(angleSlice*i - Math.PI/2); }) | |
.style("fill", "none") | |
.style("pointer-events", "all") | |
.on("mouseover", function(d,i) { | |
newX = parseFloat(d3.select(this).attr('cx')) - 10; | |
newY = parseFloat(d3.select(this).attr('cy')) - 10; | |
tooltip | |
.attr('x', newX) | |
.attr('y', newY) | |
.text(function(x){return d.value;}) | |
.transition().duration(200) | |
.style('opacity', 1); | |
}) | |
.on("mouseout", function(){ | |
tooltip.transition().duration(200) | |
.style("opacity", 0); | |
}); | |
//Set up the small tooltip for when you hover over a circle | |
var tooltip = g.append("text") | |
.attr("class", "tooltip") | |
.style("opacity", 0); | |
} | |
///////////////////////////////////////////////////////// | |
/////////////////// Helper Function ///////////////////// | |
///////////////////////////////////////////////////////// | |
//Taken from http://bl.ocks.org/mbostock/7555321 | |
//Wraps SVG text | |
function 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 | |
}//RadarChart | |
/* | |
* Given a dataset which is an array of objects (that all have the same | |
* fields), filter and sort those fields | |
* | |
*/ | |
function subsetAndSortData(data, fields){ | |
data = data.map(function(row){ | |
var newRow = {}; | |
fields.map(function(key) { | |
newRow[key] = row[key]; | |
}); | |
return newRow; | |
}); | |
return data; | |
} | |
function autoScalesAxes(data){ | |
var ret = {}; | |
var fieldNames = Object.keys(data[0]); | |
fieldNames.map(function(i){ | |
// Get all data for axis | |
var axisData = data.map(function(row){ | |
return row[i]; | |
}); | |
var scale; | |
var axis; | |
// Autogenerate a scale | |
if ((typeof axisData[0] === "string") || (typeof axisData[0] === "boolean")){ | |
// Non-numeric things get an ordinal scale | |
var uniqueValues = d3.map(data, function(a){return a[i]; }).keys(); | |
uniqueValues.unshift(" "); // Padding, so it doesn't start from the center | |
scale = d3.scale.ordinal() | |
.domain(uniqueValues); | |
axis = d3.svg.axis() | |
.scale(scale) | |
.tickValues(uniqueValues) | |
.orient("bottom"); | |
} else { | |
// Numeric values get a linear scale | |
var extent = d3.extent(data, function(a){return a[i];}); | |
scale = d3.scale.linear() | |
.domain(extent); | |
axis = d3.svg.axis() | |
.scale(scale) | |
.tickFormat(function(d, i){ if(i != 0){return d + "";} else {return "";} }) | |
.orient("bottom"); | |
} | |
ret[i] = {}; | |
ret[i].scale = scale; | |
ret[i].axis = axis; | |
}); | |
return ret; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment