Index: dashboard/ui/endure_js/plotter.js |
diff --git a/dashboard/ui/endure_js/plotter.js b/dashboard/ui/endure_js/plotter.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..edbbf1191c3cf57b3afd0c59bdb6cc4063cf2079 |
--- /dev/null |
+++ b/dashboard/ui/endure_js/plotter.js |
@@ -0,0 +1,1199 @@ |
+/* |
+ Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+ Use of this source code is governed by a BSD-style license that can be |
+ found in the LICENSE file. |
+*/ |
+ |
+/** |
+ * @fileoverview Collection of functions and classes used to plot data in a |
+ * <canvas>. Create a Plotter() to generate a plot. |
+ */ |
+ |
+/** |
+ * Adds commas to a given number. |
+ * |
+ * Examples: |
+ * 1234.56 => "1,234.56" |
+ * 99999 => "99,999" |
+ * |
+ * @param {string|number} number The number to format. |
+ * @return {string} String representation of |number| with commas for every |
+ * three digits to the left of a decimal point. |
+ */ |
+function addCommas(number) { |
+ number += ''; // Convert number to string if not already a string. |
+ var numberParts = number.split('.'); |
+ var integralPart = numberParts[0]; |
+ var fractionalPart = numberParts.length > 1 ? '.' + numberParts[1] : ''; |
+ var reThreeDigits = /(\d+)(\d{3})/; |
+ while (reThreeDigits.test(integralPart)) |
+ integralPart = integralPart.replace(reThreeDigits, '$1' + ',' + '$2'); |
+ return integralPart + fractionalPart; |
+} |
+ |
+/** |
+ * Vertical marker to highlight data points that are being hovered over by the |
+ * mouse. |
+ * |
+ * @param {string} color The color to make the marker, e.g., 'rgb(100,80,240)'. |
+ * @return {Element} A div Element object representing the vertical marker. |
+ */ |
+function VerticalMarker(color) { |
+ var m = document.createElement('div'); |
+ m.style.backgroundColor = color; |
+ m.style.opacity = '0.3'; |
+ m.style.position = 'absolute'; |
+ m.style.left = '-2px'; |
+ m.style.top = '-2px'; |
+ m.style.width = '0px'; |
+ m.style.height = '0px'; |
+ return m; |
+} |
+ |
+/** |
+ * Class representing a horizontal marker at the indicated mouse location. |
+ * @constructor |
+ * |
+ * @param {Element} canvasElement The canvas bounds. |
+ * @param {Number} yValue The data value corresponding to the vertical click |
+ * location. |
+ * @param {Number} yOtherValue If the plot is overlaying two coordinate systems, |
+ * this is the data value corresponding to the vertical click location in |
+ * the second coordinate system. Can be null. |
+ */ |
+function HorizontalMarker(canvasElement, yValue, yOtherValue) { |
+ var m = document.createElement('div'); |
+ m.style.backgroundColor = HorizontalMarker.COLOR; |
+ m.style.opacity = '0.3'; |
+ m.style.position = 'absolute'; |
+ m.style.width = canvasElement.offsetWidth + 'px'; |
+ m.style.height = HorizontalMarker.HEIGHT + 'px'; |
+ |
+ this.markerDiv = m; |
+ this.value = yValue; |
+ this.otherValue = yOtherValue; |
+} |
+ |
+HorizontalMarker.HEIGHT = 5; |
+HorizontalMarker.COLOR = 'rgb(0,100,100)'; |
+ |
+/** |
+ * Locates this element at a specified position. |
+ * |
+ * @param {Element} canvasElement The canvas element at which this element is |
+ * to be placed. |
+ * @param {number} y Y position relative to the canvas element. |
+ */ |
+HorizontalMarker.prototype.locateAt = function(canvasElement, y) { |
+ var div = this.markerDiv; |
+ div.style.left = domUtils.pageXY(canvasElement).x - |
+ domUtils.pageXY(div.offsetParent) + 'px'; |
+ div.style.top = (y + domUtils.pageXY(canvasElement).y |
+ - domUtils.pageXY(div.offsetParent).y |
+ - (HorizontalMarker.HEIGHT / 2)) + 'px'; |
+}; |
+ |
+/** |
+ * Removes the horizontal marker from the graph. |
+ */ |
+HorizontalMarker.prototype.remove = function() { |
+ this.markerDiv.parentNode.removeChild(this.markerDiv); |
+}; |
+ |
+/** |
+ * An information indicator hovering around the mouse cursor on the graph. |
+ * This class is used to show a legend near the mouse cursor. |
+ * |
+ * A set of legends under the graph is managed separately in |
+ * {@code Plotter.createLegendsSummaryElement_}. |
+ * |
+ * @constructor |
+ */ |
+function HoveringInfo() { |
+ this.containerDiv_ = document.createElement('div'); |
+ this.containerDiv_.style.display = 'none'; |
+ this.containerDiv_.style.position = 'absolute'; |
+ this.containerDiv_.style.border = '1px solid #000'; |
+ this.containerDiv_.style.padding = '0.12em'; |
+ this.containerDiv_.style.backgroundColor = '#ddd'; |
+ this.colorIndicator_ = document.createElement('div'); |
+ this.colorIndicator_.style.display = 'inline-block'; |
+ this.colorIndicator_.style.width = '1em'; |
+ this.colorIndicator_.style.height = '1em'; |
+ this.colorIndicator_.style.verticalAlign = 'text-bottom'; |
+ this.colorIndicator_.style.margin = '0 0.24em 0 0'; |
+ this.colorIndicator_.style.border = '1px solid #000'; |
+ this.legendText_ = document.createElement('span'); |
+ this.itemValueText_ = document.createElement('span'); |
+ |
+ this.containerDiv_.appendChild(this.colorIndicator_); |
+ this.containerDiv_.appendChild(this.legendText_); |
+ var div = document.createElement('div'); |
+ div.appendChild(this.itemValueText_); |
+ this.containerDiv_.appendChild(div); |
+} |
+ |
+/** |
+ * Returns the container element; |
+ * |
+ * @return {Element} The container element. |
+ */ |
+HoveringInfo.prototype.getElement = function() { |
+ return this.containerDiv_; |
+}; |
+ |
+/** |
+ * Shows or hides the element. |
+ * |
+ * @param {boolean} show Shows the element if true, or hides it. |
+ */ |
+HoveringInfo.prototype.show = function(show) { |
+ this.containerDiv_.style.display = show ? 'block' : 'none'; |
+}; |
+ |
+/** |
+ * Returns the position of the container element in the page coordinate. |
+ * |
+ * @return {Object} A point object which has {@code x} and {@code y} fields. |
+ */ |
+HoveringInfo.prototype.pageXY = function() { |
+ return domUtils.pageXY(this.containerDiv_); |
+}; |
+ |
+/** |
+ * Locates the element at the specified position. |
+ * |
+ * @param {number} x X position in the page coordinate. |
+ * @param {number} y Y position in the page coordinate. |
+ */ |
+HoveringInfo.prototype.locateAtPageXY = function(x, y) { |
+ var parentXY = domUtils.pageXY(this.containerDiv_.offsetParent); |
+ this.containerDiv_.style.left = x - parentXY.x + 'px'; |
+ this.containerDiv_.style.top = y - parentXY.y + 'px'; |
+}; |
+ |
+/** |
+ * Returns the legend text. |
+ * |
+ * @return {?string} The legend text. |
+ */ |
+HoveringInfo.prototype.getLegendText = function() { |
+ return this.legendText_.textContent; |
+}; |
+ |
+/** |
+ * Changes the legend text. |
+ * |
+ * @param {string} text The new text to be set. |
+ */ |
+HoveringInfo.prototype.setLegendText = function(text) { |
+ this.legendText_.textContent = text; |
+}; |
+ |
+/** |
+ * Changes the item value. |
+ * |
+ * @param {number} value The new value to be shown. |
+ */ |
+HoveringInfo.prototype.setItemValue = function(value) { |
+ this.itemValueText_.textContent = 'Item value = ' + addCommas(value); |
+}; |
+ |
+/** |
+ * Changes the color of the color indicator. |
+ * |
+ * @param {string} color The new color to be set. |
+ */ |
+HoveringInfo.prototype.setColorIndicator = function(color) { |
+ this.colorIndicator_.style.backgroundColor = color; |
+}; |
+ |
+/** |
+ * Main class that does the actual plotting. |
+ * |
+ * Draws a chart using a canvas element. Takes an array of lines to draw. |
+ * @constructor |
+ * |
+ * @param {Array} plotData list of arrays that represent individual lines. The |
+ * line itself is an Array of points. |
+ * @param {Array} dataDescriptions list of data descriptions for each line in |
+ * |plotData|. |
+ * @param {string} eventName The string name of an event to overlay on the |
+ * graph. Should be 'null' if there are no events to overlay. |
+ * @param {Object} eventInfo If |eventName| is specified, an array of event |
+ * points to overlay on the graph. Each event point in the array is itself |
+ * a 2-element array, where the first element is the x-axis value at which |
+ * the event occurred during the test, and the second element is a |
+ * dictionary of kay/value pairs representing metadata associated with the |
+ * event. |
+ * @param {string} unitsX The x-axis units of the data being plotted. |
+ * @param {string} unitsY The y-axis units of the data being plotted. |
+ * @param {string} unitsYOther If another graph (with different y-axis units) is |
+ * being overlayed over the first graph, this represents the units of the |
+ * other graph. Otherwise, this should be 'null'. |
+ * @param {?number} graphsOtherStartIndex Specifies the starting index of |
+ * the second set of lines. {@code plotData} in the range of |
+ * [0, {@code graphsOtherStartIndex}) are treated as the first set of lines, |
+ * and ones in the range of |
+ * [{@code graphsOtherStartIndex}, {@code plotData.length}) are as |
+ * the second set. 0, {@code plotData.length} and {@code null} mean |
+ * no second set, i.e. all the data in {@code plotData} represent the single |
+ * set of lines. |
+ * @param {Element} resultNode A DOM Element object representing the DOM node to |
+ * which the plot should be attached. |
+ * @param {boolean} is_lookout Whether or not the graph should be drawn |
+ * in 'lookout' mode, which is a summarized view that is made for overview |
+ * pages when the graph is drawn in a more confined space. |
+ * @param {boolean} stackedGraph Whether or not the first set of lines is |
+ * a stacked graph. |
+ * @param {boolean} stackedGraphOther Whether or not the second set of lines is |
+ * a stacked graph. |
+ * |
+ * Example of the |plotData|: |
+ * [ |
+ * [line 1 data], |
+ * [line 2 data] |
+ * ]. |
+ * Line data looks like [[point one], [point two]]. |
+ * And individual points are [x value, y value] |
+ */ |
+function Plotter(plotData, dataDescriptions, eventName, eventInfo, unitsX, |
+ unitsY, unitsYOther, graphsOtherStartIndex, resultNode, |
+ is_lookout, stackedGraph, stackedGraphOther) { |
+ this.plotData_ = plotData; |
+ this.dataDescriptions_ = dataDescriptions; |
+ this.eventName_ = eventName; |
+ this.eventInfo_ = eventInfo; |
+ this.unitsX_ = unitsX; |
+ this.unitsY_ = unitsY; |
+ this.unitsYOther_ = unitsYOther; |
+ this.graphsOtherStartIndex_ = |
+ (0 < graphsOtherStartIndex && graphsOtherStartIndex < plotData.length) ? |
+ graphsOtherStartIndex : null; |
+ this.resultNode_ = resultNode; |
+ this.is_lookout_ = is_lookout; |
+ this.stackedGraph_ = stackedGraph; |
+ this.stackedGraphOther_ = stackedGraphOther; |
+ |
+ this.dataColors_ = []; |
+ |
+ this.coordinates = null; |
+ this.coordinatesOther = null; |
+ if (this.unitsYOther_ && this.graphsOtherStartIndex_) { |
+ // Need two different coordinate systems to overlay on the same graph. |
+ this.coordinates = new Coordinates( |
+ this.plotData_.slice(0, this.graphsOtherStartIndex_)); |
+ this.coordinatesOther = new Coordinates( |
+ this.plotData_.slice(this.graphsOtherStartIndex_)); |
+ } else { |
+ this.coordinates = new Coordinates(this.plotData_); |
+ } |
+ |
+ // A color palette that's unambigous for normal and color-deficient viewers. |
+ // Values are (red, green, blue) on a scale of 255. |
+ // Taken from http://jfly.iam.u-tokyo.ac.jp/html/manuals/pdf/color_blind.pdf. |
+ this.colors = [[0, 114, 178], // Blue. |
+ [230, 159, 0], // Orange. |
+ [0, 158, 115], // Green. |
+ [204, 121, 167], // Purplish pink. |
+ [86, 180, 233], // Sky blue. |
+ [213, 94, 0], // Dark orange. |
+ [0, 0, 0], // Black. |
+ [240, 228, 66] // Yellow. |
+ ]; |
+ |
+ for (var i = 0, colorIndex = 0; i < this.dataDescriptions_.length; ++i) |
+ this.dataColors_[i] = this.makeColor(colorIndex++); |
+} |
+ |
+/** |
+ * Generates a string representing a color corresponding to the given index |
+ * in a color array. Handles wrapping around the color array if necessary. |
+ * |
+ * @param {number} i An index into the |this.colors| array. |
+ * @return {string} A string representing a color in 'rgb(X,Y,Z)' format. |
+ */ |
+Plotter.prototype.makeColor = function(i) { |
+ var index = i % this.colors.length; |
+ return 'rgb(' + this.colors[index][0] + ',' + |
+ this.colors[index][1] + ',' + |
+ this.colors[index][2] + ')'; |
+}; |
+ |
+/** |
+ * Same as function makeColor above, but also takes a transparency value |
+ * indicating how transparent to make the color appear. |
+ * |
+ * @param {number} i An index into the |this.colors| array. |
+ * @param {number} transparencyPercent Percentage transparency to make the |
+ * color, e.g., 0.75. |
+ * @return {string} A string representing a color in 'rgb(X,Y,Z,A)' format, |
+ * where A is the percentage transparency. |
+ */ |
+Plotter.prototype.makeColorTransparent = function(i, transparencyPercent) { |
+ var index = i % this.colors.length; |
+ return 'rgba(' + this.colors[index][0] + ',' + |
+ this.colors[index][1] + ',' + |
+ this.colors[index][2] + ',' + transparencyPercent + ')'; |
+}; |
+ |
+/** |
+ * Gets the data color value associated with a specified color index. |
+ * |
+ * @param {number} i An index into the |this.colors| array. |
+ * @return {string} A string representing a color in 'rgb(X,Y,Z,A)' format, |
+ * where A is the percentage transparency. |
+ */ |
+Plotter.prototype.getDataColor = function(i) { |
+ if (this.dataColors_[i]) |
+ return this.dataColors_[i]; |
+ else |
+ return this.makeColor(i); |
+}; |
+ |
+/** |
+ * Gets the fill color value associated with a specified color index. |
+ * |
+ * @param {number} i An index into the |this.colors| array. |
+ * @return {string} A string representing a color in 'rgba(R,G,B,A)' format, |
+ * where A is the percentage transparency. |
+ */ |
+Plotter.prototype.getFillColor = function(i) { |
+ return this.makeColorTransparent(i, 0.4); |
+}; |
+ |
+/** |
+ * Does the actual plotting. |
+ */ |
+Plotter.prototype.plot = function() { |
+ var self = this; |
+ |
+ this.canvasElement_ = this.canvas_(); |
+ this.rulerDiv_ = this.ruler_(); |
+ |
+ // Markers for the result point(s)/events that the mouse is currently |
+ // hovering over. |
+ this.cursorDiv_ = new VerticalMarker('rgb(100,80,240)'); |
+ this.cursorDivOther_ = new VerticalMarker('rgb(50,50,50)'); |
+ this.eventDiv_ = new VerticalMarker('rgb(255, 0, 0)'); |
+ this.hoveringInfo_ = new HoveringInfo(); |
+ |
+ this.resultNode_.appendChild(this.canvasElement_); |
+ this.resultNode_.appendChild(this.coordinates_()); |
+ this.resultNode_.appendChild(this.rulerDiv_); |
+ this.resultNode_.appendChild(this.cursorDiv_); |
+ this.resultNode_.appendChild(this.cursorDivOther_); |
+ this.resultNode_.appendChild(this.eventDiv_); |
+ this.resultNode_.appendChild(this.hoveringInfo_.getElement()); |
+ this.attachEventListeners_(); |
+ |
+ // Now draw the canvas. |
+ var ctx = this.canvasElement_.getContext('2d'); |
+ |
+ // Clear it with white: otherwise canvas will draw on top of existing data. |
+ ctx.clearRect(0, 0, this.canvasElement_.width, this.canvasElement_.height); |
+ |
+ // Draw all data lines in the reverse order so the last graph appears on |
+ // the backmost and the first graph appears on the frontmost. |
+ function draw(plotData, coordinates, colorOffset, stack) { |
+ for (var i = plotData.length - 1; i >= 0; --i) { |
+ if (stack) { |
+ self.plotAreaUnderLine_(ctx, self.getFillColor(colorOffset + i), |
+ plotData[i], coordinates); |
+ } |
+ self.plotLine_(ctx, self.getDataColor(colorOffset + i), |
+ plotData[i], coordinates); |
+ } |
+ } |
+ draw(this.plotData_.slice(0, |
+ this.graphsOtherStartIndex_ ? |
+ this.graphsOtherStartIndex_ : |
+ this.plotData_.length), |
+ this.coordinates, 0, this.stackedGraph_); |
+ if (this.graphsOtherStartIndex_) { |
+ draw(this.plotData_.slice(this.graphsOtherStartIndex_), |
+ this.unitsYOther_ ? this.coordinatesOther : this.coordinates, |
+ this.graphsOtherStartIndex_, this.stackedGraphOther_); |
+ } |
+ |
+ // Draw events overlayed on graph if needed. |
+ if (this.eventName_ && this.eventInfo_) |
+ this.plotEvents_(ctx, 'rgb(255, 150, 150)', this.coordinates); |
+ |
+ this.graduation_divs_ = this.graduations_(this.coordinates, 0, false); |
+ if (this.unitsYOther_) { |
+ this.graduation_divs_ = this.graduation_divs_.concat( |
+ this.graduations_(this.coordinatesOther, 1, true)); |
+ } |
+ for (var i = 0; i < this.graduation_divs_.length; ++i) |
+ this.resultNode_.appendChild(this.graduation_divs_[i]); |
+}; |
+ |
+/** |
+ * Draws events overlayed on top of an existing graph. |
+ * |
+ * @param {Object} ctx A canvas element object for drawing. |
+ * @param {string} strokeStyles A string representing the drawing style. |
+ * @param {Object} coordinateSystem A Coordinates object representing the |
+ * coordinate system of the graph. |
+ */ |
+Plotter.prototype.plotEvents_ = function(ctx, strokeStyles, coordinateSystem) { |
+ ctx.strokeStyle = strokeStyles; |
+ ctx.fillStyle = strokeStyles; |
+ ctx.lineWidth = 1.0; |
+ |
+ ctx.beginPath(); |
+ var data = this.eventInfo_; |
+ for (var index = 0; index < data.length; ++index) { |
+ var event_time = data[index][0]; |
+ var x = coordinateSystem.xPixel(event_time); |
+ ctx.moveTo(x, 0); |
+ ctx.lineTo(x, this.canvasElement_.offsetHeight); |
+ } |
+ ctx.closePath(); |
+ ctx.stroke(); |
+}; |
+ |
+/** |
+ * Draws a line on the graph. |
+ * |
+ * @param {Object} ctx A canvas element object for drawing. |
+ * @param {string} strokeStyles A string representing the drawing style. |
+ * @param {Array} data A list of [x, y] values representing the line to plot. |
+ * @param {Object} coordinateSystem A Coordinates object representing the |
+ * coordinate system of the graph. |
+ */ |
+Plotter.prototype.plotLine_ = function(ctx, strokeStyles, data, |
+ coordinateSystem) { |
+ ctx.strokeStyle = strokeStyles; |
+ ctx.fillStyle = strokeStyles; |
+ ctx.lineWidth = 2.0; |
+ |
+ ctx.beginPath(); |
+ var initial = true; |
+ var allPoints = []; |
+ for (var i = 0; i < data.length; ++i) { |
+ var pointX = parseFloat(data[i][0]); |
+ var pointY = parseFloat(data[i][1]); |
+ var x = coordinateSystem.xPixel(pointX); |
+ var y = coordinateSystem.yPixel(0); |
+ if (isNaN(pointY)) { |
+ // Re-set 'initial' if we're at a gap in the data. |
+ initial = true; |
+ } else { |
+ y = coordinateSystem.yPixel(pointY); |
+ if (initial) |
+ initial = false; |
+ else |
+ ctx.lineTo(x, y); |
+ } |
+ |
+ ctx.moveTo(x, y); |
+ if (!data[i].interpolated) { |
+ allPoints.push([x, y]); |
+ } |
+ } |
+ ctx.closePath(); |
+ ctx.stroke(); |
+ |
+ if (!this.is_lookout_) { |
+ // Draw a small dot at each point. |
+ for (var i = 0; i < allPoints.length; ++i) { |
+ ctx.beginPath(); |
+ ctx.arc(allPoints[i][0], allPoints[i][1], 3, 0, Math.PI*2, true); |
+ ctx.fill(); |
+ } |
+ } |
+}; |
+ |
+/** |
+ * Fills an area under the given line on the graph. |
+ * |
+ * @param {Object} ctx A canvas element object for drawing. |
+ * @param {string} fillStyle A string representing the drawing style. |
+ * @param {Array} data A list of [x, y] values representing the line to plot. |
+ * @param {Object} coordinateSystem A Coordinates object representing the |
+ * coordinate system of the graph. |
+ */ |
+Plotter.prototype.plotAreaUnderLine_ = function(ctx, fillStyle, data, |
+ coordinateSystem) { |
+ if (!data[0]) { |
+ return; // nothing to draw |
+ } |
+ |
+ ctx.beginPath(); |
+ var x = coordinateSystem.xPixel(parseFloat(data[0][0]) || 0); |
+ var y = coordinateSystem.yPixel(parseFloat(data[0][1]) || 0); |
+ var y0 = coordinateSystem.yPixel(coordinateSystem.yMinValue()); |
+ ctx.moveTo(x, y0); |
+ for (var point, i = 0; point = data[i]; ++i) { |
+ var pointX = parseFloat(point[0]); |
+ var pointY = parseFloat(point[1]); |
+ if (isNaN(pointX)) { continue; } // Skip an invalid point. |
+ if (isNaN(pointY)) { |
+ ctx.lineTo(x, y0); |
+ var yWasNaN = true; |
+ } else { |
+ x = coordinateSystem.xPixel(pointX); |
+ y = coordinateSystem.yPixel(pointY); |
+ if (yWasNaN) { |
+ ctx.lineTo(x, y0); |
+ yWasNaN = false; |
+ } |
+ ctx.lineTo(x, y); |
+ } |
+ } |
+ ctx.lineTo(x, y0); |
+ |
+ ctx.lineWidth = 0; |
+ // Clear the area with white color first. |
+ var COLOR_WHITE = 'rgb(255,255,255)'; |
+ ctx.strokeStyle = COLOR_WHITE; |
+ ctx.fillStyle = COLOR_WHITE; |
+ ctx.fill(); |
+ // Then, fill the area with the specified color. |
+ ctx.strokeStyle = fillStyle; |
+ ctx.fillStyle = fillStyle; |
+ ctx.fill(); |
+}; |
+ |
+/** |
+ * Attaches event listeners to DOM nodes. |
+ */ |
+Plotter.prototype.attachEventListeners_ = function() { |
+ var self = this; |
+ this.canvasElement_.parentNode.addEventListener( |
+ 'mousemove', function(evt) { self.onMouseMove_(evt); }, false); |
+ this.canvasElement_.parentNode.addEventListener( |
+ 'mouseover', function(evt) { self.onMouseOver_(evt); }, false); |
+ this.canvasElement_.parentNode.addEventListener( |
+ 'mouseout', function(evt) { self.onMouseOut_(evt); }, false); |
+ this.cursorDiv_.addEventListener( |
+ 'click', function(evt) { self.onMouseClick_(evt); }, false); |
+ this.cursorDivOther_.addEventListener( |
+ 'click', function(evt) { self.onMouseClick_(evt); }, false); |
+ this.eventDiv_.addEventListener( |
+ 'click', function(evt) { self.onMouseClick_(evt); }, false); |
+}; |
+ |
+/** |
+ * Update the horizontal line that is following where the mouse is hovering. |
+ * |
+ * @param {Object} evt A mouse event object representing a mouse move event. |
+ */ |
+Plotter.prototype.updateRuler_ = function(evt) { |
+ var r = this.rulerDiv_; |
+ r.style.left = this.canvasElement_.offsetLeft + 'px'; |
+ r.style.top = this.canvasElement_.offsetTop + 'px'; |
+ r.style.width = this.canvasElement_.offsetWidth + 'px'; |
+ var h = domUtils.pageXYOfEvent(evt).y - |
+ domUtils.pageXY(this.canvasElement_).y; |
+ if (h > this.canvasElement_.offsetHeight) |
+ h = this.canvasElement_.offsetHeight; |
+ r.style.height = h + 'px'; |
+}; |
+ |
+/** |
+ * Update the highlighted data point at the x value that the mouse is hovering |
+ * over. |
+ * |
+ * @param {Object} coordinateSystem A Coordinates object representing the |
+ * coordinate system of the graph. |
+ * @param {number} currentIndex The index into the |this.plotData| array of the |
+ * data point being hovered over, for a given line. |
+ * @param {Object} cursorDiv A DOM element div object representing the highlight |
+ * itself. |
+ * @param {number} dataIndex The index into the |this.plotData| array of the |
+ * line being hovered over. |
+ */ |
+Plotter.prototype.updateCursor_ = function(coordinateSystem, currentIndex, |
+ cursorDiv, dataIndex) { |
+ var c = cursorDiv; |
+ c.style.top = this.canvasElement_.offsetTop + 'px'; |
+ c.style.height = this.canvasElement_.offsetHeight + 'px'; |
+ |
+ // Left point is half-way to the previous x value, unless it's the first |
+ // point, in which case it's the x value of the current point. |
+ var leftPoint = null; |
+ if (currentIndex == 0) { |
+ leftPoint = this.canvasElement_.offsetLeft + |
+ coordinateSystem.xPixel(this.plotData_[dataIndex][0][0]); |
+ } |
+ else { |
+ var left_x = this.canvasElement_.offsetLeft + |
+ coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex - 1][0]); |
+ var curr_x = this.canvasElement_.offsetLeft + |
+ coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); |
+ leftPoint = (left_x + curr_x) / 2; |
+ } |
+ c.style.left = leftPoint; |
+ |
+ // Width is half-way to the next x value minus the left point, unless it's |
+ // the last point, in which case it's the x value of the current point minus |
+ // the left point. |
+ if (currentIndex == this.plotData_[dataIndex].length - 1) { |
+ var curr_x = this.canvasElement_.offsetLeft + |
+ coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); |
+ c.style.width = curr_x - leftPoint; |
+ } |
+ else { |
+ var next_x = this.canvasElement_.offsetLeft + |
+ coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex + 1][0]); |
+ var curr_x = this.canvasElement_.offsetLeft + |
+ coordinateSystem.xPixel(this.plotData_[dataIndex][currentIndex][0]); |
+ c.style.width = ((next_x + curr_x) / 2) - leftPoint; |
+ } |
+}; |
+ |
+/** |
+ * Update the highlighted event at the x value that the mouse is hovering over. |
+ * |
+ * @param {number} x The x-value (pixel) at which to draw the event highlight |
+ * div. |
+ * @param {boolean} show Whether or not to show the highlight div. |
+ */ |
+Plotter.prototype.updateEventDiv_ = function(x, show) { |
+ var c = this.eventDiv_; |
+ c.style.top = this.canvasElement_.offsetTop + 'px'; |
+ c.style.height = this.canvasElement_.offsetHeight + 'px'; |
+ |
+ if (show) { |
+ c.style.left = this.canvasElement_.offsetLeft + (x - 2); |
+ c.style.width = 8; |
+ } else { |
+ c.style.width = 0; |
+ } |
+}; |
+ |
+/** |
+ * Updates the hovering information. |
+ * |
+ * @param {Event} evt An event object, which specifies the position of the mouse |
+ * cursor. |
+ * @param {boolean} show Whether or not to show the hovering info. Even if it's |
+ * true, if the cursor position is out of the appropriate area, nothing will |
+ * be shown. |
+ */ |
+Plotter.prototype.updateHoveringInfo_ = function(evt, show) { |
+ var evtPageXY = domUtils.pageXYOfEvent(evt); |
+ var hoveringInfoPageXY = this.hoveringInfo_.pageXY(); |
+ var canvasPageXY = domUtils.pageXY(this.canvasElement_); |
+ |
+ var coord = this.coordinates; |
+ // p = the mouse cursor position in value coordinates. |
+ var p = {'x': coord.xValue(evtPageXY.x - canvasPageXY.x), |
+ 'y': coord.yValue(evtPageXY.y - canvasPageXY.y)}; |
+ if (!show || |
+ !(this.stackedGraph_ || this.stackedGraphOther_) || |
+ p.x < coord.xMinValue() || coord.xMaxValue() < p.x || |
+ p.y < coord.yMinValue() || coord.yMaxValue() < p.y) { |
+ this.hoveringInfo_.show(false); |
+ return; |
+ } else { |
+ this.hoveringInfo_.show(true); |
+ } |
+ |
+ /** |
+ * Finds the closest lines (upside and downside of the cursor position). |
+ * Returns a set of upside/downside line indices and point index on success |
+ * or null. |
+ */ |
+ function findClosestLines(lines, opt_startIndex, opt_endIndex) { |
+ var offsetIndex = opt_startIndex || 0; |
+ lines = |
+ opt_endIndex != null ? lines.slice(offsetIndex, opt_endIndex) : |
+ opt_startIndex != null ? lines.slice(offsetIndex) : |
+ lines; |
+ |
+ var upsideClosestLineIndex = null; |
+ var upsideClosestYDistance = coord.yValueRange(); |
+ var downsideClosestLineIndex = null; |
+ var downsideClosestYDistance = coord.yValueRange(); |
+ var upsideClosestPointIndex = null; |
+ |
+ for (var lineIndex = 0, line; line = lines[lineIndex]; ++lineIndex) { |
+ for (var i = 1; line[i]; ++i) { |
+ var p0 = line[i - 1], p1 = line[i]; |
+ if (p0[0] <= p.x && p.x < p1[0]) { |
+ // Calculate y-value of the line at p.x, which is the cursor point. |
+ var y = (p.x - p0[0]) / (p1[0] - p0[0]) * (p1[1] - p0[1]) + p0[1]; |
+ if (p.y < y && y - p.y < upsideClosestYDistance) { |
+ upsideClosestLineIndex = lineIndex; |
+ upsideClosestYDistance = y - p.y; |
+ |
+ if (p.x - p0[0] < p1[0] - p.x) { |
+ upsideClosestPointIndex = i - 1; |
+ } else { |
+ upsideClosestPointIndex = i; |
+ } |
+ } else if (y <= p.y && p.y - y < downsideClosestYDistance) { |
+ downsideClosestLineIndex = lineIndex; |
+ downsideClosestYDistance = p.y - y; |
+ } |
+ break; |
+ } |
+ } |
+ } |
+ |
+ return (upsideClosestLineIndex != null && |
+ upsideClosestPointIndex != null) ? |
+ {'upsideLineIndex': offsetIndex + upsideClosestLineIndex, |
+ 'downsideLineIndex': downsideClosestYDistance == null ? null : |
+ offsetIndex + downsideClosestLineIndex, |
+ 'upsidePointIndex': offsetIndex + upsideClosestPointIndex} : |
+ null; |
+ } |
+ |
+ // Find the closest lines above and below the mouse cursor. |
+ var closest = null; |
+ // Since the other set of graphs are drawn over the first set, try to find |
+ // the closest lines from the other set of graphs first. |
+ if (this.graphsOtherStartIndex_ && this.stackedGraphOther_) { |
+ closest = findClosestLines(this.plotData_, this.graphsOtherStartIndex_); |
+ } |
+ if (!closest && this.stackedGraph_) { |
+ closest = this.graphsOtherStartIndex_ ? |
+ findClosestLines(this.plotData_, 0, this.graphsOtherStartIndex_) : |
+ findClosestLines(this.plotData_); |
+ } |
+ if (!closest) { |
+ this.hoveringInfo_.show(false); |
+ return; |
+ } |
+ |
+ // Update the contents of the hovering info box. |
+ // Color indicator, description and the value of the item. |
+ this.hoveringInfo_.setColorIndicator( |
+ this.getDataColor(closest.upsideLineIndex)); |
+ this.hoveringInfo_.setLegendText( |
+ this.dataDescriptions_[closest.upsideLineIndex]); |
+ var y1 = this.plotData_[closest.upsideLineIndex][closest.upsidePointIndex][1]; |
+ var y0 = closest.downsideLineIndex == null ? |
+ 0 : |
+ this.plotData_[closest.downsideLineIndex][closest.upsidePointIndex][1]; |
+ this.hoveringInfo_.setItemValue(y1 - y0); |
+ |
+ // Locate the hovering info box near the mouse cursor. |
+ var DIV_X_OFFSET = 10, DIV_Y_OFFSET = -20; |
+ if (evtPageXY.x + this.hoveringInfo_.getElement().offsetWidth < |
+ canvasPageXY.x + this.canvasElement_.offsetWidth) { |
+ this.hoveringInfo_.locateAtPageXY(evtPageXY.x + DIV_X_OFFSET, |
+ evtPageXY.y + DIV_Y_OFFSET); |
+ } else { // If lacking space at the right side, locate it at the left side. |
+ this.hoveringInfo_.locateAtPageXY( |
+ evtPageXY.x - this.hoveringInfo_.getElement().offsetWidth - DIV_X_OFFSET, |
+ evtPageXY.y + DIV_Y_OFFSET); |
+ } |
+}; |
+ |
+/** |
+ * Handle a mouse move event. |
+ * |
+ * @param {Object} evt A mouse event object representing a mouse move event. |
+ */ |
+Plotter.prototype.onMouseMove_ = function(evt) { |
+ var self = this; |
+ |
+ var canvas = evt.currentTarget.firstChild; |
+ var evtPageXY = domUtils.pageXYOfEvent(evt); |
+ var canvasPageXY = domUtils.pageXY(this.canvasElement_); |
+ var positionX = evtPageXY.x - canvasPageXY.x; |
+ var positionY = evtPageXY.y - canvasPageXY.y; |
+ |
+ // Identify the index of the x value that is closest to the mouse x value. |
+ var xValue = this.coordinates.xValue(positionX); |
+ var lineIndex = !this.stackedGraph_ ? 0 : |
+ this.graphsOtherStartIndex_ ? this.graphsOtherStartIndex_ - 1 : |
+ this.plotData_.length - 1; |
+ var line = this.plotData_[lineIndex]; |
+ var min_diff = Math.abs(line[0][0] - xValue); |
+ indexValueX = 0; |
+ for (var i = 1; i < line.length; ++i) { |
+ var diff = Math.abs(line[i][0] - xValue); |
+ if (diff < min_diff) { |
+ min_diff = diff; |
+ indexValueX = i; |
+ } |
+ } |
+ |
+ // Identify the index of the x value closest to the mouse x value for the |
+ // other graph being overlayed on top of the original graph, if one exists. |
+ if (this.unitsYOther_) { |
+ var xValue = this.coordinatesOther.xValue(positionX); |
+ var lineIndexOther = !this.stackedGraphOther_ ? |
+ this.graphsOtherStartIndex_ : this.plotData_.length - 1; |
+ var lineOther = this.plotData_[lineIndexOther]; |
+ var min_diff = Math.abs(lineOther[0][0] - xValue); |
+ var indexValueXOther = 0; |
+ for (var i = 1; i < lineOther.length; ++i) { |
+ var diff = Math.abs(lineOther[i][0] - xValue); |
+ if (diff < min_diff) { |
+ min_diff = diff; |
+ indexValueXOther = i; |
+ } |
+ } |
+ } |
+ |
+ // Update coordinate information displayed directly underneath the graph. |
+ function legendLabel(lineIndex, opt_labelText) { |
+ return '<span style="color:' + self.getDataColor(lineIndex) + '">' + |
+ (opt_labelText || self.dataDescriptions_[lineIndex]) + |
+ '</span>: '; |
+ } |
+ function valuesAtCursor(lineIndex, pointIndex, unitsY, yValue) { |
+ return '<span style="color:' + self.getDataColor(lineIndex) + '">' + |
+ self.plotData_[lineIndex][pointIndex][0] + ' ' + self.unitsX_ + ': ' + |
+ addCommas(self.plotData_[lineIndex][pointIndex][1].toFixed(2)) + ' ' + |
+ unitsY + '</span> [hovering at ' + addCommas(yValue.toFixed(2)) + |
+ ' ' + unitsY + ']'; |
+ } |
+ |
+ this.infoBox_.rows[0].label.innerHTML = legendLabel(lineIndex); |
+ this.infoBox_.rows[0].content.innerHTML = valuesAtCursor( |
+ lineIndex, indexValueX, this.unitsY_, this.coordinates.yValue(positionY)); |
+ var row = this.infoBox_.rows[1]; |
+ if (this.unitsYOther_) { |
+ row.label.innerHTML = legendLabel(lineIndexOther); |
+ row.content.innerHTML = valuesAtCursor( |
+ lineIndexOther, indexValueXOther, this.unitsYOther_, |
+ this.coordinatesOther.yValue(positionY)); |
+ } else if (this.graphsOtherStartIndex_) { |
+ row.label.innerHTML = legendLabel( |
+ this.stackedGraphOther_ ? |
+ this.plotData_.length - 1 : this.graphsOtherStartIndex_); |
+ row.content.innerHTML = valuesAtCursor( |
+ this.stackedGraphOther_ ? |
+ this.plotData_.length - 1 : this.graphsOtherStartIndex_, |
+ indexValueX, this.unitsY_, this.coordinates.yValue(positionY)); |
+ } else if (!this.stackedGraph_ && this.dataDescriptions_.length > 1) { |
+ row.label.innerHTML = legendLabel(1); |
+ row.content.innerHTML = valuesAtCursor( |
+ 1, indexValueX, this.unitsY_, this.coordinates.yValue(positionY)); |
+ } else if (row) { |
+ row.label.innerHTML = ''; |
+ row.content.innerHTML = ''; |
+ } |
+ |
+ // If there is a horizontal marker, also display deltas relative to it. |
+ if (this.horizontal_marker_) { |
+ var baseline = this.horizontal_marker_.value; |
+ var delta = this.coordinates.yValue(positionY) - baseline; |
+ var fraction = delta / baseline; // Allow division by 0. |
+ |
+ var deltaStr = (delta >= 0 ? '+' : '') + delta.toFixed(0) + ' ' + |
+ this.unitsY_; |
+ var percentStr = (fraction >= 0 ? '+' : '') + (fraction * 100).toFixed(3) + |
+ '%'; |
+ |
+ this.baselineDeltasTd_.innerHTML = deltaStr + ': ' + percentStr; |
+ |
+ if (this.unitsYOther_) { |
+ var baseline = this.horizontal_marker_.otherValue; |
+ var yValue2 = this.coordinatesOther.yValue(positionY); |
+ var delta = yValue2 - baseline; |
+ var fraction = delta / baseline; // Allow division by 0. |
+ |
+ var deltaStr = (delta >= 0 ? '+' : '') + delta.toFixed(0) + ' ' + |
+ this.unitsYOther_; |
+ var percentStr = (fraction >= 0 ? '+' : '') + |
+ (fraction * 100).toFixed(3) + '%'; |
+ this.baselineDeltasTd_.innerHTML += '<br>' + deltaStr + ': ' + percentStr; |
+ } |
+ } |
+ |
+ this.updateRuler_(evt); |
+ this.updateCursor_(this.coordinates, indexValueX, this.cursorDiv_, 0); |
+ if (this.unitsYOther_ && this.graphsOtherStartIndex_) { |
+ this.updateCursor_(this.coordinatesOther, indexValueXOther, |
+ this.cursorDivOther_, this.graphsOtherStartIndex_); |
+ } |
+ |
+ // If there are events displayed, see if we're hovering close to an existing |
+ // event on the graph, and if so, display the metadata associated with it. |
+ if (this.eventName_ != null && this.eventInfo_ != null) { |
+ this.infoBox_.rows[1].label.innerHTML = 'Event "' + this.eventName_ + |
+ '": '; |
+ var data = this.eventInfo_; |
+ var showed_event = false; |
+ var x = 0; |
+ for (var index = 0; index < data.length; ++index) { |
+ var event_time = data[index][0]; |
+ x = this.coordinates.xPixel(event_time); |
+ if (positionX >= x - 10 && positionX <= x + 10) { |
+ var metadata = data[index][1]; |
+ var metadata_str = ""; |
+ for (var meta_key in metadata) |
+ metadata_str += meta_key + ': ' + metadata[meta_key] + ', '; |
+ metadata_str = metadata_str.substring(0, metadata_str.length - 2); |
+ this.infoBox_.rows[1].content.innerHTML = event_time + ' ' + |
+ this.unitsX_ + ': {' + metadata_str + '}'; |
+ showed_event = true; |
+ this.updateEventDiv_(x, true); |
+ break; |
+ } |
+ } |
+ if (!showed_event) { |
+ this.coordinatesTdOther_.innerHTML = |
+ 'move mouse close to vertical event marker'; |
+ this.updateEventDiv_(x, false); |
+ } |
+ } |
+ |
+ this.updateHoveringInfo_(evt, true); |
+}; |
+ |
+/** |
+ * Handle a mouse over event. |
+ * |
+ * @param {Object} evt A mouse event object representing a mouse move event. |
+ */ |
+Plotter.prototype.onMouseOver_ = function(evt) { |
+ this.updateHoveringInfo_(evt, true); |
+}; |
+ |
+/** |
+ * Handle a mouse out event. |
+ * |
+ * @param {Object} evt A mouse event object representing a mouse move event. |
+ */ |
+Plotter.prototype.onMouseOut_ = function(evt) { |
+ this.updateHoveringInfo_(evt, false); |
+}; |
+ |
+/** |
+ * Handle a mouse click event. |
+ * |
+ * @param {Object} evt A mouse event object representing a mouse click event. |
+ */ |
+Plotter.prototype.onMouseClick_ = function(evt) { |
+ // Shift-click controls the horizontal reference line. |
+ if (evt.shiftKey) { |
+ if (this.horizontal_marker_) |
+ this.horizontal_marker_.remove(); |
+ |
+ var canvasY = domUtils.pageXYOfEvent(evt).y - |
+ domUtils.pageXY(this.canvasElement_).y; |
+ this.horizontal_marker_ = new HorizontalMarker( |
+ this.canvasElement_, |
+ this.coordinates.yValue(canvasY), |
+ (this.coordinatesOther ? this.coordinatesOther.yValue(canvasY) : null)); |
+ // Insert before cursor node, otherwise it catches clicks. |
+ this.cursorDiv_.parentNode.insertBefore( |
+ this.horizontal_marker_.markerDiv, this.cursorDiv_); |
+ this.horizontal_marker_.locateAt(this.canvasElement_, canvasY); |
+ } |
+}; |
+ |
+/** |
+ * Generates and returns a list of div objects representing horizontal lines in |
+ * the graph that indicate y-axis values at a computed interval. |
+ * |
+ * @param {Object} coordinateSystem a Coordinates object representing the |
+ * coordinate system for which the graduations should be created. |
+ * @param {number} colorIndex An index into the |this.colors| array representing |
+ * the color to make the graduations in the event that two graphs with |
+ * different coordinate systems are being overlayed on the same plot. |
+ * @param {boolean} isRightSide Whether or not the graduations should have |
+ * right-aligned text (used when the graduations are for a second graph |
+ * that is being overlayed on top of another graph). |
+ * @return {Array} An array of DOM Element objects representing the divs. |
+ */ |
+Plotter.prototype.graduations_ = function(coordinateSystem, colorIndex, |
+ isRightSide) { |
+ // Don't allow a graduation in the bottom 5% of the chart or the number label |
+ // would overflow the chart bounds. |
+ var yMin = coordinateSystem.yLowerLimitValue() + |
+ .05 * coordinateSystem.yValueRange(); |
+ var yRange = coordinateSystem.yUpperLimitValue() - yMin; |
+ |
+ // Use the largest scale that fits 3 or more graduations. |
+ // We allow scales of [...,500, 250, 100, 50, 25, 10,...]. |
+ var scale = 5000000000; |
+ while (scale) { |
+ if (Math.floor(yRange / scale) > 2) break; // 5s. |
+ scale /= 2; |
+ if (Math.floor(yRange / scale) > 2) break; // 2.5s. |
+ scale /= 2.5; |
+ if (Math.floor(yRange / scale) > 2) break; // 1s. |
+ scale /= 2; |
+ } |
+ |
+ var graduationPosition = yMin + (scale - yMin % scale); |
+ var graduationDivs = []; |
+ while (graduationPosition < coordinateSystem.yUpperLimitValue() || |
+ yRange == 0) { |
+ var graduation = document.createElement('div'); |
+ var canvasPosition; |
+ if (yRange == 0) { |
+ // Center the graduation vertically. |
+ canvasPosition = this.canvasElement_.offsetHeight / 2; |
+ } else { |
+ canvasPosition = coordinateSystem.yPixel(graduationPosition); |
+ } |
+ if (this.unitsYOther_) { |
+ graduation.style.borderTop = '1px dashed ' + |
+ this.makeColorTransparent(colorIndex, 0.4) |
+ } else { |
+ graduation.style.borderTop = '1px dashed rgba(0,0,0,.08)'; |
+ } |
+ graduation.style.position = 'absolute'; |
+ graduation.style.left = this.canvasElement_.offsetLeft + 'px'; |
+ graduation.style.top = canvasPosition + this.canvasElement_.offsetTop + |
+ 'px'; |
+ graduation.style.width = this.canvasElement_.offsetWidth - |
+ this.canvasElement_.offsetLeft + 'px'; |
+ graduation.style.paddingLeft = '4px'; |
+ if (this.unitsYOther_) |
+ graduation.style.color = this.makeColorTransparent(colorIndex, 0.9) |
+ else |
+ graduation.style.color = 'rgba(0,0,0,.4)'; |
+ graduation.style.fontSize = '9px'; |
+ graduation.style.paddingTop = '0'; |
+ graduation.style.zIndex = '-1'; |
+ if (isRightSide) |
+ graduation.style.textAlign = 'right'; |
+ if (yRange == 0) |
+ graduation.innerHTML = addCommas(yMin); |
+ else |
+ graduation.innerHTML = addCommas(graduationPosition); |
+ graduationDivs.push(graduation); |
+ if (yRange == 0) |
+ break; |
+ graduationPosition += scale; |
+ } |
+ return graduationDivs; |
+}; |
+ |
+/** |
+ * Generates and returns a div object representing the horizontal line that |
+ * follows the mouse pointer around the plot. |
+ * |
+ * @return {Object} A DOM Element object representing the div. |
+ */ |
+Plotter.prototype.ruler_ = function() { |
+ var ruler = document.createElement('div'); |
+ ruler.setAttribute('class', 'plot-ruler'); |
+ ruler.style.borderBottom = '1px dotted black'; |
+ ruler.style.position = 'absolute'; |
+ ruler.style.left = '-2px'; |
+ ruler.style.top = '-2px'; |
+ ruler.style.width = '0px'; |
+ ruler.style.height = '0px'; |
+ return ruler; |
+}; |
+ |
+/** |
+ * Generates and returns a canvas object representing the plot itself. |
+ * |
+ * @return {Object} A DOM Element object representing the canvas. |
+ */ |
+Plotter.prototype.canvas_ = function() { |
+ var canvas = document.createElement('canvas'); |
+ canvas.setAttribute('id', '_canvas'); |
+ canvas.setAttribute('class', 'plot'); |
+ canvas.setAttribute('width', this.coordinates.widthMax); |
+ canvas.setAttribute('height', this.coordinates.heightMax); |
+ canvas.plotter = this; |
+ return canvas; |
+}; |
+ |
+/** |
+ * Generates and returns a div object representing the coordinate information |
+ * displayed directly underneath a graph. |
+ * |
+ * @return {Object} A DOM Element object representing the div. |
+ */ |
+Plotter.prototype.coordinates_ = function() { |
+ var coordinatesDiv = document.createElement('div'); |
+ var table_html = '<table border=0 width="100%"'; |
+ if (this.is_lookout_) { |
+ table_html += ' style="font-size:0.8em"'; |
+ } |
+ table_html += '><tbody><tr>'; |
+ table_html += '<td><span class="legend_item"></span>' + |
+ '<span class="plot-coordinates"><i>move mouse over graph</i></span></td>'; |
+ table_html += '<td align="right">x-axis is ' + this.unitsX_ + '</td>'; |
+ table_html += '</tr><tr>'; |
+ table_html += '<td><span class="legend_item"></span>' + |
+ '<span class="plot-coordinates"></span></td>'; |
+ |
+ if (!this.is_lookout_) { |
+ table_html += '<td align="right" style="color: ' + HorizontalMarker.COLOR + |
+ '"><i>Shift-click to place baseline.</i></td>'; |
+ } |
+ table_html += '</tr></tbody></table>'; |
+ coordinatesDiv.innerHTML = table_html; |
+ |
+ var trs = coordinatesDiv.querySelectorAll('tr'); |
+ this.infoBox_ = {rows: []}; |
+ this.infoBox_.rows.push({ |
+ label: trs[0].querySelector('span.legend_item'), |
+ content: trs[0].querySelector('span.plot-coordinates')}); |
+ if (this.dataDescriptions_.length > 1 || this.eventName_) { |
+ this.infoBox_.rows.push({ |
+ label: trs[1].querySelector('span.legend_item'), |
+ content: trs[1].querySelector('span.plot-coordinates')}); |
+ } |
+ |
+ this.baselineDeltasTd_ = trs[1].childNodes[1]; |
+ |
+ // Add a summary of legends in case of stacked graphs. |
+ if (this.stackedGraph_ || this.stackedGraphOther_) { |
+ var legendPane = document.createElement('div'); |
+ legendPane.style.fontSize = '80%'; |
+ coordinatesDiv.appendChild(legendPane); |
+ |
+ if (this.graphsOtherStartIndex_) { |
+ legendPane.appendChild( |
+ this.createLegendsSummaryElement_( |
+ this.dataDescriptions_.slice(0, this.graphsOtherStartIndex_), |
+ 0)); |
+ legendPane.appendChild( |
+ this.createLegendsSummaryElement_( |
+ this.dataDescriptions_.slice(this.graphsOtherStartIndex_), |
+ this.graphsOtherStartIndex_)); |
+ } else { |
+ legendPane.appendChild( |
+ this.createLegendsSummaryElement_(this.dataDescriptions_, 0)); |
+ } |
+ } |
+ |
+ return coordinatesDiv; |
+}; |
+ |
+/** |
+ * Creates and returns a DOM element which shows a summary of legends. |
+ * |
+ * @param {!Array.<string>} legendTexts An array of legend texts. |
+ * @param {number} colorIndexOffset Offset index for color. i-th legend text |
+ * has an indicator in {@code (colorIndexOffset + i)}-th color |
+ * @return {!Element} An element which shows a summary of legends. |
+ */ |
+Plotter.prototype.createLegendsSummaryElement_ = function(legendTexts, |
+ colorIndexOffset) { |
+ var containerElem = document.createElement('div'); |
+ |
+ for (var i = 0, text; text = legendTexts[i]; ++i) { |
+ var colorIndicatorElem = document.createElement('div'); |
+ colorIndicatorElem.style.display = 'inline-block'; |
+ colorIndicatorElem.style.width = '1em'; |
+ colorIndicatorElem.style.height = '1em'; |
+ colorIndicatorElem.style.verticalAlign = 'text-bottom'; |
+ colorIndicatorElem.style.margin = '0 0.24em 0 0'; |
+ colorIndicatorElem.style.border = '1px solid #000'; |
+ colorIndicatorElem.style.backgroundColor = |
+ this.getDataColor(colorIndexOffset + i); |
+ var legendTextElem = document.createElement('span'); |
+ legendTextElem.textContent = text; |
+ var legendElem = document.createElement('span'); |
+ legendElem.style.whiteSpace = 'nowrap'; |
+ legendElem.appendChild(colorIndicatorElem); |
+ legendElem.appendChild(legendTextElem); |
+ legendElem.style.margin = '0 0.8em 0 0'; |
+ containerElem.appendChild(legendElem); |
+ // Add a space to break lines if necessary. |
+ containerElem.appendChild(document.createTextNode(' ')); |
+ } |
+ |
+ return containerElem; |
+}; |