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