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

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: New version with comments, event changes, etc. Created 8 years, 5 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
« no previous file with comments | « chrome/browser/resources/performance_monitor/chart.html ('k') | third_party/flot/LICENSE.txt » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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']}); 7 function querySelect(criterion) {
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() {
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, {
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 *
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. Any metric with non-empty
50 59 * div list but null data is awaiting a data response from the webui.
koz (OOO until 15th September) 2012/07/02 06:14:48 This last sentence seems to be just a repetition o
clintstaley 2012/07/02 19:09:05 I was repeating for clarification. (Teacher's ins
51 this.metricMap = {}; // MetricName => {div, lineChart, dataTable} objects 60 */
52 this.eventMap = {}; // EventName => event point lists, as returned by webui 61 this.metricMap = {
53 this.intervals = []; // Array of objects {start, end} 62 jankiness: {
54 63 divs: [],
64 yAxis: {max: 100, color: 'rgb(255, 128, 128)'},
65 data: null,
66 description: 'Jankiness',
67 units: 'milliJanks'
68 },
69 oddness: {
70 divs: [],
71 yAxis: {max: 20, color: 'rgb(0, 192, 0)'},
72 data: null,
73 description: 'Oddness',
74 units: 'kOdds'
75 }
76 };
77
78 /** @type {Object.<string, {
79 * divs: !Array.<DOMElement>,
80 * description: !string,
81 * color: !string,
82 * data: ?Array.<{time: !number, longDescription: !string}>
83 * }>}
84 *
85 * Similar data for events, though no yAxis info is needed since events
86 * are simply labelled markers at X locations. Rules regarding null data
87 * with non-empty div list apply here as for metricMap above.
88 */
89 this.eventMap = {
90 wampusAttacks: {
91 divs: [],
92 description: 'Wampus Attack',
93 color: 'rgb(0, 0, 255)',
94 data: null
95 },
96 solarEclipses: {
97 divs: [],
98 description: 'Solar Eclipse',
99 color: 'rgb(255, 0, 255)',
100 data: null
101 }
102 };
103
104 /**
105 * @type {!Array.<{start: !number, end: !number}>}
106 * Time periods in which the browser was active and collecting metrics
107 * and events.
108 */
109 this.intervals = [];
110
111 /**
112 * Set up the radio button set to choose time range. Use div#radioTemplate
113 * as a template.
114 */
115 this.setupTimeRangeChooser = function() {
116 var timeDiv = querySelect('#chooseTimeRange');
117 var radioTemplate = querySelect('#radioTemplate');
118 var controller = this;
119
120 for (var time in this.TimeRange) {
121 var timeRange = this.TimeRange[time];
122 var radio = radioTemplate.cloneNode(true);
123 var input = radio.querySelector('input');
124
125 input.value = timeRange.value;
126 input.timeRange = timeRange;
127 radio.querySelector('span').innerText = timeRange.name;
128 timeDiv.appendChild(radio);
129 timeRange.element = input;
130 }
131 timeDiv.addEventListener('click', function(e) {
132 if (!e.target.webkitMatchesSelector('input[type="radio"]'))
133 return;
134
135 controller.setTimeRange(e.target.timeRange);
136 });
137 };
138
139 /**
140 * Generalized function for setting up checkbox blocks for either events
141 * or metrics. Take a div ID |divId| into which to place the checkboxes,
142 * and a map |optionMap| with values that each include a property
143 * |description|. Set up one checkbox for each entry in |optionMap|
144 * labelled with that description. Arrange callbacks to function |check|
145 * or |uncheck|, passing them the key of the checked or unchecked option,
146 * when the relevant checkbox state changes.
147 *
148 * @param {!string} divId Id of division into which to put checkboxes
149 * @param {!Object} optionMap map of metric/event entries
150 * @param {!function(this:Controller, Object)} check
151 * function to select an entry (metric or event)
152 * @param {!function(this:Controller, Object)} uncheck
153 * function to deselect an entry (metric or event)
154 */
155
156 this.setupCheckboxes = function(divId, optionMap, check, uncheck) {
157 var checkboxTemplate = querySelect('#checkboxTemplate');
158 var chooseMetricsDiv = querySelect(divId);
159 var doCheck = check.bind(this);
160 var doUncheck = uncheck.bind(this);
161
162 for (var option in optionMap) {
163 var checkbox = checkboxTemplate.cloneNode(true);
164 checkbox.querySelector('span').innerText = 'Show ' +
165 optionMap[option].description;
166 chooseMetricsDiv.appendChild(checkbox);
167
168 var input = checkbox.querySelector('input');
169 input.option = option;
170 input.addEventListener('change', function(e) {
171 if (e.target.checked)
172 doCheck(e.target.option);
173 else
174 doUncheck(e.target.option);
175 });
176 }
177 };
178
179 /**
180 * Set up just one chart in which all metrics will be displayed
181 * initially. But, the design readily accommodates addition of
182 * new charts, and movement of metrics into those other charts.
183 */
184 this.setupMainChart = function() {
185 this.chartParent = querySelect('#charts');
186 this.charts = [document.createElement('div')];
187 this.charts[0].className = 'chart';
188 this.chartParent.appendChild(this.charts[0]);
189 };
190
191 /**
192 * Set the time range for which to display metrics and events. For
193 * now, the time range always ends at "now", but future implementations
194 * may allow time ranges not so anchored.
195 *
196 * @param {!{start: !number, end: !number, resolution: !number}} range
197 */
55 this.setTimeRange = function(range) { 198 this.setTimeRange = function(range) {
56 this.range = range; 199 this.range = range;
57 this.end = Math.floor(new Date().getTime() / range.resolution) * 200 this.end = Math.floor(Date.now() / range.resolution) *
58 range.resolution; 201 range.resolution;
202
203 // Take the GMT value of this.end ("now") and subtract from it the
204 // number of minutes by which we lag GMT in the present timezone,
205 // X mS/minute. This will show time in the present timezone.
206 this.end -= new Date().getTimezoneOffset() * 60000;
59 this.start = this.end - range.timeSpan; 207 this.start = this.end - range.timeSpan;
60 this.requestIntervals(); 208 this.requestIntervals();
61 } 209 };
62 210
63 // Return mock interval set for testing 211 /**
212 * Return mock interval set for testing
213 * @return {!Array.<{start: !number, end: !number}>} intervals
214 */
64 this.getMockIntervals = function() { 215 this.getMockIntervals = function() {
65 var interval = this.end - this.start; 216 var interval = this.end - this.start;
217
66 return [ 218 return [
67 {'start': this.start + interval * .1, 219 { start: this.start + interval * .1,
68 'end': this.start + interval * .2}, 220 end: this.start + interval * .2
69 {'start': this.start + interval * .7, 'end': this.start + interval * .9} 221 },
222 { start: this.start + interval * .7,
223 end: this.start + interval
224 }
70 ]; 225 ];
71 } 226 };
72 // Request array of objects with start and end fields showing browser 227
73 // activity intervals in the specified time range 228 /**
229 * Request activity intervals in the current time range
230 */
74 this.requestIntervals = function() { 231 this.requestIntervals = function() {
75 this.onReceiveIntervals(this.getMockIntervals()); 232 this.onReceiveIntervals(this.getMockIntervals());
76 // Replace with: chrome.send('getIntervals', this.start, this.end, 233 // Replace with: chrome.send('getIntervals', this.start, this.end,
77 // this.onReceiveIntervals); 234 // this.onReceiveIntervals);
78 } 235 };
79 236
80 // Webui callback delivering response from requestIntervals call. Assumes 237 /**
81 // this is a new time range choice, which results in complete refresh of 238 * Webui callback delivering response from requestIntervals call. Assumes
82 // all metrics and event types that are currently selected. 239 * this is a new time range choice, which results in complete refresh of
240 * all metrics and event types that are currently selected.
241 *
242 * @param {!Array.<{start: !number, end: !number}> intervals new intervals
243 */
83 this.onReceiveIntervals = function(intervals) { 244 this.onReceiveIntervals = function(intervals) {
84 this.intervals = intervals; 245 this.intervals = intervals;
85 for (var metric in this.metricMap) 246
86 this.refreshMetric(metric); 247 for (var metric in this.metricMap) {
87 for (var eventType in this.eventMap) 248 var metricValue = this.metricMap[metric];
88 this.refreshEvent(eventType); 249 if (metricValue.divs.length > 0) // If we're displaying this metric.
89 } 250 this.refreshMetric(metric);
90 251 }
91 // Add a new metric, and its associated linegraph. The linegraph for 252
92 // each metric has a discrete domain of times. This is not continuous 253 for (var eventType in this.eventMap) {
93 // because of breaks for each interval of activity. (No point in showing 254 var eventValue = this.eventMap[eventType];
94 // a lot of "dead air" when the browser wasn't running.) Column 1 of 255 if (eventValue.divs.length > 0)
95 // its DataTable is the metric data, and higher numbered columns are added 256 this.refreshEventType(eventType);
96 // in pairs for each event type currently chosen. Each pair gives the 257 }
97 // event occurence points, and mouseover detailed description for one event 258 };
98 // type. 259
260 /**
261 * Add a new metric to the main (currently only) chart.
262 *
263 * @param {!string} metric Metric to start displaying
264 */
99 this.addMetric = function(metric) { 265 this.addMetric = function(metric) {
100 if (!(metric in this.metricMap)) { 266 this.metricMap[metric].divs.push(this.charts[0]);
101 var div = document.createElement('div'); 267 this.refreshMetric(metric);
102 268 };
103 div.className = 'chart'; 269
104 this.chartDiv.appendChild(div); 270 /**
105 271 * Remove a metric from the chart(s).
106 var table = new google.visualization.DataTable(); 272 *
107 var chart = new google.visualization.LineChart(div); 273 * @param {!string} metric Metric to stop displaying
108 this.metricMap[metric] = {'div': div, 'chart': chart, 'table': table}; 274 */
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) { 275 this.dropMetric = function(metric) {
121 if (metric in this.metricMap) { 276 var metricValue = this.metricMap[metric];
122 this.chartDiv.removeChild(this.metricMap[metric].div); 277 var affectedCharts = metricValue.divs;
123 delete this.metricMap[metric]; 278 metricValue.divs = [];
124 } 279
125 } 280 affectedCharts.forEach(this.drawChart, this);
126 281 };
127 // Return mock metric data points for testing 282
128 this.getMockDataPoints = function() { 283 /**
284 * Return mock metric data points for testing. Give values ranging from
285 * offset to max-offset. (This let us avoid direct overlap of
koz (OOO until 15th September) 2012/07/02 06:14:48 let -> lets
clintstaley 2012/07/02 19:09:05 Done.
286 * different mock data sets in an ugly way that will die in the next
287 * version anyway.)
288 *
289 * @param {!number} max Max data value to return, less offset
290 * @param {!number} offset Adjustment factor
291 * @return {!Array.<{time: !number, value: !number}>}
292 */
293 this.getMockDataPoints = function(max, offset) {
129 var dataPoints = []; 294 var dataPoints = [];
130 295
131 for (var x = 0; x < this.intervals.length; x++) { 296 for (var i = 0; i < this.intervals.length; i++) {
132 // Rise from low 0 to high 100 every 20 min 297 // 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; 298 for (var time = this.intervals[i].start; time <= this.intervals[i].end;
134 time += this.range.resolution) 299 time += this.range.resolution)
135 dataPoints.push({'time': time, 'value': time % 1000000 / 10000}); 300 dataPoints.push({time: time, value: offset + time /
koz (OOO until 15th September) 2012/07/02 06:14:48 This expression could do with some extra parenthes
clintstaley 2012/07/02 19:09:05 Done.
301 this.range.resolution % 100 * (max - 2 * offset) / 100});
136 } 302 }
137 return dataPoints; 303 return dataPoints;
138 } 304 };
139 305
140 // Request new metric data, assuming the metric table and chart already 306 /**
141 // exist. 307 * Request new metric data, assuming the metric table and chart already
308 * exist.
309 *
310 * @param {!string} metric Metric for which to get data
311 */
142 this.refreshMetric = function(metric) { 312 this.refreshMetric = function(metric) {
143 this.onReceiveMetric(metric, this.getMockDataPoints()); 313 var metricValue = this.metricMap[metric];
314
315 metricValue.data = null; // Mark metric as awaiting response.
316 this.onReceiveMetric(metric,
317 this.getMockDataPoints(metricValue.yAxis.max, 5));
144 // Replace with: 318 // Replace with:
145 // chrome.send("getMetric", this.range.start, this.range.end, 319 // chrome.send("getMetric", this.range.start, this.range.end,
146 // this.range.resolution, this.onReceiveMetric); 320 // this.range.resolution, this.onReceiveMetric);
147 } 321 };
148 322
149 // Receive new datapoints for |metric|, and completely refresh the DataTable 323 /**
150 // for that metric, redrawing the chart. (We cannot preserve the event 324 * Receive new datapoints for |metric|, convert the data to Flot-usable
151 // columns because entirely new rows may be implied by the new metric 325 * form, and redraw all affected charts.
152 // datapoints.) 326 *
327 * @param {!string} metric Metric to which |points| applies
328 * @param {!Array.<{time: !number, value: !number}>} points
329 * new data points
330 */
153 this.onReceiveMetric = function(metric, points) { 331 this.onReceiveMetric = function(metric, points) {
332 var metricValue = this.metricMap[metric];
333
154 // Might have been dropped while waiting for data 334 // Might have been dropped while waiting for data
155 if (!(metric in this.metricMap)) 335 if (metricValue.divs.length == 0)
156 return; 336 return;
157 337
158 var data = this.metricMap[metric].table; 338 var series = [];
159 339 metricValue.data = [series];
160 data.removeRows(0, data.getNumberOfRows()); 340
161 341 // Traverse the points, and the intervals, in parallel. Both are in
162 // Traverse the points, which are in time order, and the intervals, 342 // ascending time order. Create a sequence of data "series" (per Flot)
163 // placing each value in the interval (if any) in which it belongs. 343 // arrays, with each series comprising all points within a given interval.
164 var interval = this.intervals[0]; 344 var interval = this.intervals[0];
165 var intervalIndex = 0; 345 var intervalIndex = 0;
166 var valueIndex = 0; 346 var point;
167 var value; 347 var pointIndex = 0;
168 while (valueIndex < points.length && 348 while (pointIndex < points.length &&
169 intervalIndex < this.intervals.length) { 349 intervalIndex < this.intervals.length) {
170 value = points[valueIndex++]; 350 point = points[pointIndex++];
171 while (value.time > interval.end && 351 while (point.time > interval.end &&
172 intervalIndex < this.intervals.length) { 352 intervalIndex < this.intervals.length) {
173 interval = this.intervals[++intervalIndex]; // Jump to new interval 353 interval = this.intervals[++intervalIndex]; // Jump to new interval
174 data.addRow(null, null); // Force gap in line chart 354 if (series.length > 0)
355 metricValue.data.push(series = []);
koz (OOO until 15th September) 2012/07/02 06:14:48 Could you put this on two lines, like: metricValu
clintstaley 2012/07/02 19:09:05 C coder's style; sorry :) Will change.
175 } 356 }
176 if (intervalIndex < this.intervals.length && value.time > interval.start) 357 if (intervalIndex < this.intervals.length && point.time > interval.start)
177 if (data.getNumberOfRows() % this.range.labelEvery == 0) 358 series.push([point.time, point.value]);
178 data.addRow([new Date(value.time).toString(this.range.format), 359 }
179 value.value]); 360
180 else 361 metricValue.divs.forEach(this.drawChart, this);
181 data.addRow(['', value.value]); 362 };
182 } 363
183 this.drawChart(metric); 364 /**
184 } 365 * Add a new event to the chart(s).
185 366 *
186 // Add a new event to all line graphs. 367 * @param {!string} eventType type of event to start displaying
368 */
187 this.addEventType = function(eventType) { 369 this.addEventType = function(eventType) {
188 if (!(eventType in this.eventMap)) { 370 this.eventMap[eventType].divs = this.charts; // Events show on all charts
189 this.eventMap[eventType] = []; 371 this.refreshEventType(eventType);
190 this.refreshEventType(eventType); 372 };
191 } 373
192 } 374 /*
193 375 * Remove an event from the chart(s)
194 // Return mock event point for testing 376 *
377 * @param {!string} eventType type of event to stop displaying
378 */
379 this.dropEventType = function(eventType) {
380 var eventValue = this.eventMap[eventType];
381 var affectedCharts = eventValue.divs;
382 eventValue.divs = [];
383
384 affectedCharts.forEach(this.drawChart, this);
385 };
386
387 /**
388 * Return mock event points for testing
389 *
390 * @param {!string} eventType type of event to generate mock data for
391 * @return {!Array.<{time: !number, longDescription: !string}>}
392 */
195 this.getMockEventValues = function(eventType) { 393 this.getMockEventValues = function(eventType) {
196 var mockValues = []; 394 var mockValues = [];
197 for (var i = 0; i < this.intervals.length; i++) { 395 for (var i = 0; i < this.intervals.length; i++) {
198 var interval = this.intervals[i]; 396 var interval = this.intervals[i];
199 397
200 mockValues.push({ 398 mockValues.push({
201 time: interval.start, 399 time: interval.start,
202 shortDescription: eventType,
203 longDescription: eventType + ' at ' + 400 longDescription: eventType + ' at ' +
204 new Date(interval.start) + ' blah, blah blah'}); 401 new Date(interval.start) + ' blah, blah blah'});
205 mockValues.push({ 402 mockValues.push({
206 time: (interval.start + interval.end) / 2, 403 time: (interval.start + interval.end) / 2,
207 shortDescription: eventType,
208 longDescription: eventType + ' at ' + 404 longDescription: eventType + ' at ' +
209 new Date((interval.start + interval.end) / 2) + ' blah, blah blah'}); 405 new Date((interval.start + interval.end) / 2) + ' blah, blah blah'});
210 mockValues.push({ 406 mockValues.push({
211 time: interval.end, 407 time: interval.end,
212 shortDescription: eventType,
213 longDescription: eventType + ' at ' + new Date(interval.end) + 408 longDescription: eventType + ' at ' + new Date(interval.end) +
214 ' blah, blah blah'}); 409 ' blah, blah blah'});
215 } 410 }
216 return mockValues; 411 return mockValues;
217 } 412 };
218 413
219 // Request new data for |eventType|, for times in the current range. 414 /**
415 * Request new data for |eventType|, for times in the current range.
416 *
417 * @param {!string} eventType type of event to get new data for
418 */
220 this.refreshEventType = function(eventType) { 419 this.refreshEventType = function(eventType) {
420 this.eventMap[eventType].data = null; // Mark eventType as awaiting response
221 this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); 421 this.onReceiveEventType(eventType, this.getMockEventValues(eventType));
222 // Replace with: 422 // Replace with:
223 // chrome.send("getEvents", eventType, this.range.start, this.range.end); 423 // chrome.send("getEvents", eventType, this.range.start, this.range.end);
koz (OOO until 15th September) 2012/07/02 06:14:48 nit: Missing final callback parameter here?
clintstaley 2012/07/02 19:09:05 Will add if it proves necessary, but the current d
224 } 424 };
225 425
226 // Add an event column pair to DataTable |table| for |eventType| 426 /**
227 this.addEventColumns = function(table, eventType) { 427 * Receive new data for |eventType|. If the event has been deselected while
228 var annotationCol = table.addColumn({'id': eventType, type: 'string', 428 * awaiting webui response, do nothing. Otherwise, save the data directly,
229 role: 'annotation'}); 429 * since events are handled differently than metrics when drawing
230 var rolloverCol = table.addColumn({'id': eventType + 'Tip', type: 'string', 430 * (no "series"), and redraw all the affected charts.
231 role: 'annotationText'}); 431 *
232 var values = this.eventMap[eventType]; 432 * @param {!string} eventType type of event the new data applies to
233 var interval = this.intervals[0], intervalIndex = 0; 433 * @param {!Array.<{time: !number, longDescription: !string}>} values
434 * new event values
435 */
436 this.onReceiveEventType = function(eventType, values) {
437 var eventValue = this.eventMap[eventType];
234 438
235 for (var i = 0; i < values.length; i++) { 439 if (eventValue.divs.length == 0)
236 var event = values[i]; 440 return;
237 var rowIndex = 0; 441
238 while (event.time > interval.end && 442 eventValue.data = values;
239 intervalIndex < this.intervals.length - 1) 443 eventValue.divs.forEach(this.drawChart, this);
240 { 444 };
241 // Skip interval times, inclusive of interval.end, and of following null 445
242 rowIndex += (interval.end - interval.start) / this.range.resolution + 2; 446 /**
243 interval = this.intervals[++intervalIndex]; 447 * Return an object containing an array of metrics and another of events
244 } 448 * that include |chart| as one of the divs into which they display.
245 if (event.time >= interval.start && event.time <= interval.end) { 449 *
246 table.setCell(rowIndex + (event.time - interval.start) / 450 * @param {DOMElement} chart div for which to get relevant items
247 this.range.resolution, annotationCol, event.shortDescription); 451 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}}
248 table.setCell(rowIndex + (event.time - interval.start) / 452 */
249 this.range.resolution, rolloverCol, event.longDescription); 453 this.getChartData = function(chart) {
454 var result = {metrics: [], events: []};
455
456 for (var metric in this.metricMap) {
457 var metricValue = this.metricMap[metric];
458
459 if (metricValue.divs.indexOf(chart) != -1)
460 result.metrics.push(metricValue);
461 }
462
463 for (var eventType in this.eventMap) {
464 var eventValue = this.eventMap[eventType];
465
466 if (eventValue.divs.length > 0)
koz (OOO until 15th September) 2012/07/02 06:14:48 Add a comment explaining that events appear on all
clintstaley 2012/07/02 19:09:05 There's one elsewhere, but it's worth repeating he
467 result.events.push(eventValue);
468 }
469
470 return result;
471 };
472
473 /**
474 * Check all entries in an object of the type returned from getChartData,
475 * above, to see if all events and metrics have completed data (none is
476 * awaiting an asynchronous webui response to get their current data).
477 *
478 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData
479 * event/metric data to check for readiness
480 * @return {boolean} is data ready?
481 */
482 this.isDataReady = function(chartData) {
483 for (var i = 0; i < chartData.metrics.length; i++) {
484 if (chartData.metrics[i].data == null)
485 return false;
486 }
487
488 for (var i = 0; i < chartData.events.length; i++) {
489 if (chartData.events[i].data == null)
490 return false;
491 }
492
493 return true;
494 };
495
496 /**
497 * Create and return an array of "markings" (per Flot), representing
498 * vertical lines at the event time, in the event's color. Also add
499 * (not per Flot) a |description| property to each, to be used for hand
500 * creating description boxes.
501 *
502 * @param {!Array.<{
503 * description: !string,
504 * color: !string,
505 * data: !Array.<{time: !number}>
506 * }>} eventValues events to make markings for
507 * @return {!Array.<{
508 * color: !string,
509 * description: !string,
510 * xaxis: {from: !number, to: !number}
511 * }>} mark data structure for Flot to use
512 */
513 this.getEventMarks = function(eventValues) {
514 var markings = [];
515
516 for (var i = 0; i < eventValues.length; i++) {
517 var eventValue = eventValues[i];
518 for (var d = 0; d < eventValue.data.length; d++) {
519 var point = eventValue.data[d];
520 markings.push({
521 color: eventValue.color,
522 description: eventValue.description,
523 xaxis: {from: point.time, to: point.time}
524 });
250 } 525 }
251 } 526 }
252 }
253 527
254 this.dropEventColumns = function(table, eventType) { 528 return markings;
255 var colIndex, numCols = table.getNumberOfColumns(); 529 };
256 530
257 for (colIndex = 0; colIndex < numCols; colIndex++) 531 /**
258 if (table.getColumnId(colIndex) == eventType) 532 * Redraw the chart in div |chart|, IF all its dependent data is present.
259 break; 533 * Otherwise simply return, and await another call when all data is
534 * available.
535 *
536 * @param {DOMElement} chart div to redraw
537 */
538 this.drawChart = function(chart) {
539 var chartData = this.getChartData(chart);
260 540
261 if (colIndex < numCols) { 541 if (!this.isDataReady(chartData))
262 table.removeColumn(colIndex + 1); 542 return;
263 table.removeColumn(colIndex); 543
544 var seriesSeq = [];
545 var yAxes = [];
546 chartData.metrics.forEach(function(value) {
547 yAxes.push(value.yAxis);
548 for (var run = 0; run < value.data.length; run++) {
koz (OOO until 15th September) 2012/07/02 06:14:48 i seems like a more idiomatic iterator variable he
clintstaley 2012/07/02 19:09:05 Done.
549 seriesSeq.push({
550 color: value.yAxis.color,
551 data: value.data[run],
552 label: run == 0 ? value.description + '(' + value.label + ')' : null,
553 yaxis: yAxes.length, // Use just-added Y axis
554 });
555 }
556 });
557
558 var markings = this.getEventMarks(chartData.events);
559 var chart = this.charts[0];
560 var plot = $.plot(chart, seriesSeq, {
561 yaxes: yAxes,
562 xaxis: {mode: 'time'},
563 grid: {markings: markings}
564 });
565
566 // For each event in |markings|, create also a label div, with left
567 // edge colinear with the event vertical-line. Top of label is
568 // presently a hack-in, putting labels in three tiers of 25px height
569 // each to avoid overlap. Will need something better.
570 var labelTemplate = querySelect('#labelTemplate');
571 for (var i = 0; i < markings.length; i++) {
572 var mark = markings[i];
573 var point =
574 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1});
575 var labelDiv = labelTemplate.cloneNode(true);
576 labelDiv.innerText = mark.description;
577 labelDiv.style.left = point.left + 'px';
578 labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px';
579 chart.appendChild(labelDiv);
264 } 580 }
265 } 581 };
266 582
267 // Receive new data for |eventType|. Save this in eventMap for future 583 this.setupCheckboxes(
268 // redraws. Then, for each metric linegraph remove any current column pair 584 '#chooseMetrics', this.metricMap, this.addMetric, this.dropMetric);
269 // for |eventType| and replace it with a new pair, which will reflect the 585 this.setupCheckboxes(
270 // new data. Redraw the linegraph. 586 '#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(); 587 this.setupTimeRangeChooser();
588 this.setupMainChart();
361 this.TimeRange.day.element.click(); 589 this.TimeRange.day.element.click();
362 } 590 }
OLDNEW
« no previous file with comments | « chrome/browser/resources/performance_monitor/chart.html ('k') | third_party/flot/LICENSE.txt » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698