Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 /* Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 * Use of this source code is governed by a BSD-style license that can be | 2 * Use of this source code is governed by a BSD-style license that can be |
| 3 * found in the LICENSE file. */ | 3 * found in the LICENSE file. */ |
| 4 | 4 |
| 5 'use strict'; | 5 'use strict'; |
| 6 | 6 |
| 7 google.load('visualization', '1', {packages: ['corechart']}); | 7 function querySelect(criterion) { |
|
Dan Beam
2012/07/02 21:33:10
put this inside controller if it's only used in th
clintstaley
2012/07/02 23:05:29
It loses all its brevity, though, if I do that. I
Dan Beam
2012/07/03 01:23:46
I'm fine with using jQuery as a selector engine (j
| |
| 8 | |
| 9 function $(criterion) { | |
| 10 return document.querySelector(criterion); | 8 return document.querySelector(criterion); |
| 11 } | 9 } |
| 12 | 10 |
| 11 | |
| 13 var controller = new function() { | 12 var controller = new function() { |
|
Dan Beam
2012/07/02 21:33:10
why are you using
new function() {};
here?
clintstaley
2012/07/02 23:05:29
[amusement] Because I had a more conventional pat
Dan Beam
2012/07/03 01:23:46
Generally we do stuff like:
cr.define('namespac
clintstaley
2012/07/04 00:01:03
Perhaps you're right. I lack the depth to comment
| |
| 14 // Tabular setup for various time ranges, giving a descriptive name, time span | 13 /** |
| 15 // prior to |now|, data point resolution, and time-label frequency and format | 14 * @enum {{ |
| 16 // for each. | 15 * value: !number, |
| 16 * name: !string, | |
| 17 * timeSpan: !number, | |
| 18 * resolution: !number, | |
| 19 * labelEvery: !number, | |
| 20 * format: !string | |
| 21 * }} | |
| 22 * | |
| 23 * Enum for time ranges, giving a descriptive name, time span prior to |now|, | |
| 24 * data point resolution, and time-label frequency and format for each. | |
| 25 */ | |
| 17 this.TimeRange = { | 26 this.TimeRange = { |
| 18 // Prior day, resolution of 2 min, at most 720 points, | 27 // Prior day, resolution of 2 min, at most 720 points, |
| 19 // Labels at 90 point (3 hour) intervals | 28 // Labels at 90 point (3 hour) intervals |
| 20 day: {value: 0, name: 'Last Day', timeSpan: 24 * 3600 * 1000, | 29 day: {value: 0, name: 'Last Day', timeSpan: 24 * 3600 * 1000, |
| 21 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, | 30 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, |
| 22 | 31 |
| 23 // Prior week, resolution of 15 min -- at most 672 data points | 32 // Prior week, resolution of 15 min -- at most 672 data points |
| 24 // Labels at 96 point (daily) intervals | 33 // Labels at 96 point (daily) intervals |
| 25 week: {value: 1, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, | 34 week: {value: 1, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, |
| 26 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, | 35 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, |
| 27 | 36 |
| 28 // Prior month (30 days), resolution of 1 hr -- at most 720 data points | 37 // Prior month (30 days), resolution of 1 hr -- at most 720 data points |
| 29 // Labels at 168 point (weekly) intervals | 38 // Labels at 168 point (weekly) intervals |
| 30 month: {value: 2, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, | 39 month: {value: 2, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, |
| 31 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, | 40 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, |
| 32 | 41 |
| 33 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points | 42 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points |
| 34 // Labels at 112 point (fortnightly) intervals | 43 // Labels at 112 point (fortnightly) intervals |
| 35 quarter: {value: 3, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, | 44 quarter: {value: 3, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, |
| 36 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, | 45 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, |
| 37 }; | 46 }; |
| 38 | 47 |
| 39 // Parent container of all line graphs | 48 /** @type {Object.<string, { |
|
Dan Beam
2012/07/02 21:33:10
move @type down a line past /**
clintstaley
2012/07/02 23:05:29
Done.
| |
| 40 this.chartDiv = $('#charts'); | 49 * divs: !Array.<DOMElement>, |
| 41 | 50 * yAxis: !{max: !number, color: !string}, |
| 42 // Parent container of checkboxes to choose metrics to display | 51 * data: ?Array.<{time: !number, value: !number}>, |
| 43 this.chooseMetricsDiv = $('#chooseMetrics'); | 52 * description: !string, |
| 44 | 53 * units: !string |
| 45 // Parent container of checkboxes to choose event types to display | 54 * }>} |
| 46 this.chooseEventsDiv = $('#chooseEvents'); | 55 * |
|
Dan Beam
2012/07/02 21:33:10
nit: we usually put a method/members's description
clintstaley
2012/07/02 23:05:29
Done.
| |
| 47 | 56 * All metrics have entries, but those not displayed have an empty div list. |
| 48 // Parent container of radio buttons to select time range | 57 * If a div list is not empty, the associated data will be non-null, or |
| 49 this.timeDiv = $('#chooseTimeRange'); | 58 * null but about to be filled by webui response. Thus, any metric with |
|
Dan Beam
2012/07/02 21:33:10
only 1 space between sentences in comments (everyw
clintstaley
2012/07/02 23:05:29
Oh all right. But comments are monospace font, an
Dan Beam
2012/07/03 01:24:28
This is a convention throughout all of chrome.
clintstaley
2012/07/04 00:01:03
And I'm really not objecting so much as joking abo
| |
| 50 | 59 * non-empty div list but null data is awaiting a data response from the |
| 51 this.metricMap = {}; // MetricName => {div, lineChart, dataTable} objects | 60 * webui. |
| 52 this.eventMap = {}; // EventName => event point lists, as returned by webui | 61 */ |
| 53 this.intervals = []; // Array of objects {start, end} | 62 this.metricMap = { |
|
Dan Beam
2012/07/02 21:33:10
btw, are any of these methods/members only used in
clintstaley
2012/07/02 23:05:29
Done.
| |
| 54 | 63 jankiness: { |
| 64 divs: [], | |
| 65 yAxis: {max: 100, color: 'rgb(255, 128, 128)'}, | |
| 66 data: null, | |
| 67 description: 'Jankiness', | |
| 68 units: 'milliJanks' | |
| 69 }, | |
| 70 oddness: { | |
| 71 divs: [], | |
| 72 yAxis: {max: 20, color: 'rgb(0, 192, 0)'}, | |
| 73 data: null, | |
| 74 description: 'Oddness', | |
| 75 units: 'kOdds' | |
| 76 } | |
| 77 }; | |
| 78 | |
| 79 /** @type {Object.<string, { | |
| 80 * divs: !Array.<DOMElement>, | |
| 81 * description: !string, | |
| 82 * color: !string, | |
| 83 * data: ?Array.<{time: !number, longDescription: !string}> | |
| 84 * }>} | |
| 85 * | |
| 86 * Similar data for events, though no yAxis info is needed since events | |
| 87 * are simply labelled markers at X locations. Rules regarding null data | |
| 88 * with non-empty div list apply here as for metricMap above. | |
| 89 */ | |
| 90 this.eventMap = { | |
| 91 wampusAttacks: { | |
| 92 divs: [], | |
| 93 description: 'Wampus Attack', | |
| 94 color: 'rgb(0, 0, 255)', | |
| 95 data: null | |
| 96 }, | |
| 97 solarEclipses: { | |
| 98 divs: [], | |
| 99 description: 'Solar Eclipse', | |
| 100 color: 'rgb(255, 0, 255)', | |
| 101 data: null | |
| 102 } | |
| 103 }; | |
| 104 | |
| 105 /** | |
| 106 * @type {!Array.<{start: !number, end: !number}>} | |
| 107 * Time periods in which the browser was active and collecting metrics | |
| 108 * and events. | |
| 109 */ | |
| 110 this.intervals = []; | |
| 111 | |
| 112 /** | |
| 113 * Set up the radio button set to choose time range. Use div#radioTemplate | |
| 114 * as a template. | |
| 115 */ | |
| 116 this.setupTimeRangeChooser = function() { | |
| 117 var timeDiv = querySelect('#chooseTimeRange'); | |
| 118 var radioTemplate = querySelect('#radioTemplate'); | |
| 119 var controller = this; | |
| 120 | |
| 121 for (var time in this.TimeRange) { | |
| 122 var timeRange = this.TimeRange[time]; | |
| 123 var radio = radioTemplate.cloneNode(true); | |
| 124 var input = radio.querySelector('input'); | |
| 125 | |
| 126 input.value = timeRange.value; | |
| 127 input.timeRange = timeRange; | |
| 128 radio.querySelector('span').innerText = timeRange.name; | |
| 129 timeDiv.appendChild(radio); | |
| 130 timeRange.element = input; | |
| 131 } | |
|
Dan Beam
2012/07/02 21:33:10
nit: +1 \n
clintstaley
2012/07/02 23:05:29
Done.
| |
| 132 timeDiv.addEventListener('click', function(e) { | |
| 133 if (!e.target.webkitMatchesSelector('input[type="radio"]')) | |
| 134 return; | |
| 135 | |
| 136 controller.setTimeRange(e.target.timeRange); | |
| 137 }); | |
| 138 }; | |
| 139 | |
| 140 /** | |
| 141 * Generalized function for setting up checkbox blocks for either events | |
| 142 * or metrics. Take a div ID |divId| into which to place the checkboxes, | |
| 143 * and a map |optionMap| with values that each include a property | |
| 144 * |description|. Set up one checkbox for each entry in |optionMap| | |
| 145 * labelled with that description. Arrange callbacks to function |check| | |
| 146 * or |uncheck|, passing them the key of the checked or unchecked option, | |
| 147 * when the relevant checkbox state changes. | |
| 148 * | |
| 149 * @param {!string} divId Id of division into which to put checkboxes | |
| 150 * @param {!Object} optionMap map of metric/event entries | |
| 151 * @param {!function(this:Controller, Object)} check | |
| 152 * function to select an entry (metric or event) | |
| 153 * @param {!function(this:Controller, Object)} uncheck | |
| 154 * function to deselect an entry (metric or event) | |
| 155 */ | |
|
Dan Beam
2012/07/02 21:33:10
nit: no \n
clintstaley
2012/07/02 23:05:29
Done.
| |
| 156 | |
| 157 this.setupCheckboxes = function(divId, optionMap, check, uncheck) { | |
| 158 var checkboxTemplate = querySelect('#checkboxTemplate'); | |
| 159 var chooseMetricsDiv = querySelect(divId); | |
| 160 var doCheck = check.bind(this); | |
| 161 var doUncheck = uncheck.bind(this); | |
| 162 | |
| 163 for (var option in optionMap) { | |
| 164 var checkbox = checkboxTemplate.cloneNode(true); | |
| 165 checkbox.querySelector('span').innerText = 'Show ' + | |
| 166 optionMap[option].description; | |
| 167 chooseMetricsDiv.appendChild(checkbox); | |
| 168 | |
| 169 var input = checkbox.querySelector('input'); | |
| 170 input.option = option; | |
| 171 input.addEventListener('change', function(e) { | |
| 172 if (e.target.checked) | |
| 173 doCheck(e.target.option); | |
|
Dan Beam
2012/07/02 21:33:10
check.call(this, e.target.option);
clintstaley
2012/07/02 23:05:29
Ah. Thanks. That is better. I had earlier been
| |
| 174 else | |
| 175 doUncheck(e.target.option); | |
|
Dan Beam
2012/07/02 21:33:10
uncheck.call(this, e.target.option);
clintstaley
2012/07/02 23:05:29
Done.
| |
| 176 }); | |
|
Dan Beam
2012/07/02 21:33:10
}.bind(this));
clintstaley
2012/07/02 23:05:29
Done.
| |
| 177 } | |
| 178 }; | |
| 179 | |
| 180 /** | |
| 181 * Set up just one chart in which all metrics will be displayed | |
| 182 * initially. But, the design readily accommodates addition of | |
| 183 * new charts, and movement of metrics into those other charts. | |
| 184 */ | |
| 185 this.setupMainChart = function() { | |
| 186 this.chartParent = querySelect('#charts'); | |
| 187 this.charts = [document.createElement('div')]; | |
| 188 this.charts[0].className = 'chart'; | |
| 189 this.chartParent.appendChild(this.charts[0]); | |
| 190 }; | |
| 191 | |
| 192 /** | |
| 193 * Set the time range for which to display metrics and events. For | |
| 194 * now, the time range always ends at "now", but future implementations | |
| 195 * may allow time ranges not so anchored. | |
| 196 * | |
| 197 * @param {!{start: !number, end: !number, resolution: !number}} range | |
| 198 */ | |
| 55 this.setTimeRange = function(range) { | 199 this.setTimeRange = function(range) { |
| 56 this.range = range; | 200 this.range = range; |
| 57 this.end = Math.floor(new Date().getTime() / range.resolution) * | 201 this.end = Math.floor(Date.now() / range.resolution) * |
| 58 range.resolution; | 202 range.resolution; |
| 203 | |
| 204 // Take the GMT value of this.end ("now") and subtract from it the | |
| 205 // number of minutes by which we lag GMT in the present timezone, | |
| 206 // X mS/minute. This will show time in the present timezone. | |
| 207 this.end -= new Date().getTimezoneOffset() * 60000; | |
| 59 this.start = this.end - range.timeSpan; | 208 this.start = this.end - range.timeSpan; |
| 60 this.requestIntervals(); | 209 this.requestIntervals(); |
| 61 } | 210 }; |
| 62 | 211 |
| 63 // Return mock interval set for testing | 212 /** |
| 213 * Return mock interval set for testing | |
| 214 * @return {!Array.<{start: !number, end: !number}>} intervals | |
| 215 */ | |
| 64 this.getMockIntervals = function() { | 216 this.getMockIntervals = function() { |
| 65 var interval = this.end - this.start; | 217 var interval = this.end - this.start; |
| 218 | |
| 66 return [ | 219 return [ |
| 67 {'start': this.start + interval * .1, | 220 { start: this.start + interval * .1, |
|
Dan Beam
2012/07/02 21:33:10
nit: no space after {
{start: ...
end: ...},
clintstaley
2012/07/02 23:05:29
Really? Doesn't this clash with 2-space indent ru
Dan Beam
2012/07/03 01:29:14
I break into a floating { when the params are too
clintstaley
2012/07/04 00:01:03
OK. Thanks for clarification. Fixed in latest.
| |
| 68 'end': this.start + interval * .2}, | 221 end: this.start + interval * .2 |
| 69 {'start': this.start + interval * .7, 'end': this.start + interval * .9} | 222 }, |
| 223 { start: this.start + interval * .7, | |
| 224 end: this.start + interval | |
| 225 } | |
| 70 ]; | 226 ]; |
| 71 } | 227 }; |
| 72 // Request array of objects with start and end fields showing browser | 228 |
| 73 // activity intervals in the specified time range | 229 /** |
| 230 * Request activity intervals in the current time range | |
| 231 */ | |
| 74 this.requestIntervals = function() { | 232 this.requestIntervals = function() { |
| 75 this.onReceiveIntervals(this.getMockIntervals()); | 233 this.onReceiveIntervals(this.getMockIntervals()); |
| 76 // Replace with: chrome.send('getIntervals', this.start, this.end, | 234 // Replace with: chrome.send('getIntervals', this.start, this.end, |
| 77 // this.onReceiveIntervals); | 235 // this.onReceiveIntervals); |
| 78 } | 236 }; |
| 79 | 237 |
| 80 // Webui callback delivering response from requestIntervals call. Assumes | 238 /** |
| 81 // this is a new time range choice, which results in complete refresh of | 239 * Webui callback delivering response from requestIntervals call. Assumes |
| 82 // all metrics and event types that are currently selected. | 240 * this is a new time range choice, which results in complete refresh of |
| 241 * all metrics and event types that are currently selected. | |
| 242 * | |
| 243 * @param {!Array.<{start: !number, end: !number}> intervals new intervals | |
| 244 */ | |
| 83 this.onReceiveIntervals = function(intervals) { | 245 this.onReceiveIntervals = function(intervals) { |
| 84 this.intervals = intervals; | 246 this.intervals = intervals; |
| 85 for (var metric in this.metricMap) | 247 |
| 86 this.refreshMetric(metric); | 248 for (var metric in this.metricMap) { |
| 87 for (var eventType in this.eventMap) | 249 var metricValue = this.metricMap[metric]; |
| 88 this.refreshEvent(eventType); | 250 if (metricValue.divs.length > 0) // If we're displaying this metric. |
|
Dan Beam
2012/07/02 21:33:10
if (metricValue.divs.length > 0) // If we're disp
clintstaley
2012/07/02 23:05:29
Done.
| |
| 89 } | 251 this.refreshMetric(metric); |
| 90 | 252 } |
| 91 // Add a new metric, and its associated linegraph. The linegraph for | 253 |
| 92 // each metric has a discrete domain of times. This is not continuous | 254 for (var eventType in this.eventMap) { |
| 93 // because of breaks for each interval of activity. (No point in showing | 255 var eventValue = this.eventMap[eventType]; |
| 94 // a lot of "dead air" when the browser wasn't running.) Column 1 of | 256 if (eventValue.divs.length > 0) |
| 95 // its DataTable is the metric data, and higher numbered columns are added | 257 this.refreshEventType(eventType); |
| 96 // in pairs for each event type currently chosen. Each pair gives the | 258 } |
| 97 // event occurence points, and mouseover detailed description for one event | 259 }; |
| 98 // type. | 260 |
| 261 /** | |
| 262 * Add a new metric to the main (currently only) chart. | |
| 263 * | |
| 264 * @param {!string} metric Metric to start displaying | |
| 265 */ | |
| 99 this.addMetric = function(metric) { | 266 this.addMetric = function(metric) { |
| 100 if (!(metric in this.metricMap)) { | 267 this.metricMap[metric].divs.push(this.charts[0]); |
| 101 var div = document.createElement('div'); | 268 this.refreshMetric(metric); |
| 102 | 269 }; |
| 103 div.className = 'chart'; | 270 |
| 104 this.chartDiv.appendChild(div); | 271 /** |
| 105 | 272 * Remove a metric from the chart(s). |
| 106 var table = new google.visualization.DataTable(); | 273 * |
| 107 var chart = new google.visualization.LineChart(div); | 274 * @param {!string} metric Metric to stop displaying |
| 108 this.metricMap[metric] = {'div': div, 'chart': chart, 'table': table}; | 275 */ |
| 109 | |
| 110 table.addColumn('string', 'Time'); // Default domain column | |
| 111 table.addColumn('number', 'Value'); // Only numerical range column | |
| 112 for (var event in this.events) | |
| 113 this.addEventColumns(table, event); | |
| 114 | |
| 115 this.refreshMetric(metric); | |
| 116 } | |
| 117 } | |
| 118 | |
| 119 // Remove a metric from the UI | |
| 120 this.dropMetric = function(metric) { | 276 this.dropMetric = function(metric) { |
| 121 if (metric in this.metricMap) { | 277 var metricValue = this.metricMap[metric]; |
| 122 this.chartDiv.removeChild(this.metricMap[metric].div); | 278 var affectedCharts = metricValue.divs; |
| 123 delete this.metricMap[metric]; | 279 metricValue.divs = []; |
| 124 } | 280 |
| 125 } | 281 affectedCharts.forEach(this.drawChart, this); |
| 126 | 282 }; |
| 127 // Return mock metric data points for testing | 283 |
| 128 this.getMockDataPoints = function() { | 284 /** |
| 285 * Return mock metric data points for testing. Give values ranging from | |
| 286 * offset to max-offset. (This lets us avoid direct overlap of | |
| 287 * different mock data sets in an ugly way that will die in the next | |
| 288 * version anyway.) | |
| 289 * | |
| 290 * @param {!number} max Max data value to return, less offset | |
| 291 * @param {!number} offset Adjustment factor | |
|
Dan Beam
2012/07/02 21:33:10
* @param {!number} offset Adjustment factor
(ther
clintstaley
2012/07/02 23:05:29
Done.
| |
| 292 * @return {!Array.<{time: !number, value: !number}>} | |
| 293 */ | |
| 294 this.getMockDataPoints = function(max, offset) { | |
| 129 var dataPoints = []; | 295 var dataPoints = []; |
| 130 | 296 |
| 131 for (var x = 0; x < this.intervals.length; x++) { | 297 for (var i = 0; i < this.intervals.length; i++) { |
| 132 // Rise from low 0 to high 100 every 20 min | 298 // Rise from low offset to high max-offset in 100 point steps |
|
Dan Beam
2012/07/02 21:33:10
end comments with .
clintstaley
2012/07/02 23:05:29
Done, but..
Does this include sentence fragments?
Dan Beam
2012/07/03 01:29:14
yes, but the solution is to just write sentences w
clintstaley
2012/07/04 00:01:03
OK, will do.
| |
| 133 for (var time = this.intervals[x].start; time <= this.intervals[x].end; | 299 for (var time = this.intervals[i].start; time <= this.intervals[i].end; |
| 134 time += this.range.resolution) | 300 time += this.range.resolution) { |
|
Dan Beam
2012/07/02 21:33:10
+ 1 \s before time +=
clintstaley
2012/07/02 23:05:29
Sorry, I'm confused. It's a 4-space indent.. ?
Dan Beam
2012/07/03 01:23:46
I've generally seen lined up with (, i.e.
for (
clintstaley
2012/07/04 00:01:03
Done.
| |
| 135 dataPoints.push({'time': time, 'value': time % 1000000 / 10000}); | 301 var numPoints = time / this.range.resolution; |
| 302 dataPoints.push({time: time, value: offset + (numPoints % 100) * | |
| 303 (max - 2 * offset) / 100}); | |
| 304 } | |
| 136 } | 305 } |
| 137 return dataPoints; | 306 return dataPoints; |
| 138 } | 307 }; |
| 139 | 308 |
| 140 // Request new metric data, assuming the metric table and chart already | 309 /** |
| 141 // exist. | 310 * Request new metric data, assuming the metric table and chart already |
| 311 * exist. | |
| 312 * | |
| 313 * @param {!string} metric Metric for which to get data | |
| 314 */ | |
| 142 this.refreshMetric = function(metric) { | 315 this.refreshMetric = function(metric) { |
| 143 this.onReceiveMetric(metric, this.getMockDataPoints()); | 316 var metricValue = this.metricMap[metric]; |
| 317 | |
| 318 metricValue.data = null; // Mark metric as awaiting response. | |
| 319 this.onReceiveMetric(metric, | |
| 320 this.getMockDataPoints(metricValue.yAxis.max, 5)); | |
| 144 // Replace with: | 321 // Replace with: |
| 145 // chrome.send("getMetric", this.range.start, this.range.end, | 322 // chrome.send("getMetric", this.range.start, this.range.end, |
| 146 // this.range.resolution, this.onReceiveMetric); | 323 // this.range.resolution, this.onReceiveMetric); |
| 147 } | 324 }; |
| 148 | 325 |
| 149 // Receive new datapoints for |metric|, and completely refresh the DataTable | 326 /** |
| 150 // for that metric, redrawing the chart. (We cannot preserve the event | 327 * Receive new datapoints for |metric|, convert the data to Flot-usable |
| 151 // columns because entirely new rows may be implied by the new metric | 328 * form, and redraw all affected charts. |
| 152 // datapoints.) | 329 * |
| 330 * @param {!string} metric Metric to which |points| applies | |
| 331 * @param {!Array.<{time: !number, value: !number}>} points | |
| 332 * new data points | |
| 333 */ | |
| 153 this.onReceiveMetric = function(metric, points) { | 334 this.onReceiveMetric = function(metric, points) { |
| 335 var metricValue = this.metricMap[metric]; | |
| 336 | |
| 154 // Might have been dropped while waiting for data | 337 // Might have been dropped while waiting for data |
| 155 if (!(metric in this.metricMap)) | 338 if (metricValue.divs.length == 0) |
| 156 return; | 339 return; |
| 157 | 340 |
| 158 var data = this.metricMap[metric].table; | 341 var series = []; |
| 159 | 342 metricValue.data = [series]; |
| 160 data.removeRows(0, data.getNumberOfRows()); | 343 |
| 161 | 344 // Traverse the points, and the intervals, in parallel. Both are in |
| 162 // Traverse the points, which are in time order, and the intervals, | 345 // ascending time order. Create a sequence of data "series" (per Flot) |
| 163 // placing each value in the interval (if any) in which it belongs. | 346 // arrays, with each series comprising all points within a given interval. |
| 164 var interval = this.intervals[0]; | 347 var interval = this.intervals[0]; |
| 165 var intervalIndex = 0; | 348 var intervalIndex = 0; |
| 166 var valueIndex = 0; | 349 var point; |
|
Dan Beam
2012/07/02 21:33:10
remove
clintstaley
2012/07/02 23:05:29
Done.
| |
| 167 var value; | 350 var pointIndex = 0; |
| 168 while (valueIndex < points.length && | 351 while (pointIndex < points.length && |
| 169 intervalIndex < this.intervals.length) { | 352 intervalIndex < this.intervals.length) { |
| 170 value = points[valueIndex++]; | 353 point = points[pointIndex++]; |
|
Dan Beam
2012/07/02 21:33:10
var point = ...
(just use var even though it's no
clintstaley
2012/07/02 23:05:29
Done.
| |
| 171 while (value.time > interval.end && | 354 while (intervalIndex < this.intervals.length && |
| 172 intervalIndex < this.intervals.length) { | 355 point.time > interval.end) { |
| 173 interval = this.intervals[++intervalIndex]; // Jump to new interval | 356 interval = this.intervals[++intervalIndex]; // Jump to new interval |
| 174 data.addRow(null, null); // Force gap in line chart | 357 if (series.length > 0) { |
| 358 series = []; // Start a new series | |
| 359 metricValue.data.push(series); // Put it on the end of the data | |
| 360 } | |
| 175 } | 361 } |
| 176 if (intervalIndex < this.intervals.length && value.time > interval.start) | 362 if (intervalIndex < this.intervals.length && point.time > interval.start) |
| 177 if (data.getNumberOfRows() % this.range.labelEvery == 0) | 363 series.push([point.time, point.value]); |
|
Dan Beam
2012/07/02 21:33:10
-2 \s
clintstaley
2012/07/02 23:05:29
Done.
| |
| 178 data.addRow([new Date(value.time).toString(this.range.format), | 364 } |
| 179 value.value]); | 365 |
| 180 else | 366 metricValue.divs.forEach(this.drawChart, this); |
| 181 data.addRow(['', value.value]); | 367 }; |
| 182 } | 368 |
| 183 this.drawChart(metric); | 369 /** |
| 184 } | 370 * Add a new event to the chart(s). |
| 185 | 371 * |
| 186 // Add a new event to all line graphs. | 372 * @param {!string} eventType type of event to start displaying |
| 373 */ | |
| 187 this.addEventType = function(eventType) { | 374 this.addEventType = function(eventType) { |
| 188 if (!(eventType in this.eventMap)) { | 375 this.eventMap[eventType].divs = this.charts; // Events show on all charts |
|
Dan Beam
2012/07/02 21:33:10
2 spaces between ; and //
clintstaley
2012/07/02 23:05:29
Done.
| |
| 189 this.eventMap[eventType] = []; | 376 this.refreshEventType(eventType); |
| 190 this.refreshEventType(eventType); | 377 }; |
| 191 } | 378 |
| 192 } | 379 /* |
| 193 | 380 * Remove an event from the chart(s) |
| 194 // Return mock event point for testing | 381 * |
| 382 * @param {!string} eventType type of event to stop displaying | |
| 383 */ | |
| 384 this.dropEventType = function(eventType) { | |
| 385 var eventValue = this.eventMap[eventType]; | |
| 386 var affectedCharts = eventValue.divs; | |
| 387 eventValue.divs = []; | |
| 388 | |
| 389 affectedCharts.forEach(this.drawChart, this); | |
| 390 }; | |
| 391 | |
| 392 /** | |
| 393 * Return mock event points for testing | |
| 394 * | |
| 395 * @param {!string} eventType type of event to generate mock data for | |
| 396 * @return {!Array.<{time: !number, longDescription: !string}>} | |
| 397 */ | |
| 195 this.getMockEventValues = function(eventType) { | 398 this.getMockEventValues = function(eventType) { |
| 196 var mockValues = []; | 399 var mockValues = []; |
| 197 for (var i = 0; i < this.intervals.length; i++) { | 400 for (var i = 0; i < this.intervals.length; i++) { |
| 198 var interval = this.intervals[i]; | 401 var interval = this.intervals[i]; |
| 199 | 402 |
| 200 mockValues.push({ | 403 mockValues.push({ |
| 201 time: interval.start, | 404 time: interval.start, |
| 202 shortDescription: eventType, | |
| 203 longDescription: eventType + ' at ' + | 405 longDescription: eventType + ' at ' + |
| 204 new Date(interval.start) + ' blah, blah blah'}); | 406 new Date(interval.start) + ' blah, blah blah'}); |
|
Dan Beam
2012/07/02 21:33:10
I assume this'll be real text eventually?
clintstaley
2012/07/02 23:05:29
Yes, supplied from the C++ side. The getMock... m
| |
| 205 mockValues.push({ | 407 mockValues.push({ |
| 206 time: (interval.start + interval.end) / 2, | 408 time: (interval.start + interval.end) / 2, |
| 207 shortDescription: eventType, | |
| 208 longDescription: eventType + ' at ' + | 409 longDescription: eventType + ' at ' + |
| 209 new Date((interval.start + interval.end) / 2) + ' blah, blah blah'}); | 410 new Date((interval.start + interval.end) / 2) + ' blah, blah blah'}); |
| 210 mockValues.push({ | 411 mockValues.push({ |
| 211 time: interval.end, | 412 time: interval.end, |
| 212 shortDescription: eventType, | |
| 213 longDescription: eventType + ' at ' + new Date(interval.end) + | 413 longDescription: eventType + ' at ' + new Date(interval.end) + |
| 214 ' blah, blah blah'}); | 414 ' blah, blah blah'}); |
| 215 } | 415 } |
| 216 return mockValues; | 416 return mockValues; |
| 217 } | 417 }; |
| 218 | 418 |
| 219 // Request new data for |eventType|, for times in the current range. | 419 /** |
| 420 * Request new data for |eventType|, for times in the current range. | |
| 421 * | |
| 422 * @param {!string} eventType type of event to get new data for | |
| 423 */ | |
| 220 this.refreshEventType = function(eventType) { | 424 this.refreshEventType = function(eventType) { |
| 425 this.eventMap[eventType].data = null; // Mark eventType as awaiting response | |
|
Dan Beam
2012/07/02 21:33:10
2 spaces in front of comment
clintstaley
2012/07/02 23:05:29
Done.
| |
| 221 this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); | 426 this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); |
| 222 // Replace with: | 427 // Replace with: |
| 223 // chrome.send("getEvents", eventType, this.range.start, this.range.end); | 428 // chrome.send("getEvents", eventType, this.range.start, this.range.end); |
| 224 } | 429 }; |
| 225 | 430 |
| 226 // Add an event column pair to DataTable |table| for |eventType| | 431 /** |
| 227 this.addEventColumns = function(table, eventType) { | 432 * Receive new data for |eventType|. If the event has been deselected while |
| 228 var annotationCol = table.addColumn({'id': eventType, type: 'string', | 433 * awaiting webui response, do nothing. Otherwise, save the data directly, |
| 229 role: 'annotation'}); | 434 * since events are handled differently than metrics when drawing |
| 230 var rolloverCol = table.addColumn({'id': eventType + 'Tip', type: 'string', | 435 * (no "series"), and redraw all the affected charts. |
| 231 role: 'annotationText'}); | 436 * |
| 232 var values = this.eventMap[eventType]; | 437 * @param {!string} eventType type of event the new data applies to |
| 233 var interval = this.intervals[0], intervalIndex = 0; | 438 * @param {!Array.<{time: !number, longDescription: !string}>} values |
| 439 * new event values | |
| 440 */ | |
| 441 this.onReceiveEventType = function(eventType, values) { | |
| 442 var eventValue = this.eventMap[eventType]; | |
| 234 | 443 |
| 235 for (var i = 0; i < values.length; i++) { | 444 if (eventValue.divs.length == 0) |
| 236 var event = values[i]; | 445 return; |
| 237 var rowIndex = 0; | 446 |
| 238 while (event.time > interval.end && | 447 eventValue.data = values; |
| 239 intervalIndex < this.intervals.length - 1) | 448 eventValue.divs.forEach(this.drawChart, this); |
| 240 { | 449 }; |
| 241 // Skip interval times, inclusive of interval.end, and of following null | 450 |
| 242 rowIndex += (interval.end - interval.start) / this.range.resolution + 2; | 451 /** |
| 243 interval = this.intervals[++intervalIndex]; | 452 * Return an object containing an array of metrics and another of events |
| 244 } | 453 * that include |chart| as one of the divs into which they display. |
| 245 if (event.time >= interval.start && event.time <= interval.end) { | 454 * |
| 246 table.setCell(rowIndex + (event.time - interval.start) / | 455 * @param {DOMElement} chart div for which to get relevant items |
|
Dan Beam
2012/07/02 21:33:10
Element, HTMLElement, or HTMLDivElement (there's n
clintstaley
2012/07/02 23:05:29
Fixed throughout.
| |
| 247 this.range.resolution, annotationCol, event.shortDescription); | 456 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} |
| 248 table.setCell(rowIndex + (event.time - interval.start) / | 457 */ |
| 249 this.range.resolution, rolloverCol, event.longDescription); | 458 this.getChartData = function(chart) { |
| 459 var result = {metrics: [], events: []}; | |
| 460 | |
| 461 for (var metric in this.metricMap) { | |
| 462 var metricValue = this.metricMap[metric]; | |
| 463 | |
| 464 if (metricValue.divs.indexOf(chart) != -1) | |
| 465 result.metrics.push(metricValue); | |
| 466 } | |
| 467 | |
| 468 for (var eventType in this.eventMap) { | |
| 469 var eventValue = this.eventMap[eventType]; | |
| 470 | |
| 471 // Events post to all divs, if they post to any | |
|
Dan Beam
2012/07/02 21:33:10
. at end of comment
clintstaley
2012/07/02 23:05:29
Done.
| |
| 472 if (eventValue.divs.length > 0) | |
| 473 result.events.push(eventValue); | |
| 474 } | |
| 475 | |
| 476 return result; | |
| 477 }; | |
| 478 | |
| 479 /** | |
| 480 * Check all entries in an object of the type returned from getChartData, | |
| 481 * above, to see if all events and metrics have completed data (none is | |
| 482 * awaiting an asynchronous webui response to get their current data). | |
| 483 * | |
| 484 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData | |
| 485 * event/metric data to check for readiness | |
| 486 * @return {boolean} is data ready? | |
| 487 */ | |
| 488 this.isDataReady = function(chartData) { | |
| 489 for (var i = 0; i < chartData.metrics.length; i++) { | |
| 490 if (chartData.metrics[i].data == null) | |
|
Dan Beam
2012/07/02 21:33:10
is there a reason you're loosely checking if it's
clintstaley
2012/07/02 23:05:29
Unfamiliarity with JS :). Wasn't sure what negati
| |
| 491 return false; | |
| 492 } | |
| 493 | |
| 494 for (var i = 0; i < chartData.events.length; i++) { | |
| 495 if (chartData.events[i].data == null) | |
| 496 return false; | |
| 497 } | |
| 498 | |
| 499 return true; | |
| 500 }; | |
| 501 | |
| 502 /** | |
| 503 * Create and return an array of "markings" (per Flot), representing | |
| 504 * vertical lines at the event time, in the event's color. Also add | |
| 505 * (not per Flot) a |description| property to each, to be used for hand | |
| 506 * creating description boxes. | |
| 507 * | |
| 508 * @param {!Array.<{ | |
| 509 * description: !string, | |
| 510 * color: !string, | |
| 511 * data: !Array.<{time: !number}> | |
| 512 * }>} eventValues events to make markings for | |
| 513 * @return {!Array.<{ | |
| 514 * color: !string, | |
| 515 * description: !string, | |
| 516 * xaxis: {from: !number, to: !number} | |
| 517 * }>} mark data structure for Flot to use | |
| 518 */ | |
| 519 this.getEventMarks = function(eventValues) { | |
| 520 var markings = []; | |
| 521 | |
| 522 for (var i = 0; i < eventValues.length; i++) { | |
| 523 var eventValue = eventValues[i]; | |
| 524 for (var d = 0; d < eventValue.data.length; d++) { | |
| 525 var point = eventValue.data[d]; | |
| 526 markings.push({ | |
| 527 color: eventValue.color, | |
| 528 description: eventValue.description, | |
| 529 xaxis: {from: point.time, to: point.time} | |
| 530 }); | |
| 250 } | 531 } |
| 251 } | 532 } |
| 252 } | |
| 253 | 533 |
| 254 this.dropEventColumns = function(table, eventType) { | 534 return markings; |
| 255 var colIndex, numCols = table.getNumberOfColumns(); | 535 }; |
| 256 | 536 |
| 257 for (colIndex = 0; colIndex < numCols; colIndex++) | 537 /** |
| 258 if (table.getColumnId(colIndex) == eventType) | 538 * Redraw the chart in div |chart|, IF all its dependent data is present. |
|
Dan Beam
2012/07/02 21:33:10
s/IF/if/
clintstaley
2012/07/02 23:05:29
That's emphasis, not a misspelling. Is there an a
Dan Beam
2012/07/03 01:29:14
sure, that works
| |
| 259 break; | 539 * Otherwise simply return, and await another call when all data is |
| 540 * available. | |
| 541 * | |
| 542 * @param {DOMElement} chart div to redraw | |
|
Dan Beam
2012/07/02 21:33:10
same type nit as above (no DOMElement)
clintstaley
2012/07/02 23:05:29
Done.
| |
| 543 */ | |
| 544 this.drawChart = function(chart) { | |
| 545 var chartData = this.getChartData(chart); | |
| 260 | 546 |
| 261 if (colIndex < numCols) { | 547 if (!this.isDataReady(chartData)) |
| 262 table.removeColumn(colIndex + 1); | 548 return; |
| 263 table.removeColumn(colIndex); | 549 |
| 550 var seriesSeq = []; | |
| 551 var yAxes = []; | |
| 552 chartData.metrics.forEach(function(value) { | |
| 553 yAxes.push(value.yAxis); | |
| 554 for (var i = 0; i < value.data.length; i++) { | |
| 555 seriesSeq.push({ | |
| 556 color: value.yAxis.color, | |
| 557 data: value.data[i], | |
| 558 label: i == 0 ? value.description + ' (' + value.units + ')' : null, | |
| 559 yaxis: yAxes.length, // Use just-added Y axis | |
|
Dan Beam
2012/07/02 21:33:10
. at end.
clintstaley
2012/07/02 23:05:29
Fixed these throughout -- about a dozen cases -- s
| |
| 560 }); | |
| 561 } | |
| 562 }); | |
| 563 | |
| 564 var markings = this.getEventMarks(chartData.events); | |
| 565 var chart = this.charts[0]; | |
| 566 var plot = $.plot(chart, seriesSeq, { | |
| 567 yaxes: yAxes, | |
| 568 xaxis: {mode: 'time'}, | |
| 569 grid: {markings: markings} | |
| 570 }); | |
| 571 | |
| 572 // For each event in |markings|, create also a label div, with left | |
| 573 // edge colinear with the event vertical-line. Top of label is | |
| 574 // presently a hack-in, putting labels in three tiers of 25px height | |
| 575 // each to avoid overlap. Will need something better. | |
| 576 var labelTemplate = querySelect('#labelTemplate'); | |
| 577 for (var i = 0; i < markings.length; i++) { | |
| 578 var mark = markings[i]; | |
| 579 var point = | |
| 580 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); | |
| 581 var labelDiv = labelTemplate.cloneNode(true); | |
| 582 labelDiv.innerText = mark.description; | |
| 583 labelDiv.style.left = point.left + 'px'; | |
| 584 labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; | |
| 585 chart.appendChild(labelDiv); | |
| 264 } | 586 } |
| 265 } | 587 }; |
| 266 | 588 |
| 267 // Receive new data for |eventType|. Save this in eventMap for future | 589 this.setupCheckboxes( |
| 268 // redraws. Then, for each metric linegraph remove any current column pair | 590 '#chooseMetrics', this.metricMap, this.addMetric, this.dropMetric); |
| 269 // for |eventType| and replace it with a new pair, which will reflect the | 591 this.setupCheckboxes( |
| 270 // new data. Redraw the linegraph. | 592 '#chooseEvents', this.eventMap, this.addEventType, this.dropEventType); |
| 271 this.onReceiveEventType = function(eventType, values) { | |
| 272 this.eventMap[eventType] = values; | |
| 273 | |
| 274 for (var metric in this.metricMap) { | |
| 275 var table = this.metricMap[metric].table; | |
| 276 | |
| 277 this.dropEventColumns(table, eventType); | |
| 278 this.addEventColumns(table, eventType); | |
| 279 this.drawChart(metric); | |
| 280 } | |
| 281 } | |
| 282 | |
| 283 this.dropEventType = function(eventType) { | |
| 284 delete this.eventMap[eventType]; | |
| 285 | |
| 286 for (var metric in this.metricMap) { | |
| 287 var table = this.metricMap[metric].table; | |
| 288 | |
| 289 this.dropEventColumns(table, eventType); | |
| 290 this.drawChart(metric); | |
| 291 } | |
| 292 } | |
| 293 | |
| 294 | |
| 295 // Redraw the linegraph for |metric|, assuming its DataTable is fully up to | |
| 296 // date. | |
| 297 this.drawChart = function(metric) { | |
| 298 var entry = this.metricMap[metric]; | |
| 299 | |
| 300 entry.chart.draw(entry.table, {title: metric + ' for ' + this.range.name, | |
| 301 hAxis: {showTextEvery: this.range.labelEvery}}); | |
| 302 } | |
| 303 | |
| 304 this.setupTimeRangeChooser = function() { | |
| 305 var controller = this; | |
| 306 var radioTemplate = $('#radioTemplate'); | |
| 307 | |
| 308 for (var time in this.TimeRange) { | |
| 309 var range = this.TimeRange[time]; | |
| 310 var radio = radioTemplate.cloneNode(true); | |
| 311 var input = radio.querySelector('input'); | |
| 312 | |
| 313 input.value = range.value; | |
| 314 radio.querySelector('span').innerText = range.name; | |
| 315 this.timeDiv.appendChild(radio); | |
| 316 range.element = input; | |
| 317 radio.range = range; | |
| 318 radio.addEventListener('click', function() { | |
| 319 controller.setTimeRange(this.range); | |
| 320 }); | |
| 321 } | |
| 322 } | |
| 323 | |
| 324 this.setupMetricChooser = function(metricTypes) { | |
| 325 var checkboxTemplate = $('#checkboxTemplate'); | |
| 326 | |
| 327 metricTypes.forEach(function(metric) { | |
| 328 var checkbox = checkboxTemplate.cloneNode(true); | |
| 329 var input = checkbox.querySelector('input'); | |
| 330 input.addEventListener('change', function() { | |
| 331 if (input.checked) | |
| 332 this.addMetric(metric); | |
| 333 else | |
| 334 this.dropMetric(metric); | |
| 335 }.bind(this)); | |
| 336 checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + metric; | |
| 337 this.chooseMetricsDiv.appendChild(checkbox); | |
| 338 }, this); | |
| 339 } | |
| 340 | |
| 341 this.setupEventChooser = function(eventTypes) { | |
| 342 var checkboxTemplate = $('#checkboxTemplate'); | |
| 343 | |
| 344 eventTypes.forEach(function(event) { | |
| 345 var checkbox = checkboxTemplate.cloneNode(true); | |
| 346 var input = checkbox.querySelector('input'); | |
| 347 input.addEventListener('change', function() { | |
| 348 if (input.checked) | |
| 349 this.addEventType(event); | |
| 350 else | |
| 351 this.dropEventType(event); | |
| 352 }.bind(this)); | |
| 353 checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + event; | |
| 354 this.chooseEventsDiv.appendChild(checkbox); | |
| 355 }, this); | |
| 356 } | |
| 357 | |
| 358 this.setupMetricChooser(['Oddness', 'Jankiness']); | |
| 359 this.setupEventChooser(['Wampus Attack', 'Solar Eclipse']); | |
| 360 this.setupTimeRangeChooser(); | 593 this.setupTimeRangeChooser(); |
| 594 this.setupMainChart(); | |
| 361 this.TimeRange.day.element.click(); | 595 this.TimeRange.day.element.click(); |
| 362 } | 596 } |
|
Dan Beam
2012/07/02 21:33:10
};
clintstaley
2012/07/02 23:05:29
lint tool dings me for this, oddly.
Dan Beam
2012/07/03 01:23:46
which tool? the js presubmit check?
| |
| OLD | NEW |