198 lines
5.8 KiB
JavaScript
198 lines
5.8 KiB
JavaScript
'use strict'
|
|
|
|
import angular from 'angular'
|
|
import d3 from 'd3'
|
|
import clone from 'lodash.clonedeep'
|
|
|
|
module.exports = angular.module('xoWebApp.directives')
|
|
.directive('sunburstChart', function ($parse) {
|
|
function link(scope, element, attrs) {
|
|
var width, height, radius, color, b, breadcrumbs, svg, arc, partition
|
|
|
|
width = attrs.width ? parseInt(attrs.width, 10) : 2000
|
|
height = attrs.height ? parseInt(attrs.height, 10) : width + 50 /* breadcrumbs */
|
|
radius = Math.min(width, height) / 2
|
|
color = d3.scale.category20c()
|
|
b = {
|
|
w: 75, h: 30, s: 3, t: 10
|
|
}
|
|
|
|
breadcrumbs = d3.select(element[0])
|
|
.append('div')
|
|
.attr('class', 'breadcrumbs-container')
|
|
.append('svg:svg')
|
|
.attr('class', 'breadcrumbs')
|
|
.attr('width', width)
|
|
.attr('height', 50)
|
|
|
|
// took responsive svg trick from http://demosthenes.info/blog/744/Make-SVG-Responsive
|
|
svg = d3.select(element[0])
|
|
.append('div')
|
|
.attr('style', 'display: inline-block;position: relative;width: 80%;padding-bottom: 100%; vertical-align: middle;overflow: hidden;')
|
|
.append('svg')
|
|
.attr('style', 'display: block;position: absolute; top: 0;left: 0;')
|
|
.attr('width', '100%')
|
|
.attr('height', '100%')
|
|
.attr('preserveAspectRatio', 'xMinYMin meet')
|
|
.attr('viewBox', '0 0 2000 2000')
|
|
.append('g')
|
|
.attr('transform', 'translate(' + width / 2 + ',' + height * 0.52 + ')')
|
|
|
|
arc = d3.svg.arc()
|
|
.startAngle((d) => d.x)
|
|
.endAngle((d) => d.x + d.dx)
|
|
.innerRadius((d) => Math.sqrt(d.y))
|
|
.outerRadius((d) => Math.sqrt(d.y + d.dy))
|
|
|
|
scope.$watch('selected', function (newVal) {
|
|
if (newVal && newVal.id) {
|
|
highlight(newVal.id)
|
|
}
|
|
})
|
|
|
|
partition = d3.layout.partition()
|
|
.sort(null)
|
|
.size([2 * Math.PI, radius * radius])
|
|
.value((d) => d.size)
|
|
|
|
scope.$watch(() => scope.chartData, function (newVal) {
|
|
var g, chart_data
|
|
if (!scope.chartData || !scope.chartData.children || !scope.chartData.children.length) {
|
|
svg.selectAll('path').remove()
|
|
breadcrumbs.selectAll('g').remove()
|
|
return
|
|
}
|
|
//d3 add a lot aff data to nodex. It triggers watch without rest
|
|
chart_data = clone(scope.chartData)
|
|
|
|
g = svg
|
|
.datum(chart_data)
|
|
.selectAll('path')
|
|
.data(partition.nodes)
|
|
|
|
g.enter().append('path')
|
|
|
|
g.attr('display', (d) => d.depth ? null : 'none') // hide inner ring
|
|
.attr('d', arc)
|
|
.style('stroke', '#fff')
|
|
.style('fill', function (d) {
|
|
return d.color ? d.color : color((d.children ? d : d.parent).name)
|
|
})
|
|
.style('fill-rule', 'evenodd')
|
|
.on('mouseover', mouseover)
|
|
.on('click', click)
|
|
|
|
g.exit().remove()
|
|
}, true)
|
|
|
|
function mouseover(d) {
|
|
if (scope.over) {
|
|
scope.over.apply(null, [{ d: d }])
|
|
}
|
|
}
|
|
|
|
function click(d) {
|
|
if (scope.click) {
|
|
scope.click.apply(null, [{ d: d }])
|
|
}
|
|
}
|
|
|
|
function highlight(id) {
|
|
var sequenceArray = getAncestors(id)
|
|
updateBreadcrumbs(sequenceArray)
|
|
// Fade all the segments.
|
|
svg.selectAll('path')
|
|
.style('opacity', 0.3)
|
|
|
|
// Then highlight only those that are an ancestor of the current segment.
|
|
svg.selectAll('path')
|
|
.filter(function (node) {
|
|
return (sequenceArray.indexOf(node) >= 0)
|
|
})
|
|
.style('opacity', 1)
|
|
}
|
|
|
|
// Generate a string that describes the points of a breadcrumb polygon.
|
|
function breadcrumbPoints(d, i) {
|
|
var points = []
|
|
points.push('0,0')
|
|
points.push(b.w + ',0')
|
|
points.push(b.w + b.t + ',' + (b.h / 2))
|
|
points.push(b.w + ',' + b.h)
|
|
points.push('0,' + b.h)
|
|
if (i > 0) { // Leftmost breadcrumb don't include 6th vertex.
|
|
points.push(b.t + ',' + (b.h / 2))
|
|
}
|
|
return points.join(' ')
|
|
}
|
|
|
|
// Update the breadcrumb trail to show the current sequence and percentage.
|
|
function updateBreadcrumbs(nodeArray, percentageString) {
|
|
var g = breadcrumbs
|
|
.selectAll('g')
|
|
.data(nodeArray, function (d) {
|
|
return d.name + d.depth
|
|
})
|
|
|
|
// Add breadcrumb and label for entering nodes.
|
|
var entering = g.enter().append('svg:g')
|
|
|
|
entering.append('svg:polygon')
|
|
.attr('points', breadcrumbPoints)
|
|
.style('fill', function (d) {
|
|
return d.color ? d.color : color((d.children ? d : d.parent).name)
|
|
|
|
})
|
|
|
|
entering.append('svg:text')
|
|
.attr('x', (b.w + b.t) / 2)
|
|
.attr('y', b.h / 2)
|
|
.attr('dy', '0.35em')
|
|
.attr('text-anchor', 'middle')
|
|
.text((d) => d.name)
|
|
|
|
// Set position for entering and updating nodes.
|
|
g.attr('transform', function (d, i) {
|
|
return 'translate(' + i * (b.w + b.s) + ', 0)'
|
|
})
|
|
|
|
// Remove exiting nodes.
|
|
g.exit().remove()
|
|
|
|
// Make the breadcrumb trail visible, if it's hidden.
|
|
d3.select('.breadcrumbs')
|
|
.style('visibility', '')
|
|
|
|
}
|
|
|
|
function getAncestors(id) {
|
|
var path = []
|
|
svg.selectAll('path')
|
|
.each(function (node) {
|
|
if (node.id === id) {
|
|
var current = node
|
|
while (current.parent) {
|
|
path.unshift(current)
|
|
current = current.parent
|
|
}
|
|
}
|
|
})
|
|
return path
|
|
}
|
|
}
|
|
|
|
return {
|
|
resctict: 'E',
|
|
replace: false,
|
|
scope: {
|
|
chartData: '=',
|
|
selected: '=',
|
|
over: '&',
|
|
click: '&'
|
|
},
|
|
link: link
|
|
}
|
|
|
|
})
|
|
// A module exports its name.
|
|
.name |