Last active
August 29, 2015 14:23
-
-
Save byronmansfield/285e87b4453465d19b9e to your computer and use it in GitHub Desktop.
Angular Directive for top 5 D3 Donut Chart with Legend
This file contains hidden or 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
'use strict'; | |
/** | |
* @ngdoc directive | |
* @name perfmanApp.directive:donut | |
* @description | |
* # donut | |
*/ | |
angular.module('perfmanApp') | |
.directive('donut', function() { | |
return { | |
restrict: 'E' | |
, scope: { | |
data: '=data' | |
} | |
, link: function postLink(scope, element, attrs) { | |
/** | |
* Data formatting function | |
* @params - {Array} - data - Array of data used from data attribute on directive | |
* @return - {Array} - Original array just formated so that we can populate the d3 donut | |
*/ | |
function formatData(data) { | |
/** if data array is not empty */ | |
if(data.length) { | |
/** Global Variables */ | |
var total = 0 | |
/** Calculate Total */ | |
for(var i in data) { | |
total += data[i].percent | |
} | |
/** Sort data by percentages */ | |
data.sort(function(a, b) { | |
return a.percent - b.percent | |
}) | |
/** Turn percentages into shorter whole numbers for display purposes */ | |
for(var i in data) { | |
data[i].percent = Math.round(data[i].percent) | |
} | |
/** Shorten the length of the name for display purposes */ | |
if(attrs.title === 'Top 5 Industries') { | |
for(var i in data) { | |
var name = data[i].name.split(' ') | |
data[i].name = name[0] | |
} | |
} | |
/** Catch anything that requires Other to be in the chart */ | |
if(data.length > 5 || total < 100) { | |
var shortListTotal = 0 | |
/** Cut off all after the 5th object */ | |
data.splice(0, data.length - 5) | |
/** Calculate Total */ | |
for(var i in data) { | |
shortListTotal += data[i].percent | |
} | |
/** Tack the Other onto the end */ | |
if(shortListTotal < 100) { | |
data.unshift({ | |
name: "Other" | |
, percent: 100 - shortListTotal | |
}) | |
} | |
} | |
/** Return formated data for rendering d3 chart */ | |
return data.reverse() | |
} | |
else { /** else handle empty arrays */ | |
var obj = { | |
name: 'Blank' | |
, percent: 100 | |
} | |
data.push(obj) | |
return data | |
} | |
} | |
/** | |
* tween function for changing start angle | |
*/ | |
function tweenPie(b) { | |
var i = d3.interpolate({ | |
startAngle: 1.1 * Math.PI, endAngle: 1.1 * Math.PI | |
}, b) | |
return function(t) { | |
return arc(i(t)) | |
} | |
} | |
/** | |
* Arc Hover Over Function | |
*/ | |
function archOver(d, i) { | |
/** Grow effect */ | |
d3.select(this) | |
.transition() | |
.duration(300) | |
.attr("d", arcOver) | |
/** Addition of text in center of chart */ | |
d3.select(this.parentNode) | |
.append('text') | |
.text(function(d) { | |
return d.data.percent + '%'; | |
}) | |
.attr('font-family', '"Proxima Nova Bold", sans-serif') | |
.attr('font-size', '30px') | |
.attr('fill', '#004388') | |
.attr('x', function(d) { | |
if(d.data.percent === 100) { | |
return -35 | |
} | |
else { | |
return -25 | |
} | |
}) | |
.attr('y', 10) | |
.attr("class", "percent") | |
/** highlight the respected legend */ | |
d3.select(this.parentNode).select('rect') | |
.attr('fill', function() { return fills(i) }) | |
.transition() | |
.duration(100) | |
.ease('linear-in') | |
.attr('fill', '#bfe4f7') | |
} | |
/** | |
* Arc Hover Off Function | |
*/ | |
function archOff(d, i) { | |
/** set arc back to original size */ | |
d3.select(this) | |
.transition() | |
.duration(500) | |
.attr("d", arc) | |
/** remove percentage text in center */ | |
d3.select(this.parentNode).selectAll('.percent').remove() | |
/** set rectangle background back to original color */ | |
d3.select(this.parentNode).select('rect') | |
.attr('fill', '#bfe4f7') | |
.transition() | |
.duration(200) | |
.ease('linear-out') | |
.attr('fill', function() { return fills(i) }) | |
} | |
/** | |
* Legend Hover Over Function | |
*/ | |
function legendOver(d, i) { | |
/** highlight the legend background */ | |
d3.select(this) | |
.attr('fill', function() { return fills(i) }) | |
.transition() | |
.duration(100) | |
.ease('linear-in') | |
.attr('fill', '#bfe4f7') | |
/** Grow respected arc */ | |
d3.select(this.parentNode).select('.arc') | |
.transition() | |
.duration(300) | |
.attr("d", arcOver) | |
/** Addition of text in center of chart */ | |
d3.select(this.parentNode) | |
.append('text') | |
.text(function(d) { | |
return d.data.percent + '%'; | |
}) | |
.attr('font-family', '"Proxima Nova Bold", sans-serif') | |
.attr('font-size', '30px') | |
.attr('fill', '#004388') | |
.attr('x', function(d) { | |
if(d.data.percent === 100) { | |
return -35 | |
} | |
else { | |
return -25 | |
} | |
}) | |
.attr('y', 10) | |
.attr("class", "percent") | |
} | |
/** | |
* Legend Hover Off Function | |
*/ | |
function legendOff(d, i) { | |
/** unhighlight legend background */ | |
d3.select(this) | |
.attr('fill', '#bfe4f7') | |
.transition() | |
.duration(200) | |
.ease('linear-out') | |
.attr('fill', function() { return fills(i) }) | |
/** remove percentage text in center */ | |
d3.select(this.parentNode).selectAll('.percent').remove() | |
/** return arc to original size */ | |
d3.select(this.parentNode).select('.arc') | |
.transition() | |
.duration(500) | |
.attr("d", arc) | |
} | |
/** | |
* Set up SVG stage dimensions | |
*/ | |
var margin = { | |
top: 20 | |
, right: 20 | |
, bottom: 20 | |
, left: 20 | |
} | |
, width = 220 - margin.left - margin.right | |
, height = width - margin.top - margin.bottom | |
/** | |
* Construct svg element and apply dimensions | |
*/ | |
var chart = d3.select(element[0]) | |
.append('svg') | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.top + margin.bottom) | |
.append("g") | |
.attr("transform", "translate(" + ((width/2) + margin.left) + "," + ((height/2) + margin.top) + ")") | |
/** Colors for arch's */ | |
var color = d3.scale.ordinal() | |
.range([ | |
'#004e88' | |
, '#5cbbec' | |
, '#bfe4f7' | |
, '#ff8426' | |
, '#ffcea8' | |
]) | |
/** legend background fill */ | |
var fills = d3.scale.ordinal() | |
.range([ | |
'#edf4f8' | |
,'#fff' | |
,'#edf4f8' | |
,'#fff' | |
,'#edf4f8' | |
,'#fff' | |
]) | |
/** Legend Params */ | |
var legendRectHeight = 30 | |
, vertStartOffset = 160 | |
/** Add the Title */ | |
chart.append('text') | |
.text(function(d) { return attrs.title.toUpperCase() }) | |
.attr('font-family', '"Proxima Nova", sans-serif') | |
.attr('font-size', '16px') | |
.attr('fill', '#869aaa') | |
.attr('x', -80) | |
.attr('y', -100) | |
/** | |
* Construct Donut supporting pieces | |
*/ | |
var radius = Math.min(width, height) / 2 | |
, arc = d3.svg.arc() | |
.outerRadius(radius) | |
.innerRadius(radius - 24) | |
, pie = d3.layout.pie() | |
.sort(null) | |
.startAngle(Math.PI / 4) | |
.endAngle( (2 * Math.PI) + (Math.PI / 4) ) | |
.value(function(d) { | |
return d.percent | |
}) | |
/** hover over grow effect function */ | |
var arcOver = d3.svg.arc() | |
.innerRadius(radius - 24) | |
.outerRadius(radius + 8) | |
/** | |
* Drawing the SVG element | |
* @params - {Array} - data - Array of data used from data attribute on directive | |
*/ | |
scope.render = function(data) { | |
/** format the data */ | |
formatData(data) | |
/** | |
* To ensure that chart is cleared to render | |
* Important step for refresh of data on change | |
*/ | |
chart.selectAll('g.slice').remove() | |
/** The parent g element for each piece of data */ | |
var g = chart.selectAll(".slice") | |
.data(pie(data)) | |
.enter() | |
.append("g") | |
.attr("class", "slice") | |
/** process donut for blank chart */ | |
if(data[0].name === 'Blank') { | |
/** append the path element for the arc */ | |
g.append("path") | |
.attr("class", "arc") | |
.style("fill", '#edf3f7') | |
/** smooth transition on draw */ | |
.transition() | |
.ease("exp") | |
.duration(1000) | |
.attrTween("d", tweenPie) | |
} | |
else { /** process for everything else other than blank */ | |
/** append the path element for the arc */ | |
g.append("path") | |
.attr("class", "arc") | |
.style("fill", function(d, i) { | |
if(d.data.name === 'Other') { | |
return '#e1e1e1' | |
} | |
else { | |
return color(i) | |
} | |
}) | |
/** hover over listener */ | |
.on("mouseover", archOver) | |
/** hover off listener */ | |
.on("mouseout", archOff) | |
/** smooth transition on draw */ | |
.transition() | |
.ease("exp") | |
.duration(1000) | |
.attrTween("d", tweenPie) | |
/** append the rectangle element for the legend */ | |
g.append('rect') | |
.attr('width', '235') | |
.attr('height', legendRectHeight) | |
.attr('fill', function(d, i) { return fills(i) }) | |
.attr("transform", function(d, i) { return 'translate(-100,' + ( (legendRectHeight * i) + 100 ) + ')' }) | |
/** hover over for legend */ | |
.on('mouseover', legendOver) | |
/** hover off for legend */ | |
.on('mouseout', legendOff) | |
/** append the circle element for the legend */ | |
g.append('circle') | |
.attr('fill', function(d, i) { | |
if(d.data.name === 'Other') { | |
return '#e1e1e1' | |
} | |
else { | |
return color(i) | |
} | |
}) | |
.attr('stroke', '#fff') | |
.attr('stroke-width', '2px') | |
.attr('r', 6) | |
.attr('cx', legendRectHeight - 15) | |
.attr('cy', legendRectHeight - 15) | |
.attr("transform", function(d, i) { return 'translate(-100,' + ( (legendRectHeight * i) + 100 ) + ')' }) | |
/** append description text for the legend */ | |
g.append('text') | |
.attr('class', 'description') | |
.attr('x', legendRectHeight) | |
.attr('y', legendRectHeight - 10) | |
.attr('font-family', '"Proxima Nova", sans-serif') | |
.attr('font-size', '12px') | |
.attr('fill', '#869aaa') | |
.attr("transform", function(d, i) { return 'translate(-100,' + ( (legendRectHeight * i) + 100 ) + ')' }) | |
.text(function(d, i) { return data[i].name }) | |
/** append percentage text to legend */ | |
g.append('text') | |
.attr('class', 'amount') | |
.attr('x', legendRectHeight + 160) | |
.attr('y', legendRectHeight - 10) | |
.attr('font-family', '"Proxima Nova", sans-serif') | |
.attr('font-size', '12px') | |
.attr('fill', '#869aaa') | |
.attr("transform", function(d, i) { return 'translate(-100,' + ( (legendRectHeight * i) + 100 ) + ')' }) | |
.text(function(d, i) { return data[i].percent + '%' }) | |
} | |
} | |
/** | |
* Watch for data changes | |
*/ | |
scope.$watch('data', function(values) { | |
if(values) scope.render(scope.data) | |
}) | |
} | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment