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

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

Issue 10900035: Version 2 of CPM UI, with rearranged controls, multiple graphs, event rollovers, etc. (Closed) Base URL: http://git.chromium.org/chromium/src.git@master
Patch Set: Latest fixes per jyasskin's review Created 8 years, 3 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') | no next file » | 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
6 cr.define('performance_monitor', function() { 5 cr.define('performance_monitor', function() {
7 'use strict'; 6 'use strict';
8 7
9 /** 8 /**
10 * Enum for time ranges, giving a descriptive name, time span prior to |now|, 9 * Enum for time ranges, giving for each a descriptive name, time span in ms
11 * data point resolution, and time-label frequency and format for each. 10 * prior to |now|, data point resolution in ms, time-label frequency in data
11 * points per label, and format using Date.js standards, for each.
12 *
13 * The additional |element| field is added in setupTimeRangeTab_, giving
14 * the HTML input tag for the radio button selecting the given time range.
12 * @enum {{ 15 * @enum {{
13 * value: number, 16 * value: number,
14 * name: string, 17 * name: string,
15 * timeSpan: number, 18 * timeSpan: number,
16 * resolution: number, 19 * resolution: number,
17 * labelEvery: number, 20 * labelEvery: number,
18 * format: string 21 * format: string,
22 * element: HTMLElement
19 * }} 23 * }}
20 * @private 24 * @private
21 */ 25 */
22 var TimeRange_ = { 26 var TimeRange_ = {
23 // Prior 12 min, resolution of 1s, at most 720 points. 27 // Prior 15 min, resolution of 5s, at most 180 points.
24 // Labels at 60 point (1 min) intervals. 28 // Labels at 12 point (1 min) intervals.
25 minutes: {value: 0, name: 'Last 12 min', timeSpan: 720 * 1000, 29 minutes: {value: 0, name: 'Last 15 min', timeSpan: 900 * 1000,
26 resolution: 1000, labelEvery: 60, format: 'MM-dd'}, 30 resolution: 1000 * 5, labelEvery: 12, format: 'MM-dd'},
27 31
28 // Prior hour, resolution of 5s, at most 720 points. 32 // Prior hour, resolution of 20s, at most 180 points.
29 // Labels at 60 point (5 min) intervals. 33 // Labels at 15 point (5 min) intervals.
30 hour: {value: 1, name: 'Last Hour', timeSpan: 3600 * 1000, 34 hour: {value: 1, name: 'Last Hour', timeSpan: 3600 * 1000,
31 resolution: 1000 * 5, labelEvery: 60, format: 'MM-dd'}, 35 resolution: 1000 * 20, labelEvery: 15, format: 'MM-dd'},
32 36
33 // Prior day, resolution of 2 min, at most 720 points. 37 // Prior day, resolution of 5 min, at most 288 points.
34 // Labels at 90 point (3 hour) intervals. 38 // Labels at 36 point (3 hour) intervals.
35 day: {value: 2, name: 'Last Day', timeSpan: 24 * 3600 * 1000, 39 day: {value: 2, name: 'Last Day', timeSpan: 24 * 3600 * 1000,
36 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, 40 resolution: 1000 * 60 * 5, labelEvery: 36, format: 'MM-dd'},
37 41
38 // Prior week, resolution of 15 min -- at most 672 data points. 42 // Prior week, resolution of 1 hr -- at most 168 data points.
39 // Labels at 96 point (daily) intervals. 43 // Labels at 24 point (daily) intervals.
40 week: {value: 3, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, 44 week: {value: 3, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000,
41 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, 45 resolution: 1000 * 3600, labelEvery: 24, format: 'M/d'},
42 46
43 // Prior month (30 days), resolution of 1 hr -- at most 720 data points. 47 // Prior month (30 days), resolution of 4 hr -- at most 180 data points.
44 // Labels at 168 point (weekly) intervals. 48 // Labels at 42 point (weekly) intervals.
45 month: {value: 4, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, 49 month: {value: 4, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000,
46 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, 50 resolution: 1000 * 3600 * 4, labelEvery: 42, format: 'M/d'},
47 51
48 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points. 52 // Prior quarter (90 days), resolution of 12 hr -- at most 180 data points.
49 // Labels at 112 point (fortnightly) intervals. 53 // Labels at 28 point (fortnightly) intervals.
50 quarter: {value: 5, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, 54 quarter: {value: 5, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000,
51 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, 55 resolution: 1000 * 3600 * 12, labelEvery: 28, format: 'M/yy'},
52 }; 56 };
53 57
54 /* 58 /*
55 * Offset, in ms, by which to subtract to convert GMT to local time 59 * Table of colors to use for metrics and events. Basically boxing the
56 * @type {number} 60 * colorwheel, but leaving out yellows and fully saturated colors.
57 */ 61 * @type {Array.<string>}
58 var timezoneOffset = new Date().getTimezoneOffset() * 60000; 62 * @private
63 */
64 var ColorTable_ = [
65 'rgb(255, 128, 128)', 'rgb(128, 255, 128)', 'rgb(128, 128, 255)',
66 'rgb(128, 255, 255)', 'rgb(255, 128, 255)', // No bright yellow
67 'rgb(255, 64, 64)', 'rgb( 64, 255, 64)', 'rgb( 64, 64, 255)',
68 'rgb( 64, 255, 255)', 'rgb(255, 64, 255)', // No medium yellow either
69 'rgb(128, 64, 64)', 'rgb( 64, 128, 64)', 'rgb( 64, 64, 128)',
70 'rgb( 64, 128, 128)', 'rgb(128, 64, 128)', 'rgb(128, 128, 64)'
71 ];
72
73 /*
74 * Offset, in ms, by which to subtract to convert GMT to local time.
75 * @type {number}
76 * @private
77 */
78 var timezoneOffset_ = new Date().getTimezoneOffset() * 60000;
79
80 /*
81 * Additional range multiplier to ensure that points don't hit the top of
82 * the graph.
83 * @type {number}
84 * @private
85 */
86 var yAxisMargin_ = 1.05;
87
88 /*
89 * Number of time resolution periods to wait between automated update of
90 * graphs.
91 * @type {number}
92 * @private
93 */
94 var intervalMultiple_ = 2;
95
96 /*
97 * Number of milliseconds to wait before deciding that the most recent
98 * resize event is not going to be followed immediately by another, and
99 * thus needs handling.
100 * @type {number}
101 * @private
102 */
103 var resizeDelay_ = 500;
Jeffrey Yasskin 2012/09/05 22:21:54 Fair enough.
59 104
60 /** @constructor */ 105 /** @constructor */
61 function PerformanceMonitor() { 106 function PerformanceMonitor() {
62 this.__proto__ = PerformanceMonitor.prototype; 107 this.__proto__ = PerformanceMonitor.prototype;
63 /** 108
64 * All metrics have entries, but those not displayed have an empty div list. 109 /**
65 * If a div list is not empty, the associated data will be non-null, or 110 * Detailed information on a metric in the UI. MetricId is a unique
66 * null but about to be filled by webui handler response. Thus, any metric 111 * identifying number for the metric, provided by the webui, and assumed
67 * with non-empty div list but null data is awaiting a data response 112 * to be densely populated. All metrics also have a description and
68 * from the webui handler. The webui handler uses numbers to uniquely 113 * an associated category giving their unit information and home chart.
69 * identify metric and event types, so we use the same numbers (in 114 * They also have a color in which they are displayed, and a maximum value
70 * string form) for the map key, repeating the id number in the |id| 115 * by which to scale their y-axis.
71 * field, as a number. 116 *
72 * @type {Object.<string, { 117 * Although in the present UI each metric appears only in the home chart of
73 * id: number, 118 * its metric category, we keep the divs property to allow future
74 * description: string, 119 * modifications in which the same metric might appear in several charts
75 * units: string, 120 * for side-by-side comparisons. Metrics not being displayed have an
76 * yAxis: !{max: number, color: string}, 121 * empty div list. If a div list is not empty, the associated data will
77 * divs: !Array.<HTMLDivElement>, 122 * be non-null, or null but about to be filled by webui handler response.
78 * data: ?Array.<{time: number, value: number}> 123 * Thus, any metric with non-empty div list but null data is awaiting a
79 * }>} 124 * data response from the webui handler.
80 * @private 125 * @typedef {{
81 */ 126 * metricId: number,
82 this.metricMap_ = {}; 127 * description: string,
83 128 * category: !Object,
84 /* 129 * color: string,
85 * Similar data for events, though no yAxis info is needed since events 130 * maxValue: number,
86 * are simply labelled markers at X locations. Rules regarding null data 131 * divs: !Array.<HTMLDivElement>,
87 * with non-empty div list apply here as for metricMap_ above. 132 * data: ?Array.<{time: number, value: number}>
88 * @type {Object.<number, { 133 * }}
89 * id: number, 134 */
135 PerformanceMonitor.MetricDetails;
136
137 /**
138 * Similar data for events as for metrics, though no yAxis info is needed
Devlin 2012/09/07 18:44:54 nit: Be consistent in spelling y-axis/yAxis in com
clintstaley 2012/09/07 20:39:52 Done.
139 * since events are simply labelled markers at X locations. Rules regarding
140 * null data with non-empty div list apply here as for metricDetailsMap_
141 * above. The |data| field follows a special rule not describable in
142 * JSDoc: Each array element may include arbitrary other properties with
143 * value {label: 'some label', value: 'some value'} for display in the
144 * event mouseover. These will vary per event type.
Jeffrey Yasskin 2012/09/05 22:21:54 I see. So effectively each array element maps a ti
clintstaley 2012/09/07 20:39:52 I assume you mean for me to revise the comment acc
145 * @typedef {{
146 * eventId: number,
147 * name: string,
148 * popupTitle: string,
90 * description: string, 149 * description: string,
91 * color: string, 150 * color: string,
92 * divs: !Array.<HTMLDivElement>, 151 * divs: !Array.<HTMLDivElement>,
93 * data: ?Array.<{time: number, longDescription: string}> 152 * data: ?Array.<{time: number}>
153 * }}
154 */
155 PerformanceMonitor.EventDetails;
156
157 /**
158 * Metrics fall into categories that have common units and thus may
159 * share a common graph, or share y-axes within a multi-y-axis graph.
160 * Each category has one home chart in which metrics of that category
161 * are displayed. Currently this is also the only chart in which such
162 * metrics are displayed, but the design permits a metric to show in
163 * several charts if this is useful later on.
164 * @type {Object.<string, {
165 * metricCategoryId: number,
166 * name: string,
167 * description: string,
168 * unit: string,
169 * details: Array.<{!PerformanceMonitor.MetricDetails}>,
170 * homeChart: !HTMLDivElement
94 * }>} 171 * }>}
95 * @private 172 * @private
96 */ 173 */
97 this.eventMap_ = {}; 174 this.metricCategoryMap_ = {};
175
176 /**
177 * Comprehensive map from metricId to MetricDetails.
178 * @type {Object.<number, {PerformanceMonitor.MetricDetails}>}
179 * @private
180 */
181 this.metricDetailsMap_ = {};
182
183 /**
184 * Events fall into categories just like metrics, above. This category
185 * grouping is not as important as that for metrics, since events
186 * needn't share maxima, y-axes, nor units, and since events appear on
187 * all charts. But grouping of event categories in the event-selection
188 * UI is still useful.
189 * @type {Object.<string, {
190 * eventCategoryId: number,
191 * name: string,
192 * description: string,
193 * details: !Array.<!PerformanceMonitor.EventDetails>,
194 * }>}
195 * @private
196 */
197 this.eventCategoryMap_ = {};
198
199 /**
200 * Comprehensive map from eventId to EventDetails.
201 * @type {Object.<number, {PerformanceMonitor.EventDetails}>}
202 * @private
203 */
204 this.eventDetailsMap_ = {};
98 205
99 /** 206 /**
100 * Time periods in which the browser was active and collecting metrics 207 * Time periods in which the browser was active and collecting metrics
101 * and events. 208 * and events.
102 * @type {!Array.<{start: number, end: number}>} 209 * @type {!Array.<{start: number, end: number}>}
103 * @private 210 * @private
104 */ 211 */
105 this.intervals_ = []; 212 this.intervals_ = [];
106 213
107 this.setupTimeRangeChooser_(); 214 /**
108 chrome.send('getAllEventTypes'); 215 * Handle of timer interval function used to update charts
109 chrome.send('getAllMetricTypes'); 216 * @type {Object}
110 this.setupMainChart_(); 217 * @private
218 */
219 this.updateTimer_ = null;
220
221 /**
222 * Handle of timer interval function used to check for resizes. Nonnull
223 * only when resize events are coming steadily.
224 * @type {Object}
225 * @private
226 */
227 this.resizeTimer_ = null;
228
229 /**
230 * Time of last UNHANDLED resize event, or null if no resizes or if
231 * last one has been handled.
232 * @type {number}
233 * @private
234 */
235 this.lastResizeTime_ = null;
236
237 /**
238 * All chart divs in the display, whether hidden or not. Presently
239 * this has one entry for each metric category in |this.metricCategoryMap|.
240 * @type {Array.<HTMLDivElement>}
241 * @private
242 */
243 this.charts_ = [];
244
245 this.setupTimeRangeTab_();
246 chrome.send('getEventTypes');
247 chrome.send('getMetricTypes');
111 TimeRange_.day.element.click(); 248 TimeRange_.day.element.click();
112 } 249 }
113 250
114 PerformanceMonitor.prototype = { 251 PerformanceMonitor.prototype = {
115 /** 252 /**
116 * Receive a list of all metrics, and populate |this.metricMap_| to 253 * Receive a list of all metric categories, each with its correspoinding
Devlin 2012/09/07 18:44:54 nit: corresponding
clintstaley 2012/09/07 20:39:52 Done.
117 * reflect said list. Reconfigure the checkbox set for metric selection. 254 * list of metric details. Populate |this.metricCategoryMap_| and
118 * @param {Array.<{metricType: string, shortDescription: string}>} 255 * |this.metrictDetailsMap_| to reflect said list. Reconfigure the
119 * allMetrics All metrics from which to select. 256 * checkbox set for metric selection.
257 * @param {Array.<{
258 * metricCategoryId: number,
259 * name: string,
260 * unit: string,
261 * description: string,
262 * details: Array.<{
263 * metricId: number,
264 * name: string,
265 * description: string
266 * }>
267 * }>} categories All metric categories needing charts and checkboxes.
120 */ 268 */
121 getAllMetricTypesCallback: function(allMetrics) { 269 getMetricTypesCallback: function(categories) {
122 for (var i = 0; i < allMetrics.length; i++) { 270 categories.forEach(function(category) {
123 var metric = allMetrics[i]; 271 this.addCategoryChart_(category);
272 this.metricCategoryMap_[category.metricCategoryId] = category;
124 273
125 this.metricMap_[metric.metricType] = { 274 category.details.forEach(function(metric) {
126 id: metric.metricType, 275 metric.color = ColorTable_[metric.metricId % ColorTable_.length];
127 description: metric.shortDescription, 276 metric.maxValue = 1;
128 units: metric.units, 277 metric.divs = [];
129 yAxis: {min: 0, max: metric.maxValue, 278 metric.data = null;
130 color: jQuery.color.parse('blue')}, 279 metric.category = category;
131 divs: [], 280 this.metricDetailsMap_[metric.metricId] = metric;
132 data: null 281 }, this);
133 }; 282 }, this);
134 }
135 283
136 this.setupCheckboxes_($('#choose-metrics')[0], 284 this.setupCheckboxes_($('#choose-metrics')[0],
137 this.metricMap_, this.addMetric, this.dropMetric); 285 this.metricCategoryMap_, 'metricId', this.addMetric, this.dropMetric);
138 }, 286 },
139 287
140 /** 288 /**
141 * Receive a list of all event types, and populate |this.eventMap_| to 289 * Receive a list of all event categories, each with its correspoinding
142 * reflect said list. Reconfigure the checkbox set for event selection. 290 * list of event details. Populate |this.eventCategoryMap_| and
143 * @param {Array.<{eventType: string, shortDescription: string}>} 291 * |this.eventDetailsMap| to reflect said list. Reconfigure the
144 * allEvents All events from which to select. 292 * checkbox set for event selection.
293 * @param {Array.<{
294 * eventCategoryId: number,
295 * name: string,
296 * description: string,
297 * details: Array.<{
298 * eventId: number,
299 * name: string,
300 * description: string
301 * }>
302 * }>} categories All event categories needing charts checkboxes.
145 */ 303 */
146 getAllEventTypesCallback: function(allEvents) { 304 getEventTypesCallback: function(categories) {
147 for (var i = 0; i < allEvents.length; i++) { 305 categories.forEach(function(category) {
148 var eventInfo = allEvents[i]; 306 this.eventCategoryMap_[category.eventCategoryId] = category;
149 307
150 this.eventMap_[eventInfo.eventType] = { 308 category.details.forEach(function(event) {
151 id: eventInfo.eventType, 309 event.color = ColorTable_[event.eventId % ColorTable_.length];
152 color: jQuery.color.parse('red'), 310 event.divs = [];
153 data: null, 311 event.data = null;
154 description: eventInfo.shortDescription, 312 this.eventDetailsMap_[event.eventId] = event;
155 divs: [] 313 }, this);
156 }; 314 }, this);
157 }
158 315
159 this.setupCheckboxes_($('#choose-events')[0], 316 this.setupCheckboxes_($('#choose-events')[0], this.eventCategoryMap_,
160 this.eventMap_, this.addEventType, this.dropEventType); 317 'eventId', this.addEventType, this.dropEventType);
161 }, 318 },
162 319
163 /** 320 /**
164 * Set up the radio button set to choose time range. Use div#radio-template 321 * Set up the radio button set to choose time range. Use div#radio-template
165 * as a template. 322 * as a template.
166 * @private 323 * @private
167 */ 324 */
168 setupTimeRangeChooser_: function() { 325 setupTimeRangeTab_: function() {
169 var timeDiv = $('#choose-time-range')[0]; 326 var timeDiv = $('#choose-time-range')[0];
327 var backButton = $('#back-time')[0];
328 var forwardButton = $('#forward-time')[0];
170 var radioTemplate = $('#radio-template')[0]; 329 var radioTemplate = $('#radio-template')[0];
171 330
172 for (var time in TimeRange_) { 331 for (var time in TimeRange_) {
173 var timeRange = TimeRange_[time]; 332 var timeRange = TimeRange_[time];
174 var radio = radioTemplate.cloneNode(true); 333 var radio = radioTemplate.cloneNode(true);
175 var input = radio.querySelector('input'); 334 var input = radio.querySelector('input');
176 335
177 input.value = timeRange.value; 336 input.value = timeRange.value;
178 input.timeRange = timeRange; 337 input.timeRange = timeRange;
179 radio.querySelector('span').innerText = timeRange.name; 338 radio.querySelector('span').innerText = timeRange.name;
180 timeDiv.appendChild(radio); 339 timeDiv.appendChild(radio);
181 timeRange.element = input; 340 timeRange.element = input;
182 } 341 }
183 342
184 timeDiv.addEventListener('click', function(e) { 343 timeDiv.addEventListener('click', function(e) {
185 if (!e.target.webkitMatchesSelector('input[type="radio"]')) 344 if (!e.target.webkitMatchesSelector('input[type="radio"]'))
186 return; 345 return;
187 346
188 this.setTimeRange(e.target.timeRange); 347 this.setTimeRange(e.target.timeRange, Date.now(), true);
189 }.bind(this)); 348 }.bind(this));
349
350 forwardButton.addEventListener('click', this.forwardTime.bind(this));
351 backButton.addEventListener('click', this.backTime.bind(this));
190 }, 352 },
191 353
192 /** 354 /**
193 * Generalized function for setting up checkbox blocks for either events 355 * Generalized function for setting up checkbox blocks for either events
194 * or metrics. Take a div |div| into which to place the checkboxes, 356 * or metrics. Take a div |div| into which to place the checkboxes,
195 * and a map |optionMap| with values that each include a property 357 * and a map |optionCategoryMap| with values that each include a property
196 * |description|. Set up one checkbox for each entry in |optionMap| 358 * |name|, and a property |details|, which gives an array of objects
197 * labelled with that description. Arrange callbacks to function |check| 359 * that in turn each have an id property with key given by parameter
198 * or |uncheck|, passing them the key of the checked or unchecked option, 360 * |idKey|, a property |name| and a property |color|.
199 * when the relevant checkbox state changes. 361 *
362 * For instance, |optionCategoryMap| might be metricCategoryMap_, and
363 * |idKey| would be 'metricID'. MetricCategoryMap_'s values each
364 * have a name and details property. The details property in
365 * this case would be an array of MetricDetails, each having properties
366 * metricId, name, and color. The |check| and |uncheck| parameters
367 * would be addMetric and dropMetric. So, assuming the following data:
368 *
369 * optionCategoryMap : {
Jeffrey Yasskin 2012/09/05 22:21:54 Ok, great. Now you can simplify the previous two p
clintstaley 2012/09/07 20:39:52 I rephrased the comment to put the concrete exampl
370 * 1: {
371 * name: 'CPU',
372 * details: [
373 * {metricId: 1, name: 'CPU Usage', color: 'rgb(255, 128, 128)'}
374 * ],
375 * 2: {
376 * name : 'Memory',
377 * details: [
378 * {metricId: 2, name: 'Private Memory Usage',
379 * color: 'rgb(128, 255, 128)'},
380 * {metricId: 3, name: 'Shared Memory Usage',
381 * color: 'rgb(128, 128, 255)'}
382 * ]
383 * }
384 *
385 * we'd get HTML thus, with each checkbox's check and uncheck events
386 * handled by addMetric and dropMetric, with parameter equal to the
387 * corresponding metricId of the metric in question.
388 *
389 * <div id="category-label-template" class="category-label">CPU</div>
390 * <div id="detail-checkbox-template" class="detail-checkbox">
391 * <div class="horizontal-box">
392 * <input type="checkbox">
393 * <div class="detail-label">CPU Usage</div>
394 * <div class="spacer"></div>
395 * <div class="color-icon"
396 * style="background-color: rgb(255, 128, 128);"></div>
397 * </div>
398 * </div>
399 * </div>
400 *
401 * <div id="category-label-template" class="category-label">Memory</div>
402 * <div id="detail-checkbox-template" class="detail-checkbox">
403 * <div class="horizontal-box">
404 * <input type="checkbox">
405 * <div class="detail-label">Private Memory Usage</div>
406 * <div class="spacer"></div>
407 * <div class="color-icon"
408 * style="background-color: rgb(128, 255, 128);"></div>
409 * </div>
410 * </div>
411 * </div><div id="detail-checkbox-template" class="detail-checkbox">
412 * <div class="horizontal-box">
413 * <input type="checkbox">
414 * <div class="detail-label">Shared Memory Usage</div>
415 * <div class="spacer"></div>
416 * <div class="color-icon"
417 * style="background-color: rgb(128, 128, 255);"></div>
418 * </div>
419 * </div>
420 * </div>
421 *
200 * @param {!HTMLDivElement} div A <div> into which to put checkboxes. 422 * @param {!HTMLDivElement} div A <div> into which to put checkboxes.
201 * @param {!Object} optionMap A map of metric/event entries. 423 * @param {!Object} optionCategory Map A map of metric/event categories.
Devlin 2012/09/07 18:44:54 nit: This should be optionCategoryMap (no space),
clintstaley 2012/09/07 20:39:52 Good eyes. Done
424 * @param {string} idKey The key of the id property.
202 * @param {!function(this:Controller, Object)} check 425 * @param {!function(this:Controller, Object)} check
203 * The function to select an entry (metric or event). 426 * The function to select an entry (metric or event).
204 * @param {!function(this:Controller, Object)} uncheck 427 * @param {!function(this:Controller, Object)} uncheck
205 * The function to deselect an entry (metric or event). 428 * The function to deselect an entry (metric or event).
206 * @private 429 * @private
207 */ 430 */
208 setupCheckboxes_: function(div, optionMap, check, uncheck) { 431 setupCheckboxes_: function(div, optionCategoryMap, idKey, check, uncheck) {
209 var checkboxTemplate = $('#checkbox-template')[0]; 432 var checkboxTemplate = $('#detail-checkbox-template')[0];
210 433 var labelTemplate = $('#category-label-template')[0];
211 for (var option in optionMap) { 434
212 var checkbox = checkboxTemplate.cloneNode(true); 435 for (var c in optionCategoryMap) {
213 checkbox.querySelector('span').innerText = 'Show ' + 436 var category = optionCategoryMap[c];
214 optionMap[option].description; 437 var label = labelTemplate.cloneNode(true);
215 div.appendChild(checkbox); 438
216 439 label.innerText = category.name;
217 var input = checkbox.querySelector('input'); 440 div.appendChild(label);
218 input.option = optionMap[option].id; 441
219 input.addEventListener('change', function(e) { 442 category.details.forEach(function(details) {
220 (e.target.checked ? check : uncheck).call(this, e.target.option); 443 var checkbox = checkboxTemplate.cloneNode(true);
221 }.bind(this)); 444 var input = checkbox.querySelector('input');
445 var label = checkbox.querySelector('.detail-label');
446 var icon = checkbox.querySelector('.color-icon');
447
448 input.option = details[idKey];
449 input.icon = icon;
450 input.addEventListener('change', function(e) {
451 (e.target.checked ? check : uncheck).call(this, e.target.option);
452 e.target.icon.style.visibility =
453 e.target.checked ? 'visible' : 'hidden';
454 }.bind(this));
455
456 label.innerText = details.name;
457
458 icon.style.backgroundColor = details.color;
459
460 div.appendChild(checkbox);
461 }, this);
222 } 462 }
223 }, 463 },
224 464
225 /** 465 /**
226 * Set up just one chart in which all metrics will be displayed 466 * Add a new chart for |category|, making it initially hidden,
227 * initially. But, the design readily accommodates addition of 467 * with no metrics displayed in it.
228 * new charts, and movement of metrics into those other charts. 468 * @param {!Object} category The metric category for which to create
469 * the chart. Category is a value from metricCategoryMap_.
229 * @private 470 * @private
230 */ 471 */
231 setupMainChart_: function() { 472 addCategoryChart_: function(category) {
232 this.chartParent = $('#charts')[0]; 473 var chartParent = $('#charts')[0];
233 this.charts = [document.createElement('div')]; 474 var chart = document.createElement('div');
234 this.charts[0].className = 'chart'; 475
235 this.chartParent.appendChild(this.charts[0]); 476 chart.className = 'chart';
477 chart.hidden = true;
478 chartParent.appendChild(chart);
479 this.charts_.push(chart);
480 category.homeChart = chart;
481 chart.refs = 0;
482 chart.hovers = [];
483
484 // Receive hover events from Flot.
485 // Attached to chart will be properties 'hovers', a list of {x, div}
486 // pairs. As pos events arrive, check each hover to see if it should
487 // be hidden or made visible.
488 $(chart).bind('plothover', function(event, pos, item) {
489 var tolerance = this.range.resolution;
490
491 chart.hovers.forEach(function(hover) {
492 hover.div.hidden = hover.x < pos.x - tolerance ||
493 hover.x > pos.x + tolerance;
494 });
495
496 }.bind(this));
497
498 $(window).resize(function() {
499 this.lastResizeTime_ = Date.now();
500 if (this.resizeTimer_ == null)
501 this.resizeTimer_ = setInterval(this.checkResize_.bind(this), 100);
Jeffrey Yasskin 2012/09/05 22:21:54 Ah, *I* had missed that you reset lastResizeTime o
clintstaley 2012/09/07 20:39:52 I considered this, but didn't like recreating and
Jeffrey Yasskin 2012/09/07 21:27:17 I don't care about this enough to insist either wa
clintstaley 2012/09/07 21:46:14 I changed it anyway. Code is cleaner, at least.
502 }.bind(this));
503 },
504
505 /**
506 * Check to see if resize events have recently occured, and if a long
Devlin 2012/09/07 18:44:54 nit: occurred
clintstaley 2012/09/07 20:39:52 Done.
507 * enough time has passed since the last one to justify actually redrawing
508 * all the charts. If so, redraw them all, and clear the resize state.
509 * @private
510 */
511 checkResize_: function() {
512 if (this.lastResizeTime_ != null && Date.now() - this.lastResizeTime_ >
513 resizeDelay_) {
514 this.lastResizeTime_ = null;
515
516 // Timer's done its job. Turn it off till more resize events arrive.
517 clearInterval(this.resizeTimer_);
518 this.resizeTimer_ = null;
519
520 this.charts_.forEach(function(chart) {this.drawChart(chart);}, this);
521 }
236 }, 522 },
237 523
238 /** 524 /**
239 * Set the time range for which to display metrics and events. For 525 * Set the time range for which to display metrics and events. For
240 * now, the time range always ends at "now", but future implementations 526 * now, the time range always ends at 'now', but future implementations
241 * may allow time ranges not so anchored. 527 * may allow time ranges not so anchored.
242 * @param {!{start: number, end: number, resolution: number}} range 528 * @param {!{start: number, end: number, resolution: number}} range
243 * The time range for which to get display data. 529 * The time range for which to get display data.
244 */ 530 * @param {number} end Ending time, in ms since epoch, to which to
245 setTimeRange: function(range) { 531 * set the new time range.
532 * @param {boolean} startTimer Indicates whether we should restart the
533 * range-update timer.
534 */
535 setTimeRange: function(range, end, startTimer) {
246 this.range = range; 536 this.range = range;
247 this.end = Math.floor(Date.now() / range.resolution) * 537 if (this.updateTimer_ != null)
538 clearInterval(this.updateTimer_);
539 if (startTimer)
540 this.updateTimer_ = setInterval(this.forwardTime.bind(this),
541 intervalMultiple_ * range.resolution);
542 this.end = Math.floor(end / range.resolution) *
248 range.resolution; 543 range.resolution;
249 544
250 this.start = this.end - range.timeSpan; 545 this.start = this.end - range.timeSpan;
251 this.requestIntervals(); 546 this.requestIntervals();
252 }, 547 },
253 548
254 /** 549 /**
550 * Back up the time range by 1/2 of its current span, and cause chart
551 * redraws.
552 */
553 backTime: function() {
554 this.setTimeRange(this.range, this.end - this.range.timeSpan / 2, false);
555 },
556
557 /**
558 * Advance the time range by 1/2 of its current span, or up to the point
559 * where it ends at the present time, whichever is less.
560 */
561 forwardTime: function() {
562 var now = Date.now();
563 var newEnd = Math.min(now, this.end + this.range.timeSpan / 2);
564
565 this.setTimeRange(this.range, newEnd, newEnd == now);
566 },
567
568 /**
255 * Request activity intervals in the current time range. 569 * Request activity intervals in the current time range.
256 */ 570 */
257 requestIntervals: function() { 571 requestIntervals: function() {
258 chrome.send('getActiveIntervals', [this.start, this.end]); 572 chrome.send('getActiveIntervals', [this.start, this.end]);
259 }, 573 },
260 574
261 /** 575 /**
262 * Webui callback delivering response from requestIntervals call. Assumes 576 * Webui callback delivering response from requestIntervals call. Assumes
263 * this is a new time range choice, which results in complete refresh of 577 * this is a new time range choice, which results in complete refresh of
264 * all metrics and event types that are currently selected. 578 * all metrics and event types that are currently selected.
265 * @param {!Array.<{start: number, end: number}>} intervals 579 * @param {!Array.<{start: number, end: number}>} intervals
266 * The new intervals. 580 * The new intervals.
267 */ 581 */
268 getActiveIntervalsCallback: function(intervals) { 582 getActiveIntervalsCallback: function(intervals) {
269 this.intervals_ = intervals; 583 this.intervals_ = intervals;
270 584
271 for (var metric in this.metricMap_) { 585 for (var metric in this.metricDetailsMap_) {
272 var metricValue = this.metricMap_[metric]; 586 var metricDetails = this.metricDetailsMap_[metric];
273 if (metricValue.divs.length > 0) // if we're displaying this metric. 587 if (metricDetails.divs.length > 0) // if we're displaying this metric.
274 this.refreshMetric(metric); 588 this.refreshMetric(metricDetails);
275 } 589 }
276 590
277 for (var eventType in this.eventMap_) { 591 for (var eventType in this.eventDetailsMap_) {
278 var eventValue = this.eventMap_[eventType]; 592 var eventValue = this.eventDetailsMap_[eventType];
279 if (eventValue.divs.length > 0) 593 if (eventValue.divs.length > 0)
280 this.refreshEventType(eventValue.id); 594 this.refreshEventType(eventValue.eventId);
281 } 595 }
282 }, 596 },
283 597
284 /** 598 /**
285 * Add a new metric to the main (currently only) chart. 599 * Add a new metric to the display, showing it on its category's home
286 * @param {string} metric The metric to start displaying. 600 * chart. Un-hide the home chart if needed.
601 * @param {number} metricId The id of the metric to start displaying.
287 */ 602 */
288 addMetric: function(metric) { 603 addMetric: function(metricId) {
289 this.metricMap_[metric].divs.push(this.charts[0]); 604 var details = this.metricDetailsMap_[metricId];
290 this.refreshMetric(metric); 605 var homeChart = details.category.homeChart;
606
607 details.divs.push(homeChart);
608 if (homeChart.refs == 0)
609 homeChart.hidden = false;
610 homeChart.refs++;
611
612 this.refreshMetric(details);
291 }, 613 },
292 614
293 /** 615 /**
294 * Remove a metric from the chart(s). 616 * Remove a metric from the chart(s).
295 * @param {string} metric The metric to stop displaying. 617 * @param {string} metric The metric to stop displaying.
296 */ 618 */
297 dropMetric: function(metric) { 619 dropMetric: function(metric) {
298 var metricValue = this.metricMap_[metric]; 620 var details = this.metricDetailsMap_[metric];
299 var affectedCharts = metricValue.divs; 621 var affectedCharts = details.divs;
300 metricValue.divs = [];
301 622
302 affectedCharts.forEach(this.drawChart, this); 623 details.divs = []; // Gotta do this now to get correct drawChart result.
624 affectedCharts.forEach(function(chart) {
625 chart.refs--;
626 if (chart.refs == 0)
627 chart.hidden = true;
628 this.drawChart(chart);
629 }, this);
630
303 }, 631 },
304 632
305 /** 633 /**
306 * Request new metric data, assuming the metric table and chart already 634 * Request new metric data, assuming the metric table and chart already
307 * exist. 635 * exist.
308 * @param {string} metric The metric for which to get data. 636 * @param {string} metricDetails The metric details for which to get data.
309 */ 637 */
310 refreshMetric: function(metric) { 638 refreshMetric: function(metricDetails) {
311 var metricValue = this.metricMap_[metric]; 639 metricDetails.data = null; // Mark metric as awaiting response.
312 640 chrome.send('getMetric', [metricDetails.metricId,
313 metricValue.data = null; // Mark metric as awaiting response.
314 chrome.send('getMetric', [metricValue.id,
315 this.start, this.end, this.range.resolution]); 641 this.start, this.end, this.range.resolution]);
316 }, 642 },
317 643
318 /** 644 /**
319 * Receive new datapoints for a metric, convert the data to Flot-usable 645 * Receive new datapoints for a metric, convert the data to Flot-usable
320 * form, and redraw all affected charts. 646 * form, and redraw all affected charts.
321 * @param {!{ 647 * @param {!{
322 * type: number, 648 * id: number,
323 * points: !Array.<{time: number, value: number}> 649 * max: number,
324 * }} result An object giving metric ID, and time/value point pairs for 650 * metrics: !Array.<{time: number, value: number}>
325 * that id. 651 * }} result An object giving metric ID, max expected value of the data,
652 * and time/value point pairs for that id.
326 */ 653 */
327 getMetricCallback: function(result) { 654 getMetricCallback: function(result) {
328 var metricValue = this.metricMap_[result.metricType]; 655
656 var metricDetails = this.metricDetailsMap_[result.metricId];
329 // Might have been dropped while waiting for data. 657 // Might have been dropped while waiting for data.
330 if (metricValue.divs.length == 0) 658 if (metricDetails.divs.length == 0)
331 return; 659 return;
332 660
333 var series = []; 661 var series = [];
334 metricValue.data = [series]; // Data ends with current open series. 662 metricDetails.data = [series]; // Data ends with current open series.
335 663
336 // Traverse the points, and the intervals, in parallel. Both are in 664 // Traverse the points, and the intervals, in parallel. Both are in
337 // ascending time order. Create a sequence of data "series" (per Flot) 665 // ascending time order. Create a sequence of data 'series' (per Flot)
338 // arrays, with each series comprising all points within a given interval. 666 // arrays, with each series comprising all points within a given interval.
339 var interval = this.intervals_[0]; 667 var interval = this.intervals_[0];
340 var intervalIndex = 0; 668 var intervalIndex = 0;
341 var pointIndex = 0; 669 var pointIndex = 0;
342 while (pointIndex < result.points.length && 670 while (pointIndex < result.metrics.length &&
343 intervalIndex < this.intervals_.length) { 671 intervalIndex < this.intervals_.length) {
344 var point = result.points[pointIndex++]; 672 var point = result.metrics[pointIndex++];
345 while (intervalIndex < this.intervals_.length && 673 while (intervalIndex < this.intervals_.length &&
346 point.time > interval.end) { 674 point.time > interval.end) {
347 interval = this.intervals_[++intervalIndex]; // Jump to new interval. 675 interval = this.intervals_[++intervalIndex]; // Jump to new interval.
348 if (series.length > 0) { 676 if (series.length > 0) {
349 series = []; // Start a new series. 677 series = []; // Start a new series.
350 metricValue.data.push(series); // Put it on the end of the data. 678 metricDetails.data.push(series); // Put it on the end of the data.
351 } 679 }
352 } 680 }
353 if (intervalIndex < this.intervals_.length && 681 if (intervalIndex < this.intervals_.length &&
354 point.time > interval.start) { 682 point.time > interval.start) {
355 series.push([point.time - timezoneOffset, point.value]); 683 series.push([point.time - timezoneOffset_, point.value]);
356 } 684 }
357 } 685 }
358 metricValue.divs.forEach(this.drawChart, this); 686 metricDetails.maxValue = Math.max(metricDetails.maxValue,
687 result.maxValue);
688
689 metricDetails.divs.forEach(this.drawChart, this);
359 }, 690 },
360 691
361 /** 692 /**
362 * Add a new event to the chart(s). 693 * Add a new event to the chart(s).
363 * @param {string} eventType The type of event to start displaying. 694 * @param {string} eventType The type of event to start displaying.
364 */ 695 */
365 addEventType: function(eventType) { 696 addEventType: function(eventType) {
366 // Events show on all charts. 697 // Events show on all charts.
367 this.eventMap_[eventType].divs = this.charts; 698 this.eventDetailsMap_[eventType].divs = this.charts_;
368 this.refreshEventType(eventType); 699 this.refreshEventType(eventType);
369 }, 700 },
370 701
371 /* 702 /*
372 * Remove an event from the chart(s). 703 * Remove an event from the chart(s).
373 * @param {string} eventType The type of event to stop displaying. 704 * @param {string} eventType The type of event to stop displaying.
374 */ 705 */
375 dropEventType: function(eventType) { 706 dropEventType: function(eventType) {
376 var eventValue = this.eventMap_[eventType]; 707 var eventValue = this.eventDetailsMap_[eventType];
377 var affectedCharts = eventValue.divs; 708 var affectedCharts = eventValue.divs;
378 eventValue.divs = [];
379 709
710 eventValue.divs = []; // Gotta do this now for correct drawChart results.
380 affectedCharts.forEach(this.drawChart, this); 711 affectedCharts.forEach(this.drawChart, this);
381 }, 712 },
382 713
383 /** 714 /**
384 * Request new data for |eventType|, for times in the current range. 715 * Request new data for |eventType|, for times in the current range.
385 * @param {string} eventType The type of event to get new data for. 716 * @param {string} eventType The type of event to get new data for.
386 */ 717 */
387 refreshEventType: function(eventType) { 718 refreshEventType: function(eventType) {
388 // Mark eventType as awaiting response. 719 // Mark eventType as awaiting response.
389 this.eventMap_[eventType].data = null; 720 this.eventDetailsMap_[eventType].data = null;
390 721
391 chrome.send('getEvents', [eventType, this.start, this.end]); 722 chrome.send('getEvents', [eventType, this.start, this.end]);
392 }, 723 },
393 724
394 /** 725 /**
395 * Receive new events for |eventType|. If |eventType| has been deselected 726 * Receive new events for |eventType|. If |eventType| has been deselected
396 * while awaiting webui handler response, do nothing. Otherwise, save the 727 * while awaiting webui handler response, do nothing. Otherwise, save the
397 * data directly, since events are handled differently than metrics 728 * data directly, since events are handled differently than metrics
398 * when drawing (no "series"), and redraw all the affected charts. 729 * when drawing (no 'series'), and redraw all the affected charts.
399 * @param {!{ 730 * @param {!{
400 * type: number, 731 * id: number,
401 * points: !Array.<{time: number, longDescription: string}> 732 * events: !Array.<{time: number}>
402 * }} result An object giving eventType ID, and time/description pairs for 733 * }} result An object giving eventType id, and times at which that event
403 * each event of that type in the range requested. 734 * type occured in the range requested. Each object in the array may
735 * also have an arbitrary list of properties to be displayed as
736 * a tooltip message for the event.
404 */ 737 */
405 getEventsCallback: function(result) { 738 getEventsCallback: function(result) {
406 var eventValue = this.eventMap_[result.eventType]; 739 var eventValue = this.eventDetailsMap_[result.eventId];
407 740
408 if (eventValue.divs.length == 0) 741 if (eventValue.divs.length == 0)
409 return; 742 return;
410 743
411 result.points.forEach(function(element) { 744 result.events.forEach(function(element) {
412 element.time -= timezoneOffset; 745 element.time -= timezoneOffset_;
413 }); 746 });
414 747
415 eventValue.data = result.points; 748 eventValue.data = result.events;
416 eventValue.divs.forEach(this.drawChart, this); 749 eventValue.divs.forEach(this.drawChart, this);
417 }, 750 },
418 751
419 /** 752 /**
420 * Return an object containing an array of metrics and another of events 753 * Return an object containing an array of metrics and another of events
421 * that include |chart| as one of the divs into which they display. 754 * that include |chart| as one of the divs into which they display.
422 * @param {HTMLDivElement} chart The <div> for which to get relevant items. 755 * @param {HTMLDivElement} chart The <div> for which to get relevant items.
423 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} 756 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}}
424 * @private 757 * @private
425 */ 758 */
426 getChartData_: function(chart) { 759 getChartData_: function(chart) {
427 var result = {metrics: [], events: []}; 760 var result = {metrics: [], events: []};
428 761
429 for (var metric in this.metricMap_) { 762 for (var metric in this.metricDetailsMap_) {
430 var metricValue = this.metricMap_[metric]; 763 var metricDetails = this.metricDetailsMap_[metric];
431 764
432 if (metricValue.divs.indexOf(chart) != -1) 765 if (metricDetails.divs.indexOf(chart) != -1)
433 result.metrics.push(metricValue); 766 result.metrics.push(metricDetails);
434 } 767 }
435 768
436 for (var eventType in this.eventMap_) { 769 for (var eventType in this.eventDetailsMap_) {
437 var eventValue = this.eventMap_[eventType]; 770 var eventValue = this.eventDetailsMap_[eventType];
438 771
439 // Events post to all divs, if they post to any. 772 // Events post to all divs, if they post to any.
440 if (eventValue.divs.length > 0) 773 if (eventValue.divs.length > 0)
441 result.events.push(eventValue); 774 result.events.push(eventValue);
442 } 775 }
443 776
444 return result; 777 return result;
445 }, 778 },
446 779
447 /** 780 /**
448 * Check all entries in an object of the type returned from getChartData, 781 * Check all entries in an object of the type returned from getChartData,
449 * above, to see if all events and metrics have completed data (none is 782 * above, to see if all events and metrics have completed data (none is
450 * awaiting an asynchronous webui handler response to get their current 783 * awaiting an asynchronous webui handler response to get their current
451 * data). 784 * data).
452 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData 785 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData
453 * The event/metric data to check for readiness. 786 * The event/metric data to check for readiness.
454 * @return {boolean} Whether data is ready. 787 * @return {boolean} Whether data is ready.
455 * @private 788 * @private
456 */ 789 */
457 isDataReady_: function(chartData) { 790 isDataReady_: function(chartData) {
458 for (var i = 0; i < chartData.metrics.length; i++) { 791 return chartData.metrics.every(function(metric) {return metric.data;}) &&
459 if (!chartData.metrics[i].data) 792 chartData.events.every(function(event) {return event.data;});
460 return false;
461 }
462
463 for (var i = 0; i < chartData.events.length; i++) {
464 if (!chartData.events[i].data)
465 return false;
466 }
467
468 return true;
469 }, 793 },
470 794
471 /** 795 /**
472 * Create and return an array of "markings" (per Flot), representing 796 * Create and return an array of 'markings' (per Flot), representing
473 * vertical lines at the event time, in the event's color. Also add 797 * vertical lines at the event time, in the event's color. Also add
474 * (not per Flot) a |description| property to each, to be used for hand 798 * (not per Flot) a |popupTitle| property to each, to be used for
475 * creating description boxes. 799 * labelling description popups.
476 * @param {!Array.<{ 800 * @param {!Array.<{
477 * description: string, 801 * description: string,
478 * color: string, 802 * color: string,
479 * data: !Array.<{time: number}> 803 * data: !Array.<{time: number}>
480 * }>} eventValues The events to make markings for. 804 * }>} eventValues The events to make markings for.
481 * @return {!Array.<{ 805 * @return {!Array.<{
482 * color: string, 806 * color: string,
483 * description: string, 807 * description: string,
484 * xaxis: {from: number, to: number} 808 * xaxis: {from: number, to: number}
485 * }>} A marks data structure for Flot to use. 809 * }>} A marks data structure for Flot to use.
486 * @private 810 * @private
487 */ 811 */
488 getEventMarks_: function(eventValues) { 812 getEventMarks_: function(eventValues) {
489 var markings = []; 813 var markings = [];
814 var explanation;
815 var date, hours, minutes;
490 816
491 for (var i = 0; i < eventValues.length; i++) { 817 eventValues.forEach(function(eventValue) {
492 var eventValue = eventValues[i]; 818 eventValue.data.forEach(function(point) {
493 for (var d = 0; d < eventValue.data.length; d++) { 819 if (point.time >= this.start - timezoneOffset_ &&
494 var point = eventValue.data[d]; 820 point.time <= this.end - timezoneOffset_) {
495 if (point.time >= this.start - timezoneOffset && 821
496 point.time <= this.end - timezoneOffset) { 822 // Date wants Zulu time.
823 date = new Date(point.time + timezoneOffset_);
824 hours = date.getHours();
825 minutes = date.getMinutes();
826 explanation = eventValue.popupTitle + '\nAt: ' +
827 (date.getMonth() + 1) + '/' + date.getDate() + ' ' +
828 (hours < 10 ? '0' : '') + hours + ':' +
829 (minutes < 10 ? '0' : '') + minutes + '\n';
830
831 for (var key in point) {
832 if (key != 'time') {
833 var datum = point[key];
834 if ('label' in datum && 'value' in datum)
835 explanation = explanation + datum.label + ': ' +
836 datum.value + '\n';
837 }
838 }
497 markings.push({ 839 markings.push({
498 color: eventValue.color, 840 color: eventValue.color,
499 description: eventValue.description, 841 popupContent: explanation,
500 xaxis: {from: point.time, to: point.time} 842 xaxis: {from: point.time, to: point.time}
501 }); 843 });
502 } else { 844 } else {
503 console.log('Event out of time range ' + this.start + ' -> ' + 845 console.log('Event out of time range ' + this.start + ' -> ' +
504 this.end + ' at: ' + point.time); 846 this.end + ' at: ' + point.time);
505 } 847 }
506 } 848 }, this);
507 } 849 }, this);
508 850
509 return markings; 851 return markings;
510 }, 852 },
511 853
512 /** 854 /**
513 * Redraw the chart in div |chart|, *if* all its dependent data is present. 855 * Redraw the chart in div |chart|, *if* all its dependent data is present.
514 * Otherwise simply return, and await another call when all data is 856 * Otherwise simply return, and await another call when all data is
515 * available. 857 * available.
516 * @param {HTMLDivElement} chart The <div> to redraw. 858 * @param {HTMLDivElement} chart The <div> to redraw.
517 */ 859 */
518 drawChart: function(chart) { 860 drawChart: function(chart) {
519 var chartData = this.getChartData_(chart); 861 var chartData = this.getChartData_(chart);
862 var axisMap = {}; // Maps category ids to yAxis numbers
Devlin 2012/09/07 18:44:54 nit: |yAxisNumber|s? Make it clear this is a varia
clintstaley 2012/09/07 20:39:52 I'm going to differ on this. yAxisNumber is just
520 863
521 if (!this.isDataReady_(chartData)) 864 if (chart.hidden || !this.isDataReady_(chartData))
522 return; 865 return;
523 866
524 var seriesSeq = []; 867 var seriesSeq = [];
525 var yAxes = []; 868 var yAxes = [];
526 chartData.metrics.forEach(function(value) { 869 chartData.metrics.forEach(function(metricDetails) {
527 yAxes.push(value.yAxis); 870 var metricCategory = metricDetails.category;
528 for (var i = 0; i < value.data.length; i++) { 871 var yAxisNumber = axisMap[metricCategory.metricCategoryId];
872
873 // Add a new y-axis if we are encountering this category of metric
874 // for the first time. Otherwise, update the existing y-axis with
875 // a new max value if needed. (Presently, we expect only one category
876 // of metric per chart, but this design permits more in the future.)
877 if (yAxisNumber === undefined) {
878 yAxes.push({min: 0, max: metricDetails.maxValue * yAxisMargin_,
879 labelWidth: 60});
880 axisMap[metricCategory.metricCategoryId] = yAxisNumber = yAxes.length;
881 } else {
882 yAxes[yAxisNumber - 1].max = Math.max(yAxes[yAxisNumber - 1].max,
883 metricDetails.maxValue * yAxisMargin_);
884 }
885
886 for (var i = 0; i < metricDetails.data.length; i++) {
529 seriesSeq.push({ 887 seriesSeq.push({
530 color: value.yAxis.color, 888 color: metricDetails.color,
531 data: value.data[i], 889 data: metricDetails.data[i],
532 label: i == 0 ? value.description + ' (' + value.units + ')' : null, 890 label: i == 0 ? metricDetails.name +
533 yaxis: yAxes.length, // Use just-added Y axis. 891 ' (' + metricCategory.unit + ')' : null,
892 yaxis: yAxisNumber
534 }); 893 });
535 } 894 }
536 }); 895 });
537 896
538 // Ensure at least one y axis, as a reference for event placement.
539 if (yAxes.length == 0)
540 yAxes.push({max: 1.0});
541
542 var markings = this.getEventMarks_(chartData.events); 897 var markings = this.getEventMarks_(chartData.events);
543 var chart = this.charts[0];
544 var plot = $.plot(chart, seriesSeq, { 898 var plot = $.plot(chart, seriesSeq, {
545 yaxes: yAxes, 899 yaxes: yAxes,
546 xaxis: { 900 xaxis: {
547 mode: 'time', 901 mode: 'time',
548 min: this.start - timezoneOffset, 902 min: this.start - timezoneOffset_,
549 max: this.end - timezoneOffset 903 max: this.end - timezoneOffset_
550 }, 904 },
551 points: {show: true, radius: 1}, 905 points: {show: true, radius: 1},
552 lines: {show: true}, 906 lines: {show: true},
553 grid: {markings: markings} 907 grid: {markings: markings, hoverable: true, autoHighlight: true}
554 }); 908 });
555 909
556 // For each event in |markings|, create also a label div, with left 910 // For each event in |markings|, create also a label div, with left
557 // edge colinear with the event vertical line. Top of label is 911 // edge colinear with the event vertical line. Top of label is
558 // presently a hack-in, putting labels in three tiers of 25px height 912 // presently a hack-in, putting labels in three tiers of 25px height
559 // each to avoid overlap. Will need something better. 913 // each to avoid overlap. Will need something better.
560 var labelTemplate = $('#label-template')[0]; 914 var labelTemplate = $('#label-template')[0];
561 for (var i = 0; i < markings.length; i++) { 915 for (var i = 0; i < markings.length; i++) {
562 var mark = markings[i]; 916 var mark = markings[i];
563 var point = 917 var point =
564 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); 918 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1});
565 var labelDiv = labelTemplate.cloneNode(true); 919 var labelDiv = labelTemplate.cloneNode(true);
566 labelDiv.innerText = mark.description; 920 labelDiv.innerText = mark.popupContent;
567 labelDiv.style.left = point.left + 'px'; 921 labelDiv.style.left = point.left + 'px';
568 labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; 922 labelDiv.style.top = (point.top + 100 * (i % 3)) + 'px';
569 923
570 chart.appendChild(labelDiv); 924 chart.appendChild(labelDiv);
925 labelDiv.hidden = true;
926 chart.hovers.push({x: mark.xaxis.to, div: labelDiv});
571 } 927 }
572 } 928 }
573 }; 929 };
574 return { 930 return {
575 PerformanceMonitor: PerformanceMonitor 931 PerformanceMonitor: PerformanceMonitor
576 }; 932 };
577 }); 933 });
578 934
579 var PerformanceMonitor = new performance_monitor.PerformanceMonitor(); 935 var PerformanceMonitor = new performance_monitor.PerformanceMonitor();
OLDNEW
« no previous file with comments | « chrome/browser/resources/performance_monitor/chart.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698