Skip to content

Instantly share code, notes, and snippets.

@matt-mcdaniel
Last active May 7, 2016 18:08
Show Gist options
  • Save matt-mcdaniel/c2949ffddf5705c93d89509e685bbb6e to your computer and use it in GitHub Desktop.
Save matt-mcdaniel/c2949ffddf5705c93d89509e685bbb6e to your computer and use it in GitHub Desktop.
Interactive Radial Chart
<!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