Skip to content

Instantly share code, notes, and snippets.

@mjmdavis
Created April 12, 2017 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mjmdavis/a0abaa06f53bd65899d2190562cacd3b to your computer and use it in GitHub Desktop.
Save mjmdavis/a0abaa06f53bd65899d2190562cacd3b to your computer and use it in GitHub Desktop.
noisy radar
<!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'>
<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://d3js.org/d3-random.v1.min.js"></script>
<script src="https://d3js.org/d3-array.v1.min.js"></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;
}
.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},
width = Math.min(700, window.innerWidth - 10) - margin.left - margin.right,
height = Math.min(width, window.innerHeight - margin.top - margin.bottom - 20);
//////////////////////////////////////////////////////////////
////////////////////////// Data //////////////////////////////
//////////////////////////////////////////////////////////////
var companies = ["Adobe",
"Amazon",
"Apple",
"Cisco",
"eBay",
"Facebook",
"Google",
"HP",
"IBM",
"Intel",
"LinkedIn",
"Microsoft",
"Oracle",
"Qualcomm",
"Salesforce",
"Samsung",
"SpaceX",
"Tesla"];
var attributes = ["Early Career Median Pay",
"Mid-career Median Pay",
"Median Age",
"Median Years Experience",
"Median years w/Company",
"% Female",
"% High Job Satisfaction",
"% High Job Meaning",
"% High Job Stress"];
var data = [];
const rand = d3.randomUniform(0,18);
for(var i=0; i < companies.length; i++) {
var new_obj = [
{axis:"Early Career Median Pay", value:rand()},
{axis:"Mid-career Median Pay", value:rand()},
{axis:"Median Age", value:rand()},
{axis:"Median Years Experience", value:rand()},
{axis:"Median years w/Company", value:rand()},
{axis:"% Female", value:rand()},
{axis:"% High Job Satisfaction", value:rand()},
{axis:"% High Job Meaning", value:rand()},
{axis:"% High Job Stress", value:rand()}
];
data[i] = new_obj;
}
for(var a_i=0; a_i<attributes.length; a_i++) {
var possible_values = d3.range(1, 1 + companies.length);
d3.shuffle(possible_values);
for(var c_i=0; c_i< companies.length; c_i++) {
data[c_i][a_i]['value'] = possible_values.pop();
}
}
console.table(data);
//////////////////////////////////////////////////////////////
//////////////////// Draw the Chart //////////////////////////
//////////////////////////////////////////////////////////////
var color = d3.scale.category20();
var radarChartOptions = {
w: width,
h: height,
margin: margin,
maxValue: 10,
levels: 10,
roundStrokes: true,
color: color,
labels: companies
};
//Call function to draw the Radar chart
RadarChart(".radarChart", data, radarChartOptions);
</script>
</body>
</html>
// Radar chard modified by mjmdavis
// attributions below
/////////////////////////////////////////////////////////
/////////////// The Radar Chart Function ////////////////
/////////////// Written by Nadieh Bremer ////////////////
////////////////// VisualCinnamon.com ///////////////////
/////////// Inspired by the code of alangrafu ///////////
/////////////////////////////////////////////////////////
// d3.legend.js
// (C) 2012 ziggy.jonsson.nyc@gmail.com
// MIT licence
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
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.0, //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
opacityAreaSelected: 0.8,
opacityLine: 0.8,
opacityLineFade: 0.3,
labels: null
};
//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
const highlight_area = function(parent_i) {
d3.selectAll(".radarArea")
.filter(function(d,i){
const area_id = parseInt(this.parentNode.getAttribute('id'));
return area_id == parent_i;})
.transition().duration(200)
.style("fill-opacity", cfg.opacityAreaSelected);
d3.selectAll(".radarStroke")
.filter(function(d,i){
const area_id = parseInt(this.parentNode.getAttribute('id'));
return area_id != parent_i;})
.transition().duration(200)
.style("stroke-opacity", cfg.opacityLineFade);
d3.selectAll(".legend-items > text")
.filter(function(d,i){
const area_id = parseInt(this.getAttribute('id'));
return area_id != parent_i})
.attr("opacity", "0.2")
};
const un_highlight_area = function(){
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", cfg.opacityArea);
d3.selectAll(".radarStroke")
.transition().duration(200)
.style("stroke-opacity", cfg.opacityLine);
d3.selectAll(".legend-items > text")
.attr("opacity", "1.0")
};
d3.legend = function(g) {
g.each(function() {
var g= d3.select(this),
items = {},
svg = d3.select(g.property("nearestViewportElement")),
legendPadding = g.attr("data-style-padding") || 5,
lb = g.selectAll(".legend-box").data([true]),
li = g.selectAll(".legend-items").data([true]);
lb.enter().append("rect").classed("legend-box",true).attr("fill", "white");
li.enter().append("g").classed("legend-items",true);
svg.selectAll("[data-legend]").each(function() {
var self = d3.select(this);
items[self.attr("data-legend")] = {
pos : self.attr("data-legend-pos") || this.getBBox().y,
color : self.attr("data-legend-color") != undefined ? self.attr("data-legend-color") : self.style("fill") != 'none' ? self.style("fill") : self.style("stroke")
}
});
items = d3.entries(items).sort(function(a,b) { return a.value.pos-b.value.pos});
li.selectAll("text")
.data(items,function(d) { return d.key})
.call(function(d) { d.enter().append("text")})
.call(function(d) { d.exit().remove()})
.attr("y",function(d,i) { return i+"em"})
.attr("x","1em")
.attr('id', function(d,i){return i})
.text(function(d) { ;return d.key})
.on("mouseover", function(d, i){
var parent_i = parseInt(this.getAttribute('id'));
highlight_area(parent_i);
})
.on("mouseout", function(d, i){
un_highlight_area();
});
li.selectAll("circle")
.data(items,function(d) { return d.key})
.call(function(d) { d.enter().append("circle")})
.call(function(d) { d.exit().remove()})
.attr("cy",function(d,i) { return i-0.25+"em"})
.attr("cx",0)
.attr("r","0.4em")
.style("fill",function(d) { console.log(d.value.color);return d.value.color})
// Reposition and resize the box
var lbbox = li[0][0].getBBox()
lb.attr("x",(lbbox.x-legendPadding))
.attr("y",(lbbox.y-legendPadding))
.attr("height",(lbbox.height+2*legendPadding))
.attr("width",(lbbox.width+2*legendPadding))
})
return g;
}
//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(i, j){return 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('%'), //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 legend
var legend = g.append("g").attr("class", "legend");
legend.data(cfg.labels)
.enter()
.append("g")
.attr("class", "legendEntry")
/////////////////////////////////////////////////////////
//////////////////// 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", "gray")
.style("stroke-opacity", 0.5)
.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")
.attr("id", function(d, i){return i});
//Append the backgrounds
blobWrapper
.append("path")
.attr("class", "radarArea")
.attr("d", function(d,i) { return radarLine(d); })
.attr("id", function(d,i){return i})
.style("fill", function(d,i) { return cfg.color(i); })
.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)")
.attr("data-legend", function(d, i){return cfg.labels[i];})
.attr("data-legend-pos", function(d,i){return i})
;
//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")
.attr("id", function(d,i){return i;});
//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*2.0)
// .attr("id", function(d,i){return i})
.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);
var parent_i = parseInt(this.parentNode.getAttribute('id'));
highlight_area(parent_i);
// d3.selectAll(".radarStroke")
// .transition().duration(200)
// .style("stroke-opacity", cfg.opacityLineFade);
//Bring back the hovered over blob
// d3.select(this)
// .transition().duration(200)
// .style("fill-opacity", 0.7);
})
.on("mouseout", function(){
tooltip.transition().duration(200)
.style("opacity", 0);
un_highlight_area();
});
//Set up the small tooltip for when you hover over a circle
var tooltip = g.append("text")
.attr("class", "tooltip")
.style("opacity", 0);
legend = svg.append("g")
.attr("class","legend")
.attr("transform","translate(20,30)")
.style("font-size","12px")
.style("fill-color", "white")
.call(d3.legend);
/////////////////////////////////////////////////////////
/////////////////// 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment