OLD | NEW |
| (Empty) |
1 /* | |
2 Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 Use of this source code is governed by a BSD-style license that can be | |
4 found in the LICENSE file. | |
5 */ | |
6 | |
7 /** | |
8 * @fileoverview Collection of functions and classes used to plot data in a | |
9 * <canvas>. Create a Plotter() to generate a plot. | |
10 */ | |
11 | |
12 /** | |
13 * Adds commas to a given number. | |
14 * | |
15 * Examples: | |
16 * 1234.56 => "1,234.56" | |
17 * 99999 => "99,999" | |
18 * | |
19 * @param {string|number} number The number to format. | |
20 * @return {string} String representation of |number| with commas for every | |
21 * three digits to the left of a decimal point. | |
22 */ | |
23 function addCommas(number) { | |
24 number += ''; // Convert number to string if not already a string. | |
25 var numberParts = number.split('.'); | |
26 var integralPart = numberParts[0]; | |
27 var fractionalPart = numberParts.length > 1 ? '.' + numberParts[1] : ''; | |
28 var reThreeDigits = /(\d+)(\d{3})/; | |
29 while (reThreeDigits.test(integralPart)) | |
30 integralPart = integralPart.replace(reThreeDigits, '$1' + ',' + '$2'); | |
31 return integralPart + fractionalPart; | |
32 } | |
33 | |
34 /** | |
35 * Vertical marker to highlight data points that are being hovered over by the | |
36 * mouse. | |
37 * | |
38 * @param {string} color The color to make the marker, e.g., 'rgb(100,80,240)'. | |
39 * @return {Element} A div Element object representing the vertical marker. | |
40 */ | |
41 function VerticalMarker(color) { | |
42 var m = document.createElement('div'); | |
43 m.style.backgroundColor = color; | |
44 m.style.opacity = '0.3'; | |
45 m.style.position = 'absolute'; | |
46 m.style.left = '-2px'; | |
47 m.style.top = '-2px'; | |
48 m.style.width = '0px'; | |
49 m.style.height = '0px'; | |
50 return m; | |
51 } | |
52 | |
53 /** | |
54 * Class representing a horizontal marker at the indicated mouse location. | |
55 * @constructor | |
56 * | |
57 * @param {Object} canvasRect The canvas bounds (in client coords). | |
58 * @param {Number} yPixelClicked The vertical mouse click location that spawned | |
59 * the marker, in the client coordinate space. | |
60 * @param {Number} yValue The data value corresponding to the vertical click | |
61 * location. | |
62 * @param {Number} yOtherValue If the plot is overlaying two coordinate systems, | |
63 * this is the data value corresponding to the vertical click location in | |
64 * the second coordinate system. Can be null. | |
65 */ | |
66 function HorizontalMarker(canvasRect, yPixelClicked, yValue, yOtherValue) { | |
67 var m = document.createElement('div'); | |
68 m.style.backgroundColor = HorizontalMarker.COLOR; | |
69 m.style.opacity = '0.3'; | |
70 m.style.position = 'absolute'; | |
71 m.style.left = canvasRect.offsetLeft; | |
72 var h = HorizontalMarker.HEIGHT; | |
73 m.style.top = (yPixelClicked + document.body.scrollTop - (h / 2)).toFixed(0) + | |
74 'px'; | |
75 m.style.width = canvasRect.offsetWidth + 'px'; | |
76 m.style.height = h + 'px'; | |
77 | |
78 this.markerDiv = m; | |
79 this.value = yValue; | |
80 this.otherValue = yOtherValue; | |
81 } | |
82 | |
83 HorizontalMarker.HEIGHT = 5; | |
84 HorizontalMarker.COLOR = 'rgb(0,100,100)'; | |
85 | |
86 /** | |
87 * Removes the horizontal marker from the graph. | |
88 */ | |
89 HorizontalMarker.prototype.remove = function() { | |
90 this.markerDiv.parentNode.removeChild(this.markerDiv); | |
91 }; | |
92 | |
93 /** | |
94 * Main class that does the actual plotting. | |
95 * | |
96 * Draws a chart using a canvas element. Takes an array of lines to draw. | |
97 * @constructor | |
98 * | |
99 * @param {Array} plotData list of arrays that represent individual lines. The | |
100 * line itself is an Array of points. | |
101 * @param {Array} dataDescriptions list of data descriptions for each line in | |
102 * |plotData|. | |
103 * @param {string} eventName The string name of an event to overlay on the | |
104 * graph. Should be 'null' if there are no events to overlay. | |
105 * @param {Object} eventInfo If |eventName| is specified, an array of event | |
106 * points to overlay on the graph. Each event point in the array is itself | |
107 * a 2-element array, where the first element is the x-axis value at which | |
108 * the event occurred during the test, and the second element is a | |
109 * dictionary of kay/value pairs representing metadata associated with the | |
110 * event. | |
111 * @param {string} unitsX The x-axis units of the data being plotted. | |
112 * @param {string} unitsY The y-axis units of the data being plotted. | |
113 * @param {string} unitsYOther If another graph (with different y-axis units) is | |
114 * being overlayed over the first graph, this represents the units of the | |
115 * other graph. Otherwise, this should be 'null'. | |
116 * @param {string} resultNode A DOM Element object representing the DOM node to | |
117 * which the plot should be attached. | |
118 * @param {boolean} Whether or not the graph should be drawn in 'lookout' mode, | |
119 * which is a summarized view that is made for overview pages when the graph | |
120 * is drawn in a more confined space. | |
121 * | |
122 * Example of the |plotData|: | |
123 * [ | |
124 * [line 1 data], | |
125 * [line 2 data] | |
126 * ]. | |
127 * Line data looks like [[point one], [point two]]. | |
128 * And individual points are [x value, y value] | |
129 */ | |
130 function Plotter(plotData, dataDescriptions, eventName, eventInfo, unitsX, | |
131 unitsY, unitsYOther, resultNode, is_lookout) { | |
132 this.plotData_ = plotData; | |
133 this.dataDescriptions_ = dataDescriptions; | |
134 this.eventName_ = eventName; | |
135 this.eventInfo_ = eventInfo; | |
136 this.unitsX_ = unitsX; | |
137 this.unitsY_ = unitsY; | |
138 this.unitsYOther_ = unitsYOther; | |
139 this.resultNode_ = resultNode; | |
140 this.is_lookout_ = is_lookout; | |
141 | |
142 this.dataColors_ = []; | |
143 | |
144 this.coordinates = null; | |
145 this.coordinatesOther = null; | |
146 if (this.unitsYOther_) { | |
147 // Need two different coordinate systems to overlay on the same graph. | |
148 this.coordinates = new Coordinates([plotData[0]]); | |
149 this.coordinatesOther = new Coordinates([plotData[1]]); | |
150 } else { | |
151 this.coordinates = new Coordinates(plotData); | |
152 } | |
153 | |
154 // A color palette that's unambigous for normal and color-deficient viewers. | |
155 // Values are (red, green, blue) on a scale of 255. | |
156 // Taken from http://jfly.iam.u-tokyo.ac.jp/html/manuals/pdf/color_blind.pdf. | |
157 this.colors = [[0, 114, 178], // Blue. | |
158 [230, 159, 0], // Orange. | |
159 [0, 158, 115], // Green. | |
160 [204, 121, 167], // Purplish pink. | |
161 [86, 180, 233], // Sky blue. | |
162 [213, 94, 0], // Dark orange. | |
163 [0, 0, 0], // Black. | |
164 [240, 228, 66] // Yellow. | |
165 ]; | |
166 | |
167 for (var i = 0, colorIndex = 0; i < this.dataDescriptions_.length; ++i) | |
168 this.dataColors_[i] = this.makeColor(colorIndex++); | |
169 } | |
170 | |
171 /** | |
172 * Generates a string representing a color corresponding to the given index | |
173 * in a color array. Handles wrapping around the color array if necessary. | |
174 * | |
175 * @param {number} i An index into the |this.colors| array. | |
176 * @return {string} A string representing a color in 'rgb(X,Y,Z)' format. | |
177 */ | |
178 Plotter.prototype.makeColor = function(i) { | |
179 var index = i % this.colors.length; | |
180 return 'rgb(' + this.colors[index][0] + ',' + | |
181 this.colors[index][1] + ',' + | |
182 this.colors[index][2] + ')'; | |
183 }; | |
184 | |
185 /** | |
186 * Same as function makeColor above, but also takes a transparency value | |
187 * indicating how transparent to make the color appear. | |
188 * | |
189 * @param {number} i An index into the |this.colors| array. | |
190 * @param {number} transparencyPercent Percentage transparency to make the | |
191 * color, e.g., 0.75. | |
192 * @return {string} A string representing a color in 'rgb(X,Y,Z,A)' format, | |
193 * where A is the percentage transparency. | |
194 */ | |
195 Plotter.prototype.makeColorTransparent = function(i, transparencyPercent) { | |
196 var index = i % this.colors.length; | |
197 return 'rgba(' + this.colors[index][0] + ',' + | |
198 this.colors[index][1] + ',' + | |
199 this.colors[index][2] + ',' + transparencyPercent + ')'; | |
200 }; | |
201 | |
202 /** | |
203 * Gets the data color value associated with a specified color index. | |
204 * | |
205 * @param {number} i An index into the |this.colors| array. | |
206 * @return {string} A string representing a color in 'rgb(X,Y,Z,A)' format, | |
207 * where A is the percentage transparency. | |
208 */ | |
209 Plotter.prototype.getDataColor = function(i) { | |
210 if (this.dataColors_[i]) | |
211 return this.dataColors_[i]; | |
212 else | |
213 return this.makeColor(i); | |
214 }; | |
215 | |
216 /** | |
217 * Does the actual plotting. | |
218 */ | |
219 Plotter.prototype.plot = function() { | |
220 this.canvasElement_ = this.canvas_(); | |
221 this.rulerDiv_ = this.ruler_(); | |
222 | |
223 // Markers for the result point(s)/events that the mouse is currently | |
224 // hovering over. | |
225 this.cursorDiv_ = new VerticalMarker('rgb(100,80,240)'); | |
226 this.cursorDivOther_ = new VerticalMarker('rgb(50,50,50)'); | |
227 this.eventDiv_ = new VerticalMarker('rgb(255, 0, 0)'); | |
228 | |
229 this.resultNode_.appendChild(this.canvasElement_); | |
230 this.resultNode_.appendChild(this.coordinates_()); | |
231 this.resultNode_.appendChild(this.rulerDiv_); | |
232 this.resultNode_.appendChild(this.cursorDiv_); | |
233 this.resultNode_.appendChild(this.cursorDivOther_); | |
234 this.resultNode_.appendChild(this.eventDiv_); | |
235 this.attachEventListeners_(); | |
236 | |
237 // Now draw the canvas. | |
238 var ctx = this.canvasElement_.getContext('2d'); | |
239 | |
240 // Clear it with white: otherwise canvas will draw on top of existing data. | |
241 ctx.clearRect(0, 0, this.canvasElement_.width, this.canvasElement_.height); | |
242 | |
243 // Draw all data lines. | |
244 for (var i = 0; i < this.plotData_.length; ++i) { | |
245 var coordinateSystem = this.coordinates; | |
246 if (i > 0 && this.unitsYOther_) | |
247 coordinateSystem = this.coordinatesOther; | |
248 this.plotLine_(ctx, this.getDataColor(i), this.plotData_[i], | |
249 coordinateSystem); | |
250 } | |
251 | |
252 // Draw events overlayed on graph if needed. | |
253 if (this.eventName_ && this.eventInfo_) | |
254 this.plotEvents_(ctx, 'rgb(255, 150, 150)', this.coordinates); | |
255 | |
256 this.graduation_divs_ = this.graduations_(this.coordinates, 0, false); | |
257 if (this.unitsYOther_) { | |
258 this.graduation_divs_ = this.graduation_divs_.concat( | |
259 this.graduations_(this.coordinatesOther, 1, true)); | |
260 } | |
261 for (var i = 0; i < this.graduation_divs_.length; ++i) | |
262 this.resultNode_.appendChild(this.graduation_divs_[i]); | |
263 }; | |
264 | |
265 /** | |
266 * Draws events overlayed on top of an existing graph. | |
267 * | |
268 * @param {Object} ctx A canvas element object for drawing. | |
269 * @param {string} strokeStyles A string representing the drawing style. | |
270 * @param {Object} coordinateSystem A Coordinates object representing the | |
271 * coordinate system of the graph. | |
272 */ | |
273 Plotter.prototype.plotEvents_ = function(ctx, strokeStyles, coordinateSystem) { | |
274 ctx.strokeStyle = strokeStyles; | |
275 ctx.fillStyle = strokeStyles; | |
276 ctx.lineWidth = 1.0; | |
277 | |
278 ctx.beginPath(); | |
279 var data = this.eventInfo_; | |
280 for (var index = 0; index < data.length; ++index) { | |
281 var event_time = data[index][0]; | |
282 var x = coordinateSystem.xPixel(event_time); | |
283 ctx.moveTo(x, 0); | |
284 ctx.lineTo(x, this.canvasElement_.offsetHeight); | |
285 } | |
286 ctx.closePath(); | |
287 ctx.stroke(); | |
288 }; | |
289 | |
290 /** | |
291 * Draws a line on the graph. | |
292 * | |
293 * @param {Object} ctx A canvas element object for drawing. | |
294 * @param {string} strokeStyles A string representing the drawing style. | |
295 * @param {Array} data A list of [x, y] values representing the line to plot. | |
296 * @param {Object} coordinateSystem A Coordinates object representing the | |
297 * coordinate system of the graph. | |
298 */ | |
299 Plotter.prototype.plotLine_ = function(ctx, strokeStyles, data, | |
300 coordinateSystem) { | |
301 ctx.strokeStyle = strokeStyles; | |
302 ctx.fillStyle = strokeStyles; | |
303 ctx.lineWidth = 2.0; | |
304 | |
305 ctx.beginPath(); | |
306 var initial = true; | |
307 var allPoints = []; | |
308 for (var i = 0; i < data.length; ++i) { | |
309 var pointX = parseFloat(data[i][0]); | |
310 var pointY = parseFloat(data[i][1]); | |
311 var x = coordinateSystem.xPixel(pointX); | |
312 var y = 0.0; | |
313 if (isNaN(pointY)) { | |
314 // Re-set 'initial' if we're at a gap in the data. | |
315 initial = true; | |
316 } else { | |
317 y = coordinateSystem.yPixel(pointY); | |
318 if (initial) | |
319 initial = false; | |
320 else | |
321 ctx.lineTo(x, y); | |
322 } | |
323 | |
324 ctx.moveTo(x, y); | |
325 allPoints.push([x, y]); | |
326 } | |
327 ctx.closePath(); | |
328 ctx.stroke(); | |
329 | |
330 if (!this.is_lookout_) { | |
331 // Draw a small dot at each point. | |
332 for (var i = 0; i < allPoints.length; ++i) { | |
333 ctx.beginPath(); | |
334 ctx.arc(allPoints[i][0], allPoints[i][1], 3, 0, Math.PI*2, true); | |
335 ctx.fill(); | |
336 } | |
337 } | |
338 }; | |
339 | |
340 /** | |
341 * Attaches event listeners to DOM nodes. | |
342 */ | |
343 Plotter.prototype.attachEventListeners_ = function() { | |
344 var self = this; | |
345 this.canvasElement_.parentNode.addEventListener( | |
346 'mousemove', function(evt) { self.onMouseMove_(evt); }, false); | |
347 this.cursorDiv_.addEventListener( | |
348 'click', function(evt) { self.onMouseClick_(evt); }, false); | |
349 this.cursorDivOther_.addEventListener( | |
350 'click', function(evt) { self.onMouseClick_(evt); }, false); | |
351 this.eventDiv_.addEventListener( | |
352 'click', function(evt) { self.onMouseClick_(evt); }, false); | |
353 }; | |
354 | |
355 /** | |
356 * Update the horizontal line that is following where the mouse is hovering. | |
357 * | |
358 * @param {Object} evt A mouse event object representing a mouse move event. | |
359 */ | |
360 Plotter.prototype.updateRuler_ = function(evt) { | |
361 var r = this.rulerDiv_; | |
362 r.style.left = this.canvasElement_.offsetLeft + 'px'; | |
363 r.style.top = this.canvasElement_.offsetTop + 'px'; | |
364 r.style.width = this.canvasElement_.offsetWidth + 'px'; | |
365 var h = evt.clientY + document.body.scrollTop - this.canvasElement_.offsetTop; | |
366 if (h > this.canvasElement_.offsetHeight) | |
367 h = this.canvasElement_.offsetHeight; | |
368 r.style.height = h + 'px'; | |
369 }; | |
370 | |
371 /** | |
372 * Update the highlighted data point at the x value that the mouse is hovering | |
373 * over. | |
374 * | |
375 * @param {Object} coordinateSystem A Coordinates object representing the | |
376 * coordinate system of the graph. | |
377 * @param {number} currentIndex The index into the |this.plotData| array of the | |
378 * data point being hovered over, for a given line. | |
379 * @param {Object} cursorDiv A DOM element div object representing the highlight | |
380 * itself. | |
381 * @param {number} dataIndex The index into the |this.plotData| array of the | |
382 * line being hovered over. | |
383 */ | |
384 Plotter.prototype.updateCursor_ = function(coordinateSystem, currentIndex, | |
385 cursorDiv, dataIndex) { | |
386 var c = cursorDiv; | |
387 c.style.top = this.canvasElement_.offsetTop + 'px'; | |
388 c.style.height = this.canvasElement_.offsetHeight + 'px'; | |
389 | |
390 // Left point is half-way to the previous x value, unless it's the first | |
391 // point, in which case it's the x value of the current point. | |
392 var leftPoint = null; | |
393 if (currentIndex == 0) { | |
394 leftPoint = this.canvasElement_.offsetLeft + | |
395 coordinateSystem.xPixel(this.plotData_[dataIndex][0][0]); | |
396 } | |
397 else { | |
398 var left_x = this.canvasElement_.offsetLeft + | |
399 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex - 1][0]); | |
400 var curr_x = this.canvasElement_.offsetLeft + | |
401 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); | |
402 leftPoint = (left_x + curr_x) / 2; | |
403 } | |
404 c.style.left = leftPoint; | |
405 | |
406 // Width is half-way to the next x value minus the left point, unless it's | |
407 // the last point, in which case it's the x value of the current point minus | |
408 // the left point. | |
409 if (currentIndex == this.plotData_[dataIndex].length - 1) { | |
410 var curr_x = this.canvasElement_.offsetLeft + | |
411 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); | |
412 c.style.width = curr_x - left_point; | |
413 } | |
414 else { | |
415 var next_x = this.canvasElement_.offsetLeft + | |
416 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex + 1][0]); | |
417 var curr_x = this.canvasElement_.offsetLeft + | |
418 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); | |
419 c.style.width = ((next_x + curr_x) / 2) - leftPoint; | |
420 } | |
421 }; | |
422 | |
423 /** | |
424 * Update the highlighted event at the x value that the mouse is hovering over. | |
425 * | |
426 * @param {number} x The x-value (pixel) at which to draw the event highlight | |
427 * div. | |
428 * @param {boolean} show Whether or not to show the highlight div. | |
429 */ | |
430 Plotter.prototype.updateEventDiv_ = function(x, show) { | |
431 var c = this.eventDiv_; | |
432 c.style.top = this.canvasElement_.offsetTop + 'px'; | |
433 c.style.height = this.canvasElement_.offsetHeight + 'px'; | |
434 | |
435 if (show) { | |
436 c.style.left = this.canvasElement_.offsetLeft + (x - 2); | |
437 c.style.width = 8; | |
438 } else { | |
439 c.style.width = 0; | |
440 } | |
441 }; | |
442 | |
443 /** | |
444 * Handle a mouse move event. | |
445 * | |
446 * @param {Object} evt A mouse event object representing a mouse move event. | |
447 */ | |
448 Plotter.prototype.onMouseMove_ = function(evt) { | |
449 var canvas = evt.currentTarget.firstChild; | |
450 var positionX = evt.clientX + document.body.scrollLeft - | |
451 this.canvasElement_.offsetLeft; | |
452 var positionY = evt.clientY + document.body.scrollTop - | |
453 this.canvasElement_.offsetTop; | |
454 | |
455 // Identify the index of the x value that is closest to the mouse x value. | |
456 var xValue = this.coordinates.xValue(positionX); | |
457 var min_diff = Math.abs(this.plotData_[0][0][0] - xValue); | |
458 indexValueX = 0; | |
459 for (var i = 1; i < this.plotData_[0].length; ++i) { | |
460 var diff = Math.abs(this.plotData_[0][i][0] - xValue); | |
461 if (diff < min_diff) { | |
462 min_diff = diff; | |
463 indexValueX = i; | |
464 } | |
465 } | |
466 | |
467 // Identify the index of the x value closest to the mouse x value for the | |
468 // other graph being overlayed on top of the original graph, if one exists. | |
469 if (this.unitsYOther_) { | |
470 var xValue = this.coordinatesOther.xValue(positionX); | |
471 var min_diff = Math.abs(this.plotData_[1][0][0] - xValue); | |
472 var indexValueXOther = 0; | |
473 for (var i = 1; i < this.plotData_[1].length; ++i) { | |
474 var diff = Math.abs(this.plotData_[1][i][0] - xValue); | |
475 if (diff < min_diff) { | |
476 min_diff = diff; | |
477 indexValueXOther = i; | |
478 } | |
479 } | |
480 } | |
481 | |
482 // Update coordinate information displayed directly underneath the graph. | |
483 var yValue = this.coordinates.yValue(positionY); | |
484 | |
485 this.coordinatesTd_.innerHTML = | |
486 '<font style="color:' + this.dataColors_[0] + '">' + | |
487 this.plotData_[0][indexValueX][0] + ' ' + this.unitsX_ + ': ' + | |
488 addCommas(this.plotData_[0][indexValueX][1].toFixed(2)) + ' ' + | |
489 this.unitsY_ + '</font> [hovering at ' + addCommas(yValue.toFixed(2)) + | |
490 ' ' + this.unitsY_ + ']'; | |
491 | |
492 if (this.unitsYOther_) { | |
493 var yValue2 = this.coordinatesOther.yValue(positionY); | |
494 this.coordinatesTdOther_.innerHTML = | |
495 '<font style="color:' + this.dataColors_[1] + '">' + | |
496 this.plotData_[1][indexValueXOther][0] + ' ' + this.unitsX_ + ': ' + | |
497 addCommas(this.plotData_[1][indexValueXOther][1].toFixed(2)) + ' ' + | |
498 (this.unitsYOther_ ? this.unitsYOther_ : this.unitsY_) + | |
499 '</font> [hovering at ' + addCommas(yValue2.toFixed(2)) + ' ' + | |
500 this.unitsYOther_ + ']'; | |
501 } | |
502 else if (this.dataDescriptions_.length > 1) { | |
503 this.coordinatesTdOther_.innerHTML = | |
504 '<font style="color:' + this.dataColors_[1] + '">' + | |
505 this.plotData_[1][indexValueX][0] + ' ' + this.unitsX_ + ': ' + | |
506 addCommas(this.plotData_[1][indexValueX][1].toFixed(2)) + ' ' + | |
507 (this.unitsYOther_ ? this.unitsYOther_ : this.unitsY_) + '</font>'; | |
508 } | |
509 | |
510 // If there is a horizontal marker, also display deltas relative to it. | |
511 if (this.horizontal_marker_) { | |
512 var baseline = this.horizontal_marker_.value; | |
513 var delta = yValue - baseline; | |
514 var fraction = delta / baseline; // Allow division by 0. | |
515 | |
516 var deltaStr = (delta >= 0 ? '+' : '') + delta.toFixed(0) + ' ' + | |
517 this.unitsY_; | |
518 var percentStr = (fraction >= 0 ? '+' : '') + (fraction * 100).toFixed(3) + | |
519 '%'; | |
520 | |
521 this.baselineDeltasTd_.innerHTML = deltaStr + ': ' + percentStr; | |
522 | |
523 if (this.unitsYOther_) { | |
524 var baseline = this.horizontal_marker_.otherValue; | |
525 var yValue2 = this.coordinatesOther.yValue(positionY); | |
526 var delta = yValue2 - baseline; | |
527 var fraction = delta / baseline; // Allow division by 0. | |
528 | |
529 var deltaStr = (delta >= 0 ? '+' : '') + delta.toFixed(0) + ' ' + | |
530 this.unitsYOther_; | |
531 var percentStr = (fraction >= 0 ? '+' : '') + | |
532 (fraction * 100).toFixed(3) + '%'; | |
533 this.baselineDeltasTd_.innerHTML += '<br>' + deltaStr + ': ' + percentStr; | |
534 } | |
535 } | |
536 | |
537 this.updateRuler_(evt); | |
538 this.updateCursor_(this.coordinates, indexValueX, this.cursorDiv_, 0); | |
539 if (this.unitsYOther_) { | |
540 this.updateCursor_(this.coordinatesOther, indexValueXOther, | |
541 this.cursorDivOther_, 1); | |
542 } | |
543 | |
544 // If there are events displayed, see if we're hovering close to an existing | |
545 // event on the graph, and if so, display the metadata associated with it. | |
546 if (this.eventName_ != null && this.eventInfo_ != null) { | |
547 var data = this.eventInfo_; | |
548 var showed_event = false; | |
549 var x = 0; | |
550 for (var index = 0; index < data.length; ++index) { | |
551 var event_time = data[index][0]; | |
552 x = this.coordinates.xPixel(event_time); | |
553 if (positionX >= x - 10 && positionX <= x + 10) { | |
554 var metadata = data[index][1]; | |
555 var metadata_str = ""; | |
556 for (var meta_key in metadata) | |
557 metadata_str += meta_key + ': ' + metadata[meta_key] + ', '; | |
558 metadata_str = metadata_str.substring(0, metadata_str.length - 2); | |
559 this.coordinatesTdOther_.innerHTML = event_time + ' ' + this.unitsX_ + | |
560 ': {' + metadata_str + '}'; | |
561 showed_event = true; | |
562 this.updateEventDiv_(x, true); | |
563 break; | |
564 } | |
565 } | |
566 if (!showed_event) { | |
567 this.coordinatesTdOther_.innerHTML = | |
568 'move mouse close to vertical event marker'; | |
569 this.updateEventDiv_(x, false); | |
570 } | |
571 } | |
572 }; | |
573 | |
574 /** | |
575 * Handle a mouse click event. | |
576 * | |
577 * @param {Object} evt A mouse event object representing a mouse click event. | |
578 */ | |
579 Plotter.prototype.onMouseClick_ = function(evt) { | |
580 // Shift-click controls the horizontal reference line. | |
581 if (evt.shiftKey) { | |
582 if (this.horizontal_marker_) | |
583 this.horizontal_marker_.remove(); | |
584 | |
585 var canvasY = evt.clientY - this.canvasElement_.offsetTop; | |
586 this.horizontal_marker_ = new HorizontalMarker( | |
587 this.canvasElement_, evt.clientY, this.coordinates.yValue(canvasY), | |
588 (this.coordinatesOther ? this.coordinatesOther.yValue(canvasY) : null)); | |
589 // Insert before cursor node, otherwise it catches clicks. | |
590 this.cursorDiv_.parentNode.insertBefore( | |
591 this.horizontal_marker_.markerDiv, this.cursorDiv_); | |
592 } | |
593 }; | |
594 | |
595 /** | |
596 * Generates and returns a list of div objects representing horizontal lines in | |
597 * the graph that indicate y-axis values at a computed interval. | |
598 * | |
599 * @param {Object} coordinateSystem a Coordinates object representing the | |
600 * coordinate system for which the graduations should be created. | |
601 * @param {number} colorIndex An index into the |this.colors| array representing | |
602 * the color to make the graduations in the event that two graphs with | |
603 * different coordinate systems are being overlayed on the same plot. | |
604 * @param {boolean} isRightSide Whether or not the graduations should have | |
605 * right-aligned text (used when the graduations are for a second graph | |
606 * that is being overlayed on top of another graph). | |
607 * @return {Array} An array of DOM Element objects representing the divs. | |
608 */ | |
609 Plotter.prototype.graduations_ = function(coordinateSystem, colorIndex, | |
610 isRightSide) { | |
611 // Don't allow a graduation in the bottom 5% of the chart or the number label | |
612 // would overflow the chart bounds. | |
613 var yMin = coordinateSystem.yMinValue + .05 * coordinateSystem.yValueRange(); | |
614 var yRange = coordinateSystem.yMaxValue - yMin; | |
615 | |
616 // Use the largest scale that fits 3 or more graduations. | |
617 // We allow scales of [...,500, 250, 100, 50, 25, 10,...]. | |
618 var scale = 5000000000; | |
619 while (scale) { | |
620 if (Math.floor(yRange / scale) > 2) break; // 5s. | |
621 scale /= 2; | |
622 if (Math.floor(yRange / scale) > 2) break; // 2.5s. | |
623 scale /= 2.5; | |
624 if (Math.floor(yRange / scale) > 2) break; // 1s. | |
625 scale /= 2; | |
626 } | |
627 | |
628 var graduationPosition = yMin + (scale - yMin % scale); | |
629 var graduationDivs = []; | |
630 while (graduationPosition < coordinateSystem.yMaxValue || yRange == 0) { | |
631 var graduation = document.createElement('div'); | |
632 var canvasPosition; | |
633 if (yRange == 0) { | |
634 // Center the graduation vertically. | |
635 canvasPosition = this.canvasElement_.offsetHeight / 2; | |
636 } else { | |
637 canvasPosition = coordinateSystem.yPixel(graduationPosition); | |
638 } | |
639 if (this.unitsYOther_) { | |
640 graduation.style.borderTop = '1px dashed ' + | |
641 this.makeColorTransparent(colorIndex, 0.4) | |
642 } else { | |
643 graduation.style.borderTop = '1px dashed rgba(0,0,0,.08)'; | |
644 } | |
645 graduation.style.position = 'absolute'; | |
646 graduation.style.left = this.canvasElement_.offsetLeft + 'px'; | |
647 graduation.style.top = canvasPosition + this.canvasElement_.offsetTop + | |
648 'px'; | |
649 graduation.style.width = this.canvasElement_.offsetWidth - | |
650 this.canvasElement_.offsetLeft + 'px'; | |
651 graduation.style.paddingLeft = '4px'; | |
652 if (this.unitsYOther_) | |
653 graduation.style.color = this.makeColorTransparent(colorIndex, 0.9) | |
654 else | |
655 graduation.style.color = 'rgba(0,0,0,.4)'; | |
656 graduation.style.fontSize = '9px'; | |
657 graduation.style.paddingTop = '0'; | |
658 graduation.style.zIndex = '-1'; | |
659 if (isRightSide) | |
660 graduation.style.textAlign = 'right'; | |
661 if (yRange == 0) | |
662 graduation.innerHTML = addCommas(yMin); | |
663 else | |
664 graduation.innerHTML = addCommas(graduationPosition); | |
665 graduationDivs.push(graduation); | |
666 if (yRange == 0) | |
667 break; | |
668 graduationPosition += scale; | |
669 } | |
670 return graduationDivs; | |
671 }; | |
672 | |
673 /** | |
674 * Generates and returns a div object representing the horizontal line that | |
675 * follows the mouse pointer around the plot. | |
676 * | |
677 * @return {Object} A DOM Element object representing the div. | |
678 */ | |
679 Plotter.prototype.ruler_ = function() { | |
680 var ruler = document.createElement('div'); | |
681 ruler.setAttribute('class', 'plot-ruler'); | |
682 ruler.style.borderBottom = '1px dotted black'; | |
683 ruler.style.position = 'absolute'; | |
684 ruler.style.left = '-2px'; | |
685 ruler.style.top = '-2px'; | |
686 ruler.style.width = '0px'; | |
687 ruler.style.height = '0px'; | |
688 return ruler; | |
689 }; | |
690 | |
691 /** | |
692 * Generates and returns a canvas object representing the plot itself. | |
693 * | |
694 * @return {Object} A DOM Element object representing the canvas. | |
695 */ | |
696 Plotter.prototype.canvas_ = function() { | |
697 var canvas = document.createElement('canvas'); | |
698 canvas.setAttribute('id', '_canvas'); | |
699 canvas.setAttribute('class', 'plot'); | |
700 canvas.setAttribute('width', this.coordinates.widthMax); | |
701 canvas.setAttribute('height', this.coordinates.heightMax); | |
702 canvas.plotter = this; | |
703 return canvas; | |
704 }; | |
705 | |
706 /** | |
707 * Generates and returns a div object representing the coordinate information | |
708 * displayed directly underneath a graph. | |
709 * | |
710 * @return {Object} A DOM Element object representing the div. | |
711 */ | |
712 Plotter.prototype.coordinates_ = function() { | |
713 var coordinatesDiv = document.createElement('div'); | |
714 var table_html = '<table border=0 width="100%"'; | |
715 if (this.is_lookout_) { | |
716 table_html += ' style="font-size:0.8em"'; | |
717 } | |
718 table_html += '><tbody><tr>' | |
719 | |
720 table_html += '<td><span class="legend_item" style="color:' + | |
721 this.getDataColor(0) + '">' + this.dataDescriptions_[0] + | |
722 '</span>: <span class="plot-coordinates">' + | |
723 '<i>move mouse over graph</i></span></td>'; | |
724 | |
725 table_html += '<td align="right">x-axis is ' + this.unitsX_ + '</td>'; | |
726 | |
727 table_html += '</tr><tr>' | |
728 | |
729 table_html += '<td>'; | |
730 if (this.dataDescriptions_.length > 1) { | |
731 // A second line will be drawn, so add information about it. | |
732 table_html += '<span class="legend_item" style="color:' + | |
733 this.getDataColor(1) + '">' + this.dataDescriptions_[1] + | |
734 '</span>: <span class="plot-coordinates">' + | |
735 '<i>move mouse over graph</i></span>'; | |
736 } else if (this.eventName_ != null) { | |
737 // Event information will be overlayed on the graph, so add info about it. | |
738 table_html += '<span class="legend_item" style="color:' + | |
739 this.getDataColor(1) + '">Event "' + this.eventName_ + '":</span>' + | |
740 ' <span class="plot-coordinates">' + | |
741 '<i>move mouse over graph</i></span>'; | |
742 } | |
743 table_html += '</td>'; | |
744 | |
745 if (!this.is_lookout_) { | |
746 table_html += '<td align="right" style="color: ' + HorizontalMarker.COLOR + | |
747 '"><i>Shift-click to place baseline.</i></td>'; | |
748 } | |
749 table_html += '</tr></tbody></table>'; | |
750 coordinatesDiv.innerHTML = table_html; | |
751 | |
752 var tr = coordinatesDiv.firstChild.firstChild.childNodes[0]; | |
753 this.coordinatesTd_ = tr.childNodes[0].childNodes[2]; | |
754 tr = coordinatesDiv.firstChild.firstChild.childNodes[1]; | |
755 if (this.dataDescriptions_.length > 1) { | |
756 // For second graph line. | |
757 this.coordinatesTdOther_ = tr.childNodes[0].childNodes[2]; | |
758 } else if (this.eventName_ != null) { | |
759 // For event metadata. | |
760 this.coordinatesTdOther_ = tr.childNodes[0].childNodes[2]; | |
761 } | |
762 this.baselineDeltasTd_ = tr.childNodes[1]; | |
763 | |
764 return coordinatesDiv; | |
765 }; | |
OLD | NEW |