| // 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); |
| } |
| } |