sunburst directive : externalized in a module, react to vm changes

This commit is contained in:
Florent BEAUCHAMP
2015-08-27 17:01:24 +02:00
committed by Fabrice Marsaud
parent a6bedea4b6
commit f0f4e0985a
4 changed files with 304 additions and 395 deletions
+2
View File
@@ -4,3 +4,5 @@
/node_modules/*
!/node_modules/*.js
/node_modules/*.js/
jsconfig.json
+96 -395
View File
@@ -1,425 +1,126 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import uiSelect from 'angular-ui-select';
import d3 from 'd3';
import filter from 'lodash.filter';
'use strict'
import xoApi from 'xo-api';
import xoServices from 'xo-services';
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import foreach from 'lodash.foreach'
import view from './view';
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import sunburstChart from 'xo-sunburst-d3'
import view from './view'
export default angular.module('dashboard.dataviz', [
uiRouter,
uiSelect,
xoApi,
xoServices,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.dataviz', {
controller: 'Dataviz as ctrl',
url: '/dataviz',
template: view
});
})
})
.controller('Dataviz', function (xoApi, $scope, $timeout) {
.directive('sunburstChart', function ($parse) {
function link(scope, element, attrs) {
if (!scope.chartData) {
console.log(' no data, no graph')
return;
}
var 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
};
var 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
var 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 * .52 +')');
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function (d) {
return d.size;
});
var arc = d3.svg.arc()
.startAngle(function (d) {
return d.x;
$scope.charts = {
selected: {},
ram: {
name: 'ram',
children: []
},
cpu: {
name: 'cpu',
children: []
},
over: function (d) {
$scope.$apply(function () {
$scope.charts.selected = d
})
.endAngle(function (d) {
return d.x + d.dx;
})
.innerRadius(function (d) {
return Math.sqrt(d.y);
})
.outerRadius(function (d) {
return Math.sqrt(d.y + d.dy);
});
}
}
var path = svg.datum(scope.chartData).selectAll("path")
.data(partition.nodes)
.enter().append("path")
.attr("display", function (d) {
return 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);
function populateChartsData() {
let ram_children, cpu_children, pools, vmsByContainer, hostsByPool
function mouseover(d) {
if (scope.over) {
scope.over.apply(null, [{d: d}]);
ram_children = []
cpu_children = []
pools = xoApi.getView('pools')
vmsByContainer = xoApi.getIndex('vmsByContainer')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_cpu = {
name: pool.name_label,
id: pool_id,
children: []
}
}
function click(d) {
if (scope.click) {
scope.click.apply(null, [{d: d}]);
let pool_ram = {
name: pool.name_label,
id: pool_id,
children: []
}
}
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(function (d) {
return 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", "");
}
scope.$watch('selected', function (newVal) {
if (newVal && newVal.id) {
highlight(newVal.id);
}
});
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;
}
let hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
let host_cpu = {
name: host.name_label,
id: host_id,
children: []
}
let host_ram = {
name: host.name_label,
id: host_id,
children: []
}
let VMs = vmsByContainer[host_id]
foreach(VMs, function (VM, vm_id) {
let vm_ram = {
name: VM.name_label,
id: vm_id,
size: VM.memory.size
}
let vm_cpu = {
name: VM.name_label,
id: vm_id,
size: VM.CPUs.number
}
if (vm_cpu.size) {
host_cpu.children.push(vm_cpu)
}
if (vm_ram.size) {
host_ram.children.push(vm_ram)
}
})
return path;
}
}
return {
resctict: 'E',
replace: false,
scope: {
chartData: '=',
selected: '=',
over: '&',
click: '&'
},
link: link
}
})
.controller('Dataviz', function (xoApi, $scope) {
$scope.charts = {};
$scope.charts.selected = {};
$scope.charts.over = function (d) {
$scope.$apply(function () {
$scope.charts.selected = d;
if (host_ram.children.length) {
pool_ram.children.push(host_ram)
pool_cpu.children.push(host_cpu)
}
})
if (pool_ram.children.length) {
ram_children.push(pool_ram)
cpu_children.push(pool_cpu)
}
})
};
$scope.charts.ram =
{
name: "ram",
children: [{
name: " first pool",
id: 'pool1',
children: [{
id: 'server11',
name: "server1 ",
children: [{
id: 'vm11',
name: "VM11",
size: 512
}, {
id: 'vm12',
name: "VM12",
size: 1024
},
{
id: 'server11unallocated',
name: "unallocated",
size: 8048,
color: '#000'
}
]
}, {
name: "server2",
id: 'server12',
children: [{
id: 'VM121',
name: "VM21 et gs ert",
size: 256
}, {
id: 'VM122',
name: "VM22 seghtrsedg",
size: 2048
}
]
}
]
},
{
name: " second pool",
id: 'pool2',
children: [{
id: 'server21',
name: "server1 ",
children: [{
name: "VM11",
size: 768
}, {
id: 'vm211',
name: "VM12",
size: 1284
},
{
id: 'server21unallocated',
name: "unallocated",
size: 512,
color: '#000'
}
]
}, {
name: "server2",
id: 'server22',
children: [{
id: 'VMM221',
name: "VM21",
size: 256
}, {
name: "VM22",
id: 'server222',
size: 2048
},
{
id: 'server22unallocated',
name: "unallocated",
size: 4000,
color: '#000'
}
]
}
]
}]
};
$scope.charts.cpu =
{
name: "ram",
children: [{
name: " first pool",
id: 'pool1',
children: [{
id: 'server11',
name: "server1 ",
children: [{
id: 'vm11',
name: "VM11",
size: 512
}, {
id: 'vm12',
name: "VM12",
size: 1024
},
{
id: 'server11unallocated',
name: "unallocated",
size: 8048,
color: '#000'
}
]
}, {
name: "server2",
id: 'server12',
children: [{
id: 'VM121',
name: "VM21 et gs ert",
size: 256
}, {
id: 'VM122',
name: "VM22 seghtrsedg",
size: 2048
}
]
}
]
},
{
name: " second pool",
id: 'pool2',
children: [{
id: 'server21',
name: "server1 ",
children: [{
name: "VM11",
size: 768
}, {
id: 'vm211',
name: "VM12",
size: 1284
},
{
id: 'server21unallocated',
name: "unallocated",
size: 512,
color: '#000'
}
]
}, {
name: "server2",
id: 'server22',
children: [{
id: 'VMM221',
name: "VM21",
size: 256
}, {
name: "VM22",
id: 'server222',
size: 2048
},
{
id: 'server22unallocated',
name: "unallocated",
size: 4000,
color: '#000'
}
]
}
]
}]
};
$scope.charts.cpu.children = cpu_children
$scope.charts.ram.children = ram_children
}
//extract cpu from bytypes
$scope.$watch(() => xoApi.all, function () {
console.log(' XOAPI .ALL CHANGHE')
$timeout(function () { // all semmes to be unpopulated for now
populateChartsData()
}, 0)
},
true)
/*
Object.defineProperties($scope, {
xo: { get: -> xoApi.byTypes.xo?[0] },
pools: { get: -> xoApi.byTypes.pool },
hosts: { get: -> xoApi.byTypes.host },
VMs: { get: -> xoApi.byTypes.VM },
})*/
})
// A module exports its name.
.name
;
// A module exports its name.
.name
+198
View File
@@ -0,0 +1,198 @@
'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
+8
View File
@@ -0,0 +1,8 @@
{
"private": true,
"browserify": {
"transform": [
"babelify"
]
}
}