Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(141)

Side by Side Diff: chrome/browser/resources/performance_monitor/chart.js

Issue 10679009: Revision to CPM UI using Flot and JQuery (Closed) Base URL: http://git.chromium.org/chromium/src.git@master
Patch Set: Version without nonminified Flot code Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698