dstat_graph/js/dashboard.js
Ian Wienand 358c6a7f7f Fix refresh for embedded csv
When we don't have files, refresh from the embedded CSV's

Change-Id: I6fef7426b2feda2cf8505c261eecdcc6bd6c9285
2021-12-07 11:12:17 +11:00

607 lines
17 KiB
JavaScript

/*
* Global variables
*/
var gGraphs = {};
var gCSVs = [];
var gFiles = [];
var brush = d3.svg.brush()
.on("brushend", brushed);
var margin = {top: 10, right: 10, bottom: 30, left: 40},
height = 70 - margin.top,
width = 600 - margin.right - margin.left;
var x = d3.time.scale().range([0, width]),
y = d3.scale.linear().range([height, 0]);
/*
* DOM functions
*/
$(document).on('dragenter', function (e) {
e.stopPropagation();
e.preventDefault();
$('#drop').css('border', '4px solid #fff')
});
$(document).on('dragover', function (e) {
e.stopPropagation();
e.preventDefault();
$('#drop').css('border', '4px solid red')
});
$(document).on('drop', function (e) {
e.stopPropagation();
e.preventDefault();
processFiles(e.originalEvent.target.files || e.originalEvent.dataTransfer.files);
$('#drop-background').hide();
});
/*
* Init functions
*/
$(document).ready(function() {
// initialize graphs contained in gCSVs
if (gCSVs !== undefined && gCSVs.length > 0) {
$('#drop-background').hide();
for (let i = 0; i < gCSVs.length; ++i) {
processCSV(gCSVs[i], "csv " + i)
}
}
})
/*
* Settings functions
*/
var settings = {
"interface": "standard",
"xaxis": 'time',
"granularity": undefined, // set while browsing the first file
"agg_function": "avg"
}
/*
* CSV Processing functions
*/
function processFiles(files) {
if ((files.length + gFiles.length) > 1) { // switch to sequential if multiple files are present
settings.xaxis = 'sequential';
}
for (i = 0; i < files.length; i++) {
gFiles.push(files[i]);
processFile(files[i]);
}
}
function processFile(file) {
var reader = new FileReader();
var name = file.name;
reader.onload = function(e) {
text = e.target.result;
processCSV(text, name);
}
reader.readAsText(file);
}
/*
* return dstat header end line
* TODO: Improve the detection mechanism
*/
function findHeaderEnd(lines) {
host = "";
dataIn = -1
// Let's browse only 20 lines, if there is more, it could mean it's not a valid file
for (var i = 0; i < 20; i++) {
line = lines[i].replace(/"/g, '').split(',');
if (line[0] == "Host:") {
host = line[1]
}
if (lines[i].length > 0 && lines[i][0] != '"') {
dataIn = i;
break;
}
}
// Not a valid file
if (dataIn == -1) {
return null;
}
return { "host" : host,
"groups" : lines[dataIn - 2].replace(/"/g, '').split(','),
"headers" : lines[dataIn - 1].replace(/"/g, '').split(','),
"nlines" : lines.length - dataIn,
"dataIn" : dataIn
};
}
/*
* Entry point for parsing CSV
* CSV file is read and dispatch line by line in a 2D array
* Then we browse these 2D array to create the required d3 structure
*
*/
function processCSV(csv, filename) {
lines = csv.split('\n');
l_env = findHeaderEnd(lines);
if (l_env == null) {
alert('Non valid CSV file: ' + filename + '. Failed at parsing header')
return;
}
host = l_env.host;
groups = l_env.groups;
headers = l_env.headers;
nlines = l_env.nlines;
graphs = [];
map = [];
gindex = -1;
/* Browse headers */
for (var i = 0, j = 0; i < headers.length; i++, j++) {
if (groups[i] != "") {
last_group = groups[i];
j = 0;
graphs.push({name: last_group, d: []});
gindex++;
}
graphs[gindex].d.push({key: headers[i], values: []});
map[i] = {group: gindex, index: j, name: headers[i]};
}
/* try to increase granularity to reduce memory consumption if required */
// TODO be smarter here, save different configuration per file?
if (settings.granularity === undefined) {
if (lines.length > 7200) {
settings.granularity = 15
} else if (lines.length > 3600) {
settings.granularity = 5
} else {
settings.granularity = 1
}
}
// First, let's merge headers and groups in a single object
xValues = getValues(graphs, 'system', 'time');
/* Use time for XAxis */
if (xValues !== null && settings.xaxis == "time") {
graphs.xAxis = function (xa) {
xa.axisLabel('Time').tickFormat(function(d) {
if (typeof d === 'string') {
return d;
}
return d3.time.format('%Hh %Mm %Ss')(new Date(d));
})
};
for (var lindex = l_env.dataIn, iindex = 0; lindex < lines.length; lindex++, iindex++) {
line = lines[lindex].replace(/"/g, '').split(',');
for (var cindex = 0; cindex < line.length; cindex++) {
lmap = map[cindex];
if (lmap != null && lmap.name === 'time' && line[cindex].length > 0) {
xValues.push(Date.parse(line[cindex].replace(/(\d+)-(\d+)\s+(\d+):(\d+):(\d+)/, '1942/$2/$1 $3:$4:$5')));
break;
}
}
} /* Use sequence for xAxis */
} else {
xValues = d3.range(nlines);
graphs.xAxis = function (xa) {
xa.axisLabel('').tickFormat(function(d) {
return d3.format('d')(d);
})
};
}
/* Then, populate the graphs object with the CSV values, aggregate data while browsing */
tmp_values = {}
for (var lindex = l_env.dataIn, iindex = 0; lindex < lines.length; lindex++, iindex++) {
line = lines[lindex].replace(/"/g, '').split(',');
for (var cindex = 0; cindex < line.length; cindex++) {
lmap = map[cindex];
if (lmap != null && lmap.name != 'time') {
nVal = parseFloat(line[cindex]);
/* handle non numerical line */
if (isNaN(nVal)) {
val = line[cindex]
graphs[lmap.group].yformat = function(_) {return _};
}
else {
val = nVal
graphs[lmap.group].yformat = d3.format('<-,02f');
}
if (tmp_values[lmap.group] == undefined) {
tmp_values[lmap.group] = []
}
if (tmp_values[lmap.group][lmap.index] == undefined) {
tmp_values[lmap.group][lmap.index] = []
}
tmp_values[lmap.group][lmap.index].push(val)
if (!((iindex + 1) % settings.granularity)) { // add a new entry in the graph
if (settings.agg_function == "avg") {
graphs[lmap.group].d[lmap.index].values.push({y: tmp_values[lmap.group][lmap.index].average(), x: xValues[iindex]});
} if (settings.agg_function == "max") {
graphs[lmap.group].d[lmap.index].values.push({y: Math.max(...tmp_values[lmap.group][lmap.index]), x: xValues[iindex]});
}
tmp_values = {}
}
}
}
}
/* Apply specificities for each data headers */
for (var i in graphs) {
name = graphs[i].name;
name = name.replace(/[&\/\\#,+()$~%.'":*?<>{}\s]/g,'_');
sfname = name + "_data";
ofname = name + "_options";
gdfunction = undefined;
options = {};
if (typeof window[sfname] == "function") {
gdfunction = window[sfname];
}
if (typeof window[ofname] == "function") {
options = window[ofname]();
}
for (var j in graphs[i].d) {
if (gdfunction !== undefined) {
graphs[i].d[j].values = gdfunction(graphs[i].d[j].values);
}
if (options !== undefined) {
if (options.area === true) {
graphs[i].d[j].area = true;
}
}
}
}
/* Create the brush */
dmin = graphs[1].d[1].values[0].x;
dmax = graphs[1].d[0].values[graphs[1].d[0].values.length -1].x;
// If there is many point, let's start by focusing on the last ones to reduce loading time
if ((lines.length / settings.granularity) > 500) {
dmin = graphs[1].d[0].values[graphs[1].d[0].values.length - 500].x;
}
displayFocusGraph(graphs, dmin, dmax);
/* create & display the graphs */
for (var gindex = 0; gindex < graphs.length; gindex++) {
if (graphs[gindex].name != "system" && graphs[gindex].d[0].key != "time") {
graphName = graphs[gindex].name;
graphData = graphs[gindex].d;
graphFormat = graphs[gindex].yformat;
panel = createPanel(graphName, graphData, filename)
displayGraph(graphName, graphData, graphFormat, panel, dmin, dmax, filename);
}
}
}
/*
* Create or use an already existing panel to display the graph
*/
function createPanel(graphName, graphData, filename) {
id = graphName.replace(/[&\/\\#,+()$~%.'":*?<>{}\s]/g,'_');
data = reduceData(graphData)
div = d3.select('#' + id);
if (! settings.compact) {
if (div.empty()) {
div = d3.select('#dashboard').append('li').attr('class', ' list-group-item').attr('id', id);
header = div.append('h3').text(graphName);
}
elt = div.append('p').text(filename)
elt.append('svg').datum(reduceData(graphData));
} else if (settings.compact) {
if (div.empty()) {
div = d3.select('#dashboard').append('div').attr('class', 'list-group-item-compact').attr('id', id);
header = d3.select('#dashboard').append('div').attr('class', 'panel-left')
info = d3.select('#dashboard').append('div').attr('class', 'panel-left')
d3.select('#dashboard').append('div').attr('class', 'panel-clear')
header.append('p').attr('class', 'panel-left').text(graphName)
colors = nv.utils.getColor()
for (var i in graphData) {
header.append('p').attr('class', 'panel-left').text(graphData[i].key).style({color: colors(i), 'margin-left': '15px'});
}
}
elt = div.append('div').attr('class', 'row list-body');
elt.append('svg').datum(data);
}
return div;
}
/*
* Create generic graph options
*/
function createInitialOptions(graphData) {
options = {}
return options;
}
/*
* Create the graph d3 object
*/
function displayGraph(graphName, graphData, graphFormat, panel, dmin, dmax, filename) {
panel.selectAll('svg').each(function() {
var elt = d3.select(this);
nv.addGraph(function() {
var graphId = graphName.replace(/[&\/\\#,+()$~%.'":*?<>{}\s]/g,'_');
var graphFu = graphId + '_graph';
var graphFuPre = graphId + '_options';
var options = createInitialOptions(graphData);
var chart = null;
if (typeof window[graphFuPre] == "function") {
options = window[graphFuPre]();
}
if (options.type == 'stacked') {
chart = nv.models.stackedAreaChart()
.margin({left: 100})
.useInteractiveGuideline(! settings.compact)
.showLegend(! settings.compact)
.style('expand')
.interpolate("basis")
.showControls(false)
.showXAxis(! settings.compact)
.showYAxis(true)
;
} else if (options.type == 'pie') {
chart = nv.models.bulletChart()
.margin({left: 100})
;
} else {
chart = nv.models.lineChart()
.margin({left: 100})
.useInteractiveGuideline(! settings.compact)
.interpolate("basis")
.showLegend(! settings.compact)
.showXAxis(! settings.compact)
.showYAxis(true)
;
}
graphs.xAxis(chart.xAxis);
if (typeof window[graphFu] == "function") {
window[graphFu](chart);
}
var pb = d3.select(elt[0][0].parentNode.parentNode).select('.clickable').on("click", function() {
pb = d3.select(this.parentNode.parentNode.parentNode).selectAll('.list-body');
isHidden = pb.style('display') == 'none';
pb.style('display', isHidden ? 'inherit' : 'none');
chart.update()
});
elt.call(chart);
nv.utils.windowResize(chart.update);
return chart;
}, function(chart) {
if (gGraphs[graphName] == undefined) gGraphs[graphName] = {};
gGraphs[graphName][filename] = {elt: elt, chart: chart, data: graphData, filename: filename};
});
});
}
function getValues(graphs, group, header) {
for (var i in graphs) {
if (graphs[i].name == group) {
for (var j in graphs[i].d) {
if (graphs[i].d[j].key == header) {
return graphs[i].d[j].values;
}
}
}
}
return null;
}
function getExists(graphs, group, header) {
for (var i in graphs) {
if (graphs[i].name == group) {
for (var j in graphs[i].d) {
if (graphs[i].d[j].key == header) {
return true;
}
}
}
}
return false;
}
/*
* Create the focus graph
* By default, use the oposite of idle cpu time
* If not found in the CSV, take the first element
*/
function displayFocusGraph(graphs, dmin, dmax) {
if ($('#focus').children().length > 0) {
return;
}
/* original dstat has "total cpu usage" with fields "usr", "idl" ...
* pcp dstat has "total usage" with fields "total usage:usr", "total usage:sys" ...
*/
if (getExists(graphs, "total usage", "total usage:idl")) {
data = getValues(graphs, "total usage", "total usage:idl")
} else if (getExists(graphs, "total cpu usage", "idl")) {
data = getValues(graphs, "total cpu usage", "idl")
} else {
alert("Failed to parse: idle CPU stats not found")
}
if (data) { // Rollback to the first element if not found
data = data.map(function(idl) { return {x: idl.x, y: (100 - parseFloat(idl.y)) };});
}
else {
data = graphs[0].d[0].values;
data = data.map(function(idl) { return {x: idl.x, y: idl.y };});
}
x.domain(d3.extent(data.map(function(val) { return val.x })));
y.domain([0, d3.max(data.map(function(val) { return val.y }))]);
brush.x(x).extent([dmin, dmax]);
var area = d3.svg.area()
.interpolate("basis")
.x(function(d) { return x(d.x) })
.y1(function(d) { return y(d.y) })
.y0(height);
var svg = d3.select('#focus').append('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var xAxis = d3.svg.axis().scale(x).orient("bottom");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
context.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
context.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis.ticks(5));
context.append("g")
.attr("class", "x brush")
.call(brush)
.selectAll("rect")
.attr("y", -6)
.attr("height", height + 7);
brushed();
}
function reduceData(data) {
var ext = (brush.empty() || brush.extent() == null ? x.domain() : brush.extent());
var extD = [ext[0] instanceof String ? Date.parse(ext[0]) : ext[0],
ext[1] instanceof String ? Date.parse(ext[1]) : ext[1]];
var ndata = data.map(function(d, i) {
return {
key: d.key,
area: d.area,
values: d.values.filter(function(d, i) {
return d.x > extD[0] && d.x < extD[1];
})
}});
return ndata;
}
function brushed() {
var ext = (brush.empty() || brush.extent() == null ? x.domain() : brush.extent());
for (var name in gGraphs) {
if (gGraphs.hasOwnProperty(name)) {
for (gIndex in gGraphs[name]) {
var graph = gGraphs[name][gIndex];
var chart = graph.chart;
var elt = graph.elt;
var data = graph.data;
var ndata = reduceData(data);
chart.xDomain(ext);
elt.call(chart.xAxis);
elt.datum(ndata);
elt.call(chart.update);
}
}
}
}
function change_xaxis(type) {
settings.xaxis = type;
refresh();
}
function change_interface(type) {
settings.compact = (type == 'compact');
refresh();
}
function change_granularity(granularity, aggr_function) {
settings.granularity = granularity
settings.agg_function = aggr_function
refresh();
}
/*
* Destroy all graphs & the focus bars
* and recreate them using the content of
* gFiles (all uploaded files)
*/
function refresh() {
d3.select('#dashboard').html("");
d3.select('#focus').html("");
if (gFiles.length > 0) {
for (let i = 0; i < gFiles.length; ++i) {
processFile(gFiles[i]);
}
} else {
// If we don't have any files, refresh from the
// embedded CSV's directly
for (let i = 0; i < gCSVs.length; ++i) {
processCSV(gCSVs[i], "csv " + i)
}
}
}
/*
* Toggle header menu
*/
function toggle_menu() {
if ($('#menu').is(":hidden")) {
$('#menu').slideDown('slow');
$('#dashboard').animate({ "padding-top": "+=50px" }, 'slow');
} else {
$('#menu').slideUp('slow');
$('#dashboard').animate({ "padding-top": "-=50px" }, 'slow');
}
}
/*
* Type extension
*/
Array.prototype.sum = Array.prototype.sum || function() {
return this.reduce(function(sum, a) { return sum + Number(a) }, 0);
}
Array.prototype.average = Array.prototype.average || function() {
return this.sum() / (this.length || 1);
}