this version adds a legend!
to enable the legend symbol to interact with the areas, when you call RadarChart()
, specify the field radarChart.js
should use to assign an areaName
to each area.
you can also specify the legend position as a parameter, where x
and y
are pixel coordinates offset from an origin at the top left of the parent svg
.
var legendPosition = {x: 25, y: 25}
var radarChartOptions {
areaName: "device",
legendPosition: legendPosition
// ...
};
to get the interaction to work, we add a data-driven class to each area that we can later select in the cellover()
function that is called when you mouse over the legend symbol.
blobWrapper
.append("path")
.attr("class", function(d) {
return "radarArea" + " " + d[0][areaName].replace(/\s+/g, '') //Remove spaces from the areaName string to make one valid class name
})
// ...
the legend is created with the delightful d3-legend component from susielu
an iteration on the bl.ock radar chart with smallest area on top by micahstubbs
a further iteration on the bl.ock Radar Chart Redesign created by nbremer, which is described nicely in this blog post
<!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='https://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://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.3.0/d3-legend.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;
}
.tooltip {
fill: #333333;
}
</style>
</head>
<body>
<div class="radarChart"></div>
<script src="radarChart.js"></script>
<script>
//////////////////////////////////////////////////////////////
//////////////////////// Set-Up //////////////////////////////
//////////////////////////////////////////////////////////////
var margin = {top: 100, right: 100, bottom: 100, left: 100},
legendPosition = {x: 25, y: 25},
width = Math.min(700, window.innerWidth - 10) - margin.left - margin.right,
height = Math.min(width, window.innerHeight - margin.top - margin.bottom - 20);
//////////////////////////////////////////////////////////////
//////////////////// Draw the Chart //////////////////////////
//////////////////////////////////////////////////////////////
var color = d3.scale.ordinal()
.range(["#EDC951","#CC333F","#00A0B0"]);
var radarChartOptions = {
w: width,
h: height,
margin: margin,
legendPosition: legendPosition,
maxValue: 0.5,
wrapWidth: 60,
levels: 5,
roundStrokes: true,
color: color,
axisName: "reason",
areaName: "device",
value: "value"
};
//Load the data and Call function to draw the Radar chart
d3.json("data.json", function(error, data){
RadarChart(".radarChart", data, radarChartOptions);
});
</script>
</body>
</html>
[
{
"key":"iPhone",
"values":[
{
"reason":"Battery Life",
"device":"iPhone",
"value":0.22
},
{
"reason":"Brand",
"device":"iPhone",
"value":0.28
},
{
"reason":"Contract Cost",
"device":"iPhone",
"value":0.29
},
{
"reason":"Design And Quality",
"device":"iPhone",
"value":0.17
},
{
"reason":"Have Internet Connectivity",
"device":"iPhone",
"value":0.22
},
{
"reason":"Large Screen",
"device":"iPhone",
"value":0.02
},
{
"reason":"Price Of Device",
"device":"iPhone",
"value":0.21
},
{
"reason":"To Be A Smartphone",
"device":"iPhone",
"value":0.50
}
]
},
{
"key":"Samsung",
"values":[
{
"reason":"Battery Life",
"device":"Samsung",
"value":0.27
},
{
"reason":"Brand",
"device":"Samsung",
"value":0.16
},
{
"reason":"Contract Cost",
"device":"Samsung",
"value":0.35
},
{
"reason":"Design And Quality",
"device":"Samsung",
"value":0.13
},
{
"reason":"Have Internet Connectivity",
"device":"Samsung",
"value":0.20
},
{
"reason":"Large Screen",
"device":"Samsung",
"value":0.13
},
{
"reason":"Price Of Device",
"device":"Samsung",
"value":0.35
},
{
"reason":"To Be A Smartphone",
"device":"Samsung",
"value":0.38
}
]
},
{
"key":"Nokia Smartphone",
"values":[
{
"reason":"Battery Life",
"device":"Nokia Smartphone",
"value":0.26
},
{
"reason":"Brand",
"device":"Nokia Smartphone",
"value":0.10
},
{
"reason":"Contract Cost",
"device":"Nokia Smartphone",
"value":0.30
},
{
"reason":"Design And Quality",
"device":"Nokia Smartphone",
"value":0.14
},
{
"reason":"Have Internet Connectivity",
"device":"Nokia Smartphone",
"value":0.22
},
{
"reason":"Large Screen",
"device":"Nokia Smartphone",
"value":0.04
},
{
"reason":"Price Of Device",
"device":"Nokia Smartphone",
"value":0.41
},
{
"reason":"To Be A Smartphone",
"device":"Nokia Smartphone",
"value":0.30
}
]
}
]
/////////////////////////////////////////////////////////
/////////////// The Radar Chart Function ////////////////
/////////////// Written by Nadieh Bremer ////////////////
////////////////// VisualCinnamon.com ///////////////////
/////////// Inspired by the code of alangrafu ///////////
/////////////////////////////////////////////////////////
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 around the circle
legendPosition: {x: 20, y: 20}, // the position of the legend, from the top-left corner 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.scale.category10(), //Color function
axisName: "axis",
areaName:"areaName",
value: "value",
sortAreas: true,
};
//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
//Map the fields specified in the configuration
// to the axis and value variables
var axisName = cfg["axisName"],
areaName = cfg["areaName"],
value = cfg["value"];
//Calculate the average value for each area
data.forEach(function(d){
d[value + "Average"] = d3.mean(d.values, function(e){ return e[value] });
})
//Sort the data for the areas from largest to smallest
//by average value as an approximation of actual blob area
//so that that the smallest area is drawn last
//and therefore appears on top
data = data.sort(function(a, b){
var a = a[value + "Average"],
b = b[value + "Average"];
return b - a;
})
//Convert the nested data passed in
// into an array of values arrays
data = data.map(function(d) { return d.values })
//If the supplied maxValue is smaller than the actual one, replace by the max in the data
var maxValue = Math.max(cfg.maxValue, d3.max(data, function(i){
return d3.max(i.map(
function(o){ return o[value]; }
))
}));
var allAxis = (data[0].map(function(d, i){ return d[axisName] })), //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('%'), //Percentage formatting
angleSlice = Math.PI * 2 / total; //The width in radians of each "slice"
//Scale for the radius
var rScale = d3.scale.linear()
.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(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) + ")");
/////////////////////////////////////////////////////////
////////// Glow filter for some extra pizzazz ///////////
/////////////////////////////////////////////////////////
//Filter for the outside glow
var 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
var 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", function(d, i){return 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", function(d){return -d*radius/cfg.levels;})
.attr("dy", "0.4em")
.style("font-size", "10px")
.attr("fill", "#737373")
.text(function(d,i) { return Format(maxValue * d/cfg.levels); });
/////////////////////////////////////////////////////////
//////////////////// 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", function(d, i){ return rScale(maxValue*1.1) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("y2", function(d, i){ return 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", function(d, i){ return rScale(maxValue * cfg.labelFactor) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("y", function(d, i){ return rScale(maxValue * cfg.labelFactor) * Math.sin(angleSlice*i - Math.PI/2); })
.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) { return rScale(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", function(d) {
return "radarArea" + " " + d[0][areaName].replace(/\s+/g, '') //Remove spaces from the areaName string to make one valid class name
})
.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){
console.log("d", d);
console.log("this", this);
//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(){
//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")
.style("filter" , "url(#glow)");
//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 rScale(d[value]) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("cy", function(d,i){ return rScale(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 ///////////
/////////////////////////////////////////////////////////
//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 rScale(d[value]) * Math.cos(angleSlice*i - Math.PI/2); })
.attr("cy", function(d,i){ return rScale(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(Format(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 Functions ////////////////////
/////////////////////////////////////////////////////////
//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
// on mouseover for the legend symbol
function cellover(d) {
//Dim all blobs
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", 0.1);
//Bring back the hovered over blob
d3.select("." + data[d][0][areaName].replace(/\s+/g, ''))
.transition().duration(200)
.style("fill-opacity", 0.7);
}
// on mouseout for the legend symbol
function cellout() {
//Bring back all blobs
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", cfg.opacityArea);
}
/////////////////////////////////////////////////////////
/////////////////// Draw the Legend /////////////////////
/////////////////////////////////////////////////////////
svg.append("g")
.attr("class", "legendOrdinal")
.attr("transform", "translate(" + cfg["legendPosition"]["x"] + "," + cfg["legendPosition"]["y"] + ")");
var legendOrdinal = d3.legend.color()
//d3 symbol creates a path-string, for example
//"M0,-8.059274488676564L9.306048591020996,
//8.059274488676564 -9.306048591020996,8.059274488676564Z"
.shape("path", d3.svg.symbol().type("triangle-up").size(150)())
.shapePadding(10)
.scale(cfg.color)
.labels(cfg.color.domain().map(function(d){
return data[d][0][areaName];
}))
.on("cellover", function(d){ cellover(d); })
.on("cellout", function(d) { cellout(); });
svg.select(".legendOrdinal")
.call(legendOrdinal);
}//RadarChart