cockpit/pkg/kubernetes/scripts/charts.js

558 lines
24 KiB
JavaScript

/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
(function() {
var angular = require('angular');
require('angular-bootstrap-npm/dist/angular-bootstrap.js');
var d3 = require('d3');
var focusedClasses = {
"chart-focused": true,
"chart-unfocused": false
};
var unfocusedClasses = {
"chart-focused": false,
"chart-unfocused": true
};
var focusResetClasses = {
"chart-focused": false,
"chart-unfocused": false
};
function parsePXVal(val) {
var n;
if (val)
n = parseInt(val.slice(0, val.length - 2), 10);
if (!n || isNaN(n))
n = 0;
return n;
}
function getPadding(el) {
// May return null on FF when iframe is hidden.
var style = window.getComputedStyle(el, null);
if (style) {
return {
left: parsePXVal(style.getPropertyValue('padding-left')),
right: parsePXVal(style.getPropertyValue('padding-right')),
top: parsePXVal(style.getPropertyValue('padding-top')),
bottom: parsePXVal(style.getPropertyValue('padding-bottom'))
};
} else {
return {
left: 0,
right: 0,
top: 0,
bottom: 0
};
}
}
function getSize(el) {
var p = getPadding(el);
var width = el.clientWidth - p.left - p.right;
var height = el.clientHeight - p.top - p.bottom;
if (width < 0)
width = 0;
if (height < 0)
height = 0;
return {
width: width,
height: height
};
}
angular.module('ui.charts', [
'ui.bootstrap',
])
.directive('thresholdHeatMap', [
"$window",
"KubeTranslate",
"$timeout",
function ($window, translate, $timeout) {
const _ = translate.gettext;
return {
restrict: 'A',
scope: {
data: '=?',
legendLabels: '=?',
thresholds: '=?',
colors: '=?',
},
link: function($scope, element, attributes) {
var data;
var padding = 2;
var thresholdDefaults = [0, 0.7, 0.8, 0.9];
var heatmapColorDefaults = ['#bbbbbb', '#d4f0fa', '#F9D67A', '#EC7A08', '#CE0000'];
var legendLabelDefaults = [_("Unavailable"), '< 70%', '70-80%', '80-90%', '> 90%'];
var maxSize = attributes['maxBlockSize'];
if (!maxSize || isNaN(maxSize)) {
maxSize = 64;
} else {
maxSize = parseInt(maxSize, 10);
if (maxSize < 5)
maxSize = 5;
else if (maxSize > 64)
maxSize = 64;
}
if (!$scope.thresholds)
$scope.thresholds = thresholdDefaults;
if (!$scope.colors)
$scope.colors = heatmapColorDefaults;
if (!$scope.legendLabels)
$scope.legendLabels = legendLabelDefaults;
var svg = d3.select(element[0]).append("svg")
.classed("heatmap-pf-svg", true)
.style("width", "100%");
if (attributes['legend'])
buildLegend();
function getBlockSize(n, x, y) {
if (x < 1 || y < 1)
return 0;
if (n === 0)
return maxSize;
var px = Math.ceil(Math.sqrt(n * x / y));
var py = Math.ceil(Math.sqrt(n * y / x));
var sx, sy;
if (Math.floor(px * y / x) * px < n)
sx = y / Math.ceil(px * y / x);
else
sx = x / px;
if (Math.floor(py * x / y) * py < n)
sy = x / Math.ceil(x * py / y);
else
sy = y / py;
return Math.max(sx, sy);
}
function getSizeInfo() {
var length = data ? data.length : 0;
var size = getSize(element[0]);
var h = size.height;
var w = size.width;
var rows;
var blockSize = getBlockSize(length, w, h);
if ((blockSize - padding) > maxSize) {
blockSize = padding + maxSize;
// Attempt to square off the area, check if square fits
rows = Math.ceil(Math.sqrt(length));
if (blockSize * rows > w ||
blockSize * rows > h) {
rows = (blockSize === 0) ? 0 : Math.floor(h / blockSize);
}
} else {
rows = (blockSize === 0) ? 0 : Math.floor(h / blockSize);
}
return {
rows: rows,
block: blockSize
};
}
function buildLegend() {
var colors = $scope.colors.slice(0);
var labels = $scope.legendLabels.slice(0);
labels.reverse();
colors.reverse();
var legend = d3.select("#" + attributes['legend'])
.append('ul')
.classed('chart-legend', true);
var li = legend.selectAll("li").data(labels);
var newLi = li.enter().append("li")
.classed('chart-legend-item', true)
.on("mouseover", function(d, i) {
var color = colors[i];
var rsel = "rect[data-color='" + color + "']";
var lsel = "li[data-color='" + color + "']";
legend.selectAll("li").classed(unfocusedClasses);
legend.select(lsel).classed(focusedClasses);
svg.selectAll("rect").classed(unfocusedClasses);
svg.selectAll(rsel).classed(focusedClasses);
})
.on("mouseout", function (d, i) {
svg.selectAll("rect").classed(focusResetClasses);
legend.selectAll("li").classed(focusResetClasses);
});
newLi.append("span")
.classed('legend-pf-color-box', true);
newLi.append("span")
.classed('legend-pf-text', true);
li.attr("data-color", function (d, i) {
return colors[i];
});
li.select("span.legend-pf-color-box")
.style("background-color", function (d, i) {
return colors[i];
});
li.select("span.legend-pf-text")
.text(function (d) {
return d;
});
}
function getcolor(d, colorFunc) {
var value = d;
if (d && d.value !== undefined)
value = d.value;
if (isNaN(value))
value = -1;
return colorFunc(value);
}
function refresh() {
var colorFunc = d3.scale.threshold()
.domain($scope.thresholds)
.range($scope.colors);
var size = getSizeInfo();
if (!data)
data = [ ];
var fillSize = size.block - padding;
if (fillSize < 1)
return;
svg.attr("height", size.block * size.rows);
var blocks = svg.selectAll('rect').data(data);
blocks.enter().append('rect')
.on("mouseover", function(d, i) {
svg.selectAll('rect').classed(unfocusedClasses);
d3.select(this).classed(focusedClasses);
})
.on("mouseout", function (d, i) {
svg.selectAll('rect').classed(focusResetClasses);
})
.append("title");
blocks
.attr('x', function (d, i) {
return Math.floor(i / size.rows) * size.block;
})
.attr('y', function (d, i) {
return i % size.rows * size.block;
})
.attr('width', fillSize)
.attr('height', fillSize)
.attr('data-color', function (d) {
return getcolor(d, colorFunc);
})
.style('fill', function (d) {
return getcolor(d, colorFunc);
})
.on('click', function (d) {
if (d && d.name)
$scope.$emit("boxClick", d.name);
})
.select("title")
.text(function(d) {
return d.tooltip;
});
blocks.exit().remove();
}
$scope.$watchCollection('data', function(newValue) {
data = newValue;
refresh();
});
angular.element($window).bind('resize', function () {
refresh();
});
$scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
refresh();
}
);
}
};
}
])
.directive('donutPctChart', [
"$window",
function ($window) {
return {
restrict: 'A',
scope: {
data: '=?',
largeTitle: '=?',
smallTitle: '=?',
},
link: function($scope, element, attributes) {
var arc, selectedArc, data;
var colors = d3.scale.category20();
var pie = d3.layout.pie().value(function (d) {
if (typeof d === 'object')
return d.value;
else
return d;
});
var legend;
if (attributes['legend']) {
legend = d3.select("#" + attributes['legend'])
.append('ul')
.classed('chart-legend', true);
}
var svg = d3.select(element[0]).append("svg");
svg.style("width", "100%");
svg.style("height", "100%");
var g = svg.append("g");
g.append('text').attr('class', "chart-title");
function updateSize() {
var size = getSize(element[0]);
var width = size.width;
var height = size.height;
var c;
var radius = Math.min(width, height) / 2;
var barSize = parseInt(attributes['barSize'], 10);
if (isNaN(barSize))
barSize = 20;
if ((barSize * 2) > radius)
barSize = radius;
arc = d3.svg.arc()
.innerRadius(radius - (barSize * 2))
.outerRadius(radius - barSize);
selectedArc = d3.svg.arc()
.innerRadius(radius - (barSize * 2))
.outerRadius(radius - (barSize - 2));
c = (radius - barSize - 2) * 2;
g.attr('data-innersize', Math.sqrt((c * c) / 2));
updateTitle();
g.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
refresh();
}
function calFontSize() {
var width = this.getComputedTextLength();
var inner = parseInt(g.attr("data-innersize"), 10);
var style = window.getComputedStyle(this, null);
var size, ratio;
if (style && width) {
size = style.getPropertyValue('font-size');
ratio = inner / width;
} else {
width = 0;
}
if (!isNaN(inner) && width > inner)
return "calc(" + size + " * " + ratio + ")";
}
function updateTitle() {
var title = g.select('text.chart-title');
var size = parseInt(g.attr("data-innersize"), 10);
if (isNaN(size))
return;
title.selectAll('tspan').remove();
if ($scope.largeTitle) {
title.insert('tspan').text($scope.largeTitle)
.classed('donut-title-big-pf', true)
.attr('dy', 0)
.attr('x', 0)
.style("font-size", calFontSize);
}
if ($scope.smallTitle) {
title.insert('tspan').text($scope.smallTitle)
.classed('donut-title-small-pf', true)
.attr('dy', 20)
.attr('x', 0)
.style("font-size", calFontSize);
}
}
function select(id) {
var sel = "path[data-id='" + id + "']";
var lsel = "li[data-id='" + id + "']";
g.selectAll("path").classed(unfocusedClasses);
g.select(sel).attr("d", function (d, i) {
return selectedArc(d, i);
});
g.select(sel).classed(focusedClasses);
if (legend) {
legend.selectAll("li").classed(unfocusedClasses);
legend.select(lsel).classed(focusedClasses);
}
}
function unselect() {
g.selectAll("path")
.classed(focusResetClasses)
.attr("d", arc);
if (legend) {
legend.selectAll("li")
.classed(focusResetClasses);
}
}
function refreshLegend() {
var li = legend.selectAll("li").data(data);
var newLi = li.enter().append("li")
.classed('chart-legend-item', true)
.on("mouseover", function() {
select(this.getAttribute('data-id'));
})
.on("mouseout", unselect);
newLi.append("span").classed('legend-pf-color-box', true);
newLi.append("span").classed('legend-pf-text', true);
li.attr("data-id", function (d, i) {
return i;
});
li.select("span.legend-pf-color-box")
.style("background-color", function (d, i) {
if (d && d.color)
return d.color;
else
return colors(i);
});
li.select("span.legend-pf-text")
.text(function (d) {
if (d && d.label)
return d.label;
else
return d;
});
li.exit().remove();
}
function refresh() {
if (!data)
data = [];
var path = g.selectAll("path")
.data(pie(data));
path.enter().append("path")
.on("mouseover", function(i) {
select(this.getAttribute('data-id'));
})
.on("mouseout", unselect)
.append("title");
path.attr("fill", function(d, i) {
if (d.data && d.data.color)
return d.data.color;
else
return colors(i);
})
.attr("d", arc)
.attr("data-id", function (d, i) {
return i;
})
.select("title")
.text(function(d) {
if (d.data && d.data.tooltip)
return d.data.tooltip;
});
path.exit().remove();
if (legend)
refreshLegend();
}
$scope.$watchCollection('data', function(newValue) {
data = newValue;
refresh();
});
/* Watch the selection for changes */
$scope.$watchGroup(["largeTitle", "smallTitle"], function() {
updateTitle();
});
angular.element($window).bind('resize', function () {
updateSize();
});
$scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
updateSize();
}
);
}
};
}
]);
}());