Skip to content

Instantly share code, notes, and snippets.

@makmanalp
Last active February 4, 2021 13:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save makmanalp/9cb99571ee74b169dc109ec3fc3d4920 to your computer and use it in GitHub Desktop.
Save makmanalp/9cb99571ee74b169dc109ec3fc3d4920 to your computer and use it in GitHub Desktop.
Multivariate radar charts with different axes
license: mit
scrolling: yes

Scroll to see all the examples. Blablabla bla bla. Credits to visual cinnamon.

<!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>
/*
* 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