Skip to content

Instantly share code, notes, and snippets.

@byronmansfield
Last active August 29, 2015 14:23
Show Gist options
  • Save byronmansfield/285e87b4453465d19b9e to your computer and use it in GitHub Desktop.
Save byronmansfield/285e87b4453465d19b9e to your computer and use it in GitHub Desktop.
Angular Directive for top 5 D3 Donut Chart with Legend
'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