blob: 0de14adf689bc2bc5bc41b4a9b23a0624ed278c7 [file] [log] [blame]
// Copyright 2015 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview StateGraph class to generate d3 visualization of the service
* states, wifi network scans and system suspend/resume events.
*/
var stateGraph = {};
/**
* This method is responsible for creating the d3 elements needed for the
* service, scan and system suspend visualization. The visualization is built
* with the d3 library.
*
* @param {Node} node element that will the parent of the visualization in the
* resulting document.
* @param {Manager} manager instance containing the log state information.
*/
stateGraph.createGraph = function(node, manager) {
var lineData = [];
var scans = manager.scanDetails;
var suspends = manager.suspendDetails;
var darkSuspends = [];
for (var i = 0; i < manager.services.length; i++) {
if (manager.services[i].isActive) {
lineData.push(manager.services[i].graphData);
lineData[lineData.length - 1].serviceId = manager.services[i].serviceId;
}
}
if (lineData.length == 0)
return;
var color;
if (lineData.length < 10) {
color = d3.scale.category10();
} else if (lineData.length < 20) {
color = d3.scale.category20();
} else {
console.error('Too many active services to graph.');
return;
}
var states = [
'Failure',
'Idle',
'Associating',
'Configuring',
'Connected',
'Online',
'Portal'
];
var map = {};
map['Failure'] = 0;
map['Idle'] = 1;
map['Associating'] = 2;
map['Configuring'] = 3;
map['Connected'] = 4;
map['Online'] = 5;
map['Portal'] = 6;
var label = 'manager_viz_' + manager.id;
var svg = d3.select(node).append('svg:svg')
.attr('id', label)
.attr('width', 1000)
.attr('height', 500);
var width = 1000;
var focusHeight = 430;
var contextHeight = 40;
var focusMargin = {
top: 40,
right: 20,
bottom: 40,
left: 75
};
var contextMargin = {
top: 430,
right: 20,
bottom: 100,
left: 75
};
var focusX = d3.scale.linear()
.range([0, width - focusMargin.right - focusMargin.left])
.domain([0, manager.endTime - manager.startTime]);
var focusY = d3.scale.linear()
.range([focusHeight - focusMargin.bottom - focusMargin.top,
focusMargin.top])
.domain([0, 6]);
var contextX = d3.scale.linear()
.range([0, width - contextMargin.right - contextMargin.left])
.domain(focusX.domain());
var contextY = d3.scale.linear()
.range([contextHeight, 0])
.domain([0, 6]);
var xAxisFocus = d3.svg.axis()
.scale(focusX)
.tickSize(5)
.tickSubdivide(true)
.tickFormat(function(d) {
return getXTickLabels(d);
});
var yAxis = d3.svg.axis()
.scale(focusY)
.tickSize(5)
.innerTickSize(-width + focusMargin.left + focusMargin.right)
.ticks(6)
.orient('left')
.tickSubdivide(true)
.tickFormat(function(d) {
return states[d];
});
var xAxisContext = d3.svg.axis()
.scale(contextX)
.orient('bottom')
.tickFormat(function(d) {
return getXTickLabels(d);
});
var brush = d3.svg.brush()
.x(contextX)
.on('brush', brushed);
var focusArea = d3.svg.line()
.interpolate('linear')
.x(function(d) {
return focusX(d.x);
})
.y(function(d) {
return focusY(map[d.y]);
});
var contextArea = d3.svg.line()
.interpolate('linear')
.x(function(d) {
return contextX(d.x);
})
.y(function(d) {
return contextY(map[d.y]);
});
svg.selectAll('legend')
.data(lineData)
.enter()
.append('text')
.attr('class', 'legend')
.attr('x', function(d, i) {
return i * (4 * focusMargin.right);
})
.attr('y', (focusHeight - 14))
.attr('stroke', function(d, i) {
return d.color = color(i);
})
.text(function(d) {
return 'service ' + d.serviceId;
});
var haveScans = false;
var haveScansDefault = false;
var haveScansError = false;
var haveSuspends = false;
var haveSuspendsPartial = false;
var haveDarkSuspends = false;
if (scans.length > 0) {
for (var s = 0; s < scans.length; s++) {
var temp = scans[s];
if ((temp.start && temp.end) || (temp.start >= 0 && temp.end >= 0)) {
haveScans = true;
} else if (temp.end || temp.end == 0) {
haveScansDefault = true;
} else {
haveScansError = true;
}
}
}
if (suspends.length > 0) {
for (var s = 0; s < suspends.length; s++) {
if (suspends[s].start && suspends[s].end) {
haveSuspends = true;
} else {
haveSuspendsPartial = true;
}
if (suspends[s].darkSuspends != null) {
haveDarkSuspends = true;
Array.prototype.push.apply(darkSuspends, suspends[s].darkSuspends);
}
}
}
var colorLabels = [];
if (haveSuspends) {
colorLabels.push({label: 'complete', text: ['suspend']});
}
if (haveSuspendsPartial) {
colorLabels.push({label: 'partial', text: ['suspend', 'unbounded']});
}
if (haveDarkSuspends) {
colorLabels.push({label: 'dark', text: ['suspend', 'dark']});
}
if (haveScans) {
colorLabels.push({label: 'detailed', text: ['scan']});
}
if (haveScansDefault) {
colorLabels.push({label: 'default', text: ['scan', 'done']});
}
if (haveScansError) {
colorLabels.push({label: 'error', text: ['scan', 'error']});
}
if (colorLabels.length > 0) {
svg.selectAll('colorLegend')
.data(colorLabels)
.enter()
.append('rect')
.attr('width', 70)
.attr('height', 30)
.attr('x', function(d, i) {
return (width - (i + 1) * 75);
})
.attr('y', 2)
.attr('class', function(d) {
return d.label;
});
svg.selectAll('colorLabels')
.data(colorLabels)
.enter()
.append('text')
.attr('x', function(d, i) {
return (width - ((i + 1) * 75) + 2);
})
.attr('y', 18)
.text(function(d) {
return d.text[0];
});
svg.selectAll('colorLabels')
.data(colorLabels)
.enter()
.append('text')
.attr('x', function(d, i) {
return (width - ((i + 1) * 75) + 2);
})
.attr('y', 30)
.text(function(d) {
return d.text[1];
});
}
svg.append('svg:g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + focusMargin.left + ',0)')
.call(yAxis);
svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width - focusMargin.left - focusMargin.right)
.attr('height', focusHeight - focusMargin.top - focusMargin.bottom);
var focus = svg.append('g')
.attr('class', 'focus')
.attr('transform', 'translate(' + focusMargin.left + ',0)');
var context = svg.append('g')
.attr('class', 'context')
.attr('transform',
'translate(' + contextMargin.left + ',' + contextMargin.top + ')');
focus.append('g')
.attr('class', 'x axis')
.attr('transform',
'translate(0,' +
(focusHeight - focusMargin.bottom - focusMargin.top) + ')')
.call(xAxisFocus);
focus.selectAll('scanned')
.data(scans)
.enter()
.append('rect')
.attr('class', function(d, i) {
return getScanLabel(d, i);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('y', focusMargin.top)
.attr('width', function(d) {
return getWidth(d);
})
.attr('height', focusY(0) - focusY(6))
.attr('clip-path', 'url(#clip)');
context.selectAll('scan')
.data(scans)
.enter()
.append('rect')
.attr('d', contextArea)
.attr('x', function(d) {
return contextX(getX(d));
})
.attr('y', 0)
.attr('height', contextY(0) - contextY(6))
.attr('width', function(d) {
return getContextWidth(d);
})
.attr('class', function(d, i) {
return getScanLabel(d, i);
});
focus.selectAll('suspended')
.data(suspends)
.enter()
.append('rect')
.attr('class', function(d, i) {
return getSuspendLabel(d, i);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('y', focusMargin.top)
.attr('width', function(d) {
return getWidth(d);
})
.attr('height', focusY(0) - focusY(6))
.attr('clip-path', 'url(#clip)');
context.selectAll('suspend')
.data(suspends)
.enter()
.append('rect')
.attr('d', contextArea)
.attr('x', function(d) {
return contextX(getX(d));
})
.attr('y', 0)
.attr('height', contextY(0) - contextY(6))
.attr('width', function(d) {
return getContextWidth(d);
})
.attr('class', function(d, i) {
return getSuspendLabel(d, i);
});
focus.selectAll('darksuspended')
.data(darkSuspends)
.enter()
.append('rect')
.attr('class', function(d, i) {
return ('zoom_' + i + ' suspend dark');
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('y', focusMargin.top)
.attr('width', function(d) {
return getWidth(d);
})
.attr('height', focusY(0) - focusY(6))
.attr('clip-path', 'url(#clip)');
context.selectAll('darksuspend')
.data(darkSuspends)
.enter()
.append('rect')
.attr('d', contextArea)
.attr('x', function(d) {
return contextX(getX(d));
})
.attr('y', 0)
.attr('height', contextY(0) - contextY(6))
.attr('width', function(d) {
return getContextWidth(d);
})
.attr('class', function(d, i) {
return ('zoom_' + i + ' suspend dark');
});
focus.selectAll('data')
.data(lineData)
.enter()
.append('path')
.attr('stroke', function(d, i) {
return d.color = color(i);
})
.attr('stroke-width', 2)
.attr('fill', 'none')
.attr('class', function(d, i) {
return 'area_' + i + ' data';
})
.attr('clip-path', 'url(#clip)')
.attr('d', focusArea);
context.selectAll('path')
.data(lineData)
.enter()
.append('path')
.attr('class', function(d, i) {
return 'line_' + i;
})
.attr('d', contextArea)
.attr('stroke', function(d, i) {
return d.color = color(i);
})
.attr('stroke-width', 2)
.attr('fill', 'none');
context.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + contextHeight + ')')
.call(xAxisContext);
context.append('g')
.attr('class', 'x brush')
.call(brush)
.selectAll('rect')
.attr('y', -6)
.attr('height', contextHeight + 7);
function brushed() {
focusX.domain(brush.empty() ? contextX.domain() : brush.extent());
focus.selectAll('path.data').attr('d', focusArea);
focus.selectAll('rect.suspend')
.attr('width', function(d) {
return getWidth(d);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('class', function(d, i) {
return ('zoom_' + i + ' suspend dark');
});
focus.selectAll('rect.suspends')
.attr('width', function(d) {
return getWidth(d);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('class', function(d, i) {
return getSuspendLabel(d, i);
});
focus.selectAll('rect.scans')
.attr('width', function(d) {
return getWidth(d);
})
.attr('x', function(d) {
return focusX(getX(d));
})
.attr('class', function(d, i) {
return getScanLabel(d, i);
});
focus.select('.x.axis').call(xAxisFocus);
}
function getXTickLabels(d) {
var format = d3.time.format.utc('%H:%M:%S');
var date = new Date(manager.startTime + d + manager.timeOffset);
return format(date);
}
function getX(d) {
if (d.start || d.start == 0) {
return d.start;
}
else {
return d.end;
}
}
function getWidth(d) {
var width = 2;
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
width = focusX(d.end) - focusX(d.start);
}
if (width < 2) {
return 2;
}
return width;
}
function getContextWidth(d) {
var width = 2;
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
width = contextX(d.end) - contextX(d.start);
}
if (width < 2) {
return 2;
}
return width;
}
function getScanLabel(d, i) {
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
return ('zoom_' + i + ' scans detailed');
}
if (d.end || d.end == 0) {
return ('zoom_' + i + ' scans default');
}
return ('zoom_' + i + ' scans error');
}
function getSuspendLabel(d, i) {
if ((d.start && d.end) || (d.start >= 0 && d.end >= 0)) {
return ('zoom_' + i + ' suspends complete');
}
return ('zoom_' + i + ' suspends partial');
}
}