blob: 0def99c919542d8e816411d802293159e8fa7a6e [file] [log] [blame]
// Copyright 2016 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 AndroidStateGraph class to generate d3 visualization of the
* wifi state machine network detailed and supplicant states.
* TODO: incorporate WifiStateMachine states.
* TODO: incorporate wifi network scan events.
*/
var androidStateGraph = {};
/**
* This method is responsible for creating the d3 elements needed for the
* WifiStateMachine and supplicant state graphs. The visualization is built
* with the d3 library.
*
* @param {Node} node element that will the parent of the visualization in the
* resulting document.
* @param {WifiStateMachine} wsm instance containing the log state information.
*/
androidStateGraph.createGraph = function(node, wsm) {
if (wsm.networkDetailedStates.length == 0 &&
wsm.supplicantStates.length == 0) {
// there is nothing to graph, return.
return;
}
var lineData = [wsm.networkDetailedStates];
var color = d3.scale.category10();
/**
* WiFiState Machine State Strings
*
* WifiStateMachine state ordering is determined by a combination of
* steps needed to create a connection and also to highlight a disconnect.
* The tracking visualization moves up the y-axis as the connection
* progresses. When a disconnect or failure occurs, the line drops to the
* bottom of the y-axis.
*
* @const
*/
var WSM_NETWORK_DETAILED_STATES = [
'Failed',
'Disconnected',
'Disconnecting',
'Suspended',
'Idle',
'Scanning',
'Connecting',
'Authenticating',
'ObtainingIP',
'Connected',
'CptPrtCheck',
'Blocked',
'VerifyPoorLink'
];
/**
* Mapping for WifiStateMachine State Strings to y-axis graph value.
*
* @const
*/
var WSM_STATE_STRING_TO_GRAPH_VALUE = {
'FAILED': 0,
'DISCONNECTED': 1,
'DISCONNECTING': 2,
'SUSPENDED': 3,
'IDLE': 4,
'SCANNING': 5,
'CONNECTING': 6,
'AUTHENTICATING': 7,
'OBTAINING_IPADDR': 8,
'CONNECTED': 9,
'CAPTIVE_PORTAL_CHECK': 10,
'BLOCKED': 11,
'VERIFYING_POOR_LINK': 12
};
/**
* Supplicant State Strings
*
* @const
*/
var SUPPLICANT_STATES = [
'Disconnected',
'IntfDisabled',
'Inactive',
'Scanning',
'Authenticating',
'Associating',
'Associated',
'4WayHandsh',
'GroupHandsh',
'Completed'
];
/**
* Mapping for Supplicant State Strings to y-axis graph value.
*
* @const
*/
var SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE = {
'DISCONNECTED': 0,
'INTERFACE_DISABLED': 1,
'INACTIVE': 2,
'SCANNING': 3,
'AUTHENTICATING': 4,
'ASSOCIATING': 5,
'ASSOCIATED': 6,
'4WAY_HANDSHAKE': 7,
'GROUP_HANDSHAKE': 8,
'COMPLETED': 9
};
/**
* Visualization dom element Label.
*
* @const
*/
var LABEL = 'wsm_viz';
/*
* Dimensions for the entire graphing area
*
* @const
*/
var WIDTH_PX = 1000;
// @const
var RIGHT_MARGIN_PX = 20;
// @const
var LEFT_MARGIN_PX = 80;
// @const
var HEIGHT_PX = 800;
// @const
var TOP_MARGIN_PX = 40;
// @const
var BOTTOM_MARGIN_PX = 40;
/*
* Stroke width and spacing for plots, text and labels.
*
* @const
*/
var STROKE_WIDTH_PX = 2;
// @const
var VERTICAL_SPACE_PX = 6;
// @const
var HORIZONTAL_SPACE_PX = 4;
/*
* General structure for the visualization layout.
*
*‚ÄČ+-------------------------------------------------------+ -
* | | TOP_MARGIN_PX
* | +------------------------------------------------+ | -
* | | Zoomed State Graph | |
* | | | |
* | | | |
* | | | |
* | | | |
* | | | |
* | +------------------------------------------------+ |
* | |
* | +------------------------------------------------+ |
* | | Full State Graph | |
* | +------------------------------------------------+ |
* | |
* | +------------------------------------------------+ |
* | | Supplicant Graph | |
* | | | |
* | | | |
* | | | |
* | +------------------------------------------------+ | -
* | | BOTTOM_MARGIN_PX
* +-------------------------------------------------------+ -
* | | | |
* LEFT_MARGIN_PX RIGHT_MARGIN_PX
*
*/
var svgDomNode = d3.select(node).append('svg:svg')
.attr('id', LABEL)
.attr('width', WIDTH_PX)
.attr('height', HEIGHT_PX);
/*
* Define heights of subgraphs
*
* @const
*/
var ZOOMED_STATE_GRAPH_HEIGHT_PX = 450;
// @const
var FULL_STATE_GRAPH_HEIGHT_PX = 40;
// @const
var SUPPLICANT_GRAPH_HEIGHT_PX = 180;
/*
* Define tick size for graphs.
*
* @const
*/
var AXIS_TICK_SIZE_PX = 5;
// Define margins for individual graphs
var zoomedStateGraphMargin = {
top: TOP_MARGIN_PX,
right: RIGHT_MARGIN_PX,
bottom: BOTTOM_MARGIN_PX,
left: LEFT_MARGIN_PX
};
var fullStateGraphMargin = {
top: ZOOMED_STATE_GRAPH_HEIGHT_PX,
right: RIGHT_MARGIN_PX,
bottom: BOTTOM_MARGIN_PX,
left: LEFT_MARGIN_PX
};
var supplicantGraphMargin = {
top: HEIGHT_PX - BOTTOM_MARGIN_PX - SUPPLICANT_GRAPH_HEIGHT_PX,
right: RIGHT_MARGIN_PX,
bottom: BOTTOM_MARGIN_PX,
left: LEFT_MARGIN_PX
};
// Define x- and y-axis values and lines for WifiStateMachine zoomedStateGraph
var zoomedStateGraphXAxis = d3.scale.linear()
.domain([0, wsm.endTime - wsm.startTime])
.range([0, WIDTH_PX - zoomedStateGraphMargin.right -
zoomedStateGraphMargin.left]);
var zoomedStateGraphYAxis = d3.scale.linear()
.domain([0, Object.keys(WSM_STATE_STRING_TO_GRAPH_VALUE).length - 1])
.range([ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.bottom -
zoomedStateGraphMargin.top, zoomedStateGraphMargin.top]);
// Define x- and y-axis values and lines for WifiStateMachine fullStateGraph
var fullStateGraphXAxis = d3.scale.linear()
.domain(zoomedStateGraphXAxis.domain())
.range([0,
WIDTH_PX - fullStateGraphMargin.right -
fullStateGraphMargin.left]);
var fullStateGraphYAxis = d3.scale.linear()
.domain([0, Object.keys(WSM_STATE_STRING_TO_GRAPH_VALUE).length - 1])
.range([FULL_STATE_GRAPH_HEIGHT_PX, 0]);
// define x- and y-axis values and lines for supplicantStateGraph
var supplicantGraphXAxis = d3.scale.linear()
.domain(zoomedStateGraphXAxis.domain())
.range([0,
WIDTH_PX - supplicantGraphMargin.right -
supplicantGraphMargin.left]);
var supplicantGraphYAxis = d3.scale.linear()
.domain([0,
Object.keys(SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE).length - 1])
.range([SUPPLICANT_GRAPH_HEIGHT_PX, 0]);
/*
* Axis are generated using d3.svg.axis functions. These functions create the
* svg elemts to draw the axis, ticks and labels on the screen. The axis
* details are defined in the following objects. The axis are not appended to
* the graph until a selection is made. At that point, the selection will
* call the appropriate axis function to generate and append the axis svg
* elements to the DOM. This function is described below.
*
* Example:
* zoomedStateGraph.append('g')
* .attr('class', 'x axis')
* .attr('transform', 'translate(0,' +
* (ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.bottom -
* zoomedStateGraphMargin.top) + ')')
* .call(createStateGraphZoomedXAxis);
*
* zoomedStateGraph is the svg element in the DOM - where the axis elements
* will be added.
* We append a group element ('g') to the zoomedStateGraph. This group
* element allows us to contain/group other elements and apply
* transformations (used to position groups of svg elements by adapting
* x- and y-coordinates). This group does not result in a visible element
* in the page.
* Attributes for the new group element are added (class name and transform).
* The function call() is then called on the new group element. The call()
* function takes the incoming selection (new group element) and hands it
* to a function (createStateGraphZoomedXAxis) to create and append the
* axis svg elements (ticks and labels).
*
* Note: The specification for the axis could be directly entered in the call
* function without creating the createStateGraphZoomedXAxis, but this would
* introduce duplicate x-axis specifications for the default case
* (zoomedStateGraph displays the same data as fullStateGraph) and when a
* portion of the fullStateGraph is selected and onBrushed() is called to
* display a subset of the data in the zoomedStateGraph.
*/
var createStateGraphZoomedXAxis = d3.svg.axis()
.scale(zoomedStateGraphXAxis)
.tickSize(AXIS_TICK_SIZE_PX)
.tickSubdivide(true)
.tickFormat(getXTickLabels);
var createStateGraphZoomedYAxis = d3.svg.axis()
.scale(zoomedStateGraphYAxis)
.tickSize(AXIS_TICK_SIZE_PX)
.innerTickSize(-WIDTH_PX + zoomedStateGraphMargin.left +
zoomedStateGraphMargin.right)
.ticks(Object.keys(WSM_STATE_STRING_TO_GRAPH_VALUE).length)
.orient('left')
.tickSubdivide(true)
.tickFormat(function(stateIndex) {
return WSM_NETWORK_DETAILED_STATES[stateIndex];
});
var createFullStateGraphXAxis = d3.svg.axis()
.scale(fullStateGraphXAxis)
.orient('bottom')
.tickFormat(getXTickLabels);
var createSupplicantGraphYAxis = d3.svg.axis()
.scale(supplicantGraphYAxis)
.tickSize(AXIS_TICK_SIZE_PX)
.innerTickSize(-WIDTH_PX + zoomedStateGraphMargin.left +
zoomedStateGraphMargin.right)
.ticks(Object.keys(SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE).length)
.orient('left')
.tickSubdivide(true)
.tickFormat(function(stateIndex) {
return SUPPLICANT_STATES[stateIndex];
});
var createSupplicantXAxis = d3.svg.axis()
.scale(supplicantGraphXAxis)
.tickSize(AXIS_TICK_SIZE_PX)
.tickSubdivide(true)
.tickFormat(getXTickLabels);
var brush = d3.svg.brush()
.x(fullStateGraphXAxis)
.on('brush', onBrushed);
var createZoomedStateGraphDataPoint = d3.svg.line()
.interpolate('linear')
.x(function(wsmState) {
return zoomedStateGraphXAxis(wsmState.x);
})
.y(function(wsmState) {
return zoomedStateGraphYAxis(
WSM_STATE_STRING_TO_GRAPH_VALUE[wsmState.y]);
});
var createFullStateGraphDataPoint = d3.svg.line()
.interpolate('linear')
.x(function(wsmState) {
return fullStateGraphXAxis(wsmState.x);
})
.y(function(wsmState) {
return fullStateGraphYAxis(WSM_STATE_STRING_TO_GRAPH_VALUE[wsmState.y]);
});
var createSupplicantZoomedDataPoint = d3.svg.line()
.interpolate('linear')
.x(function(supplicantState) {
return supplicantGraphXAxis(supplicantState.x);
})
.y(function(supplicantState) {
return supplicantGraphYAxis(
SUPPLICANT_STATE_STRING_TO_GRAPH_VALUE[supplicantState.y]);
});
svgDomNode.selectAll('legend')
.data(lineData)
.enter()
.append('text')
.attr('class', 'legend')
.attr('x', function(wsm, wsmIndex) {
return wsmIndex * (HORIZONTAL_SPACE_PX * zoomedStateGraphMargin.right);
})
.attr('y', (ZOOMED_STATE_GRAPH_HEIGHT_PX - VERTICAL_SPACE_PX))
.attr('fill', function(wsm, wsmIndex) {
return wsm.color = color(wsmIndex);
})
.text(function(wsm, wsmIndex) {
return 'WifiNetworkDetailed ' + wsmIndex + ' states';
});
svgDomNode.append('svg:g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + zoomedStateGraphMargin.left + ',0)')
.call(createStateGraphZoomedYAxis);
svgDomNode.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width',
WIDTH_PX - zoomedStateGraphMargin.left -
zoomedStateGraphMargin.right)
.attr('height',
ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.top -
zoomedStateGraphMargin.bottom);
var zoomedStateGraph = svgDomNode.append('g')
.attr('class', 'zoomedStateGraph')
.attr('transform', 'translate(' + zoomedStateGraphMargin.left + ',0)');
var fullStateGraph = svgDomNode.append('g')
.attr('class', 'fullStateGraph')
.attr('transform',
'translate(' + fullStateGraphMargin.left + ',' +
fullStateGraphMargin.top + ')');
var supplicant = svgDomNode.append('g')
.attr('transform',
'translate(' + supplicantGraphMargin.left + ',' +
supplicantGraphMargin.top + ')');
/*
* Append graph data points, axis and brush elements to the graphs.
*/
zoomedStateGraph.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' +
(ZOOMED_STATE_GRAPH_HEIGHT_PX - zoomedStateGraphMargin.bottom -
zoomedStateGraphMargin.top) + ')')
.call(createStateGraphZoomedXAxis);
zoomedStateGraph.selectAll('data')
.data(lineData)
.enter()
.append('path')
.attr('stroke', function(wsmLine, wsmId) {
return wsmLine.color = color(wsmId);
})
.attr('stroke-width', STROKE_WIDTH_PX)
.attr('fill', 'none')
.attr('class', function(wsmLine, wsmId) {
return 'area_' + wsmId + ' data';
})
.attr('clip-path', 'url(#clip)')
.attr('d', createZoomedStateGraphDataPoint);
fullStateGraph.selectAll('path')
.data(lineData)
.enter()
.append('path')
.attr('class', function(wsmLine, wsmId) {
return 'line_' + wsmId;
})
.attr('d', createFullStateGraphDataPoint)
.attr('stroke', function(wsmLine, wsmId) {
return wsmLine.color = color(wsmId);
})
.attr('stroke-width', STROKE_WIDTH_PX)
.attr('fill', 'none');
fullStateGraph.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + FULL_STATE_GRAPH_HEIGHT_PX + ')')
.call(createFullStateGraphXAxis);
fullStateGraph.append('g')
.attr('class', 'x brush')
.call(brush)
.selectAll('rect')
.attr('y', -VERTICAL_SPACE_PX)
.attr('height',
FULL_STATE_GRAPH_HEIGHT_PX + VERTICAL_SPACE_PX);
supplicant.append('g')
.attr('class', 'x axis')
.attr('transform',
'translate(0,' + SUPPLICANT_GRAPH_HEIGHT_PX + ')')
.call(createSupplicantXAxis);
supplicant.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(0,0)')
.call(createSupplicantGraphYAxis);
supplicant.append('text')
.attr('x', -supplicantGraphMargin.left)
.attr('y',
SUPPLICANT_GRAPH_HEIGHT_PX + supplicantGraphMargin.bottom -
VERTICAL_SPACE_PX)
.attr('class', 'supplicant')
.text('wpa_supplicant state');
supplicant.append('path')
.attr('fill', 'none')
.attr('class', 'supp_line')
.attr('clip-path', 'url(#clip)')
.attr('d', createSupplicantZoomedDataPoint(wsm.supplicantStates));
function onBrushed() {
zoomedStateGraphXAxis.domain(
brush.empty() ? fullStateGraphXAxis.domain() : brush.extent());
supplicantGraphXAxis.domain(
brush.empty() ? fullStateGraphXAxis.domain() : brush.extent());
zoomedStateGraph.selectAll('path.data')
.attr('d', createZoomedStateGraphDataPoint);
supplicant.select('path.supp_line')
.attr('d', createSupplicantZoomedDataPoint(wsm.supplicantStates));
zoomedStateGraph.select('.x.axis').call(createStateGraphZoomedXAxis);
supplicant.select('.x.axis').call(createSupplicantXAxis);
}
function getXTickLabels(elapsedMS) {
var format = d3.time.format('%H:%M:%S');
var date = new Date(wsm.startTime + elapsedMS);
return format(date);
}
}