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 {Element} canvasElement The canvas bounds. | |
58 * @param {Number} yValue The data value corresponding to the vertical click | |
59 * location. | |
60 * @param {Number} yOtherValue If the plot is overlaying two coordinate systems, | |
61 * this is the data value corresponding to the vertical click location in | |
62 * the second coordinate system. Can be null. | |
63 */ | |
64 function HorizontalMarker(canvasElement, yValue, yOtherValue) { | |
65 var m = document.createElement('div'); | |
66 m.style.backgroundColor = HorizontalMarker.COLOR; | |
67 m.style.opacity = '0.3'; | |
68 m.style.position = 'absolute'; | |
69 m.style.width = canvasElement.offsetWidth + 'px'; | |
70 m.style.height = HorizontalMarker.HEIGHT + 'px'; | |
71 | |
72 this.markerDiv = m; | |
73 this.value = yValue; | |
74 this.otherValue = yOtherValue; | |
75 } | |
76 | |
77 HorizontalMarker.HEIGHT = 5; | |
78 HorizontalMarker.COLOR = 'rgb(0,100,100)'; | |
79 | |
80 /** | |
81 * Locates this element at a specified position. | |
82 * | |
83 * @param {Element} canvasElement The canvas element at which this element is | |
84 * to be placed. | |
85 * @param {number} y Y position relative to the canvas element. | |
86 */ | |
87 HorizontalMarker.prototype.locateAt = function(canvasElement, y) { | |
88 var div = this.markerDiv; | |
89 div.style.left = domUtils.pageXY(canvasElement).x - | |
90 domUtils.pageXY(div.offsetParent) + 'px'; | |
91 div.style.top = (y + domUtils.pageXY(canvasElement).y | |
92 - domUtils.pageXY(div.offsetParent).y | |
93 - (HorizontalMarker.HEIGHT / 2)) + 'px'; | |
94 }; | |
95 | |
96 /** | |
97 * Removes the horizontal marker from the graph. | |
98 */ | |
99 HorizontalMarker.prototype.remove = function() { | |
100 this.markerDiv.parentNode.removeChild(this.markerDiv); | |
101 }; | |
102 | |
103 /** | |
104 * An information indicator hovering around the mouse cursor on the graph. | |
105 * This class is used to show a legend near the mouse cursor. | |
106 * | |
107 * A set of legends under the graph is managed separately in | |
108 * {@code Plotter.createLegendsSummaryElement_}. | |
109 * | |
110 * @constructor | |
111 */ | |
112 function HoveringInfo() { | |
113 this.containerDiv_ = document.createElement('div'); | |
114 this.containerDiv_.style.display = 'none'; | |
115 this.containerDiv_.style.position = 'absolute'; | |
116 this.containerDiv_.style.border = '1px solid #000'; | |
117 this.containerDiv_.style.padding = '0.12em'; | |
118 this.containerDiv_.style.backgroundColor = '#ddd'; | |
119 this.colorIndicator_ = document.createElement('div'); | |
120 this.colorIndicator_.style.display = 'inline-block'; | |
121 this.colorIndicator_.style.width = '1em'; | |
122 this.colorIndicator_.style.height = '1em'; | |
123 this.colorIndicator_.style.verticalAlign = 'text-bottom'; | |
124 this.colorIndicator_.style.margin = '0 0.24em 0 0'; | |
125 this.colorIndicator_.style.border = '1px solid #000'; | |
126 this.legendText_ = document.createElement('span'); | |
127 this.itemValueText_ = document.createElement('span'); | |
128 | |
129 this.containerDiv_.appendChild(this.colorIndicator_); | |
130 this.containerDiv_.appendChild(this.legendText_); | |
131 var div = document.createElement('div'); | |
132 div.appendChild(this.itemValueText_); | |
133 this.containerDiv_.appendChild(div); | |
134 } | |
135 | |
136 /** | |
137 * Returns the container element; | |
138 * | |
139 * @return {Element} The container element. | |
140 */ | |
141 HoveringInfo.prototype.getElement = function() { | |
142 return this.containerDiv_; | |
143 }; | |
144 | |
145 /** | |
146 * Shows or hides the element. | |
147 * | |
148 * @param {boolean} show Shows the element if true, or hides it. | |
149 */ | |
150 HoveringInfo.prototype.show = function(show) { | |
151 this.containerDiv_.style.display = show ? 'block' : 'none'; | |
152 }; | |
153 | |
154 /** | |
155 * Returns the position of the container element in the page coordinate. | |
156 * | |
157 * @return {Object} A point object which has {@code x} and {@code y} fields. | |
158 */ | |
159 HoveringInfo.prototype.pageXY = function() { | |
160 return domUtils.pageXY(this.containerDiv_); | |
161 }; | |
162 | |
163 /** | |
164 * Locates the element at the specified position. | |
165 * | |
166 * @param {number} x X position in the page coordinate. | |
167 * @param {number} y Y position in the page coordinate. | |
168 */ | |
169 HoveringInfo.prototype.locateAtPageXY = function(x, y) { | |
170 var parentXY = domUtils.pageXY(this.containerDiv_.offsetParent); | |
171 this.containerDiv_.style.left = x - parentXY.x + 'px'; | |
172 this.containerDiv_.style.top = y - parentXY.y + 'px'; | |
173 }; | |
174 | |
175 /** | |
176 * Returns the legend text. | |
177 * | |
178 * @return {?string} The legend text. | |
179 */ | |
180 HoveringInfo.prototype.getLegendText = function() { | |
181 return this.legendText_.textContent; | |
182 }; | |
183 | |
184 /** | |
185 * Changes the legend text. | |
186 * | |
187 * @param {string} text The new text to be set. | |
188 */ | |
189 HoveringInfo.prototype.setLegendText = function(text) { | |
190 this.legendText_.textContent = text; | |
191 }; | |
192 | |
193 /** | |
194 * Changes the item value. | |
195 * | |
196 * @param {number} value The new value to be shown. | |
197 */ | |
198 HoveringInfo.prototype.setItemValue = function(value) { | |
199 this.itemValueText_.textContent = 'Item value = ' + addCommas(value); | |
200 }; | |
201 | |
202 /** | |
203 * Changes the color of the color indicator. | |
204 * | |
205 * @param {string} color The new color to be set. | |
206 */ | |
207 HoveringInfo.prototype.setColorIndicator = function(color) { | |
208 this.colorIndicator_.style.backgroundColor = color; | |
209 }; | |
210 | |
211 /** | |
212 * Main class that does the actual plotting. | |
213 * | |
214 * Draws a chart using a canvas element. Takes an array of lines to draw. | |
215 * @constructor | |
216 * | |
217 * @param {Array} plotData list of arrays that represent individual lines. The | |
218 * line itself is an Array of points. | |
219 * @param {Array} dataDescriptions list of data descriptions for each line in | |
220 * |plotData|. | |
221 * @param {string} eventName The string name of an event to overlay on the | |
222 * graph. Should be 'null' if there are no events to overlay. | |
223 * @param {Object} eventInfo If |eventName| is specified, an array of event | |
224 * points to overlay on the graph. Each event point in the array is itself | |
225 * a 2-element array, where the first element is the x-axis value at which | |
226 * the event occurred during the test, and the second element is a | |
227 * dictionary of kay/value pairs representing metadata associated with the | |
228 * event. | |
229 * @param {string} unitsX The x-axis units of the data being plotted. | |
230 * @param {string} unitsY The y-axis units of the data being plotted. | |
231 * @param {string} unitsYOther If another graph (with different y-axis units) is | |
232 * being overlayed over the first graph, this represents the units of the | |
233 * other graph. Otherwise, this should be 'null'. | |
234 * @param {?number} graphsOtherStartIndex Specifies the starting index of | |
235 * the second set of lines. {@code plotData} in the range of | |
236 * [0, {@code graphsOtherStartIndex}) are treated as the first set of lines, | |
237 * and ones in the range of | |
238 * [{@code graphsOtherStartIndex}, {@code plotData.length}) are as | |
239 * the second set. 0, {@code plotData.length} and {@code null} mean | |
240 * no second set, i.e. all the data in {@code plotData} represent the single | |
241 * set of lines. | |
242 * @param {Element} resultNode A DOM Element object representing the DOM node to | |
243 * which the plot should be attached. | |
244 * @param {boolean} is_lookout Whether or not the graph should be drawn | |
245 * in 'lookout' mode, which is a summarized view that is made for overview | |
246 * pages when the graph is drawn in a more confined space. | |
247 * @param {boolean} stackedGraph Whether or not the first set of lines is | |
248 * a stacked graph. | |
249 * @param {boolean} stackedGraphOther Whether or not the second set of lines is | |
250 * a stacked graph. | |
251 * | |
252 * Example of the |plotData|: | |
253 * [ | |
254 * [line 1 data], | |
255 * [line 2 data] | |
256 * ]. | |
257 * Line data looks like [[point one], [point two]]. | |
258 * And individual points are [x value, y value] | |
259 */ | |
260 function Plotter(plotData, dataDescriptions, eventName, eventInfo, unitsX, | |
261 unitsY, unitsYOther, graphsOtherStartIndex, resultNode, | |
262 is_lookout, stackedGraph, stackedGraphOther) { | |
263 this.plotData_ = plotData; | |
264 this.dataDescriptions_ = dataDescriptions; | |
265 this.eventName_ = eventName; | |
266 this.eventInfo_ = eventInfo; | |
267 this.unitsX_ = unitsX; | |
268 this.unitsY_ = unitsY; | |
269 this.unitsYOther_ = unitsYOther; | |
270 this.graphsOtherStartIndex_ = | |
271 (0 < graphsOtherStartIndex && graphsOtherStartIndex < plotData.length) ? | |
272 graphsOtherStartIndex : null; | |
273 this.resultNode_ = resultNode; | |
274 this.is_lookout_ = is_lookout; | |
275 this.stackedGraph_ = stackedGraph; | |
276 this.stackedGraphOther_ = stackedGraphOther; | |
277 | |
278 this.dataColors_ = []; | |
279 | |
280 this.coordinates = null; | |
281 this.coordinatesOther = null; | |
282 if (this.unitsYOther_ && this.graphsOtherStartIndex_) { | |
283 // Need two different coordinate systems to overlay on the same graph. | |
284 this.coordinates = new Coordinates( | |
285 this.plotData_.slice(0, this.graphsOtherStartIndex_)); | |
286 this.coordinatesOther = new Coordinates( | |
287 this.plotData_.slice(this.graphsOtherStartIndex_)); | |
288 } else { | |
289 this.coordinates = new Coordinates(this.plotData_); | |
290 } | |
291 | |
292 // A color palette that's unambigous for normal and color-deficient viewers. | |
293 // Values are (red, green, blue) on a scale of 255. | |
294 // Taken from http://jfly.iam.u-tokyo.ac.jp/html/manuals/pdf/color_blind.pdf. | |
295 this.colors = [[0, 114, 178], // Blue. | |
296 [230, 159, 0], // Orange. | |
297 [0, 158, 115], // Green. | |
298 [204, 121, 167], // Purplish pink. | |
299 [86, 180, 233], // Sky blue. | |
300 [213, 94, 0], // Dark orange. | |
301 [0, 0, 0], // Black. | |
302 [240, 228, 66] // Yellow. | |
303 ]; | |
304 | |
305 for (var i = 0, colorIndex = 0; i < this.dataDescriptions_.length; ++i) | |
306 this.dataColors_[i] = this.makeColor(colorIndex++); | |
307 } | |
308 | |
309 /** | |
310 * Generates a string representing a color corresponding to the given index | |
311 * in a color array. Handles wrapping around the color array if necessary. | |
312 * | |
313 * @param {number} i An index into the |this.colors| array. | |
314 * @return {string} A string representing a color in 'rgb(X,Y,Z)' format. | |
315 */ | |
316 Plotter.prototype.makeColor = function(i) { | |
317 var index = i % this.colors.length; | |
318 return 'rgb(' + this.colors[index][0] + ',' + | |
319 this.colors[index][1] + ',' + | |
320 this.colors[index][2] + ')'; | |
321 }; | |
322 | |
323 /** | |
324 * Same as function makeColor above, but also takes a transparency value | |
325 * indicating how transparent to make the color appear. | |
326 * | |
327 * @param {number} i An index into the |this.colors| array. | |
328 * @param {number} transparencyPercent Percentage transparency to make the | |
329 * color, e.g., 0.75. | |
330 * @return {string} A string representing a color in 'rgb(X,Y,Z,A)' format, | |
331 * where A is the percentage transparency. | |
332 */ | |
333 Plotter.prototype.makeColorTransparent = function(i, transparencyPercent) { | |
334 var index = i % this.colors.length; | |
335 return 'rgba(' + this.colors[index][0] + ',' + | |
336 this.colors[index][1] + ',' + | |
337 this.colors[index][2] + ',' + transparencyPercent + ')'; | |
338 }; | |
339 | |
340 /** | |
341 * Gets the data color value associated with a specified color index. | |
342 * | |
343 * @param {number} i An index into the |this.colors| array. | |
344 * @return {string} A string representing a color in 'rgb(X,Y,Z,A)' format, | |
345 * where A is the percentage transparency. | |
346 */ | |
347 Plotter.prototype.getDataColor = function(i) { | |
348 if (this.dataColors_[i]) | |
349 return this.dataColors_[i]; | |
350 else | |
351 return this.makeColor(i); | |
352 }; | |
353 | |
354 /** | |
355 * Gets the fill color value associated with a specified color index. | |
356 * | |
357 * @param {number} i An index into the |this.colors| array. | |
358 * @return {string} A string representing a color in 'rgba(R,G,B,A)' format, | |
359 * where A is the percentage transparency. | |
360 */ | |
361 Plotter.prototype.getFillColor = function(i) { | |
362 return this.makeColorTransparent(i, 0.4); | |
363 }; | |
364 | |
365 /** | |
366 * Does the actual plotting. | |
367 */ | |
368 Plotter.prototype.plot = function() { | |
369 var self = this; | |
370 | |
371 this.canvasElement_ = this.canvas_(); | |
372 this.rulerDiv_ = this.ruler_(); | |
373 | |
374 // Markers for the result point(s)/events that the mouse is currently | |
375 // hovering over. | |
376 this.cursorDiv_ = new VerticalMarker('rgb(100,80,240)'); | |
377 this.cursorDivOther_ = new VerticalMarker('rgb(50,50,50)'); | |
378 this.eventDiv_ = new VerticalMarker('rgb(255, 0, 0)'); | |
379 this.hoveringInfo_ = new HoveringInfo(); | |
380 | |
381 this.resultNode_.appendChild(this.canvasElement_); | |
382 this.resultNode_.appendChild(this.coordinates_()); | |
383 this.resultNode_.appendChild(this.rulerDiv_); | |
384 this.resultNode_.appendChild(this.cursorDiv_); | |
385 this.resultNode_.appendChild(this.cursorDivOther_); | |
386 this.resultNode_.appendChild(this.eventDiv_); | |
387 this.resultNode_.appendChild(this.hoveringInfo_.getElement()); | |
388 this.attachEventListeners_(); | |
389 | |
390 // Now draw the canvas. | |
391 var ctx = this.canvasElement_.getContext('2d'); | |
392 | |
393 // Clear it with white: otherwise canvas will draw on top of existing data. | |
394 ctx.clearRect(0, 0, this.canvasElement_.width, this.canvasElement_.height); | |
395 | |
396 // Draw all data lines in the reverse order so the last graph appears on | |
397 // the backmost and the first graph appears on the frontmost. | |
398 function draw(plotData, coordinates, colorOffset, stack) { | |
399 for (var i = plotData.length - 1; i >= 0; --i) { | |
400 if (stack) { | |
401 self.plotAreaUnderLine_(ctx, self.getFillColor(colorOffset + i), | |
402 plotData[i], coordinates); | |
403 } | |
404 self.plotLine_(ctx, self.getDataColor(colorOffset + i), | |
405 plotData[i], coordinates); | |
406 } | |
407 } | |
408 draw(this.plotData_.slice(0, | |
409 this.graphsOtherStartIndex_ ? | |
410 this.graphsOtherStartIndex_ : | |
411 this.plotData_.length), | |
412 this.coordinates, 0, this.stackedGraph_); | |
413 if (this.graphsOtherStartIndex_) { | |
414 draw(this.plotData_.slice(this.graphsOtherStartIndex_), | |
415 this.unitsYOther_ ? this.coordinatesOther : this.coordinates, | |
416 this.graphsOtherStartIndex_, this.stackedGraphOther_); | |
417 } | |
418 | |
419 // Draw events overlayed on graph if needed. | |
420 if (this.eventName_ && this.eventInfo_) | |
421 this.plotEvents_(ctx, 'rgb(255, 150, 150)', this.coordinates); | |
422 | |
423 this.graduation_divs_ = this.graduations_(this.coordinates, 0, false); | |
424 if (this.unitsYOther_) { | |
425 this.graduation_divs_ = this.graduation_divs_.concat( | |
426 this.graduations_(this.coordinatesOther, 1, true)); | |
427 } | |
428 for (var i = 0; i < this.graduation_divs_.length; ++i) | |
429 this.resultNode_.appendChild(this.graduation_divs_[i]); | |
430 }; | |
431 | |
432 /** | |
433 * Draws events overlayed on top of an existing graph. | |
434 * | |
435 * @param {Object} ctx A canvas element object for drawing. | |
436 * @param {string} strokeStyles A string representing the drawing style. | |
437 * @param {Object} coordinateSystem A Coordinates object representing the | |
438 * coordinate system of the graph. | |
439 */ | |
440 Plotter.prototype.plotEvents_ = function(ctx, strokeStyles, coordinateSystem) { | |
441 ctx.strokeStyle = strokeStyles; | |
442 ctx.fillStyle = strokeStyles; | |
443 ctx.lineWidth = 1.0; | |
444 | |
445 ctx.beginPath(); | |
446 var data = this.eventInfo_; | |
447 for (var index = 0; index < data.length; ++index) { | |
448 var event_time = data[index][0]; | |
449 var x = coordinateSystem.xPixel(event_time); | |
450 ctx.moveTo(x, 0); | |
451 ctx.lineTo(x, this.canvasElement_.offsetHeight); | |
452 } | |
453 ctx.closePath(); | |
454 ctx.stroke(); | |
455 }; | |
456 | |
457 /** | |
458 * Draws a line on the graph. | |
459 * | |
460 * @param {Object} ctx A canvas element object for drawing. | |
461 * @param {string} strokeStyles A string representing the drawing style. | |
462 * @param {Array} data A list of [x, y] values representing the line to plot. | |
463 * @param {Object} coordinateSystem A Coordinates object representing the | |
464 * coordinate system of the graph. | |
465 */ | |
466 Plotter.prototype.plotLine_ = function(ctx, strokeStyles, data, | |
467 coordinateSystem) { | |
468 ctx.strokeStyle = strokeStyles; | |
469 ctx.fillStyle = strokeStyles; | |
470 ctx.lineWidth = 2.0; | |
471 | |
472 ctx.beginPath(); | |
473 var initial = true; | |
474 var allPoints = []; | |
475 for (var i = 0; i < data.length; ++i) { | |
476 var pointX = parseFloat(data[i][0]); | |
477 var pointY = parseFloat(data[i][1]); | |
478 var x = coordinateSystem.xPixel(pointX); | |
479 var y = coordinateSystem.yPixel(0); | |
480 if (isNaN(pointY)) { | |
481 // Re-set 'initial' if we're at a gap in the data. | |
482 initial = true; | |
483 } else { | |
484 y = coordinateSystem.yPixel(pointY); | |
485 if (initial) | |
486 initial = false; | |
487 else | |
488 ctx.lineTo(x, y); | |
489 } | |
490 | |
491 ctx.moveTo(x, y); | |
492 if (!data[i].interpolated) { | |
493 allPoints.push([x, y]); | |
494 } | |
495 } | |
496 ctx.closePath(); | |
497 ctx.stroke(); | |
498 | |
499 if (!this.is_lookout_) { | |
500 // Draw a small dot at each point. | |
501 for (var i = 0; i < allPoints.length; ++i) { | |
502 ctx.beginPath(); | |
503 ctx.arc(allPoints[i][0], allPoints[i][1], 3, 0, Math.PI*2, true); | |
504 ctx.fill(); | |
505 } | |
506 } | |
507 }; | |
508 | |
509 /** | |
510 * Fills an area under the given line on the graph. | |
511 * | |
512 * @param {Object} ctx A canvas element object for drawing. | |
513 * @param {string} fillStyle A string representing the drawing style. | |
514 * @param {Array} data A list of [x, y] values representing the line to plot. | |
515 * @param {Object} coordinateSystem A Coordinates object representing the | |
516 * coordinate system of the graph. | |
517 */ | |
518 Plotter.prototype.plotAreaUnderLine_ = function(ctx, fillStyle, data, | |
519 coordinateSystem) { | |
520 if (!data[0]) { | |
521 return; // nothing to draw | |
522 } | |
523 | |
524 ctx.beginPath(); | |
525 var x = coordinateSystem.xPixel(parseFloat(data[0][0]) || 0); | |
526 var y = coordinateSystem.yPixel(parseFloat(data[0][1]) || 0); | |
527 var y0 = coordinateSystem.yPixel(coordinateSystem.yMinValue()); | |
528 ctx.moveTo(x, y0); | |
529 for (var point, i = 0; point = data[i]; ++i) { | |
530 var pointX = parseFloat(point[0]); | |
531 var pointY = parseFloat(point[1]); | |
532 if (isNaN(pointX)) { continue; } // Skip an invalid point. | |
533 if (isNaN(pointY)) { | |
534 ctx.lineTo(x, y0); | |
535 var yWasNaN = true; | |
536 } else { | |
537 x = coordinateSystem.xPixel(pointX); | |
538 y = coordinateSystem.yPixel(pointY); | |
539 if (yWasNaN) { | |
540 ctx.lineTo(x, y0); | |
541 yWasNaN = false; | |
542 } | |
543 ctx.lineTo(x, y); | |
544 } | |
545 } | |
546 ctx.lineTo(x, y0); | |
547 | |
548 ctx.lineWidth = 0; | |
549 // Clear the area with white color first. | |
550 var COLOR_WHITE = 'rgb(255,255,255)'; | |
551 ctx.strokeStyle = COLOR_WHITE; | |
552 ctx.fillStyle = COLOR_WHITE; | |
553 ctx.fill(); | |
554 // Then, fill the area with the specified color. | |
555 ctx.strokeStyle = fillStyle; | |
556 ctx.fillStyle = fillStyle; | |
557 ctx.fill(); | |
558 }; | |
559 | |
560 /** | |
561 * Attaches event listeners to DOM nodes. | |
562 */ | |
563 Plotter.prototype.attachEventListeners_ = function() { | |
564 var self = this; | |
565 this.canvasElement_.parentNode.addEventListener( | |
566 'mousemove', function(evt) { self.onMouseMove_(evt); }, false); | |
567 this.canvasElement_.parentNode.addEventListener( | |
568 'mouseover', function(evt) { self.onMouseOver_(evt); }, false); | |
569 this.canvasElement_.parentNode.addEventListener( | |
570 'mouseout', function(evt) { self.onMouseOut_(evt); }, false); | |
571 this.cursorDiv_.addEventListener( | |
572 'click', function(evt) { self.onMouseClick_(evt); }, false); | |
573 this.cursorDivOther_.addEventListener( | |
574 'click', function(evt) { self.onMouseClick_(evt); }, false); | |
575 this.eventDiv_.addEventListener( | |
576 'click', function(evt) { self.onMouseClick_(evt); }, false); | |
577 }; | |
578 | |
579 /** | |
580 * Update the horizontal line that is following where the mouse is hovering. | |
581 * | |
582 * @param {Object} evt A mouse event object representing a mouse move event. | |
583 */ | |
584 Plotter.prototype.updateRuler_ = function(evt) { | |
585 var r = this.rulerDiv_; | |
586 r.style.left = this.canvasElement_.offsetLeft + 'px'; | |
587 r.style.top = this.canvasElement_.offsetTop + 'px'; | |
588 r.style.width = this.canvasElement_.offsetWidth + 'px'; | |
589 var h = domUtils.pageXYOfEvent(evt).y - | |
590 domUtils.pageXY(this.canvasElement_).y; | |
591 if (h > this.canvasElement_.offsetHeight) | |
592 h = this.canvasElement_.offsetHeight; | |
593 r.style.height = h + 'px'; | |
594 }; | |
595 | |
596 /** | |
597 * Update the highlighted data point at the x value that the mouse is hovering | |
598 * over. | |
599 * | |
600 * @param {Object} coordinateSystem A Coordinates object representing the | |
601 * coordinate system of the graph. | |
602 * @param {number} currentIndex The index into the |this.plotData| array of the | |
603 * data point being hovered over, for a given line. | |
604 * @param {Object} cursorDiv A DOM element div object representing the highlight | |
605 * itself. | |
606 * @param {number} dataIndex The index into the |this.plotData| array of the | |
607 * line being hovered over. | |
608 */ | |
609 Plotter.prototype.updateCursor_ = function(coordinateSystem, currentIndex, | |
610 cursorDiv, dataIndex) { | |
611 var c = cursorDiv; | |
612 c.style.top = this.canvasElement_.offsetTop + 'px'; | |
613 c.style.height = this.canvasElement_.offsetHeight + 'px'; | |
614 | |
615 // Left point is half-way to the previous x value, unless it's the first | |
616 // point, in which case it's the x value of the current point. | |
617 var leftPoint = null; | |
618 if (currentIndex == 0) { | |
619 leftPoint = this.canvasElement_.offsetLeft + | |
620 coordinateSystem.xPixel(this.plotData_[dataIndex][0][0]); | |
621 } | |
622 else { | |
623 var left_x = this.canvasElement_.offsetLeft + | |
624 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex - 1][0]); | |
625 var curr_x = this.canvasElement_.offsetLeft + | |
626 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); | |
627 leftPoint = (left_x + curr_x) / 2; | |
628 } | |
629 c.style.left = leftPoint; | |
630 | |
631 // Width is half-way to the next x value minus the left point, unless it's | |
632 // the last point, in which case it's the x value of the current point minus | |
633 // the left point. | |
634 if (currentIndex == this.plotData_[dataIndex].length - 1) { | |
635 var curr_x = this.canvasElement_.offsetLeft + | |
636 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); | |
637 c.style.width = curr_x - leftPoint; | |
638 } | |
639 else { | |
640 var next_x = this.canvasElement_.offsetLeft + | |
641 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex + 1][0]); | |
642 var curr_x = this.canvasElement_.offsetLeft + | |
643 coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); | |
644 c.style.width = ((next_x + curr_x) / 2) - leftPoint; | |
645 } | |
646 }; | |
647 | |
648 /** | |
649 * Update the highlighted event at the x value that the mouse is hovering over. | |
650 * | |
651 * @param {number} x The x-value (pixel) at which to draw the event highlight | |
652 * div. | |
653 * @param {boolean} show Whether or not to show the highlight div. | |
654 */ | |
655 Plotter.prototype.updateEventDiv_ = function(x, show) { | |
656 var c = this.eventDiv_; | |
657 c.style.top = this.canvasElement_.offsetTop + 'px'; | |
658 c.style.height = this.canvasElement_.offsetHeight + 'px'; | |
659 | |
660 if (show) { | |
661 c.style.left = this.canvasElement_.offsetLeft + (x - 2); | |
662 c.style.width = 8; | |
663 } else { | |
664 c.style.width = 0; | |
665 } | |
666 }; | |
667 | |
668 /** | |
669 * Updates the hovering information. | |
670 * | |
671 * @param {Event} evt An event object, which specifies the position of the mouse | |
672 * cursor. | |
673 * @param {boolean} show Whether or not to show the hovering info. Even if it's | |
674 * true, if the cursor position is out of the appropriate area, nothing will | |
675 * be shown. | |
676 */ | |
677 Plotter.prototype.updateHoveringInfo_ = function(evt, show) { | |
678 var evtPageXY = domUtils.pageXYOfEvent(evt); | |
679 var hoveringInfoPageXY = this.hoveringInfo_.pageXY(); | |
680 var canvasPageXY = domUtils.pageXY(this.canvasElement_); | |
681 | |
682 var coord = this.coordinates; | |
683 // p = the mouse cursor position in value coordinates. | |
684 var p = {'x': coord.xValue(evtPageXY.x - canvasPageXY.x), | |
685 'y': coord.yValue(evtPageXY.y - canvasPageXY.y)}; | |
686 if (!show || | |
687 !(this.stackedGraph_ || this.stackedGraphOther_) || | |
688 p.x < coord.xMinValue() || coord.xMaxValue() < p.x || | |
689 p.y < coord.yMinValue() || coord.yMaxValue() < p.y) { | |
690 this.hoveringInfo_.show(false); | |
691 return; | |
692 } else { | |
693 this.hoveringInfo_.show(true); | |
694 } | |
695 | |
696 /** | |
697 * Finds the closest lines (upside and downside of the cursor position). | |
698 * Returns a set of upside/downside line indices and point index on success | |
699 * or null. | |
700 */ | |
701 function findClosestLines(lines, opt_startIndex, opt_endIndex) { | |
702 var offsetIndex = opt_startIndex || 0; | |
703 lines = | |
704 opt_endIndex != null ? lines.slice(offsetIndex, opt_endIndex) : | |
705 opt_startIndex != null ? lines.slice(offsetIndex) : | |
706 lines; | |
707 | |
708 var upsideClosestLineIndex = null; | |
709 var upsideClosestYDistance = coord.yValueRange(); | |
710 var downsideClosestLineIndex = null; | |
711 var downsideClosestYDistance = coord.yValueRange(); | |
712 var upsideClosestPointIndex = null; | |
713 | |
714 for (var lineIndex = 0, line; line = lines[lineIndex]; ++lineIndex) { | |
715 for (var i = 1; line[i]; ++i) { | |
716 var p0 = line[i - 1], p1 = line[i]; | |
717 if (p0[0] <= p.x && p.x < p1[0]) { | |
718 // Calculate y-value of the line at p.x, which is the cursor point. | |
719 var y = (p.x - p0[0]) / (p1[0] - p0[0]) * (p1[1] - p0[1]) + p0[1]; | |
720 if (p.y < y && y - p.y < upsideClosestYDistance) { | |
721 upsideClosestLineIndex = lineIndex; | |
722 upsideClosestYDistance = y - p.y; | |
723 | |
724 if (p.x - p0[0] < p1[0] - p.x) { | |
725 upsideClosestPointIndex = i - 1; | |
726 } else { | |
727 upsideClosestPointIndex = i; | |
728 } | |
729 } else if (y <= p.y && p.y - y < downsideClosestYDistance) { | |
730 downsideClosestLineIndex = lineIndex; | |
731 downsideClosestYDistance = p.y - y; | |
732 } | |
733 break; | |
734 } | |
735 } | |
736 } | |
737 | |
738 return (upsideClosestLineIndex != null && | |
739 upsideClosestPointIndex != null) ? | |
740 {'upsideLineIndex': offsetIndex + upsideClosestLineIndex, | |
741 'downsideLineIndex': downsideClosestYDistance == null ? null : | |
742 offsetIndex + downsideClosestLineIndex, | |
743 'upsidePointIndex': offsetIndex + upsideClosestPointIndex} : | |
744 null; | |
745 } | |
746 | |
747 // Find the closest lines above and below the mouse cursor. | |
748 var closest = null; | |
749 // Since the other set of graphs are drawn over the first set, try to find | |
750 // the closest lines from the other set of graphs first. | |
751 if (this.graphsOtherStartIndex_ && this.stackedGraphOther_) { | |
752 closest = findClosestLines(this.plotData_, this.graphsOtherStartIndex_); | |
753 } | |
754 if (!closest && this.stackedGraph_) { | |
755 closest = this.graphsOtherStartIndex_ ? | |
756 findClosestLines(this.plotData_, 0, this.graphsOtherStartIndex_) : | |
757 findClosestLines(this.plotData_); | |
758 } | |
759 if (!closest) { | |
760 this.hoveringInfo_.show(false); | |
761 return; | |
762 } | |
763 | |
764 // Update the contents of the hovering info box. | |
765 // Color indicator, description and the value of the item. | |
766 this.hoveringInfo_.setColorIndicator( | |
767 this.getDataColor(closest.upsideLineIndex)); | |
768 this.hoveringInfo_.setLegendText( | |
769 this.dataDescriptions_[closest.upsideLineIndex]); | |
770 var y1 = this.plotData_[closest.upsideLineIndex][closest.upsidePointIndex][1]; | |
771 var y0 = closest.downsideLineIndex == null ? | |
772 0 : | |
773 this.plotData_[closest.downsideLineIndex][closest.upsidePointIndex][1]; | |
774 this.hoveringInfo_.setItemValue(y1 - y0); | |
775 | |
776 // Locate the hovering info box near the mouse cursor. | |
777 var DIV_X_OFFSET = 10, DIV_Y_OFFSET = -20; | |
778 if (evtPageXY.x + this.hoveringInfo_.getElement().offsetWidth < | |
779 canvasPageXY.x + this.canvasElement_.offsetWidth) { | |
780 this.hoveringInfo_.locateAtPageXY(evtPageXY.x + DIV_X_OFFSET, | |
781 evtPageXY.y + DIV_Y_OFFSET); | |
782 } else { // If lacking space at the right side, locate it at the left side. | |
783 this.hoveringInfo_.locateAtPageXY( | |
784 evtPageXY.x - this.hoveringInfo_.getElement().offsetWidth - DIV_X_OFFSET, | |
785 evtPageXY.y + DIV_Y_OFFSET); | |
786 } | |
787 }; | |
788 | |
789 /** | |
790 * Handle a mouse move event. | |
791 * | |
792 * @param {Object} evt A mouse event object representing a mouse move event. | |
793 */ | |
794 Plotter.prototype.onMouseMove_ = function(evt) { | |
795 var self = this; | |
796 | |
797 var canvas = evt.currentTarget.firstChild; | |
798 var evtPageXY = domUtils.pageXYOfEvent(evt); | |
799 var canvasPageXY = domUtils.pageXY(this.canvasElement_); | |
800 var positionX = evtPageXY.x - canvasPageXY.x; | |
801 var positionY = evtPageXY.y - canvasPageXY.y; | |
802 | |
803 // Identify the index of the x value that is closest to the mouse x value. | |
804 var xValue = this.coordinates.xValue(positionX); | |
805 var lineIndex = !this.stackedGraph_ ? 0 : | |
806 this.graphsOtherStartIndex_ ? this.graphsOtherStartIndex_ - 1 : | |
807 this.plotData_.length - 1; | |
808 var line = this.plotData_[lineIndex]; | |
809 var min_diff = Math.abs(line[0][0] - xValue); | |
810 indexValueX = 0; | |
811 for (var i = 1; i < line.length; ++i) { | |
812 var diff = Math.abs(line[i][0] - xValue); | |
813 if (diff < min_diff) { | |
814 min_diff = diff; | |
815 indexValueX = i; | |
816 } | |
817 } | |
818 | |
819 // Identify the index of the x value closest to the mouse x value for the | |
820 // other graph being overlayed on top of the original graph, if one exists. | |
821 if (this.unitsYOther_) { | |
822 var xValue = this.coordinatesOther.xValue(positionX); | |
823 var lineIndexOther = !this.stackedGraphOther_ ? | |
824 this.graphsOtherStartIndex_ : this.plotData_.length - 1; | |
825 var lineOther = this.plotData_[lineIndexOther]; | |
826 var min_diff = Math.abs(lineOther[0][0] - xValue); | |
827 var indexValueXOther = 0; | |
828 for (var i = 1; i < lineOther.length; ++i) { | |
829 var diff = Math.abs(lineOther[i][0] - xValue); | |
830 if (diff < min_diff) { | |
831 min_diff = diff; | |
832 indexValueXOther = i; | |
833 } | |
834 } | |
835 } | |
836 | |
837 // Update coordinate information displayed directly underneath the graph. | |
838 function legendLabel(lineIndex, opt_labelText) { | |
839 return '<span style="color:' + self.getDataColor(lineIndex) + '">' + | |
840 (opt_labelText || self.dataDescriptions_[lineIndex]) + | |
841 '</span>: '; | |
842 } | |
843 function valuesAtCursor(lineIndex, pointIndex, unitsY, yValue) { | |
844 return '<span style="color:' + self.getDataColor(lineIndex) + '">' + | |
845 self.plotData_[lineIndex][pointIndex][0] + ' ' + self.unitsX_ + ': ' + | |
846 addCommas(self.plotData_[lineIndex][pointIndex][1].toFixed(2)) + ' ' + | |
847 unitsY + '</span> [hovering at ' + addCommas(yValue.toFixed(2)) + | |
848 ' ' + unitsY + ']'; | |
849 } | |
850 | |
851 this.infoBox_.rows[0].label.innerHTML = legendLabel(lineIndex); | |
852 this.infoBox_.rows[0].content.innerHTML = valuesAtCursor( | |
853 lineIndex, indexValueX, this.unitsY_, this.coordinates.yValue(positionY)); | |
854 var row = this.infoBox_.rows[1]; | |
855 if (this.unitsYOther_) { | |
856 row.label.innerHTML = legendLabel(lineIndexOther); | |
857 row.content.innerHTML = valuesAtCursor( | |
858 lineIndexOther, indexValueXOther, this.unitsYOther_, | |
859 this.coordinatesOther.yValue(positionY)); | |
860 } else if (this.graphsOtherStartIndex_) { | |
861 row.label.innerHTML = legendLabel( | |
862 this.stackedGraphOther_ ? | |
863 this.plotData_.length - 1 : this.graphsOtherStartIndex_); | |
864 row.content.innerHTML = valuesAtCursor( | |
865 this.stackedGraphOther_ ? | |
866 this.plotData_.length - 1 : this.graphsOtherStartIndex_, | |
867 indexValueX, this.unitsY_, this.coordinates.yValue(positionY)); | |
868 } else if (!this.stackedGraph_ && this.dataDescriptions_.length > 1) { | |
869 row.label.innerHTML = legendLabel(1); | |
870 row.content.innerHTML = valuesAtCursor( | |
871 1, indexValueX, this.unitsY_, this.coordinates.yValue(positionY)); | |
872 } else if (row) { | |
873 row.label.innerHTML = ''; | |
874 row.content.innerHTML = ''; | |
875 } | |
876 | |
877 // If there is a horizontal marker, also display deltas relative to it. | |
878 if (this.horizontal_marker_) { | |
879 var baseline = this.horizontal_marker_.value; | |
880 var delta = this.coordinates.yValue(positionY) - baseline; | |
881 var fraction = delta / baseline; // Allow division by 0. | |
882 | |
883 var deltaStr = (delta >= 0 ? '+' : '') + delta.toFixed(0) + ' ' + | |
884 this.unitsY_; | |
885 var percentStr = (fraction >= 0 ? '+' : '') + (fraction * 100).toFixed(3) + | |
886 '%'; | |
887 | |
888 this.baselineDeltasTd_.innerHTML = deltaStr + ': ' + percentStr; | |
889 | |
890 if (this.unitsYOther_) { | |
891 var baseline = this.horizontal_marker_.otherValue; | |
892 var yValue2 = this.coordinatesOther.yValue(positionY); | |
893 var delta = yValue2 - baseline; | |
894 var fraction = delta / baseline; // Allow division by 0. | |
895 | |
896 var deltaStr = (delta >= 0 ? '+' : '') + delta.toFixed(0) + ' ' + | |
897 this.unitsYOther_; | |
898 var percentStr = (fraction >= 0 ? '+' : '') + | |
899 (fraction * 100).toFixed(3) + '%'; | |
900 this.baselineDeltasTd_.innerHTML += '<br>' + deltaStr + ': ' + percentStr; | |
901 } | |
902 } | |
903 | |
904 this.updateRuler_(evt); | |
905 this.updateCursor_(this.coordinates, indexValueX, this.cursorDiv_, 0); | |
906 if (this.unitsYOther_ && this.graphsOtherStartIndex_) { | |
907 this.updateCursor_(this.coordinatesOther, indexValueXOther, | |
908 this.cursorDivOther_, this.graphsOtherStartIndex_); | |
909 } | |
910 | |
911 // If there are events displayed, see if we're hovering close to an existing | |
912 // event on the graph, and if so, display the metadata associated with it. | |
913 if (this.eventName_ != null && this.eventInfo_ != null) { | |
914 this.infoBox_.rows[1].label.innerHTML = 'Event "' + this.eventName_ + | |
915 '": '; | |
916 var data = this.eventInfo_; | |
917 var showed_event = false; | |
918 var x = 0; | |
919 for (var index = 0; index < data.length; ++index) { | |
920 var event_time = data[index][0]; | |
921 x = this.coordinates.xPixel(event_time); | |
922 if (positionX >= x - 10 && positionX <= x + 10) { | |
923 var metadata = data[index][1]; | |
924 var metadata_str = ""; | |
925 for (var meta_key in metadata) | |
926 metadata_str += meta_key + ': ' + metadata[meta_key] + ', '; | |
927 metadata_str = metadata_str.substring(0, metadata_str.length - 2); | |
928 this.infoBox_.rows[1].content.innerHTML = event_time + ' ' + | |
929 this.unitsX_ + ': {' + metadata_str + '}'; | |
930 showed_event = true; | |
931 this.updateEventDiv_(x, true); | |
932 break; | |
933 } | |
934 } | |
935 if (!showed_event) { | |
936 this.coordinatesTdOther_.innerHTML = | |
937 'move mouse close to vertical event marker'; | |
938 this.updateEventDiv_(x, false); | |
939 } | |
940 } | |
941 | |
942 this.updateHoveringInfo_(evt, true); | |
943 }; | |
944 | |
945 /** | |
946 * Handle a mouse over event. | |
947 * | |
948 * @param {Object} evt A mouse event object representing a mouse move event. | |
949 */ | |
950 Plotter.prototype.onMouseOver_ = function(evt) { | |
951 this.updateHoveringInfo_(evt, true); | |
952 }; | |
953 | |
954 /** | |
955 * Handle a mouse out event. | |
956 * | |
957 * @param {Object} evt A mouse event object representing a mouse move event. | |
958 */ | |
959 Plotter.prototype.onMouseOut_ = function(evt) { | |
960 this.updateHoveringInfo_(evt, false); | |
961 }; | |
962 | |
963 /** | |
964 * Handle a mouse click event. | |
965 * | |
966 * @param {Object} evt A mouse event object representing a mouse click event. | |
967 */ | |
968 Plotter.prototype.onMouseClick_ = function(evt) { | |
969 // Shift-click controls the horizontal reference line. | |
970 if (evt.shiftKey) { | |
971 if (this.horizontal_marker_) | |
972 this.horizontal_marker_.remove(); | |
973 | |
974 var canvasY = domUtils.pageXYOfEvent(evt).y - | |
975 domUtils.pageXY(this.canvasElement_).y; | |
976 this.horizontal_marker_ = new HorizontalMarker( | |
977 this.canvasElement_, | |
978 this.coordinates.yValue(canvasY), | |
979 (this.coordinatesOther ? this.coordinatesOther.yValue(canvasY) : null)); | |
980 // Insert before cursor node, otherwise it catches clicks. | |
981 this.cursorDiv_.parentNode.insertBefore( | |
982 this.horizontal_marker_.markerDiv, this.cursorDiv_); | |
983 this.horizontal_marker_.locateAt(this.canvasElement_, canvasY); | |
984 } | |
985 }; | |
986 | |
987 /** | |
988 * Generates and returns a list of div objects representing horizontal lines in | |
989 * the graph that indicate y-axis values at a computed interval. | |
990 * | |
991 * @param {Object} coordinateSystem a Coordinates object representing the | |
992 * coordinate system for which the graduations should be created. | |
993 * @param {number} colorIndex An index into the |this.colors| array representing | |
994 * the color to make the graduations in the event that two graphs with | |
995 * different coordinate systems are being overlayed on the same plot. | |
996 * @param {boolean} isRightSide Whether or not the graduations should have | |
997 * right-aligned text (used when the graduations are for a second graph | |
998 * that is being overlayed on top of another graph). | |
999 * @return {Array} An array of DOM Element objects representing the divs. | |
1000 */ | |
1001 Plotter.prototype.graduations_ = function(coordinateSystem, colorIndex, | |
1002 isRightSide) { | |
1003 // Don't allow a graduation in the bottom 5% of the chart or the number label | |
1004 // would overflow the chart bounds. | |
1005 var yMin = coordinateSystem.yLowerLimitValue() + | |
1006 .05 * coordinateSystem.yValueRange(); | |
1007 var yRange = coordinateSystem.yUpperLimitValue() - yMin; | |
1008 | |
1009 // Use the largest scale that fits 3 or more graduations. | |
1010 // We allow scales of [...,500, 250, 100, 50, 25, 10,...]. | |
1011 var scale = 5000000000; | |
1012 while (scale) { | |
1013 if (Math.floor(yRange / scale) > 2) break; // 5s. | |
1014 scale /= 2; | |
1015 if (Math.floor(yRange / scale) > 2) break; // 2.5s. | |
1016 scale /= 2.5; | |
1017 if (Math.floor(yRange / scale) > 2) break; // 1s. | |
1018 scale /= 2; | |
1019 } | |
1020 | |
1021 var graduationPosition = yMin + (scale - yMin % scale); | |
1022 var graduationDivs = []; | |
1023 while (graduationPosition < coordinateSystem.yUpperLimitValue() || | |
1024 yRange == 0) { | |
1025 var graduation = document.createElement('div'); | |
1026 var canvasPosition; | |
1027 if (yRange == 0) { | |
1028 // Center the graduation vertically. | |
1029 canvasPosition = this.canvasElement_.offsetHeight / 2; | |
1030 } else { | |
1031 canvasPosition = coordinateSystem.yPixel(graduationPosition); | |
1032 } | |
1033 if (this.unitsYOther_) { | |
1034 graduation.style.borderTop = '1px dashed ' + | |
1035 this.makeColorTransparent(colorIndex, 0.4) | |
1036 } else { | |
1037 graduation.style.borderTop = '1px dashed rgba(0,0,0,.08)'; | |
1038 } | |
1039 graduation.style.position = 'absolute'; | |
1040 graduation.style.left = this.canvasElement_.offsetLeft + 'px'; | |
1041 graduation.style.top = canvasPosition + this.canvasElement_.offsetTop + | |
1042 'px'; | |
1043 graduation.style.width = this.canvasElement_.offsetWidth - | |
1044 this.canvasElement_.offsetLeft + 'px'; | |
1045 graduation.style.paddingLeft = '4px'; | |
1046 if (this.unitsYOther_) | |
1047 graduation.style.color = this.makeColorTransparent(colorIndex, 0.9) | |
1048 else | |
1049 graduation.style.color = 'rgba(0,0,0,.4)'; | |
1050 graduation.style.fontSize = '9px'; | |
1051 graduation.style.paddingTop = '0'; | |
1052 graduation.style.zIndex = '-1'; | |
1053 if (isRightSide) | |
1054 graduation.style.textAlign = 'right'; | |
1055 if (yRange == 0) | |
1056 graduation.innerHTML = addCommas(yMin); | |
1057 else | |
1058 graduation.innerHTML = addCommas(graduationPosition); | |
1059 graduationDivs.push(graduation); | |
1060 if (yRange == 0) | |
1061 break; | |
1062 graduationPosition += scale; | |
1063 } | |
1064 return graduationDivs; | |
1065 }; | |
1066 | |
1067 /** | |
1068 * Generates and returns a div object representing the horizontal line that | |
1069 * follows the mouse pointer around the plot. | |
1070 * | |
1071 * @return {Object} A DOM Element object representing the div. | |
1072 */ | |
1073 Plotter.prototype.ruler_ = function() { | |
1074 var ruler = document.createElement('div'); | |
1075 ruler.setAttribute('class', 'plot-ruler'); | |
1076 ruler.style.borderBottom = '1px dotted black'; | |
1077 ruler.style.position = 'absolute'; | |
1078 ruler.style.left = '-2px'; | |
1079 ruler.style.top = '-2px'; | |
1080 ruler.style.width = '0px'; | |
1081 ruler.style.height = '0px'; | |
1082 return ruler; | |
1083 }; | |
1084 | |
1085 /** | |
1086 * Generates and returns a canvas object representing the plot itself. | |
1087 * | |
1088 * @return {Object} A DOM Element object representing the canvas. | |
1089 */ | |
1090 Plotter.prototype.canvas_ = function() { | |
1091 var canvas = document.createElement('canvas'); | |
1092 canvas.setAttribute('id', '_canvas'); | |
1093 canvas.setAttribute('class', 'plot'); | |
1094 canvas.setAttribute('width', this.coordinates.widthMax); | |
1095 canvas.setAttribute('height', this.coordinates.heightMax); | |
1096 canvas.plotter = this; | |
1097 return canvas; | |
1098 }; | |
1099 | |
1100 /** | |
1101 * Generates and returns a div object representing the coordinate information | |
1102 * displayed directly underneath a graph. | |
1103 * | |
1104 * @return {Object} A DOM Element object representing the div. | |
1105 */ | |
1106 Plotter.prototype.coordinates_ = function() { | |
1107 var coordinatesDiv = document.createElement('div'); | |
1108 var table_html = '<table border=0 width="100%"'; | |
1109 if (this.is_lookout_) { | |
1110 table_html += ' style="font-size:0.8em"'; | |
1111 } | |
1112 table_html += '><tbody><tr>'; | |
1113 table_html += '<td><span class="legend_item"></span>' + | |
1114 '<span class="plot-coordinates"><i>move mouse over graph</i></span></td>'; | |
1115 table_html += '<td align="right">x-axis is ' + this.unitsX_ + '</td>'; | |
1116 table_html += '</tr><tr>'; | |
1117 table_html += '<td><span class="legend_item"></span>' + | |
1118 '<span class="plot-coordinates"></span></td>'; | |
1119 | |
1120 if (!this.is_lookout_) { | |
1121 table_html += '<td align="right" style="color: ' + HorizontalMarker.COLOR + | |
1122 '"><i>Shift-click to place baseline.</i></td>'; | |
1123 } | |
1124 table_html += '</tr></tbody></table>'; | |
1125 coordinatesDiv.innerHTML = table_html; | |
1126 | |
1127 var trs = coordinatesDiv.querySelectorAll('tr'); | |
1128 this.infoBox_ = {rows: []}; | |
1129 this.infoBox_.rows.push({ | |
1130 label: trs[0].querySelector('span.legend_item'), | |
1131 content: trs[0].querySelector('span.plot-coordinates')}); | |
1132 if (this.dataDescriptions_.length > 1 || this.eventName_) { | |
1133 this.infoBox_.rows.push({ | |
1134 label: trs[1].querySelector('span.legend_item'), | |
1135 content: trs[1].querySelector('span.plot-coordinates')}); | |
1136 } | |
1137 | |
1138 this.baselineDeltasTd_ = trs[1].childNodes[1]; | |
1139 | |
1140 // Add a summary of legends in case of stacked graphs. | |
1141 if (this.stackedGraph_ || this.stackedGraphOther_) { | |
1142 var legendPane = document.createElement('div'); | |
1143 legendPane.style.fontSize = '80%'; | |
1144 coordinatesDiv.appendChild(legendPane); | |
1145 | |
1146 if (this.graphsOtherStartIndex_) { | |
1147 legendPane.appendChild( | |
1148 this.createLegendsSummaryElement_( | |
1149 this.dataDescriptions_.slice(0, this.graphsOtherStartIndex_), | |
1150 0)); | |
1151 legendPane.appendChild( | |
1152 this.createLegendsSummaryElement_( | |
1153 this.dataDescriptions_.slice(this.graphsOtherStartIndex_), | |
1154 this.graphsOtherStartIndex_)); | |
1155 } else { | |
1156 legendPane.appendChild( | |
1157 this.createLegendsSummaryElement_(this.dataDescriptions_, 0)); | |
1158 } | |
1159 } | |
1160 | |
1161 return coordinatesDiv; | |
1162 }; | |
1163 | |
1164 /** | |
1165 * Creates and returns a DOM element which shows a summary of legends. | |
1166 * | |
1167 * @param {!Array.<string>} legendTexts An array of legend texts. | |
1168 * @param {number} colorIndexOffset Offset index for color. i-th legend text | |
1169 * has an indicator in {@code (colorIndexOffset + i)}-th color | |
1170 * @return {!Element} An element which shows a summary of legends. | |
1171 */ | |
1172 Plotter.prototype.createLegendsSummaryElement_ = function(legendTexts, | |
1173 colorIndexOffset) { | |
1174 var containerElem = document.createElement('div'); | |
1175 | |
1176 for (var i = 0, text; text = legendTexts[i]; ++i) { | |
1177 var colorIndicatorElem = document.createElement('div'); | |
1178 colorIndicatorElem.style.display = 'inline-block'; | |
1179 colorIndicatorElem.style.width = '1em'; | |
1180 colorIndicatorElem.style.height = '1em'; | |
1181 colorIndicatorElem.style.verticalAlign = 'text-bottom'; | |
1182 colorIndicatorElem.style.margin = '0 0.24em 0 0'; | |
1183 colorIndicatorElem.style.border = '1px solid #000'; | |
1184 colorIndicatorElem.style.backgroundColor = | |
1185 this.getDataColor(colorIndexOffset + i); | |
1186 var legendTextElem = document.createElement('span'); | |
1187 legendTextElem.textContent = text; | |
1188 var legendElem = document.createElement('span'); | |
1189 legendElem.style.whiteSpace = 'nowrap'; | |
1190 legendElem.appendChild(colorIndicatorElem); | |
1191 legendElem.appendChild(legendTextElem); | |
1192 legendElem.style.margin = '0 0.8em 0 0'; | |
1193 containerElem.appendChild(legendElem); | |
1194 // Add a space to break lines if necessary. | |
1195 containerElem.appendChild(document.createTextNode(' ')); | |
1196 } | |
1197 | |
1198 return containerElem; | |
1199 }; | |
OLD | NEW |