A solution to proportional comparisons between independent groups, inspired by my memory of stadium checkers.
Last active
August 29, 2015 13:56
-
-
Save hobbes7878/9061986 to your computer and use it in GitHub Desktop.
Stadium Chart
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> | |
<style> | |
.major_tick.label{ | |
font-family:serif; | |
font-size:11px; | |
text-anchor:start; | |
} | |
.major_tick.ticks{ | |
stroke:white; | |
stroke-width:3px; | |
} | |
.major_tick.base{ | |
stroke:white; | |
stroke-width:4px; | |
} | |
.path_labs{ | |
font-family:sans-serif; | |
kerning:3; | |
fill:white; | |
font-weight:600; | |
font-size:16px; | |
} | |
.tick_lab{ | |
kerning:1; | |
fill:#999999; | |
} | |
.viz_key{ | |
font-family:"Times New Roman",serif; | |
font-size:20px; | |
font-style:italic; | |
} | |
.viz_instruct{ | |
font-family:sans-serif; | |
font-size:12px; | |
font-style:italic; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="viz"></div> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script> | |
//Helper function | |
//http://stackoverflow.com/questions/8273047/javascript-function-similar-to-python-range | |
function range(start, stop, step){ | |
if (typeof stop=='undefined'){ | |
// one param defined | |
stop = start; | |
start = 0; | |
}; | |
if (typeof step=='undefined'){ | |
step = 1; | |
}; | |
if ((step>0 && start>=stop) || (step<0 && start<=stop)){ | |
return []; | |
}; | |
var result = []; | |
for (var i=start; step>0 ? i<stop : i>stop; i+=step){ | |
result.push(i); | |
}; | |
return result; | |
}; | |
var data = { | |
NIR : [80,95,91,83], | |
WAL : [110,127,104,119], | |
ENG : [2190,1661,1930,2164], | |
SCO : [220,112,98,320], | |
DNK : [100,112,91,220], | |
IRE : [10,12,9,20], | |
FIN : [14,12,9,22], | |
EST : [124,212,119,222], | |
}; | |
//Color scale | |
var color_scale = ["#3A66A7", "#6b486b", "#d0743c", "#ff8c00"]; | |
//var color_scale=["#40004b", "#9970ab", "#ef3b2c", "#67000d"]; | |
var color = d3.scale.ordinal() | |
.range(color_scale); | |
//Some quick convertors | |
var radian = function(i){ return i * Math.PI/180 }; | |
var degree = function(i){ return i * 180/Math.PI }; | |
var margin = {top: 30, right: 100, bottom:30,left:30, | |
center_ring_radius:40}; | |
//padding inbetween rings as percentage of width | |
var percent_pad = .2; | |
var data_length = Object.keys(data).length; | |
var width = 500, | |
height = 470, | |
svg_width = width - margin.right - margin.left, | |
svg_height = height - margin.bottom - margin.top, | |
center_x = margin.left + svg_width/2, | |
center_y = margin.top + svg_height/2, | |
outer_radius = Math.min(svg_width,svg_height) / 2, | |
annulus_width = (outer_radius-margin.center_ring_radius)/ | |
((data_length-1) * percent_pad + data_length), | |
pad_width = annulus_width*percent_pad; | |
function arc(i){ | |
return d3.svg.arc() | |
.outerRadius(outer_radius - i * (annulus_width+pad_width)) | |
.innerRadius((outer_radius - i * (annulus_width+pad_width))-annulus_width) | |
} | |
//a starting angle | |
var angle = 0; | |
var state_angle = angle; | |
var tick_angles = [0,90,180,270]; | |
var radius = Math.min(svg_width,svg_height)/2; | |
//Canvas | |
var svg = d3.select("#viz").append("svg") | |
.attr("class",key) | |
.attr("width",width) | |
.attr("height",height); | |
var g = svg.append("g") | |
.style("pointer-events","none") | |
.attr("transform","translate("+ | |
(svg_width/2+margin.left)+","+(svg_height/2+margin.top) +")"); | |
//Tick Labels | |
//Major | |
var tick_percents = [0,50]; | |
for (tick in tick_percents){ | |
svg.append("text") | |
.text(tick_percents[tick]+"%") | |
.attr("class","tick_lab") | |
.style("font-size","13px") | |
.style("fill","black") | |
.style("text-anchor","middle") | |
.style("dominant-baseline","central") | |
//.attr("dy",function(){return this.getBBox().height/2 ;}) | |
//.attr("dx",function(){return this.getBBox().width/2 ;}) | |
.attr("angle_state",180-angle-180*(Number(tick)+0)) | |
.attr("x",center_x + (radius+12) *Math.sin(radian(180-angle-180*(Number(tick)+0)))) | |
.attr("y",center_y + (radius+12) *Math.cos(radian(180-angle-180*(Number(tick)+0)))) | |
; | |
}; | |
//Minor | |
var tick_percents = [10,20,30,40]; | |
for (tick in tick_percents){ | |
svg.append("text") | |
.attr("class","tick_lab") | |
.style("font-size","11px") | |
.style("text-anchor","middle") | |
.attr("dy",5) | |
.attr("dx",2) | |
.attr("angle_state",180-angle-36*(Number(tick)+1)) | |
.attr("x",center_x + (radius+12) *Math.sin(radian(180-angle-36*(Number(tick)+1)))) | |
.attr("y",center_y + (radius+12) *Math.cos(radian(180-angle-36*(Number(tick)+1)))) | |
.text(tick_percents[tick]); | |
}; | |
for (var key in data){ | |
var pie = d3.layout.pie().sort(null) | |
.startAngle(radian(angle)) | |
.endAngle(radian(360+angle)); | |
var path = g.selectAll("g.arc") | |
.data(pie(data[key])); | |
path.enter().append("path") | |
.style("pointer-events","fill") | |
.style("cursor","pointer") | |
.attr("class",function(d, i){ return "arc class"+i+" "+key;}) | |
.attr("id",function(d, i){ return "arcclass"+i+key;}) | |
.attr("d",arc(Object.keys(data).indexOf(key))) | |
.attr("fill", function(d, i) { return color(i); }) | |
.each(function(d) { this._current = d; }) | |
.style("stroke","black").style("stroke-width","0px") | |
.on("mouseover",function(){ | |
d3.selectAll("."+this.className["baseVal"].split(' ')[1]) | |
.style("stroke-width","1.25px"); | |
d3.select(this).style("stroke","red").style("stroke-width","2px"); | |
}) | |
.on("mouseout",function(){ | |
d3.selectAll(".arc") | |
.style("stroke-width","0px") | |
.style("stroke","black"); | |
d3.selectAll(".major_tick.ticks") | |
.transition().delay(3000) | |
.style("opacity",0); | |
d3.selectAll(".tick_lab") | |
.transition().delay(3000) | |
.style("opacity",0); | |
d3.selectAll(".arc") | |
.transition().delay(3000) | |
.style("fill-opacity",1); | |
}) | |
.on("click",function(){ | |
//disable click over animation | |
d3.selectAll(".arc") | |
.style("pointer-events","none"); | |
setTimeout(function(){ | |
d3.selectAll(".arc").style("pointer-events","fill"); | |
},2000); | |
swivel(this); | |
d3.selectAll(".major_tick.ticks") | |
.style("opacity",1); | |
d3.selectAll(".tick_lab") | |
.style("opacity",1); | |
}) | |
.append("svg:title") | |
.text(function(d){return d.value+" GPs";}); | |
for(i in data[key]){ | |
g.append("text") | |
.attr("x",10) | |
.attr("dy",annulus_width*.9) | |
.attr("class","path_labs class"+i) | |
.style("opacity",0) | |
.append("textPath") | |
.attr("xlink:href",function(d){ return "#arcclass"+i+key;}) | |
.text(key); | |
} | |
} | |
//Change Function | |
function swivel(obj){ | |
var d = degree(obj.__data__.startAngle); | |
var s=window.state_angle; | |
console.log("Start Angle: "+s) | |
console.log("Destination Angle: "+d) | |
//Guide Lines | |
var lines = d3.selectAll(".major_tick"); | |
lines.transition().duration(2000) | |
.attrTween("transform", function() { | |
return d3.interpolateString("rotate("+s+" "+center_x+","+center_y+")", | |
"rotate("+d+" "+center_x+","+center_y+")"); | |
}); | |
//Guide Labels | |
//Some Shorthanded Trig | |
function strig(x){ | |
return Number(x.getAttribute("angle_state")) | |
} | |
function xtrig(x){ | |
return center_x + (radius+12) *Math.sin(radian((s>d) ? strig(x) +(s-d) : strig(x) +(s-d))); | |
} | |
function ytrig(y){ | |
return center_y + (radius+12) *Math.cos(radian((s>d) ? strig(y) +(s-d) : strig(y) +(s-d))); | |
} | |
var ticklabs = d3.selectAll(".tick_lab"); | |
ticklabs.transition().delay(0) | |
.attr("x", function(){return xtrig(this);}) | |
.attr("y", function(){return ytrig(this);}); | |
ticklabs.transition().delay(300).attr("angle_state",function(){ | |
return ((d>s) ? 360+s-d : s-d) + Number(this.getAttribute("angle_state")) ;}); | |
//reset angle state variable | |
window.state_angle=d; | |
for (var key in data){ | |
//the angle to change | |
var compare = d3.select(".arc."+obj.className["baseVal"].split(' ')[1]+"."+key); | |
var compare_angle = degree(compare[0][0].__data__.startAngle); | |
var state_angle = degree(d3.select(".arc."+key)[0][0].__data__.startAngle); | |
var adjust_angle = d - compare_angle; | |
var pie = d3.layout.pie().sort(null) | |
.startAngle(radian(state_angle+adjust_angle)) | |
.endAngle(radian(360+state_angle+adjust_angle)); | |
var paths = d3.selectAll(".arc."+key); | |
paths.data(pie(data[key])) | |
.transition().duration(2000) | |
.attrTween("d", | |
function (d) { | |
var i = d3.interpolate(this._current, d); | |
this._current = i(0); | |
//Get index of current | |
var index = Object.keys(data).indexOf(this.className["baseVal"].split(' ')[2]); | |
return function(t) { | |
return arc(index)(i(t)); | |
}; | |
}) | |
.style("fill-opacity",function(d){ | |
if(this.className["baseVal"].split(' ')[1] == | |
obj.className["baseVal"].split(' ')[1]) | |
{ return 1; }else{ return .5; } | |
}) | |
.style("stroke-width",function(d){ | |
if(this.className["baseVal"].split(' ')[1] == | |
obj.className["baseVal"].split(' ')[1]) | |
{ return "1.25px"; }else{ return "0px"; } | |
}) | |
d3.selectAll(".path_labs") | |
.transition().duration(1000) | |
.style("opacity",0) | |
d3.selectAll(".path_labs."+obj.className["baseVal"].split(' ')[1]) | |
.transition().delay(1000).duration(1000) | |
.style("opacity",1) | |
} | |
} | |
//Ticks | |
//x = cx + r * sin(a) | |
//y = cy + r * cos(a) | |
var tick_labels = [0,20,40,60,80]; | |
for (i in range(data_length)){ | |
for (tick in tick_labels){ | |
var tock = Number(tick); | |
var tock_angle = radian(180-angle-(360/tick_labels.length*(tock))); | |
var tock_len = annulus_width*.1; | |
var trig_point = radius - (Number(i)*(annulus_width+pad_width)) | |
svg.append("line") | |
.attr("class","major_tick ticks") | |
.attr("stroke-linecap","round") | |
.attr("x1",center_x+ (trig_point -tock_len) *Math.sin(Math.PI/2-tock_angle/2)) | |
.attr("y1",center_y+ (trig_point -tock_len) *Math.cos(Math.PI/2-tock_angle/2)) | |
.attr("x2",center_x+ (trig_point) *Math.sin(Math.PI/2-tock_angle/2)) | |
.attr("y2",center_y+ (trig_point) *Math.cos(Math.PI/2-tock_angle/2)); | |
} | |
} | |
//Zero Axis | |
svg.append("line") | |
.attr("class","major_tick base") | |
.attr("x1",center_x+(margin.center_ring_radius-4)*Math.sin(radian(0+angle))) | |
.attr("y1",center_y+(margin.center_ring_radius-4)*Math.cos(radian(180-angle))) | |
.attr("x2",center_x+ (radius+4) *Math.sin(radian(0+angle))) | |
.attr("y2",center_y+ (radius+4) *Math.cos(radian(180-angle))); | |
d3.selectAll(".path_labs.class0") | |
.style("opacity",1); | |
//CUSTOM KEY | |
svg.append("rect") | |
.attr("class","arc class0") | |
.attr("x",width-70) | |
.attr("y", 100) | |
.attr("width",17) | |
.attr("height",17) | |
.attr("fill",color_scale[0]); | |
svg.append("text") | |
.attr("class", "viz_key") | |
.attr("x",width-50) | |
.attr("y", 115) | |
.text("L1") | |
svg.append("rect") | |
.attr("class","arc class1") | |
.attr("x",width-70) | |
.attr("y", 120) | |
.attr("width",17) | |
.attr("height",17) | |
.attr("fill",color_scale[1]); | |
svg.append("text") | |
.attr("class", "viz_key") | |
.attr("x",width-50) | |
.attr("y", 135) | |
.text("L2") | |
svg.append("rect") | |
.attr("class","arc class2") | |
.attr("x",width-70) | |
.attr("y", 140) | |
.attr("width",17) | |
.attr("height",17) | |
.attr("fill",color_scale[2]); | |
svg.append("text") | |
.attr("class", "viz_key") | |
.attr("x",width-50) | |
.attr("y", 155) | |
.text("L3") | |
svg.append("rect") | |
.attr("class","arc class3") | |
.attr("x",width-70) | |
.attr("y", 160) | |
.attr("width",17) | |
.attr("height",17) | |
.attr("fill",color_scale[3]); | |
svg.append("text") | |
.attr("class", "viz_key") | |
.attr("x",width-50) | |
.attr("y", 175) | |
.text("L4") | |
svg.append("text") | |
.attr("class", "viz_instruct") | |
.attr("x",width/2-150) | |
.attr("y", height-15) | |
.text("Click a group to align bars for comparison.") | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment