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 |