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