Built with blockbuilder.org
Last active
May 7, 2016 18:08
-
-
Save matt-mcdaniel/c2949ffddf5705c93d89509e685bbb6e to your computer and use it in GitHub Desktop.
Interactive Radial 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> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
<style> | |
@import url(https://fonts.googleapis.com/css?family=Open+Sans); | |
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } | |
svg { width:100%; height: 100% } | |
text { font-family: 'Open Sans'; } | |
</style> | |
</head> | |
<body> | |
<div class="svg-container"></div> | |
<script> | |
var initialData = [ | |
{ | |
name: 'JavaScript Libraries and Frameworks', | |
oddsRatio: 2.68, | |
data: [ | |
{ | |
name: 'Angular', | |
oddsRatio: 2.21 | |
}, | |
{ | |
name: 'React', | |
oddsRatio: 1.97 | |
}, | |
{ | |
name: 'Ember', | |
oddsRatio: 1.37 | |
}, | |
{ | |
name: 'Vue', | |
oddsRatio: 1.03 | |
}, | |
{ | |
name: 'D3', | |
oddsRatio: 0.87 | |
} | |
] | |
}, | |
{ | |
name: 'Podcasts', | |
oddsRatio: 2.47, | |
data: [ | |
{ | |
name: 'JavaScript Jabber', | |
oddsRatio: 1.74 | |
}, | |
{ | |
name: 'The ChangeLog', | |
oddsRatio: 1.53 | |
}, | |
{ | |
name: 'Shop Talk', | |
oddsRatio: 1.49 | |
}, | |
{ | |
name: 'JavaScript Air', | |
oddsRatio: 1.47 | |
}, | |
{ | |
name: 'Angular Air', | |
oddsRatio: 1.2 | |
}, | |
{ | |
name: 'Eat. Sleep. Code.', | |
oddsRatio: 0.99 | |
}, | |
{ | |
name: 'History of Rome', | |
oddsRatio: 0.95 | |
}, | |
] | |
}, | |
{ | |
name: 'Beer', | |
oddsRatio: 1.23, | |
data: [ | |
{ | |
name: 'Ballast Point', | |
oddsRatio: 3.49 | |
}, | |
{ | |
name: 'Mike Hess', | |
oddsRatio: 1.11 | |
}, | |
{ | |
name: 'Red Trolley', | |
oddsRatio: 1.1 | |
}, | |
{ | |
name: 'Stone', | |
oddsRatio: 1.04 | |
}, | |
] | |
}, | |
{ | |
name: 'Food', | |
oddsRatio: 0.93, | |
data: [ | |
{ | |
name: 'Ethiopian', | |
oddsRatio: 1.65 | |
}, | |
{ | |
name: 'Thai', | |
oddsRatio: 1.56 | |
}, | |
{ | |
name: 'Mexican', | |
oddsRatio: 1.45 | |
}, | |
{ | |
name: 'Southern', | |
oddsRatio: 1.19 | |
}, | |
{ | |
name: 'Creole', | |
oddsRatio: 1.03 | |
}, | |
] | |
} | |
]; | |
var min = 0.87; | |
var max = 3.49; | |
var margin = { | |
top: 20, | |
right: 20, | |
bottom: 20, | |
left: 20 | |
} | |
var el = d3.select('.svg-container') | |
.style('width', '100%') | |
.style('height', '500px'); | |
var svg = el.append('svg') | |
.attr('width', '100%') | |
.attr('height', '100%'); | |
var svgWidth = svg.node().clientWidth; | |
var svgHeight = svg.node().clientHeight; | |
var g = svg.append('g') | |
.attr('id', 'choosing-g') | |
.attr('transform', 'translate(' + (svgWidth / 2) + ',' + (svgHeight / 2) + ')'); | |
var isInitialData = true; | |
update(); | |
function update(data, tweenArr, tweenColor) { | |
var contextMax = data ? getMax(data) : getMax(initialData); | |
var radius = (Math.min( | |
svgWidth - margin.left - margin.right, | |
svgHeight - margin.top - margin.bottom | |
) / 2); | |
var innerRadius = 0; | |
arc = d3.svg.arc() | |
.innerRadius(function(d) { | |
if (d.data.oddsRatio < 1) { | |
return radius * (d.data.oddsRatio / max); | |
} else { | |
return radius * (1 / max); | |
} | |
}) | |
.outerRadius(function (d) { | |
if (d.data.oddsRatio < 1) { | |
return radius * (1 / max); | |
} else { | |
return radius * (d.data.oddsRatio / max); | |
} | |
}); | |
var pie = d3.layout.pie() | |
.sort(function (a, b) { return d3.descending(a.oddsRatio, b.oddsRatio); }) | |
.value(function (d) { return 1; }); | |
var data = data || initialData; | |
var tweenArr = tweenArr || null; | |
var colorScale = d3.scale.linear() | |
.domain([min, 0.99, 1, max]) | |
.range(['#c0392b', '#e74c3c', '#1aa153', '#003b00']); | |
var opacityScale = d3.scale.linear() | |
.domain([0, 0.99, 1, max]) | |
.range([1, 0.5, 0.5, 1]) | |
// var colorScale; | |
// if (isInitialData) { | |
// colorScale = getColorScale(data); | |
// } else if (!isInitialData && tweenColor) { | |
// colorScale = getColorScale(data, tweenColor); | |
// } | |
/*** Outer Arc ***/ | |
// remove all outer arcs before rendering | |
// g.selectAll('.outer-arc') | |
// .data([]).exit().remove(); | |
// var outerPath = g.selectAll('.outer-arc') | |
// .data(function() { | |
// var maxData = []; | |
// for (var i = 0; i < data.length; i++) { | |
// maxData.push({ | |
// //oddsRatio: 1 | |
// //oddsRatio: contextMax | |
// }) | |
// } | |
// return pie(maxData); | |
// }); | |
// outerPath.exit().remove(); | |
// outerPath.enter().append('path'); | |
// outerPath | |
// .attr('class', 'outer-arc') | |
// .style('opacity', 0) | |
// .style({ | |
// fill: 'transparent', | |
// 'stroke-width': 1, | |
// stroke: '#d3d3d3' | |
// }) | |
// .attr('d', function(d, i) { | |
// return arc(d); | |
// }) | |
// .transition() | |
// .delay(200) | |
// .duration(500) | |
// .style('opacity', 1); | |
/*** Polyline ***/ | |
// remove all polyine before rebinding | |
d3.selectAll('.arc-polyline') | |
.data([]).exit().remove(); | |
var line = g.selectAll('.arc-polyline') | |
.data(pie(data)); | |
line.exit().remove(); | |
line.enter().append('polyline'); | |
line | |
.attr('class', 'arc-polyline') | |
.style({ | |
opacity: 0, | |
stroke: '#d3d3d3', | |
'stroke-width': 1, | |
fill: 'none' | |
}) | |
.transition() | |
.delay(300) | |
.duration(500) | |
.ease('exp') | |
.style('opacity', 0.6) | |
.attr('points', function(d){ | |
var centroid = arc.centroid(d); | |
var yPoint = centroid[1] * 2; | |
var midAngle = d.startAngle + (d.endAngle - d.startAngle)/2; | |
var radiusRatio = contextMax / max; | |
var firstPoint = centroid[0] + ',' + centroid[1]; | |
var secondPointX = (radius * (radiusRatio)) * (midAngle < Math.PI ? 1 : -1); | |
var secondPoint = secondPointX + ',' + yPoint; | |
var thirdPointX = (radius * (radiusRatio + 0.1)) * (midAngle < Math.PI ? 1 : -1); | |
var thirdPoint = thirdPointX + ',' + yPoint; | |
return firstPoint + ',' + secondPoint + ',' + thirdPoint; | |
}); | |
/*** Arc ***/ | |
// remove all arcs before rendering new ones | |
g.selectAll('.arc') | |
.data([]).exit().remove(); | |
var path = g.selectAll('.arc') | |
.data(pie(data)); | |
path.exit().remove(); | |
path.enter().append('path'); | |
path | |
.style('cursor', 'pointer') | |
.attr('class', 'arc') | |
.attr('fill', tweenColor || null) | |
.style('opacity', function(d) { return opacityScale(d.data.oddsRatio); }) | |
.transition() | |
.duration(500) | |
.attr('fill', function(d, i) { | |
return colorScale(d.data.oddsRatio); | |
}) | |
.attrTween('d', function(d, i) { | |
if (!tweenArr) { | |
// if initial render, do not interpolate | |
return function() { return arc(d); }; | |
} else { | |
var tweenPie = pie(tweenArr); | |
return arcTweenSegment(d, tweenPie[i]); | |
} | |
}); | |
/*** Text ***/ | |
g.selectAll('.textGroup') | |
.data([]) | |
.exit().remove(); | |
var textGroup = g.selectAll('.textGroup') | |
.data(pie(data)); | |
textGroup.enter().append('g') | |
.style('cursor', 'pointer') | |
.attr('class', 'textGroup') | |
.attr('opacity', 0) | |
.transition() | |
.delay(300) | |
.duration(500) | |
.attr('opacity', 1); | |
textGroup.append('text') | |
.attr('class', 'arc-category') | |
.text(function(d) { return d.data.name; }) | |
.style('font-size', 14) | |
.attr('fill', '#333') | |
.attr('text-anchor', function(d) { | |
var midAngle = d.startAngle + (d.endAngle - d.startAngle)/2; | |
return midAngle < Math.PI ? 'start' : 'end'; | |
}) | |
.attr('x', function(d) { | |
var xOffset = radius * ((contextMax / max) + 0.1); | |
var midAngle = d.startAngle + (d.endAngle - d.startAngle)/2; | |
var rightSide = midAngle < Math.PI; | |
return rightSide ? xOffset : -xOffset; | |
}) | |
.attr('y', function(d) { | |
return (arc.centroid(d)[1] * 2) - 6; | |
}); | |
var fullQuestion = textGroup.append('text') | |
.attr('class', 'arc-question') | |
.attr('text-anchor', function(d) { | |
var midAngle = d.startAngle + (d.endAngle - d.startAngle)/2; | |
return midAngle < Math.PI ? 'start' : 'end'; | |
}) | |
fullQuestion | |
.text(function(d) { return d.data.text; }) | |
.style('font-size', 11) | |
.style('fill', 'rgb(180,180,180)') | |
var oddsRatio = textGroup.append('text') | |
.attr('class', 'arc-oddsRatio') | |
.attr('text-anchor', function(d) { | |
var midAngle = d.startAngle + (d.endAngle - d.startAngle)/2; | |
return midAngle < Math.PI ? 'start' : 'end'; | |
}) | |
oddsRatio | |
.text(function(d) { | |
var format = d3.format('%'); | |
var oddsRatio = d.data.oddsRatio - 1; | |
var isNegative = oddsRatio < 0; | |
return isNegative ? '-' + format(Math.abs(oddsRatio)) : format(Math.abs(oddsRatio)); | |
}); | |
var underline = textGroup.append('line') | |
.attr('class', 'arc-underline') | |
.attr({ | |
stroke: function(d) { return colorScale(d.data.oddsRatio); }, | |
'stroke-width': 1, | |
fill: 'none' | |
}); | |
// Format text | |
textGroup.each(function(d) { | |
var xOffset = radius * ((contextMax / max) + 0.1); | |
var midAngle = d.startAngle + (d.endAngle - d.startAngle)/2; | |
var rightSide = midAngle < Math.PI; | |
var boundBox = d3.select(this).select('.arc-category').node().getBBox(); | |
var categoryWidth = boundBox.width; | |
d3.select(this).select('.arc-oddsRatio') | |
.attr('x', function(d) { | |
return rightSide ? xOffset : -xOffset; | |
}) | |
.attr('y', function(d) { | |
if (isInitialData) { | |
return (arc.centroid(d)[1] * 2) + 18; | |
} else { | |
return (arc.centroid(d)[1] * 2) + 33; | |
} | |
}); | |
d3.select(this).select('.arc-question') | |
.attr('x', function(d) { | |
return rightSide ? xOffset : -xOffset; | |
}) | |
.attr('y', function(d) { | |
return (arc.centroid(d)[1] * 2) + 15; | |
}); | |
d3.select(this).select('.arc-underline') | |
.attr({ | |
x1: function(d) { | |
return rightSide ? xOffset : -xOffset; | |
}, | |
x2: function(d) { | |
return rightSide ? xOffset + categoryWidth : -xOffset - categoryWidth; | |
}, | |
y1: (arc.centroid(d)[1] * 2), | |
y2: (arc.centroid(d)[1] * 2) | |
}) | |
}); | |
/** Events */ | |
path | |
.on('mouseover', function(d) { | |
// var outerData = deepObjCopy(d); | |
// var data = deepObjCopy(d3.select(this).data()[0].data); | |
// data.oddsRatio = data.oddsRatio + 0.1; | |
// outerData.data = data; | |
// d3.select(this) | |
// .transition() | |
// .duration(200) | |
// .attr('d', arc(outerData)); | |
}) | |
.on('mouseleave', function(d) { | |
// d3.select(this) | |
// .transition() | |
// .duration(150) | |
// .attr('d', arc(d)); | |
}); | |
/** On Text Group Click */ | |
textGroup.on('click', clickEvent) | |
/** On Arc Click **/ | |
path.on('click', clickEvent); | |
function clickEvent(d) { | |
var el = d3.select(this); | |
var clickData = el.data(); | |
var color; | |
var isTextGroup = el.select('.arc-underline').node() !== null; | |
if (isTextGroup) { | |
color = el.select('.arc-underline').attr('stroke'); | |
} else { | |
color = el.attr('fill'); | |
} | |
var clickDataCopy = deepObjCopy(clickData[0]); | |
d3.selectAll('.arc-polyline') | |
.data([]).exit().remove(); | |
d3.selectAll('.textGroup') | |
.data([]) | |
.exit().remove(); | |
// remove all arcs except for selected | |
var path = d3.selectAll('.arc') | |
.data(clickData); | |
path.exit().remove(); | |
path | |
.attr('fill', color) | |
.transition() | |
.duration(500) | |
.attrTween('d', function (d) { | |
return arcTweenFullCircle(d); | |
}); | |
// wait for full circle tween to finish | |
var timeout = window.setTimeout(function() { | |
// check to see if will render initial dataset | |
isInitialData = !clickData[0].data.hasOwnProperty('data'); | |
// create deep copy of selected arc | |
var deepCopy = deepObjCopy(clickData[0]); | |
// create array of n length with clicked object | |
var dataLength = deepCopy.data.hasOwnProperty('data') ? | |
deepCopy.data.data.length : initialData.length; | |
var tweenObject = getTweenData(deepCopy, dataLength); | |
// update and tween from clicked object | |
update(d.data.data, tweenObject, color); | |
}, 500); | |
} | |
} | |
function getTweenData(copy, length) { | |
var tweenFromArr = []; | |
for (var i = 0; i < length; i++) { | |
tweenFromArr.push( | |
// sort data by value | |
copy.data | |
); | |
} | |
return tweenFromArr; | |
} | |
function arcTweenFullCircle(d) { | |
// interpolate startAngle to top center, counterclockwise | |
var interpolateStart = d3.interpolate( | |
d.startAngle, | |
0 | |
); | |
// interpolate endAngle to top center, clockwise | |
var interpolateEnd = d3.interpolate( | |
d.endAngle, | |
Math.PI * 2 | |
); | |
return function (t) { | |
d.startAngle = interpolateStart(t); | |
d.endAngle = interpolateEnd(t); | |
return arc(d); | |
} | |
} | |
// tween old arc value to new arc value | |
function arcTweenSegment(d, tweenTo) { | |
var interpolate = d3.interpolate(tweenTo, d); | |
return function (t) { | |
return arc(interpolate(t)); | |
} | |
} | |
// function getColorScale(data, color) { | |
// return d3.scale.linear() | |
// .domain([min, 0.99, 1, max]) | |
// .range(['#c0392b', '#e74c3c', '#2ecc71', '#27ae60']) | |
// if (!color) { | |
// return d3.scale.linear() | |
// .domain(initialData.map(function (d, i) { return i; })) | |
// .range(['#2ecc71', '#3498db', '#9b59b6', '#e67e22', '#e74c3c']); | |
// } else { | |
// // get color in RGB format | |
// var rgbColor = d3.rgb(color); | |
// var colors = []; | |
// for (var i = 0; i < data.length; i++) { | |
// colors.push([i, rgbColor.darker(i === 0 ? 0 : i / 2)]); | |
// } | |
// var scale = d3.scale.linear() | |
// .domain(data | |
// .map(function (d) { return d.oddsRatio; }) | |
// .sort(function (a, b) { return d3.ascending(a, b); })) | |
// .range(colors.map(function (d) { return d[1]; })); | |
// return scale; | |
// } | |
// } | |
function getMax(data) { | |
return data.reduce(function(acc, cur) { | |
return acc > cur.oddsRatio ? acc : cur.oddsRatio; | |
}, 0); | |
} | |
function deepObjCopy(dupeObj) { | |
var retObj = new Object(); | |
if (typeof (dupeObj) == 'object') { | |
if (typeof (dupeObj.length) != 'undefined') | |
var retObj = new Array(); | |
for (var objInd in dupeObj) { | |
if (typeof (dupeObj[objInd]) == 'object') { | |
retObj[objInd] = deepObjCopy(dupeObj[objInd]); | |
} else if (typeof (dupeObj[objInd]) == 'string') { | |
retObj[objInd] = dupeObj[objInd]; | |
} else if (typeof (dupeObj[objInd]) == 'number') { | |
retObj[objInd] = dupeObj[objInd]; | |
} else if (typeof (dupeObj[objInd]) == 'boolean') { | |
((dupeObj[objInd] == true) ? | |
retObj[objInd] = true : retObj[objInd] = false); | |
} | |
} | |
} | |
return retObj; | |
} | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment