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']}); | |
8 | 7 |
9 function $(criterion) { | 8 function docGet(criterion) { |
Dan Beam
2012/06/28 21:39:14
why did you change the name here to docGet? that s
clintstaley
2012/06/29 22:34:20
This was originally a hand-written $ function, cau
Dan Beam
2012/06/29 23:18:59
You *could* call jQuery.noConflict() <http://api.j
| |
10 return document.querySelector(criterion); | 9 return document.querySelector(criterion); |
11 } | 10 } |
12 | 11 |
12 | |
13 var controller = new function() { | 13 var controller = new function() { |
14 // Tabular setup for various time ranges, giving a descriptive name, time span | 14 // Tabular setup for various time ranges, giving a descriptive name, time span |
15 // prior to |now|, data point resolution, and time-label frequency and format | 15 // prior to |now|, data point resolution, and time-label frequency and format |
16 // for each. | 16 // for each. |
17 this.TimeRange = { | 17 this.TimeRange = { |
18 // Prior day, resolution of 2 min, at most 720 points, | 18 // Prior day, resolution of 2 min, at most 720 points, |
19 // Labels at 90 point (3 hour) intervals | 19 // Labels at 90 point (3 hour) intervals |
20 day: {value: 0, name: 'Last Day', timeSpan: 24 * 3600 * 1000, | 20 day: {value: 0, name: 'Last Day', timeSpan: 24 * 3600 * 1000, |
21 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, | 21 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, |
22 | 22 |
23 // Prior week, resolution of 15 min -- at most 672 data points | 23 // Prior week, resolution of 15 min -- at most 672 data points |
24 // Labels at 96 point (daily) intervals | 24 // Labels at 96 point (daily) intervals |
25 week: {value: 1, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, | 25 week: {value: 1, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, |
26 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, | 26 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, |
27 | 27 |
28 // Prior month (30 days), resolution of 1 hr -- at most 720 data points | 28 // Prior month (30 days), resolution of 1 hr -- at most 720 data points |
29 // Labels at 168 point (weekly) intervals | 29 // Labels at 168 point (weekly) intervals |
30 month: {value: 2, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, | 30 month: {value: 2, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, |
31 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, | 31 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, |
32 | 32 |
33 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points | 33 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points |
34 // Labels at 112 point (fortnightly) intervals | 34 // Labels at 112 point (fortnightly) intervals |
35 quarter: {value: 3, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, | 35 quarter: {value: 3, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, |
36 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, | 36 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, |
37 }; | 37 }; |
38 | 38 |
39 // Parent container of all line graphs | 39 // All metrics have entries, but those not displayed have empty div list. |
Dan Beam
2012/06/28 21:39:14
have an empty div list?
clintstaley
2012/06/29 22:34:20
I tend to omit definite articles in phrases qualif
| |
40 this.chartDiv = $('#charts'); | 40 // If a div list is nonempty, the associated data will be nonull, or null but |
Dan Beam
2012/06/28 21:39:14
not empty, non-null
clintstaley
2012/06/29 22:34:20
Done.
| |
41 // about to be filled by webui response. Any metric with nonempty div | |
Dan Beam
2012/06/28 21:39:14
I think all these object literals would be easier
clintstaley
2012/06/29 22:34:20
Done.
| |
42 // list but null data is awaiting a data response from the webui. | |
43 this.metricMap = { | |
44 jankiness: {divs: [], yAxis: {max: 100, color: 'rgb(255, 128, 128)'}, | |
45 data: null, description: 'Jankiness', units: 'milliJanks'}, | |
Dan Beam
2012/06/28 21:39:14
I think it'd be easier to read if indented like so
clintstaley
2012/06/29 22:34:20
Done.
| |
46 oddness: {divs: [], yAxis: {max: 20, color: 'rgb(0, 192, 0)'}, | |
47 data: null, description: 'Oddness', units: 'kOdds'} | |
48 }; | |
41 | 49 |
42 // Parent container of checkboxes to choose metrics to display | 50 // Similar data for events, though no yAxis info is needed since events |
43 this.chooseMetricsDiv = $('#chooseMetrics'); | 51 // are simply labelled markers at X locations. Rules regarding null data |
52 // with nonempty div list apply here as for metricMap above. | |
53 this.eventMap = { | |
54 wampusAttacks: {divs: [], description: 'Wampus Attack', | |
55 color: 'rgb(0, 0, 255)', data: null}, | |
56 solarEclipses: {divs: [], description: 'Solar Eclipse', | |
57 color: 'rgb(255, 0, 255)', data: null} | |
58 }; | |
44 | 59 |
45 // Parent container of checkboxes to choose event types to display | 60 // Array of objects {start, end} representing times the browser was active |
46 this.chooseEventsDiv = $('#chooseEvents'); | 61 // and collecting metrics and events. |
62 this.intervals = []; | |
47 | 63 |
48 // Parent container of radio buttons to select time range | |
49 this.timeDiv = $('#chooseTimeRange'); | |
50 | 64 |
51 this.metricMap = {}; // MetricName => {div, lineChart, dataTable} objects | 65 // Set up the radio button set to choose time range. Use div#radioTemplate |
52 this.eventMap = {}; // EventName => event point lists, as returned by webui | 66 // as a model. |
Dan Beam
2012/06/28 21:39:14
as a template. (model implies something else)
clintstaley
2012/06/29 22:34:20
Done.
| |
53 this.intervals = []; // Array of objects {start, end} | 67 this.setupTimeRangeChooser = function() { |
68 var timeDiv = docGet('#chooseTimeRange'); | |
69 var radioTemplate = docGet('#radioTemplate'); | |
70 var controller = this; | |
54 | 71 |
72 for (var time in this.TimeRange) { | |
73 var timeRange = this.TimeRange[time]; | |
74 var radio = radioTemplate.cloneNode(true); | |
75 var input = radio.querySelector('input'); | |
76 | |
77 input.value = timeRange.value; | |
78 radio.querySelector('span').innerText = timeRange.name; | |
79 timeDiv.appendChild(radio); | |
80 timeRange.element = input; | |
81 radio.timeRange = timeRange; | |
82 radio.addEventListener('click', function() { | |
Dan Beam
2012/06/28 21:39:14
instead of creating an event listener for each rad
clintstaley
2012/06/29 22:34:20
Done.
| |
83 controller.setTimeRange(this.timeRange); | |
84 }); | |
85 } | |
86 }; | |
87 | |
88 // Generalized function for setting up checkbox blocks for either events | |
89 // or metrics. Take a div ID |divId| into which to place the checkboxes, | |
Dan Beam
2012/06/28 21:39:14
comments describing the parameters to this functio
clintstaley
2012/06/29 22:34:20
thanks. I'd been wondering about these, but didn'
| |
90 // and a map |optionMap| with values that each include a property | |
91 // |description|. Set up one checkbox for each entry in |optionMap| | |
92 // labelled with that description. Arrange callbacks to function |check| | |
93 // or |uncheck|, passing them the key of the checked or unchecked option, | |
94 // when the relevant checkbox state changes. | |
95 this.setupCheckboxes = function(divId, optionMap, check, uncheck) { | |
96 var checkboxTemplate = docGet('#checkboxTemplate'); | |
97 var chooseMetricsDiv = docGet(divId); | |
98 | |
99 for (var option in optionMap) { | |
100 var checkbox = checkboxTemplate.cloneNode(true); | |
101 checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + | |
Dan Beam
2012/06/28 21:39:14
you could also just .querySelector('span') here
clintstaley
2012/06/29 22:34:20
Done.
| |
102 optionMap[option].description; | |
103 chooseMetricsDiv.appendChild(checkbox); | |
104 | |
105 var input = checkbox.querySelector('input'); | |
106 input.addEventListener('change', function(check, uncheck) { | |
107 if (this.checked) | |
108 check(); | |
109 else | |
110 uncheck(); | |
111 }.bind(input, check.bind(this, option), uncheck.bind(this, option))); | |
Dan Beam
2012/06/28 21:39:14
why are you doing all these binds?
clintstaley
2012/06/29 22:34:20
Cause I'm an idiot :). More specifically, I'm new
| |
112 } | |
113 }; | |
114 | |
115 /* Outdated specialized functions replaced by setupCheckboxes. Included | |
Dan Beam
2012/06/28 21:39:14
I'm confused, what do you want me to do with this?
clintstaley
2012/06/29 22:34:20
The function with all the binds could be viewed as
| |
116 here in case the reviewer prefers them. Will be deleted before landing CL | |
117 otherwise. | |
118 this.setupMetricChooser = function() { | |
119 var checkboxTemplate = docGet('#checkboxTemplate'); | |
120 var chooseMetricsDiv = docGet('#chooseMetrics'); | |
121 var controller = this; | |
122 | |
123 for (var metric in this.metricMap) { | |
124 var checkbox = checkboxTemplate.cloneNode(true); | |
125 checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + | |
126 this.metricMap[metric].description; | |
127 chooseMetricsDiv.appendChild(checkbox); | |
128 | |
129 var input = checkbox.querySelector('input'); | |
130 input.metric = metric; | |
131 input.addEventListener('change', function() { | |
132 if (this.checked) | |
133 controller.addMetric(this.metric); | |
134 else | |
135 controller.dropMetric(this.metric); | |
136 }); | |
137 } | |
138 }; | |
139 | |
140 this.setupEventChooser = function() { | |
141 var checkboxTemplate = docGet('#checkboxTemplate'); | |
142 var chooseEventsDiv = docGet('#chooseEvents'); | |
143 var controller = this; | |
144 | |
145 for (var event in this.eventMap) { | |
146 var checkbox = checkboxTemplate.cloneNode(true); | |
147 checkbox.getElementsByTagName('span')[0].innerText = 'Show ' + | |
148 this.eventMap[event].description; | |
149 chooseEventsDiv.appendChild(checkbox); | |
150 | |
151 var input = checkbox.querySelector('input'); | |
152 input.event = event; | |
153 input.addEventListener('change', function() { | |
154 if (this.checked) | |
155 controller.addEventType(this.event); | |
156 else | |
157 controller.dropEventType(this.event); | |
158 }); | |
159 } | |
160 }; | |
161 */ | |
162 | |
163 // Set up just one chart in which all metrics will be displayed | |
164 // initially. But, the design readily accommodates addition of | |
165 // new charts, and movement of metrics into those other charts. | |
166 this.setupMainChart = function() { | |
167 this.chartParent = docGet('#charts'); | |
168 this.charts = [document.createElement('div')]; | |
169 this.charts[0].className = 'chart'; | |
170 this.chartParent.appendChild(this.charts[0]); | |
171 } | |
Dan Beam
2012/06/28 21:39:14
all function expressions (something = function(){}
clintstaley
2012/06/29 22:34:20
Done.
| |
172 | |
173 // Set the time range for which to display metrics and events. For | |
174 // now, the time range always ends at "now", but future implementations | |
175 // may allow time ranges not so anchored. | |
55 this.setTimeRange = function(range) { | 176 this.setTimeRange = function(range) { |
56 this.range = range; | 177 this.range = range; |
57 this.end = Math.floor(new Date().getTime() / range.resolution) * | 178 this.end = Math.floor(new Date().getTime() / range.resolution) * |
Dan Beam
2012/06/28 21:39:14
instead of new Date().getTime() -> Date.now()
clintstaley
2012/06/29 22:34:20
Done.
| |
58 range.resolution; | 179 range.resolution; |
180 | |
181 // Do an offset to accommodate Flot, which has no timezone management | |
182 this.end -= new Date().getTimezoneOffset() * 60000; | |
Dan Beam
2012/06/28 21:39:14
what is this doing?
clintstaley
2012/06/29 22:34:20
Commment expanded upon...
| |
59 this.start = this.end - range.timeSpan; | 183 this.start = this.end - range.timeSpan; |
60 this.requestIntervals(); | 184 this.requestIntervals(); |
61 } | 185 } |
62 | 186 |
63 // Return mock interval set for testing | 187 // Return mock interval set for testing |
64 this.getMockIntervals = function() { | 188 this.getMockIntervals = function() { |
65 var interval = this.end - this.start; | 189 var interval = this.end - this.start; |
190 | |
66 return [ | 191 return [ |
67 {'start': this.start + interval * .1, | 192 {'start': this.start + interval * .1, |
Dan Beam
2012/06/28 21:39:14
remove quotes around 'key': names and line things
clintstaley
2012/06/29 22:34:20
Done.
| |
68 'end': this.start + interval * .2}, | 193 'end': this.start + interval * .2}, |
69 {'start': this.start + interval * .7, 'end': this.start + interval * .9} | 194 {'start': this.start + interval * .7, 'end': this.start + interval} |
70 ]; | 195 ]; |
71 } | 196 } |
72 // Request array of objects with start and end fields showing browser | 197 |
73 // activity intervals in the specified time range | 198 // Request activity intervals in the specified time range |
74 this.requestIntervals = function() { | 199 this.requestIntervals = function() { |
75 this.onReceiveIntervals(this.getMockIntervals()); | 200 this.onReceiveIntervals(this.getMockIntervals()); |
76 // Replace with: chrome.send('getIntervals', this.start, this.end, | 201 // Replace with: chrome.send('getIntervals', this.start, this.end, |
Dan Beam
2012/06/28 21:39:14
when will you do this?
clintstaley
2012/06/29 22:34:20
Next phase, once we get the UI code OK'ed and land
Dan Beam
2012/06/29 23:18:59
OK.
| |
77 // this.onReceiveIntervals); | 202 // this.onReceiveIntervals); |
78 } | 203 } |
79 | 204 |
80 // Webui callback delivering response from requestIntervals call. Assumes | 205 // Webui callback delivering response from requestIntervals call. Assumes |
81 // this is a new time range choice, which results in complete refresh of | 206 // this is a new time range choice, which results in complete refresh of |
82 // all metrics and event types that are currently selected. | 207 // all metrics and event types that are currently selected. |
83 this.onReceiveIntervals = function(intervals) { | 208 this.onReceiveIntervals = function(intervals) { |
84 this.intervals = intervals; | 209 this.intervals = intervals; |
85 for (var metric in this.metricMap) | |
86 this.refreshMetric(metric); | |
87 for (var eventType in this.eventMap) | |
88 this.refreshEvent(eventType); | |
89 } | |
90 | 210 |
91 // Add a new metric, and its associated linegraph. The linegraph for | 211 for (var metric in this.metricMap) { |
92 // each metric has a discrete domain of times. This is not continuous | 212 var metricValue = this.metricMap[metric]; |
93 // because of breaks for each interval of activity. (No point in showing | 213 if (metricValue.divs.length > 0) { // If we're displaying this metric. |
Dan Beam
2012/06/28 21:39:14
nit: no curlies for 1 line if (/* conditional */)
clintstaley
2012/06/29 22:34:20
Done.
| |
94 // a lot of "dead air" when the browser wasn't running.) Column 1 of | 214 this.refreshMetric(metric); |
95 // its DataTable is the metric data, and higher numbered columns are added | 215 } |
96 // in pairs for each event type currently chosen. Each pair gives the | 216 } |
97 // event occurence points, and mouseover detailed description for one event | |
98 // type. | |
99 this.addMetric = function(metric) { | |
100 if (!(metric in this.metricMap)) { | |
101 var div = document.createElement('div'); | |
102 | 217 |
103 div.className = 'chart'; | 218 for (var eventType in this.eventMap) { |
104 this.chartDiv.appendChild(div); | 219 var eventValue = this.eventMap[eventType]; |
105 | 220 if (eventValue.divs.length > 0) { |
Dan Beam
2012/06/28 21:39:14
nit: no curlies
clintstaley
2012/06/29 22:34:20
Done.
| |
106 var table = new google.visualization.DataTable(); | 221 this.refreshEventType(eventType); |
107 var chart = new google.visualization.LineChart(div); | 222 } |
108 this.metricMap[metric] = {'div': div, 'chart': chart, 'table': table}; | |
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 } | 223 } |
117 } | 224 } |
118 | 225 |
119 // Remove a metric from the UI | 226 // Add a new metric to the main (currently only) chart. |
120 this.dropMetric = function(metric) { | 227 this.addMetric = function(metric) { |
121 if (metric in this.metricMap) { | 228 this.metricMap[metric].divs.push(this.charts[0]); |
122 this.chartDiv.removeChild(this.metricMap[metric].div); | 229 this.refreshMetric(metric); |
123 delete this.metricMap[metric]; | |
124 } | |
125 } | 230 } |
126 | 231 |
127 // Return mock metric data points for testing | 232 // Remove a metric from the chart(s). |
128 this.getMockDataPoints = function() { | 233 this.dropMetric = function(metric) { |
234 var metricValue = this.metricMap[metric]; | |
235 var affectedCharts = metricValue.divs; | |
236 metricValue.divs = []; | |
237 | |
238 affectedCharts.forEach(this.drawChart, this); | |
239 } | |
240 | |
241 // Return mock metric data points for testing. Give values ranging from | |
242 // offset to max-offset. (This let us avoid direct overlap of | |
243 // different mock data sets in an ugly way that will die in the next | |
244 // version anyway.) | |
245 this.getMockDataPoints = function(max, offset) { | |
129 var dataPoints = []; | 246 var dataPoints = []; |
130 | 247 |
131 for (var x = 0; x < this.intervals.length; x++) { | 248 for (var x = 0; x < this.intervals.length; x++) { |
132 // Rise from low 0 to high 100 every 20 min | 249 // Rise from low offset to high max-offset in 100 point steps |
133 for (var time = this.intervals[x].start; time <= this.intervals[x].end; | 250 for (var time = this.intervals[x].start; time <= this.intervals[x].end; |
134 time += this.range.resolution) | 251 time += this.range.resolution) |
135 dataPoints.push({'time': time, 'value': time % 1000000 / 10000}); | 252 dataPoints.push({'time': time, 'value': offset + time / |
253 this.range.resolution % 100 * (max - 2 * offset) / 100}); | |
136 } | 254 } |
137 return dataPoints; | 255 return dataPoints; |
138 } | 256 } |
139 | 257 |
140 // Request new metric data, assuming the metric table and chart already | 258 // Request new metric data, assuming the metric table and chart already |
141 // exist. | 259 // exist. |
142 this.refreshMetric = function(metric) { | 260 this.refreshMetric = function(metric) { |
143 this.onReceiveMetric(metric, this.getMockDataPoints()); | 261 var metricValue = this.metricMap[metric]; |
262 | |
263 metricValue.data = null; // Mark metric as awaiting response. | |
264 this.onReceiveMetric(metric, | |
265 this.getMockDataPoints(metricValue.yAxis.max, 5)); | |
144 // Replace with: | 266 // Replace with: |
145 // chrome.send("getMetric", this.range.start, this.range.end, | 267 // chrome.send("getMetric", this.range.start, this.range.end, |
146 // this.range.resolution, this.onReceiveMetric); | 268 // this.range.resolution, this.onReceiveMetric); |
147 } | 269 } |
148 | 270 |
149 // Receive new datapoints for |metric|, and completely refresh the DataTable | 271 // Receive new datapoints for |metric|, convert the data to Flot-usable |
150 // for that metric, redrawing the chart. (We cannot preserve the event | 272 // form, and redraw all affected charts. |
151 // columns because entirely new rows may be implied by the new metric | |
152 // datapoints.) | |
153 this.onReceiveMetric = function(metric, points) { | 273 this.onReceiveMetric = function(metric, points) { |
274 var metricValue = this.metricMap[metric]; | |
275 | |
154 // Might have been dropped while waiting for data | 276 // Might have been dropped while waiting for data |
155 if (!(metric in this.metricMap)) | 277 if (metricValue.divs.length == 0) |
156 return; | 278 return; |
157 | 279 |
158 var data = this.metricMap[metric].table; | 280 var series = []; |
281 metricValue.data = [series]; | |
159 | 282 |
160 data.removeRows(0, data.getNumberOfRows()); | 283 // Traverse the points, and the intervals, in parallel. Both are in |
161 | 284 // ascending time order. Create a sequence of data "series" (per Flot) |
162 // Traverse the points, which are in time order, and the intervals, | 285 // arrays, with each series comprising all points within a given interval. |
163 // placing each value in the interval (if any) in which it belongs. | |
164 var interval = this.intervals[0]; | 286 var interval = this.intervals[0]; |
165 var intervalIndex = 0; | 287 var intervalIndex = 0; |
166 var valueIndex = 0; | 288 var point; |
167 var value; | 289 var pointIndex = 0; |
168 while (valueIndex < points.length && | 290 while (pointIndex < points.length && |
169 intervalIndex < this.intervals.length) { | 291 intervalIndex < this.intervals.length) { |
170 value = points[valueIndex++]; | 292 point = points[pointIndex++]; |
171 while (value.time > interval.end && | 293 while (point.time > interval.end && |
172 intervalIndex < this.intervals.length) { | 294 intervalIndex < this.intervals.length) { |
173 interval = this.intervals[++intervalIndex]; // Jump to new interval | 295 interval = this.intervals[++intervalIndex]; // Jump to new interval |
174 data.addRow(null, null); // Force gap in line chart | 296 if (series.length > 0) |
297 metricValue.data.push(series = []); | |
175 } | 298 } |
176 if (intervalIndex < this.intervals.length && value.time > interval.start) | 299 if (intervalIndex < this.intervals.length && point.time > interval.start) |
177 if (data.getNumberOfRows() % this.range.labelEvery == 0) | 300 series.push([point.time, point.value]); |
178 data.addRow([new Date(value.time).toString(this.range.format), | 301 } |
179 value.value]); | 302 |
180 else | 303 metricValue.divs.forEach(this.drawChart, this); |
181 data.addRow(['', value.value]); | |
182 } | |
183 this.drawChart(metric); | |
184 } | 304 } |
185 | 305 |
186 // Add a new event to all line graphs. | 306 // Add a new event to the chart(s). |
187 this.addEventType = function(eventType) { | 307 this.addEventType = function(eventType) { |
188 if (!(eventType in this.eventMap)) { | 308 this.eventMap[eventType].divs = this.charts; // Events show on all charts |
189 this.eventMap[eventType] = []; | 309 this.refreshEventType(eventType); |
190 this.refreshEventType(eventType); | |
191 } | |
192 } | 310 } |
193 | 311 |
194 // Return mock event point for testing | 312 // Remove an event from the chart(s) |
313 this.dropEventType = function(eventType) { | |
314 var eventValue = this.eventMap[eventType]; | |
315 var affectedCharts = eventValue.divs; | |
316 eventValue.divs = []; | |
317 | |
318 affectedCharts.forEach(this.drawChart, this); | |
319 } | |
320 | |
321 // Return mock event points for testing | |
195 this.getMockEventValues = function(eventType) { | 322 this.getMockEventValues = function(eventType) { |
196 var mockValues = []; | 323 var mockValues = []; |
197 for (var i = 0; i < this.intervals.length; i++) { | 324 for (var i = 0; i < this.intervals.length; i++) { |
198 var interval = this.intervals[i]; | 325 var interval = this.intervals[i]; |
199 | 326 |
200 mockValues.push({ | 327 mockValues.push({ |
201 time: interval.start, | 328 time: interval.start, |
202 shortDescription: eventType, | |
203 longDescription: eventType + ' at ' + | 329 longDescription: eventType + ' at ' + |
204 new Date(interval.start) + ' blah, blah blah'}); | 330 new Date(interval.start) + ' blah, blah blah'}); |
205 mockValues.push({ | 331 mockValues.push({ |
206 time: (interval.start + interval.end) / 2, | 332 time: (interval.start + interval.end) / 2, |
207 shortDescription: eventType, | |
208 longDescription: eventType + ' at ' + | 333 longDescription: eventType + ' at ' + |
209 new Date((interval.start + interval.end) / 2) + ' blah, blah blah'}); | 334 new Date((interval.start + interval.end) / 2) + ' blah, blah blah'}); |
210 mockValues.push({ | 335 mockValues.push({ |
211 time: interval.end, | 336 time: interval.end, |
212 shortDescription: eventType, | |
213 longDescription: eventType + ' at ' + new Date(interval.end) + | 337 longDescription: eventType + ' at ' + new Date(interval.end) + |
214 ' blah, blah blah'}); | 338 ' blah, blah blah'}); |
215 } | 339 } |
216 return mockValues; | 340 return mockValues; |
217 } | 341 } |
218 | 342 |
219 // Request new data for |eventType|, for times in the current range. | 343 // Request new data for |eventType|, for times in the current range. |
220 this.refreshEventType = function(eventType) { | 344 this.refreshEventType = function(eventType) { |
345 this.eventMap[eventType].data = null; // Mark eventType as awaiting response | |
221 this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); | 346 this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); |
222 // Replace with: | 347 // Replace with: |
223 // chrome.send("getEvents", eventType, this.range.start, this.range.end); | 348 // chrome.send("getEvents", eventType, this.range.start, this.range.end); |
224 } | 349 } |
225 | 350 |
226 // Add an event column pair to DataTable |table| for |eventType| | 351 // Receive new data for |eventType|. If the event has been deselected while |
227 this.addEventColumns = function(table, eventType) { | 352 // awaiting webui response, do nothing. Otherwise, save the data directly, |
228 var annotationCol = table.addColumn({'id': eventType, type: 'string', | 353 // since events are handled differently than metrics when drawing |
229 role: 'annotation'}); | 354 // (no "series"), and redraw all the affected charts. |
230 var rolloverCol = table.addColumn({'id': eventType + 'Tip', type: 'string', | 355 this.onReceiveEventType = function(eventType, values) { |
231 role: 'annotationText'}); | 356 var eventValue = this.eventMap[eventType]; |
232 var values = this.eventMap[eventType]; | |
233 var interval = this.intervals[0], intervalIndex = 0; | |
234 | 357 |
235 for (var i = 0; i < values.length; i++) { | 358 if (eventValue.divs.length == 0) |
236 var event = values[i]; | 359 return; |
237 var rowIndex = 0; | 360 |
238 while (event.time > interval.end && | 361 eventValue.data = values; |
239 intervalIndex < this.intervals.length - 1) | 362 eventValue.divs.forEach(this.drawChart, this); |
240 { | 363 } |
241 // Skip interval times, inclusive of interval.end, and of following null | 364 |
242 rowIndex += (interval.end - interval.start) / this.range.resolution + 2; | 365 // Return an object containing an array of metrics and another of events |
243 interval = this.intervals[++intervalIndex]; | 366 // that include |chart| as one of the divs into which they display. |
367 this.getChartData = function(chart) { | |
368 var result = {metrics: [], events: []}; | |
369 | |
370 for (var metric in this.metricMap) { | |
371 var metricValue = this.metricMap[metric]; | |
372 | |
373 if (metricValue.divs.indexOf(chart) != -1) | |
374 result.metrics.push(metricValue); | |
375 } | |
376 | |
377 for (var eventType in this.eventMap) { | |
378 var eventValue = this.eventMap[eventType]; | |
379 | |
380 if (eventValue.divs.length > 0) | |
381 result.events.push(eventValue); | |
382 } | |
383 | |
384 return result; | |
385 } | |
386 | |
387 // Check all entries in an object of the type returned from getChartData, | |
388 // above, to see if all events and metrics have completed data (none is | |
389 // awaiting an asynchronous webui response to get their current data). | |
390 this.isDataReady = function(chartData) { | |
391 for (var x = 0; x < chartData.metrics.length; x++) | |
Dan Beam
2012/06/28 21:39:14
nit: curlies around the for loop (as it's more tha
clintstaley
2012/06/29 22:34:20
But it's one statement :)
Done
| |
392 if (chartData.metrics[x].data == null) | |
393 return false; | |
394 | |
395 for (var x = 0; x < chartData.events.length; x++) | |
Dan Beam
2012/06/28 21:39:14
nit: see above, also why are you using x as a loop
clintstaley
2012/06/29 22:34:20
Seen it done other places; never sure what the rul
Dan Beam
2012/06/29 23:18:59
implied geometry, ya (you're also using {x,y}axis
| |
396 if (chartData.events[x].data == null) | |
397 return false; | |
398 | |
399 return true; | |
400 } | |
401 | |
402 // Create and return an array of "markings" (per Flot), representing | |
403 // vertical lines at the event time, in the event's color. Also add | |
404 // (not per Flot) a |description| property to each, to be used for hand | |
405 // creating description boxes. | |
406 this.getEventMarks = function(eventValues) { | |
407 var markings = []; | |
408 | |
409 for (var x = 0; x < eventValues.length; x++) { | |
410 var eventValue = eventValues[x]; | |
411 for (var d = 0; d < eventValue.data.length; d++) { | |
412 var point = eventValue.data[d]; | |
413 markings.push({xaxis: {from: point.time, to: point.time}, | |
Dan Beam
2012/06/28 21:39:14
I think it'd be cleaner to read if there were line
clintstaley
2012/06/29 22:34:20
Done.
| |
414 color: eventValue.color, description: eventValue.description}); | |
244 } | 415 } |
245 if (event.time >= interval.start && event.time <= interval.end) { | 416 } |
246 table.setCell(rowIndex + (event.time - interval.start) / | 417 |
247 this.range.resolution, annotationCol, event.shortDescription); | 418 return markings; |
248 table.setCell(rowIndex + (event.time - interval.start) / | 419 } |
249 this.range.resolution, rolloverCol, event.longDescription); | 420 |
421 // Redraw the chart in div |chart|, IF all its dependent data is present. | |
422 // Otherwise simply return, and await another call when all data is | |
423 // available. | |
424 this.drawChart = function(chart) { | |
425 var chartData = this.getChartData(chart); | |
426 var seriesSeq = []; | |
427 var yAxes = []; | |
428 | |
429 if (!this.isDataReady(chartData)) | |
Dan Beam
2012/06/28 21:39:14
move this if () to before:
var seriesSeq = [];
clintstaley
2012/06/29 22:34:20
Done.
| |
430 return; | |
431 | |
432 chartData.metrics.forEach(function(value) { | |
433 yAxes.push(value.yAxis); | |
434 for (var run = 0; run < value.data.length; run++) { | |
435 seriesSeq.push({ | |
Dan Beam
2012/06/28 21:39:14
I think only 2 spaces indented here (instead of 4)
clintstaley
2012/06/29 22:34:20
Yeah, wasn't sure if it counted as a line continua
| |
436 color: value.yAxis.color, | |
437 data: value.data[run], | |
438 label: run == 0 ? value.description + '(' + value.label + ')' : | |
439 null, | |
440 yaxis: yAxes.length, // Use just-added Y axis | |
441 }); | |
250 } | 442 } |
443 }); | |
444 | |
445 var markings = this.getEventMarks(chartData.events); | |
446 var chart = this.charts[0]; | |
447 var plot = $.plot(chart, seriesSeq, { | |
Dan Beam
2012/06/28 21:39:14
same here about 2 \s instead of 4
clintstaley
2012/06/29 22:34:20
Done.
| |
448 yaxes: yAxes, | |
449 xaxis: {mode: 'time'}, | |
450 grid: {markings: markings}}); | |
451 | |
452 // Fore each event in |markings|, create also a label div, with left | |
453 // edge colinear with the event vertical-line. Top of label is | |
454 // presently a hack-in, putting labels in three tiers of 25px height | |
455 // each to avoid overlap. Will need something better. | |
456 var labelTemplate = docGet('#labelTemplate'); | |
457 for (var x = 0; x < markings.length; x++) { | |
458 var mark = markings[x]; | |
459 var point = plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, | |
Dan Beam
2012/06/28 21:39:14
optional nit: may be easier to read if you do this
clintstaley
2012/06/29 22:34:20
Done.
| |
460 yaxis: 1}); | |
461 var labelDiv = labelTemplate.cloneNode(true); | |
462 labelDiv.innerText = mark.description; | |
463 labelDiv.style.left = point.left + 'px'; | |
464 labelDiv.style.top = (point.top + 25 * (x % 3)) + 'px'; | |
465 chart.appendChild(labelDiv); | |
251 } | 466 } |
252 } | 467 } |
253 | 468 |
254 this.dropEventColumns = function(table, eventType) { | 469 this.setupCheckboxes('#chooseMetrics', this.metricMap, this.addMetric, |
Dan Beam
2012/06/28 21:39:14
optional nit: same general readability comment her
clintstaley
2012/06/29 22:34:20
Done.
| |
255 var colIndex, numCols = table.getNumberOfColumns(); | 470 this.dropMetric); |
256 | 471 this.setupCheckboxes('#chooseEvents', this.eventMap, this.addEventType, |
257 for (colIndex = 0; colIndex < numCols; colIndex++) | 472 this.dropEventType); |
258 if (table.getColumnId(colIndex) == eventType) | |
259 break; | |
260 | |
261 if (colIndex < numCols) { | |
262 table.removeColumn(colIndex + 1); | |
263 table.removeColumn(colIndex); | |
264 } | |
265 } | |
266 | |
267 // Receive new data for |eventType|. Save this in eventMap for future | |
268 // redraws. Then, for each metric linegraph remove any current column pair | |
269 // for |eventType| and replace it with a new pair, which will reflect the | |
270 // new data. Redraw the linegraph. | |
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(); | 473 this.setupTimeRangeChooser(); |
474 this.setupMainChart(); | |
361 this.TimeRange.day.element.click(); | 475 this.TimeRange.day.element.click(); |
362 } | 476 } |
OLD | NEW |