From 8644f425d29644ee68756dae7f29326b6e4ab4da Mon Sep 17 00:00:00 2001 From: dsc Date: Thu, 29 Mar 2012 12:11:13 -0700 Subject: [PATCH] Patches Dygraphs with structural markup for the legend so it can be styled to be readable. See https://github.com/dsc/dygraphs/commit/8f19f30b77988ce085976e9a0cc17b50acbcd102 --- lib/graph/graph-edit-view.co | 27 +- static/vendor/dygraph-1.2dev.js | 1064 ++++++++++++++++++++++++++++------- static/vendor/dygraph-1.2dev.min.js | 12 +- www/css/graph.styl | 14 +- 4 files changed, 895 insertions(+), 222 deletions(-) diff --git a/lib/graph/graph-edit-view.co b/lib/graph/graph-edit-view.co index 734bc03..7957699 100644 --- a/lib/graph/graph-edit-view.co +++ b/lib/graph/graph-edit-view.co @@ -27,6 +27,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ ]> __bind__ : <[ render renderAll resizeViewport + numberFormatter numberFormatterHTML onReady onSync onModelChange onScaffoldChange onFirstClickRenderOptionsTab ]> @@ -190,14 +191,14 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ options = @chartOptions() #import size options import do labelsDiv : @$el.find '.graph-label' .0 - valueFormatter : @numberFormatter + valueFormatter : @numberFormatterHTML axes: x: axisLabelFormatter : @axisDateFormatter valueFormatter : @dateFormatter y: axisLabelFormatter : @axisFormatter @numberFormatter - valueFormatter : @numberFormatter + valueFormatter : @numberFormatterHTML # console.log "#this.render!", dataset _.dump options, 'options' @@ -253,16 +254,28 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ dateFormatter: (n, opts, g) -> moment(n).format 'DD MMM YYYY' - numberFormatter: (n, opts, g) -> - digits = opts('digitsAfterDecimal') ? 2 + _numberFormatter: (n, digits=2) -> for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] if n >= d n = n / d break - s = n.toFixed(digits) + suffix + s = n.toFixed(digits) parts = s.split '.' - parts[0] = _.rchop parts[0], 3 .join ',' - parts.join '.' + whole = _.rchop parts[0], 3 .join ',' + fraction = '.' + parts.slice(1).join '.' + { n, digits, whole, fraction, suffix } + + numberFormatter: (n, opts, g) -> + digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @_numberFormatter n, digits + "#whole#fraction#suffix" + + numberFormatterHTML: (n, opts, g) -> + digits = opts('digitsAfterDecimal') ? 2 + { whole, fraction, suffix } = @_numberFormatter n, digits + """ + #whole#fraction#suffix + """ ### }}} ### Event Handlers {{{ diff --git a/static/vendor/dygraph-1.2dev.js b/static/vendor/dygraph-1.2dev.js index 547e519..4b61bdb 100644 --- a/static/vendor/dygraph-1.2dev.js +++ b/static/vendor/dygraph-1.2dev.js @@ -215,6 +215,7 @@ DygraphLayout.prototype._evaluateLineCharts = function() { this.setPointsLengths = []; this.setPointsOffsets = []; + var connectSeparated = this.attr_('connectSeparatedPoints'); for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) { var dataset = this.datasets[setIdx]; var setName = this.setNames[setIdx]; @@ -241,6 +242,9 @@ DygraphLayout.prototype._evaluateLineCharts = function() { yval: yValue, name: setName }; + if (connectSeparated && item[1] === null) { + point.yval = null; + } this.points.push(point); setPointsLength += 1; } @@ -567,6 +571,7 @@ DygraphCanvasRenderer.prototype.render = function() { ctx.closePath(); ctx.stroke(); } + ctx.restore(); } if (this.attr_('drawXGrid')) { @@ -583,6 +588,7 @@ DygraphCanvasRenderer.prototype.render = function() { ctx.closePath(); ctx.stroke(); } + ctx.restore(); } // Do the ordinary rendering, as before @@ -965,7 +971,8 @@ DygraphCanvasRenderer.prototype._renderAnnotations = function() { var points = this.layout.annotated_points; for (var i = 0; i < points.length; i++) { var p = points[i]; - if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w) { + if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w || + p.canvasy < this.area.y || p.canvasy > this.area.y + this.area.h) { continue; } @@ -1039,6 +1046,128 @@ DygraphCanvasRenderer.prototype._renderAnnotations = function() { } }; +DygraphCanvasRenderer.makeNextPointStep_ = function(connect, points, end) { + if (connect) { + return function(j) { + while (++j < end) { + if (!(points[j].yval === null)) break; + } + return j; + } + } else { + return function(j) { return j + 1 }; + } +}; + +DygraphCanvasRenderer.prototype._drawStyledLine = function( + ctx, i, setName, color, strokeWidth, strokePattern, drawPoints, + drawPointCallback, pointSize) { + var isNullOrNaN = function(x) { + return (x === null || isNaN(x)); + }; + + var stepPlot = this.attr_("stepPlot"); + var firstIndexInSet = this.layout.setPointsOffsets[i]; + var setLength = this.layout.setPointsLengths[i]; + var afterLastIndexInSet = firstIndexInSet + setLength; + var points = this.layout.points; + var prevX = null; + var prevY = null; + var pointsOnLine = []; // Array of [canvasx, canvasy] pairs. + if (!Dygraph.isArrayLike(strokePattern)) { + strokePattern = null; + } + + var point; + var next = DygraphCanvasRenderer.makeNextPointStep_( + this.attr_('connectSeparatedPoints'), points, afterLastIndexInSet); + ctx.save(); + for (var j = firstIndexInSet; j < afterLastIndexInSet; j = next(j)) { + point = points[j]; + if (isNullOrNaN(point.canvasy)) { + if (stepPlot && prevX !== null) { + // Draw a horizontal line to the start of the missing data + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = this.attr_('strokeWidth'); + this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); + ctx.stroke(); + } + // this will make us move to the next point, not draw a line to it. + prevX = prevY = null; + } else { + // A point is "isolated" if it is non-null but both the previous + // and next points are null. + var isIsolated = (!prevX && (j == points.length - 1 || + isNullOrNaN(points[j+1].canvasy))); + if (prevX === null) { + prevX = point.canvasx; + prevY = point.canvasy; + } else { + // Skip over points that will be drawn in the same pixel. + if (Math.round(prevX) == Math.round(point.canvasx) && + Math.round(prevY) == Math.round(point.canvasy)) { + continue; + } + // TODO(antrob): skip over points that lie on a line that is already + // going to be drawn. There is no need to have more than 2 + // consecutive points that are collinear. + if (strokeWidth) { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = strokeWidth; + if (stepPlot) { + this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); + prevX = point.canvasx; + } + this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern); + prevX = point.canvasx; + prevY = point.canvasy; + ctx.stroke(); + } + } + + if (drawPoints || isIsolated) { + pointsOnLine.push([point.canvasx, point.canvasy]); + } + } + } + for (var idx = 0; idx < pointsOnLine.length; idx++) { + var cb = pointsOnLine[idx]; + ctx.save(); + drawPointCallback( + this.dygraph_, setName, ctx, cb[0], cb[1], color, pointSize); + ctx.restore(); + } + ctx.restore(); +}; + +DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) { + var setNames = this.layout.setNames; + var setName = setNames[i]; + + var strokeWidth = this.dygraph_.attr_("strokeWidth", setName); + var borderWidth = this.dygraph_.attr_("strokeBorderWidth", setName); + var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName) || + Dygraph.Circles.DEFAULT; + if (borderWidth && strokeWidth) { + this._drawStyledLine(ctx, i, setName, + this.dygraph_.attr_("strokeBorderColor", setName), + strokeWidth + 2 * borderWidth, + this.dygraph_.attr_("strokePattern", setName), + this.dygraph_.attr_("drawPoints", setName), + drawPointCallback, + this.dygraph_.attr_("pointSize", setName)); + } + + this._drawStyledLine(ctx, i, setName, + this.colors[setName], + strokeWidth, + this.dygraph_.attr_("strokePattern", setName), + this.dygraph_.attr_("drawPoints", setName), + drawPointCallback, + this.dygraph_.attr_("pointSize", setName)); +}; /** * Actually draw the lines chart, including error bars. @@ -1046,12 +1175,8 @@ DygraphCanvasRenderer.prototype._renderAnnotations = function() { * @private */ DygraphCanvasRenderer.prototype._renderLineChart = function() { - var isNullOrNaN = function(x) { - return (x === null || isNaN(x)); - }; - // TODO(danvk): use this.attr_ for many of these. - var context = this.elementContext; + var ctx = this.elementContext; var fillAlpha = this.attr_('fillAlpha'); var errorBars = this.attr_("errorBars") || this.attr_("customBars"); var fillGraph = this.attr_("fillGraph"); @@ -1079,8 +1204,8 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { } // create paths - var ctx = context; if (errorBars) { + ctx.save(); if (fillGraph) { this.dygraph_.warn("Can't use fillGraph option with error bars"); } @@ -1090,8 +1215,15 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { axis = this.dygraph_.axisPropertiesForSeries(setName); color = this.colors[setName]; + var firstIndexInSet = this.layout.setPointsOffsets[i]; + var setLength = this.layout.setPointsLengths[i]; + var afterLastIndexInSet = firstIndexInSet + setLength; + + var next = DygraphCanvasRenderer.makeNextPointStep_( + this.attr_('connectSeparatedPoints'), points, + afterLastIndexInSet); + // setup graphics context - ctx.save(); prevX = NaN; prevY = NaN; prevYs = [-1, -1]; @@ -1102,9 +1234,9 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { fillAlpha + ')'; ctx.fillStyle = err_color; ctx.beginPath(); - for (j = 0; j < pointsLength; j++) { + for (j = firstIndexInSet; j < afterLastIndexInSet; j = next(j)) { point = points[j]; - if (point.name == setName) { + if (point.name == setName) { // TODO(klausw): this is always true if (!Dygraph.isOK(point.y)) { prevX = NaN; continue; @@ -1140,7 +1272,9 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { } ctx.fill(); } + ctx.restore(); } else if (fillGraph) { + ctx.save(); var baseline = []; // for stacked graphs: baseline for filling // process sets in reverse order (needed for stacked graphs) @@ -1152,9 +1286,15 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { if (axisY < 0.0) axisY = 0.0; else if (axisY > 1.0) axisY = 1.0; axisY = this.area.h * axisY + this.area.y; + var firstIndexInSet = this.layout.setPointsOffsets[i]; + var setLength = this.layout.setPointsLengths[i]; + var afterLastIndexInSet = firstIndexInSet + setLength; + + var next = DygraphCanvasRenderer.makeNextPointStep_( + this.attr_('connectSeparatedPoints'), points, + afterLastIndexInSet); // setup graphics context - ctx.save(); prevX = NaN; prevYs = [-1, -1]; yscale = axis.yscale; @@ -1164,9 +1304,9 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { fillAlpha + ')'; ctx.fillStyle = err_color; ctx.beginPath(); - for (j = 0; j < pointsLength; j++) { + for (j = firstIndexInSet; j < afterLastIndexInSet; j = next(j)) { point = points[j]; - if (point.name == setName) { + if (point.name == setName) { // TODO(klausw): this is always true if (!Dygraph.isOK(point.y)) { prevX = NaN; continue; @@ -1196,87 +1336,13 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { } ctx.fill(); } + ctx.restore(); } // Drawing the lines. - var firstIndexInSet = 0; - var afterLastIndexInSet = 0; - var setLength = 0; for (i = 0; i < setCount; i += 1) { - firstIndexInSet = this.layout.setPointsOffsets[i]; - setLength = this.layout.setPointsLengths[i]; - afterLastIndexInSet = firstIndexInSet + setLength; - setName = setNames[i]; - color = this.colors[setName]; - var strokeWidth = this.dygraph_.attr_("strokeWidth", setName); - - // setup graphics context - context.save(); - var pointSize = this.dygraph_.attr_("pointSize", setName); - prevX = null; - prevY = null; - var drawPoints = this.dygraph_.attr_("drawPoints", setName); - var strokePattern = this.dygraph_.attr_("strokePattern", setName); - if (!Dygraph.isArrayLike(strokePattern)) { - strokePattern = null; - } - for (j = firstIndexInSet; j < afterLastIndexInSet; j++) { - point = points[j]; - if (isNullOrNaN(point.canvasy)) { - if (stepPlot && prevX !== null) { - // Draw a horizontal line to the start of the missing data - ctx.beginPath(); - ctx.strokeStyle = color; - ctx.lineWidth = this.attr_('strokeWidth'); - this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); - ctx.stroke(); - } - // this will make us move to the next point, not draw a line to it. - prevX = prevY = null; - } else { - // A point is "isolated" if it is non-null but both the previous - // and next points are null. - var isIsolated = (!prevX && (j == points.length - 1 || - isNullOrNaN(points[j+1].canvasy))); - if (prevX === null) { - prevX = point.canvasx; - prevY = point.canvasy; - } else { - // Skip over points that will be drawn in the same pixel. - if (Math.round(prevX) == Math.round(point.canvasx) && - Math.round(prevY) == Math.round(point.canvasy)) { - continue; - } - // TODO(antrob): skip over points that lie on a line that is already - // going to be drawn. There is no need to have more than 2 - // consecutive points that are collinear. - if (strokeWidth) { - ctx.beginPath(); - ctx.strokeStyle = color; - ctx.lineWidth = strokeWidth; - if (stepPlot) { - this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); - prevX = point.canvasx; - } - this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern); - prevX = point.canvasx; - prevY = point.canvasy; - ctx.stroke(); - } - } - - if (drawPoints || isIsolated) { - ctx.beginPath(); - ctx.fillStyle = color; - ctx.arc(point.canvasx, point.canvasy, pointSize, - 0, 2 * Math.PI, false); - ctx.fill(); - } - } - } + this._drawLine(ctx, i); } - - context.restore(); }; /** @@ -1447,7 +1513,7 @@ var Dygraph = function(div, data, opts) { }; Dygraph.NAME = "Dygraph"; -Dygraph.VERSION = "1.2dev"; +Dygraph.VERSION = "1.2"; Dygraph.__repr__ = function() { return "[" + this.NAME + " " + this.VERSION + "]"; }; @@ -1557,6 +1623,8 @@ Dygraph.dateAxisFormatter = function(date, granularity) { // Default attribute values. Dygraph.DEFAULT_ATTRS = { highlightCircleSize: 3, + highlightSeriesOpts: null, + highlightSeriesBackgroundAlpha: 0.5, labelsDivWidth: 250, labelsDivStyles: { @@ -1573,6 +1641,8 @@ Dygraph.DEFAULT_ATTRS = { sigFigs: null, strokeWidth: 1.0, + strokeBorderWidth: 0, + strokeBorderColor: "white", axisTickSize: 3, axisLabelFontSize: 14, @@ -1769,6 +1839,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.boundaryIds_ = []; this.setIndexByName_ = {}; + this.datasetIndex_ = []; // Create the containing DIV and other interactive elements this.createInterface_(); @@ -1813,18 +1884,31 @@ Dygraph.prototype.toString = function() { * @return { ... } The value of the option. */ Dygraph.prototype.attr_ = function(name, seriesName) { - if (this.user_attrs_ !== null && seriesName && - typeof(this.user_attrs_[seriesName]) != 'undefined' && - this.user_attrs_[seriesName] !== null && - typeof(this.user_attrs_[seriesName][name]) != 'undefined') { - return this.user_attrs_[seriesName][name]; - } else if (this.user_attrs_ !== null && typeof(this.user_attrs_[name]) != 'undefined') { - return this.user_attrs_[name]; - } else if (this.attrs_ !== null && typeof(this.attrs_[name]) != 'undefined') { - return this.attrs_[name]; - } else { - return null; + + var sources = []; + sources.push(this.attrs_); + if (this.user_attrs_) { + sources.push(this.user_attrs_); + if (seriesName) { + if (this.user_attrs_.hasOwnProperty(seriesName)) { + sources.push(this.user_attrs_[seriesName]); + } + if (seriesName === this.highlightSet_ && + this.user_attrs_.hasOwnProperty('highlightSeriesOpts')) { + sources.push(this.user_attrs_['highlightSeriesOpts']); + } + } } + + var ret = null; + for (var i = sources.length - 1; i >= 0; --i) { + var source = sources[i]; + if (source.hasOwnProperty(name)) { + ret = source[name]; + break; + } + } + return ret; }; /** @@ -2194,12 +2278,12 @@ Dygraph.prototype.createInterface_ = function() { var dygraph = this; this.mouseMoveHandler = function(e) { - dygraph.mouseMove_(e); + dygraph.mouseMove_(e); }; Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); this.mouseOutHandler = function(e) { - dygraph.mouseOut_(e); + dygraph.mouseOut_(e); }; Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); @@ -2231,6 +2315,7 @@ Dygraph.prototype.destroy = function() { // remove mouse event handlers Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); + Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseUpHandler_); removeRecursive(this.maindiv_); var nullOut = function(obj) { @@ -2357,6 +2442,7 @@ Dygraph.prototype.createStatusMessage_ = function() { "top": "0px", "left": (this.width_ - divWidth - 2) + "px", "background": "white", + "lineHeight": "normal", "textAlign": "left", "overflow": "hidden"}; Dygraph.update(messagestyle, this.attr_('labelsDivStyles')); @@ -2364,7 +2450,11 @@ Dygraph.prototype.createStatusMessage_ = function() { div.className = "dygraph-legend"; for (var name in messagestyle) { if (messagestyle.hasOwnProperty(name)) { - div.style[name] = messagestyle[name]; + try { + div.style[name] = messagestyle[name]; + } catch (e) { + this.warn("You are using unsupported css properties for your browser in labelsDivStyles"); + } } } this.graphDiv.appendChild(div); @@ -2459,6 +2549,7 @@ Dygraph.prototype.createDragInterface_ = function() { prevEndX: null, // pixel coordinates prevEndY: null, // pixel coordinates prevDragDirection: null, + cancelNextDblclick: false, // see comment in dygraph-interaction-model.js // The value on the left side of the graph when a pan operation starts. initialLeftmostDate: null, @@ -2495,6 +2586,7 @@ Dygraph.prototype.createDragInterface_ = function() { context.py = Dygraph.findPosY(g.canvas_); context.dragStartX = g.dragGetX_(event, context); context.dragStartY = g.dragGetY_(event, context); + context.cancelNextDblclick = false; } }; @@ -2518,7 +2610,7 @@ Dygraph.prototype.createDragInterface_ = function() { // If the user releases the mouse button during a drag, but not over the // canvas, then it doesn't count as a zooming action. - Dygraph.addEvent(document, 'mouseup', function(event) { + this.mouseUpHandler_ = function(event) { if (context.isZooming || context.isPanning) { context.isZooming = false; context.dragStartX = null; @@ -2534,7 +2626,9 @@ Dygraph.prototype.createDragInterface_ = function() { delete self.axes_[i].dragValueRange; } } - }); + }; + + Dygraph.addEvent(document, 'mouseup', this.mouseUpHandler_); }; /** @@ -2822,74 +2916,197 @@ Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, ne }; /** - * When the mouse moves in the canvas, display information about a nearby data - * point and draw dots over those points in the data series. This function - * takes care of cleanup of previously-drawn dots. - * @param {Object} event The mousemove event from the browser. - * @private + * Get the current graph's area object. + * + * Returns: {x, y, w, h} */ -Dygraph.prototype.mouseMove_ = function(event) { - // This prevents JS errors when mousing over the canvas before data loads. - var points = this.layout_.points; - if (points === undefined) return; +Dygraph.prototype.getArea = function() { + return this.plotter_.area; +}; +/** + * Convert a mouse event to DOM coordinates relative to the graph origin. + * + * Returns a two-element array: [X, Y]. + */ +Dygraph.prototype.eventToDomCoords = function(event) { var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); + var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_); + return [canvasx, canvasy]; +}; - var lastx = -1; - var i; - - // Loop through all the points and find the date nearest to our current - // location. - var minDist = 1e+100; +/** + * Given a canvas X coordinate, find the closest row. + * @param {Number} domX graph-relative DOM X coordinate + * Returns: row number, integer + * @private + */ +Dygraph.prototype.findClosestRow = function(domX) { + var minDistX = Infinity; var idx = -1; - for (i = 0; i < points.length; i++) { + var points = this.layout_.points; + var l = points.length; + for (var i = 0; i < l; i++) { var point = points[i]; - if (point === null) continue; - var dist = Math.abs(point.canvasx - canvasx); - if (dist > minDist) continue; - minDist = dist; - idx = i; + if (!Dygraph.isValidPoint(point, true)) continue; + var dist = Math.abs(point.canvasx - domX); + if (dist < minDistX) { + minDistX = dist; + idx = i; + } } - if (idx >= 0) lastx = points[idx].xval; + return this.idxToRow_(idx); +}; - // Extract the points we've selected - this.selPoints_ = []; - var l = points.length; - if (!this.attr_("stackedGraph")) { - for (i = 0; i < l; i++) { - if (points[i].xval == lastx) { - this.selPoints_.push(points[i]); +/** + * Given canvas X,Y coordinates, find the closest point. + * + * This finds the individual data point across all visible series + * that's closest to the supplied DOM coordinates using the standard + * Euclidean X,Y distance. + * + * @param {Number} domX graph-relative DOM X coordinate + * @param {Number} domY graph-relative DOM Y coordinate + * Returns: {row, seriesName, point} + * @private + */ +Dygraph.prototype.findClosestPoint = function(domX, domY) { + var minDist = Infinity; + var idx = -1; + var points = this.layout_.points; + var dist, dx, dy, point, closestPoint, closestSeries; + for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { + var first = this.layout_.setPointsOffsets[setIdx]; + var len = this.layout_.setPointsLengths[setIdx]; + for (var i = 0; i < len; ++i) { + var point = points[first + i]; + if (!Dygraph.isValidPoint(point)) continue; + dx = point.canvasx - domX; + dy = point.canvasy - domY; + dist = dx * dx + dy * dy; + if (dist < minDist) { + minDist = dist; + closestPoint = point; + closestSeries = setIdx; + idx = i; } } - } else { - // Need to 'unstack' points starting from the bottom - var cumulative_sum = 0; - for (i = l - 1; i >= 0; i--) { - if (points[i].xval == lastx) { - var p = {}; // Clone the point since we modify it - for (var k in points[i]) { - p[k] = points[i][k]; + } + var name = this.layout_.setNames[closestSeries]; + return { + row: idx + this.getLeftBoundary_(), + seriesName: name, + point: closestPoint + }; +}; + +/** + * Given canvas X,Y coordinates, find the touched area in a stacked graph. + * + * This first finds the X data point closest to the supplied DOM X coordinate, + * then finds the series which puts the Y coordinate on top of its filled area, + * using linear interpolation between adjacent point pairs. + * + * @param {Number} domX graph-relative DOM X coordinate + * @param {Number} domY graph-relative DOM Y coordinate + * Returns: {row, seriesName, point} + * @private + */ +Dygraph.prototype.findStackedPoint = function(domX, domY) { + var row = this.findClosestRow(domX); + var boundary = this.getLeftBoundary_(); + var rowIdx = row - boundary; + var points = this.layout_.points; + var closestPoint, closestSeries; + for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { + var first = this.layout_.setPointsOffsets[setIdx]; + var len = this.layout_.setPointsLengths[setIdx]; + if (rowIdx >= len) continue; + var p1 = points[first + rowIdx]; + if (!Dygraph.isValidPoint(p1)) continue; + var py = p1.canvasy; + if (domX > p1.canvasx && rowIdx + 1 < len) { + // interpolate series Y value using next point + var p2 = points[first + rowIdx + 1]; + if (Dygraph.isValidPoint(p2)) { + var dx = p2.canvasx - p1.canvasx; + if (dx > 0) { + var r = (domX - p1.canvasx) / dx; + py += r * (p2.canvasy - p1.canvasy); + } + } + } else if (domX < p1.canvasx && rowIdx > 0) { + // interpolate series Y value using previous point + var p0 = points[first + rowIdx - 1]; + if (Dygraph.isValidPoint(p0)) { + var dx = p1.canvasx - p0.canvasx; + if (dx > 0) { + var r = (p1.canvasx - domX) / dx; + py += r * (p0.canvasy - p1.canvasy); } - p.yval -= cumulative_sum; - cumulative_sum += p.yval; - this.selPoints_.push(p); } } - this.selPoints_.reverse(); + // Stop if the point (domX, py) is above this series' upper edge + if (setIdx == 0 || py < domY) { + closestPoint = p1; + closestSeries = setIdx; + } } + var name = this.layout_.setNames[closestSeries]; + return { + row: row, + seriesName: name, + point: closestPoint + }; +}; - if (this.attr_("highlightCallback")) { - var px = this.lastx_; - if (px !== null && lastx != px) { - // only fire if the selected point has changed. - this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx)); +/** + * When the mouse moves in the canvas, display information about a nearby data + * point and draw dots over those points in the data series. This function + * takes care of cleanup of previously-drawn dots. + * @param {Object} event The mousemove event from the browser. + * @private + */ +Dygraph.prototype.mouseMove_ = function(event) { + // This prevents JS errors when mousing over the canvas before data loads. + var points = this.layout_.points; + if (points === undefined) return; + + var canvasCoords = this.eventToDomCoords(event); + var canvasx = canvasCoords[0]; + var canvasy = canvasCoords[1]; + + var highlightSeriesOpts = this.attr_("highlightSeriesOpts"); + var selectionChanged = false; + if (highlightSeriesOpts) { + var closest; + if (this.attr_("stackedGraph")) { + closest = this.findStackedPoint(canvasx, canvasy); + } else { + closest = this.findClosestPoint(canvasx, canvasy); } + selectionChanged = this.setSelection(closest.row, closest.seriesName); + } else { + var idx = this.findClosestRow(canvasx); + selectionChanged = this.setSelection(idx); } - // Save last x position for callbacks. - this.lastx_ = lastx; + var callback = this.attr_("highlightCallback"); + if (callback && selectionChanged) { + callback(event, this.lastx_, this.selPoints_, this.lastRow_, this.highlightSet_); + } +}; - this.updateSelection_(); +/** + * Fetch left offset from first defined boundaryIds record (see bug #236). + */ +Dygraph.prototype.getLeftBoundary_ = function() { + for (var i = 0; i < this.boundaryIds_.length; i++) { + if (this.boundaryIds_[i] !== undefined) { + return this.boundaryIds_[i][0]; + } + } + return 0; }; /** @@ -2901,19 +3118,11 @@ Dygraph.prototype.mouseMove_ = function(event) { Dygraph.prototype.idxToRow_ = function(idx) { if (idx < 0) return -1; - // make sure that you get the boundaryIds record which is also defined (see bug #236) - var boundaryIdx = -1; - for (var i = 0; i < this.boundaryIds_.length; i++) { - if (this.boundaryIds_[i] !== undefined) { - boundaryIdx = i; - break; - } - } - if (boundaryIdx < 0) return -1; + var boundary = this.getLeftBoundary_(); for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { var set = this.layout_.datasets[setIdx]; if (idx < set.length) { - return this.boundaryIds_[boundaryIdx][0] + idx; + return boundary + idx; } idx -= set.length; } @@ -3020,7 +3229,7 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) { if (html !== '') html += (sepLines ? '
' : ' '); strokePattern = this.attr_("strokePattern", labels[i]); dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth); - html += "" + dash + + html += "" + dash + " " + labels[i] + ""; } return html; @@ -3048,9 +3257,10 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) { c = this.plotter_.colors[pt.name]; var yval = fmtFunc(pt.yval, yOptView, pt.name, this); + var cls = (pt.name == this.highlightSet_) ? " class='highlight'" : ""; // TODO(danvk): use a template string here and make it an attribute. - html += " " + pt.name + - ":" + yval; + html += "" + " " + pt.name + + ":" + yval + ""; } return html; }; @@ -3065,6 +3275,8 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) { */ Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { var labelsDiv = this.attr_("labelsDiv"); + if (!labelsDiv) return; + var sizeSpan = document.createElement('span'); // Calculates the width of 1em in pixels for the legend. sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;'); @@ -3082,16 +3294,68 @@ Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { } }; +Dygraph.prototype.animateSelection_ = function(direction) { + var totalSteps = 10; + var millis = 30; + if (this.fadeLevel === undefined) this.fadeLevel = 0; + if (this.animateId === undefined) this.animateId = 0; + var start = this.fadeLevel; + var steps = direction < 0 ? start : totalSteps - start; + if (steps <= 0) { + if (this.fadeLevel) { + this.updateSelection_(1.0); + } + return; + } + + var thisId = ++this.animateId; + var that = this; + Dygraph.repeatAndCleanup( + function(n) { + // ignore simultaneous animations + if (that.animateId != thisId) return; + + that.fadeLevel += direction; + if (that.fadeLevel === 0) { + that.clearSelection(); + } else { + that.updateSelection_(that.fadeLevel / totalSteps); + } + }, + steps, millis, function() {}); +}; + /** * Draw dots over the selectied points in the data series. This function * takes care of cleanup of previously-drawn dots. * @private */ -Dygraph.prototype.updateSelection_ = function() { +Dygraph.prototype.updateSelection_ = function(opt_animFraction) { // Clear the previously drawn vertical, if there is one var i; var ctx = this.canvas_ctx_; - if (this.previousVerticalX_ >= 0) { + if (this.attr_('highlightSeriesOpts')) { + ctx.clearRect(0, 0, this.width_, this.height_); + var alpha = 1.0 - this.attr_('highlightSeriesBackgroundAlpha'); + if (alpha) { + // Activating background fade includes an animation effect for a gradual + // fade. TODO(klausw): make this independently configurable if it causes + // issues? Use a shared preference to control animations? + var animateBackgroundFade = true; + if (animateBackgroundFade) { + if (opt_animFraction === undefined) { + // start a new animation + this.animateSelection_(1); + return; + } + alpha *= opt_animFraction; + } + ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')'; + ctx.fillRect(0, 0, this.width_, this.height_); + } + var setIdx = this.datasetIndexFromSetName_(this.highlightSet_); + this.plotter_._drawLine(ctx, setIdx); + } else if (this.previousVerticalX_ >= 0) { // Determine the maximum highlight circle size. var maxCircleSize = 0; var labels = this.attr_('labels'); @@ -3122,10 +3386,16 @@ Dygraph.prototype.updateSelection_ = function() { if (!Dygraph.isOK(pt.canvasy)) continue; var circleSize = this.attr_('highlightCircleSize', pt.name); - ctx.beginPath(); - ctx.fillStyle = this.plotter_.colors[pt.name]; - ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false); - ctx.fill(); + var callback = this.attr_("drawHighlightPointCallback", pt.name); + var color = this.plotter_.colors[pt.name]; + if (!callback) { + callback = Dygraph.Circles.DEFAULT; + } + ctx.lineWidth = this.attr_('strokeWidth', pt.name); + ctx.strokeStyle = color; + ctx.fillStyle = color; + callback(this.g, pt.name, ctx, canvasx, pt.canvasy, + color, circleSize); } ctx.restore(); @@ -3139,17 +3409,22 @@ Dygraph.prototype.updateSelection_ = function() { * using getSelection(). * @param { Integer } row number that should be highlighted (i.e. appear with * hover dots on the chart). Set to false to clear any selection. + * @param { seriesName } optional series name to highlight that series with the + * the highlightSeriesOpts setting. */ -Dygraph.prototype.setSelection = function(row) { +Dygraph.prototype.setSelection = function(row, opt_seriesName) { // Extract the points we've selected this.selPoints_ = []; var pos = 0; if (row !== false) { - row = row - this.boundaryIds_[0][0]; + row -= this.getLeftBoundary_(); } + var changed = false; if (row !== false && row >= 0) { + if (row != this.lastRow_) changed = true; + this.lastRow_ = row; for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { var set = this.layout_.datasets[setIdx]; if (row < set.length) { @@ -3159,19 +3434,30 @@ Dygraph.prototype.setSelection = function(row) { point = this.layout_.unstackPointAtIndex(pos+row); } - this.selPoints_.push(point); + if (!(point.yval === null)) this.selPoints_.push(point); } pos += set.length; } + } else { + if (this.lastRow_ >= 0) changed = true; + this.lastRow_ = -1; } if (this.selPoints_.length) { this.lastx_ = this.selPoints_[0].xval; - this.updateSelection_(); } else { - this.clearSelection(); + this.lastx_ = -1; } + if (opt_seriesName !== undefined) { + if (this.highlightSet_ !== opt_seriesName) changed = true; + this.highlightSet_ = opt_seriesName; + } + + if (changed) { + this.updateSelection_(undefined); + } + return changed; }; /** @@ -3195,10 +3481,17 @@ Dygraph.prototype.mouseOut_ = function(event) { */ Dygraph.prototype.clearSelection = function() { // Get rid of the overlay data + if (this.fadeLevel) { + this.animateSelection_(-1); + return; + } this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); + this.fadeLevel = 0; this.setLegendHTML_(); this.selPoints_ = []; this.lastx_ = -1; + this.lastRow_ = -1; + this.highlightSet_ = null; }; /** @@ -3213,12 +3506,16 @@ Dygraph.prototype.getSelection = function() { for (var row=0; row= 0; --k) { + // Use the first nonempty dataset to get X values. + if (!datasets[k]) continue; + for (j = 0; j < datasets[k].length; j++) { + var x = datasets[k][j][0]; + if (isNaN(cumulative_y[x])) { + // Set all Y values to NaN at that X value. + for (i = datasets.length - 1; i >= 0; i--) { + if (!datasets[i]) continue; + datasets[i][j][1] = NaN; + } + } + } + break; + } + } + return [ datasets, extremes, boundaryIds ]; }; @@ -3492,10 +3813,12 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) { if (labels.length > 0) { this.setIndexByName_[labels[0]] = 0; } + var dataIdx = 0; for (var i = 1; i < datasets.length; i++) { this.setIndexByName_[labels[i]] = i; if (!this.visibility()[i - 1]) continue; this.layout_.addDataset(labels[i], datasets[i]); + this.datasetIndex_[i] = dataIdx++; } this.computeYAxisRanges_(extremes); @@ -3818,24 +4141,19 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { * * @private */ -Dygraph.prototype.extractSeries_ = function(rawData, i, logScale, connectSeparatedPoints) { +Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) { var series = []; for (var j = 0; j < rawData.length; j++) { var x = rawData[j][0]; var point = rawData[j][i]; if (logScale) { // On the log scale, points less than zero do not exist. - // This will create a gap in the chart. Note that this ignores - // connectSeparatedPoints. + // This will create a gap in the chart. if (point <= 0) { point = null; } - series.push([x, point]); - } else { - if (point !== null || !connectSeparatedPoints) { - series.push([x, point]); - } } + series.push([x, point]); } return series; }; @@ -4010,7 +4328,7 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { // TODO(danvk): use Dygraph.numberValueFormatter here? /** @private (shut up, jsdoc!) */ this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } }; @@ -4249,7 +4567,7 @@ Dygraph.prototype.parseArray_ = function(data) { /** @private (shut up, jsdoc!) */ this.attrs_.axes.x.valueFormatter = function(x) { return x; }; this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; return data; } }; @@ -4289,7 +4607,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { } else if (indepType == 'number') { this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } else { this.error("only 'date', 'datetime' and 'number' types are supported for " + @@ -4681,6 +4999,15 @@ Dygraph.prototype.indexFromSetName = function(name) { }; /** + * Get the internal dataset index given its name. These are numbered starting from 0, + * and only count visible sets. + * @private + */ +Dygraph.prototype.datasetIndexFromSetName_ = function(name) { + return this.datasetIndex_[this.indexFromSetName(name)]; +}; + +/** * @private * Adds a default style for the annotation CSS classes to the document. This is * only executed when annotations are actually used. It is designed to only be @@ -5057,6 +5384,21 @@ Dygraph.isOK = function(x) { }; /** + * @private + * @param { Object } p The point to consider, valid points are {x, y} objects + * @param { Boolean } allowNaNY Treat point with y=NaN as valid + * @return { Boolean } Whether the point has numeric x and y. + */ +Dygraph.isValidPoint = function(p, allowNaNY) { + if (!p) return false; // null or undefined object + if (p.yval === null) return false; // missing point + if (p.x === null || p.x === undefined) return false; + if (p.y === null || p.y === undefined) return false; + if (isNaN(p.x) || (!allowNaNY && isNaN(p.y))) return false; + return true; +}; + +/** * Number formatting function which mimicks the behavior of %g in printf, i.e. * either exponential or fixed format (without trailing 0s) is used depending on * the length of the generated string. The advantage of this format is that @@ -5204,9 +5546,17 @@ Dygraph.dateParser = function(dateStr) { var dateStrSlashed; var d; - // Let the system try the format first. - d = Dygraph.dateStrToMillis(dateStr); - if (d && !isNaN(d)) return d; + // Let the system try the format first, with one caveat: + // YYYY-MM-DD[ HH:MM:SS] is interpreted as UTC by a variety of browsers. + // dygraphs displays dates in local time, so this will result in surprising + // inconsistencies. But if you specify "T" or "Z" (i.e. YYYY-MM-DDTHH:MM:SS), + // then you probably know what you're doing, so we'll let you go ahead. + // Issue: http://code.google.com/p/dygraphs/issues/detail?id=255 + if (dateStr.search("-") == -1 || + dateStr.search("T") != -1 || dateStr.search("Z") != -1) { + d = Dygraph.dateStrToMillis(dateStr); + if (d && !isNaN(d)) return d; + } if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12' dateStrSlashed = dateStr.replace("-", "/", "g"); @@ -5425,7 +5775,9 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) { 'clickCallback': true, 'digitsAfterDecimal': true, 'drawCallback': true, + 'drawHighlightPointCallback': true, 'drawPoints': true, + 'drawPointCallback': true, 'drawXGrid': true, 'drawYGrid': true, 'fillAlpha': true, @@ -5530,6 +5882,111 @@ Dygraph.compareArrays = function(array1, array2) { } return true; }; + +/** + * ctx: the canvas context + * sides: the number of sides in the shape. + * radius: the radius of the image. + * cx: center x coordate + * cy: center y coordinate + * rotationRadians: the shift of the initial angle, in radians. + * delta: the angle shift for each line. If missing, creates a regular + * polygon. + */ +Dygraph.regularShape_ = function( + ctx, sides, radius, cx, cy, rotationRadians, delta) { + rotationRadians = rotationRadians ? rotationRadians : 0; + delta = delta ? delta : Math.PI * 2 / sides; + + ctx.beginPath(); + var first = true; + var initialAngle = rotationRadians; + var angle = initialAngle; + + var computeCoordinates = function() { + var x = cx + (Math.sin(angle) * radius); + var y = cy + (-Math.cos(angle) * radius); + return [x, y]; + }; + + var initialCoordinates = computeCoordinates(); + var x = initialCoordinates[0]; + var y = initialCoordinates[1]; + ctx.moveTo(x, y); + + for (var idx = 0; idx < sides; idx++) { + angle = (idx == sides - 1) ? initialAngle : (angle + delta); + var coords = computeCoordinates(); + ctx.lineTo(coords[0], coords[1]); + } + ctx.fill(); + ctx.stroke(); +} + +Dygraph.shapeFunction_ = function(sides, rotationRadians, delta) { + return function(g, name, ctx, cx, cy, color, radius) { + ctx.strokeStyle = color; + ctx.fillStyle = "white"; + Dygraph.regularShape_(ctx, sides, radius, cx, cy, rotationRadians, delta); + }; +}; + +Dygraph.DrawPolygon_ = function(sides, rotationRadians, ctx, cx, cy, color, radius, delta) { + new Dygraph.RegularShape_(sides, rotationRadians, delta).draw(ctx, cx, cy, radius); +} + +Dygraph.Circles = { + DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) { + ctx.beginPath(); + ctx.fillStyle = color; + ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false); + ctx.fill(); + }, + TRIANGLE : Dygraph.shapeFunction_(3), + SQUARE : Dygraph.shapeFunction_(4, Math.PI / 4), + DIAMOND : Dygraph.shapeFunction_(4), + PENTAGON : Dygraph.shapeFunction_(5), + HEXAGON : Dygraph.shapeFunction_(6), + CIRCLE : function(g, name, ctx, cx, cy, color, radius) { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.fillStyle = "white"; + ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.stroke(); + }, + STAR : Dygraph.shapeFunction_(5, 0, 4 * Math.PI / 5), + PLUS : function(g, name, ctx, cx, cy, color, radius) { + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(cx + radius, cy); + ctx.lineTo(cx - radius, cy); + ctx.closePath(); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(cx, cy + radius); + ctx.lineTo(cx, cy - radius); + ctx.closePath(); + ctx.stroke(); + }, + EX : function(g, name, ctx, cx, cy, color, radius) { + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(cx + radius, cy + radius); + ctx.lineTo(cx - radius, cy - radius); + ctx.closePath(); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(cx + radius, cy - radius); + ctx.lineTo(cx - radius, cy + radius); + ctx.closePath(); + ctx.stroke(); + } +}; /** * @license * Copyright 2011 Dan Vanderkam (danvdk@gmail.com) @@ -5680,20 +6137,26 @@ Dygraph.Interaction.startPan = function(event, g, context) { // Record the range of each y-axis at the start of the drag. // If any axis has a valueRange or valueWindow, then we want a 2D pan. + // We can't store data directly in g.axes_, because it does not belong to us + // and could change out from under us during a pan (say if there's a data + // update). context.is2DPan = false; + context.axes = []; for (i = 0; i < g.axes_.length; i++) { axis = g.axes_[i]; + var axis_data = {}; var yRange = g.yAxisRange(i); // TODO(konigsberg): These values should be in |context|. // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale. if (axis.logscale) { - axis.initialTopValue = Dygraph.log10(yRange[1]); - axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]); + axis_data.initialTopValue = Dygraph.log10(yRange[1]); + axis_data.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]); } else { - axis.initialTopValue = yRange[1]; - axis.dragValueRange = yRange[1] - yRange[0]; + axis_data.initialTopValue = yRange[1]; + axis_data.dragValueRange = yRange[1] - yRange[0]; } - axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1); + axis_data.unitsPerPixel = axis_data.dragValueRange / (g.plotter_.area.h - 1); + context.axes.push(axis_data); // While calculating axes, set 2dpan. if (axis.valueWindow || axis.valueRange) context.is2DPan = true; @@ -5738,23 +6201,24 @@ Dygraph.Interaction.movePan = function(event, g, context) { // Adjust each axis appropriately. for (var i = 0; i < g.axes_.length; i++) { var axis = g.axes_[i]; + var axis_data = context.axes[i]; var pixelsDragged = context.dragEndY - context.dragStartY; - var unitsDragged = pixelsDragged * axis.unitsPerPixel; + var unitsDragged = pixelsDragged * axis_data.unitsPerPixel; var boundedValue = context.boundedValues ? context.boundedValues[i] : null; // In log scale, maxValue and minValue are the logs of those values. - var maxValue = axis.initialTopValue + unitsDragged; + var maxValue = axis_data.initialTopValue + unitsDragged; if (boundedValue) { maxValue = Math.min(maxValue, boundedValue[1]); } - var minValue = maxValue - axis.dragValueRange; + var minValue = maxValue - axis_data.dragValueRange; if (boundedValue) { if (minValue < boundedValue[0]) { // Adjust maxValue, and recompute minValue. maxValue = maxValue - (minValue - boundedValue[0]); - minValue = maxValue - axis.dragValueRange; + minValue = maxValue - axis_data.dragValueRange; } } if (axis.logscale) { @@ -5777,7 +6241,7 @@ Dygraph.Interaction.movePan = function(event, g, context) { * Custom interaction model builders can use it to provide the default * panning behavior. * - * @param { Event } event the event object which led to the startZoom call. + * @param { Event } event the event object which led to the endPan call. * @param { Dygraph} g The dygraph on which to act. * @param { Object} context The dragging context object (with * dragStartX/dragStartY/etc. properties). This function modifies the context. @@ -5794,8 +6258,6 @@ Dygraph.Interaction.endPan = function(event, g, context) { Dygraph.Interaction.treatMouseOpAsClick(g, event, context); } - // TODO(konigsberg): Clear the context data from the axis. - // (replace with "context = {}" ?) // TODO(konigsberg): mouseup should just delete the // context object, and mousedown should create a new one. context.isPanning = false; @@ -5805,6 +6267,7 @@ Dygraph.Interaction.endPan = function(event, g, context) { context.valueRange = null; context.boundedDates = null; context.boundedValues = null; + context.axes = null; }; /** @@ -5931,9 +6394,11 @@ Dygraph.Interaction.endZoom = function(event, g, context) { if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) { g.doZoomX_(Math.min(context.dragStartX, context.dragEndX), Math.max(context.dragStartX, context.dragEndX)); + context.cancelNextDblclick = true; } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) { g.doZoomY_(Math.min(context.dragStartY, context.dragEndY), Math.max(context.dragStartY, context.dragEndY)); + context.cancelNextDblclick = true; } else { g.clearZoomRect_(); } @@ -5942,6 +6407,154 @@ Dygraph.Interaction.endZoom = function(event, g, context) { }; /** + * @private + */ +Dygraph.Interaction.startTouch = function(event, g, context) { + event.preventDefault(); // touch browsers are all nice. + var touches = []; + for (var i = 0; i < event.touches.length; i++) { + var t = event.touches[i]; + // we dispense with 'dragGetX_' because all touchBrowsers support pageX + touches.push({ + pageX: t.pageX, + pageY: t.pageY, + dataX: g.toDataXCoord(t.pageX), + dataY: g.toDataYCoord(t.pageY) + // identifier: t.identifier + }); + } + context.initialTouches = touches; + + if (touches.length == 1) { + // This is just a swipe. + context.initialPinchCenter = touches[0]; + context.touchDirections = { x: true, y: true }; + } else if (touches.length == 2) { + // It's become a pinch! + + // only screen coordinates can be averaged (data coords could be log scale). + context.initialPinchCenter = { + pageX: 0.5 * (touches[0].pageX + touches[1].pageX), + pageY: 0.5 * (touches[0].pageY + touches[1].pageY), + + // TODO(danvk): remove + dataX: 0.5 * (touches[0].dataX + touches[1].dataX), + dataY: 0.5 * (touches[0].dataY + touches[1].dataY) + }; + + // Make pinches in a 45-degree swath around either axis 1-dimensional zooms. + var initialAngle = 180 / Math.PI * Math.atan2( + context.initialPinchCenter.pageY - touches[0].pageY, + touches[0].pageX - context.initialPinchCenter.pageX); + + // use symmetry to get it into the first quadrant. + initialAngle = Math.abs(initialAngle); + if (initialAngle > 90) initialAngle = 90 - initialAngle; + + context.touchDirections = { + x: (initialAngle < (90 - 45/2)), + y: (initialAngle > 45/2) + }; + } + + // save the full x & y ranges. + context.initialRange = { + x: g.xAxisRange(), + y: g.yAxisRange() + }; +}; + +/** + * @private + */ +Dygraph.Interaction.moveTouch = function(event, g, context) { + var i, touches = []; + for (i = 0; i < event.touches.length; i++) { + var t = event.touches[i]; + touches.push({ + pageX: t.pageX, + pageY: t.pageY + }); + } + var initialTouches = context.initialTouches; + + var c_now; + + // old and new centers. + var c_init = context.initialPinchCenter; + if (touches.length == 1) { + c_now = touches[0]; + } else { + c_now = { + pageX: 0.5 * (touches[0].pageX + touches[1].pageX), + pageY: 0.5 * (touches[0].pageY + touches[1].pageY) + }; + } + + // this is the "swipe" component + // we toss it out for now, but could use it in the future. + var swipe = { + pageX: c_now.pageX - c_init.pageX, + pageY: c_now.pageY - c_init.pageY + }; + var dataWidth = context.initialRange.x[1] - context.initialRange.x[0]; + var dataHeight = context.initialRange.y[0] - context.initialRange.y[1]; + swipe.dataX = (swipe.pageX / g.plotter_.area.w) * dataWidth; + swipe.dataY = (swipe.pageY / g.plotter_.area.h) * dataHeight; + var xScale, yScale; + + // The residual bits are usually split into scale & rotate bits, but we split + // them into x-scale and y-scale bits. + if (touches.length == 1) { + xScale = 1.0; + yScale = 1.0; + } else if (touches.length == 2) { + var initHalfWidth = (initialTouches[1].pageX - c_init.pageX); + xScale = (touches[1].pageX - c_now.pageX) / initHalfWidth; + + var initHalfHeight = (initialTouches[1].pageY - c_init.pageY); + yScale = (touches[1].pageY - c_now.pageY) / initHalfHeight; + } + + // Clip scaling to [1/8, 8] to prevent too much blowup. + xScale = Math.min(8, Math.max(0.125, xScale)); + yScale = Math.min(8, Math.max(0.125, yScale)); + + if (context.touchDirections.x) { + g.dateWindow_ = [ + c_init.dataX - swipe.dataX + (context.initialRange.x[0] - c_init.dataX) / xScale, + c_init.dataX - swipe.dataX + (context.initialRange.x[1] - c_init.dataX) / xScale + ]; + } + + if (context.touchDirections.y) { + for (i = 0; i < 1 /*g.axes_.length*/; i++) { + var axis = g.axes_[i]; + if (axis.logscale) { + // TODO(danvk): implement + } else { + axis.valueWindow = [ + c_init.dataY - swipe.dataY + (context.initialRange.y[0] - c_init.dataY) / yScale, + c_init.dataY - swipe.dataY + (context.initialRange.y[1] - c_init.dataY) / yScale + ]; + } + } + } + + g.drawGraph_(false); +}; + +/** + * @private + */ +Dygraph.Interaction.endTouch = function(event, g, context) { + if (event.touches.length != 0) { + // this is effectively a "reset" + Dygraph.Interaction.startTouch(event, g, context); + } +}; + +/** * Default interation model for dygraphs. You can refer to specific elements of * this when constructing your own interaction model, e.g.: * g.updateOptions( { @@ -5953,6 +6566,9 @@ Dygraph.Interaction.endZoom = function(event, g, context) { Dygraph.Interaction.defaultModel = { // Track the beginning of drag events mousedown: function(event, g, context) { + // Right-click should not initiate a zoom. + if (event.button && event.button == 2) return; + context.initializeMouseDown(event, g, context); if (event.altKey || event.shiftKey) { @@ -5979,6 +6595,16 @@ Dygraph.Interaction.defaultModel = { } }, + touchstart: function(event, g, context) { + Dygraph.Interaction.startTouch(event, g, context); + }, + touchmove: function(event, g, context) { + Dygraph.Interaction.moveTouch(event, g, context); + }, + touchend: function(event, g, context) { + Dygraph.Interaction.endTouch(event, g, context); + }, + // Temporarily cancel the dragging event when the mouse leaves the graph mouseout: function(event, g, context) { if (context.isZooming) { @@ -5989,6 +6615,10 @@ Dygraph.Interaction.defaultModel = { // Disable zooming out if panning. dblclick: function(event, g, context) { + if (context.cancelNextDblclick) { + context.cancelNextDblclick = false; + return; + } if (event.altKey || event.shiftKey) { return; } @@ -6760,6 +7390,14 @@ DygraphRangeSelector.prototype.getZoomHandleStatus_ = function() { /*global Dygraph:false */ "use strict"; +Dygraph.numericLinearTicks = function(a, b, pixels, opts, dygraph, vals) { + var nonLogscaleOpts = function(opt) { + if (opt === 'logscale') return false; + return opts(opt); + }; + return Dygraph.numericTicks(a, b, pixels, nonLogscaleOpts, dygraph, vals); +}; + Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) { var pixels_per_tick = opts('pixelsPerLabel'); var ticks = []; @@ -6860,12 +7498,12 @@ Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) { var k_labels = []; if (opts("labelsKMB")) { k = 1000; - k_labels = [ "K", "M", "B", "T" ]; + k_labels = [ "K", "M", "B", "T", "Q" ]; } if (opts("labelsKMG2")) { if (k) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!"); k = 1024; - k_labels = [ "k", "M", "G", "T" ]; + k_labels = [ "k", "M", "G", "T", "P", "E" ]; } var formatter = opts('axisLabelFormatter'); @@ -6880,8 +7518,8 @@ Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) { if (k_labels.length > 0) { // TODO(danvk): should this be integrated into the axisLabelFormatter? // Round up to an appropriate unit. - var n = k*k*k*k; - for (j = 3; j >= 0; j--, n /= k) { + var n = Math.pow(k, k_labels.length); + for (j = k_labels.length - 1; j >= 0; j--, n /= k) { if (absTickV >= n) { label = Dygraph.round_(tickV / n, opts('digitsAfterDecimal')) + k_labels[j]; diff --git a/static/vendor/dygraph-1.2dev.min.js b/static/vendor/dygraph-1.2dev.min.js index b9e5ecc..4ef6394 100644 --- a/static/vendor/dygraph-1.2dev.min.js +++ b/static/vendor/dygraph-1.2dev.min.js @@ -1 +1,11 @@ -"use strict";var DygraphLayout=function(a){this.dygraph_=a;this.datasets=[];this.setNames=[];this.annotations=[];this.yAxes_=null;this.xTicks_=null;this.yTicks_=null};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets.push(b);this.setNames.push(a)};DygraphLayout.prototype.getPlotArea=function(){return this.computePlotArea_()};DygraphLayout.prototype.computePlotArea_=function(){var a={x:0,y:0};if(this.attr_("drawYAxis")){a.x=this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize")}a.w=this.dygraph_.width_-a.x-this.attr_("rightGap");a.h=this.dygraph_.height_;if(this.attr_("drawXAxis")){if(this.attr_("xAxisHeight")){a.h-=this.attr_("xAxisHeight")}else{a.h-=this.attr_("axisLabelFontSize")+2*this.attr_("axisTickSize")}}if(this.dygraph_.numAxes()==2){a.w-=(this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize"))}else{if(this.dygraph_.numAxes()>2){this.dygraph_.error("Only two y-axes are supported at this time. (Trying to use "+this.dygraph_.numAxes()+")")}}if(this.attr_("title")){a.h-=this.attr_("titleHeight");a.y+=this.attr_("titleHeight")}if(this.attr_("xlabel")){a.h-=this.attr_("xLabelHeight")}if(this.attr_("ylabel")){}if(this.attr_("y2label")){}if(this.attr_("showRangeSelector")){a.h-=this.attr_("rangeSelectorHeight")+4}return a};DygraphLayout.prototype.setAnnotations=function(d){this.annotations=[];var e=this.attr_("xValueParser")||function(a){return a};for(var c=0;c1){var b=d[0][0];if(!this.minxval||bthis.maxxval){this.maxxval=a}}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!==0?1/this.xrange:1);for(var c=0;c=0)&&(f<=1)){this.xticks.push([f,b])}}this.yticks=[];for(d=0;d=0)&&(f<=1)){this.yticks.push([d,f,b])}}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!(this.attr_("errorBars")||this.attr_("customBars"))){return}var h=0;for(var a=0;a=0;e--){if(f.childNodes[e].className==g){f.removeChild(f.childNodes[e])}}var c=document.bgColor;var d=this.dygraph_.graphDiv;while(d!=document){var a=d.currentStyle.backgroundColor;if(a&&a!="transparent"){c=a;break}d=d.parentNode}function b(j){if(j.w===0||j.h===0){return}var i=document.createElement("div");i.className=g;i.style.backgroundColor=c;i.style.position="absolute";i.style.left=j.x+"px";i.style.top=j.y+"px";i.style.width=j.w+"px";i.style.height=j.h+"px";f.appendChild(i)}var h=this.area;b({x:0,y:0,w:h.x,h:this.height});b({x:h.x,y:0,w:this.width-h.x,h:h.y});b({x:h.x+h.w,y:0,w:this.width-h.x-h.w,h:this.height});b({x:h.x,y:h.y+h.h,w:this.width-h.x,h:this.height-h.h-h.y})};DygraphCanvasRenderer.prototype._renderAxis=function(){if(!this.attr_("drawXAxis")&&!this.attr_("drawYAxis")){return}function q(i){return Math.round(i)+0.5}function p(i){return Math.round(i)-0.5}var d=this.elementContext;var l,n,m,s,r;var a={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,color:this.attr_("axisLabelColor"),width:this.attr_("axisLabelWidth")+"px",lineHeight:"normal",overflow:"hidden"};var g=function(i,v,w){var x=document.createElement("div");for(var u in a){if(a.hasOwnProperty(u)){x.style[u]=a[u]}}var t=document.createElement("div");t.className="dygraph-axis-label dygraph-axis-label-"+v+(w?" dygraph-axis-label-"+w:"");t.innerHTML=i;x.appendChild(t);return x};d.save();d.strokeStyle=this.attr_("axisLineColor");d.lineWidth=this.attr_("axisLineWidth");if(this.attr_("drawYAxis")){if(this.layout.yticks&&this.layout.yticks.length>0){var b=this.dygraph_.numAxes();for(r=0;rthis.height){l.style.bottom="0px"}else{l.style.top=o+"px"}if(s[0]===0){l.style.left=(this.area.x-this.attr_("yAxisLabelWidth")-this.attr_("axisTickSize"))+"px";l.style.textAlign="right"}else{if(s[0]==1){l.style.left=(this.area.x+this.area.w+this.attr_("axisTickSize"))+"px";l.style.textAlign="left"}}l.style.width=this.attr_("yAxisLabelWidth")+"px";this.container.appendChild(l);this.ylabels.push(l)}var h=this.ylabels[0];var e=this.attr_("axisLabelFontSize");var k=parseInt(h.style.top,10)+e;if(k>this.height-e){h.style.top=(parseInt(h.style.top,10)-e/2)+"px"}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y));d.lineTo(q(this.area.x),p(this.area.y+this.area.h));d.closePath();d.stroke();if(this.dygraph_.numAxes()==2){d.beginPath();d.moveTo(p(this.area.x+this.area.w),p(this.area.y));d.lineTo(p(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}}if(this.attr_("drawXAxis")){if(this.layout.xticks){for(r=0;rthis.width){f=this.width-this.attr_("xAxisLabelWidth");l.style.textAlign="right"}if(f<0){f=0;l.style.textAlign="left"}l.style.left=f+"px";l.style.width=this.attr_("xAxisLabelWidth")+"px";this.container.appendChild(l);this.xlabels.push(l)}}d.beginPath();d.moveTo(q(this.area.x),p(this.area.y+this.area.h));d.lineTo(q(this.area.x+this.area.w),p(this.area.y+this.area.h));d.closePath();d.stroke()}d.restore()};DygraphCanvasRenderer.prototype._renderChartLabels=function(){var d,a;if(this.attr_("title")){d=document.createElement("div");d.style.position="absolute";d.style.top="0px";d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("titleHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("titleHeight")-8)+"px";d.style.fontWeight="bold";a=document.createElement("div");a.className="dygraph-label dygraph-title";a.innerHTML=this.attr_("title");d.appendChild(a);this.container.appendChild(d);this.chartLabels.title=d}if(this.attr_("xlabel")){d=document.createElement("div");d.style.position="absolute";d.style.bottom=0;d.style.left=this.area.x+"px";d.style.width=this.area.w+"px";d.style.height=this.attr_("xLabelHeight")+"px";d.style.textAlign="center";d.style.fontSize=(this.attr_("xLabelHeight")-2)+"px";a=document.createElement("div");a.className="dygraph-label dygraph-xlabel";a.innerHTML=this.attr_("xlabel");d.appendChild(a);this.container.appendChild(d);this.chartLabels.xlabel=d}var c=this;function b(h,g,f){var i={left:0,top:c.area.y,width:c.attr_("yLabelWidth"),height:c.area.h};d=document.createElement("div");d.style.position="absolute";if(h==1){d.style.left=i.left}else{d.style.right=i.left}d.style.top=i.top+"px";d.style.width=i.width+"px";d.style.height=i.height+"px";d.style.fontSize=(c.attr_("yLabelWidth")-2)+"px";var e=document.createElement("div");e.style.position="absolute";e.style.width=i.height+"px";e.style.height=i.width+"px";e.style.top=(i.height/2-i.width/2)+"px";e.style.left=(i.width/2-i.height/2)+"px";e.style.textAlign="center";var j="rotate("+(h==1?"-":"")+"90deg)";e.style.transform=j;e.style.WebkitTransform=j;e.style.MozTransform=j;e.style.OTransform=j;e.style.msTransform=j;if(typeof(document.documentMode)!=="undefined"&&document.documentMode<9){e.style.filter="progid:DXImageTransform.Microsoft.BasicImage(rotation="+(h==1?"3":"1")+")";e.style.left="0px";e.style.top="0px"}a=document.createElement("div");a.className=g;a.innerHTML=f;e.appendChild(a);d.appendChild(e);return d}var d;if(this.attr_("ylabel")){d=b(1,"dygraph-label dygraph-ylabel",this.attr_("ylabel"));this.container.appendChild(d);this.chartLabels.ylabel=d}if(this.attr_("y2label")&&this.dygraph_.numAxes()==2){d=b(2,"dygraph-label dygraph-y2label",this.attr_("y2label"));this.container.appendChild(d);this.chartLabels.y2label=d}};DygraphCanvasRenderer.prototype._renderAnnotations=function(){var h={position:"absolute",fontSize:this.attr_("axisLabelFontSize")+"px",zIndex:10,overflow:"hidden"};var j=function(i,q,r,a){return function(s){var p=r.annotation;if(p.hasOwnProperty(i)){p[i](p,r,a.dygraph_,s)}else{if(a.dygraph_.attr_(q)){a.dygraph_.attr_(q)(p,r,a.dygraph_,s)}}}};var m=this.layout.annotated_points;for(var g=0;gthis.area.x+this.area.w){continue}var k=e.annotation;var l=6;if(k.hasOwnProperty("tickHeight")){l=k.tickHeight}var c=document.createElement("div");for(var b in h){if(h.hasOwnProperty(b)){c.style[b]=h[b]}}if(!k.hasOwnProperty("icon")){c.className="dygraphDefaultAnnotation"}if(k.hasOwnProperty("cssClass")){c.className+=" "+k.cssClass}var d=k.hasOwnProperty("width")?k.width:16;var n=k.hasOwnProperty("height")?k.height:16;if(k.hasOwnProperty("icon")){var f=document.createElement("img");f.src=k.icon;f.width=d;f.height=n;c.appendChild(f)}else{if(e.annotation.hasOwnProperty("shortText")){c.appendChild(document.createTextNode(e.annotation.shortText))}}c.style.left=(e.canvasx-d/2)+"px";if(k.attachAtBottom){c.style.top=(this.area.h-n-l)+"px"}else{c.style.top=(e.canvasy-n-l)+"px"}c.style.width=d+"px";c.style.height=n+"px";c.title=e.annotation.text;c.style.color=this.colors[e.name];c.style.borderColor=this.colors[e.name];k.div=c;Dygraph.addEvent(c,"click",j("clickHandler","annotationClickHandler",e,this));Dygraph.addEvent(c,"mouseover",j("mouseOverHandler","annotationMouseOverHandler",e,this));Dygraph.addEvent(c,"mouseout",j("mouseOutHandler","annotationMouseOutHandler",e,this));Dygraph.addEvent(c,"dblclick",j("dblClickHandler","annotationDblClickHandler",e,this));this.container.appendChild(c);this.annotations.push(c);var o=this.elementContext;o.strokeStyle=this.colors[e.name];o.beginPath();if(!k.attachAtBottom){o.moveTo(e.canvasx,e.canvasy);o.lineTo(e.canvasx,e.canvasy-2-l)}else{o.moveTo(e.canvasx,this.area.h);o.lineTo(e.canvasx,this.area.h-2-l)}o.closePath();o.stroke()}};DygraphCanvasRenderer.prototype._renderLineChart=function(){var F=function(i){return(i===null||isNaN(i))};var L=this.elementContext;var l=this.attr_("fillAlpha");var B=this.attr_("errorBars")||this.attr_("customBars");var k=this.attr_("fillGraph");var v=this.attr_("stackedGraph");var u=this.attr_("stepPlot");var z=this.layout.points;var y=z.length;var t,J,H,b,a,f,s,E,m,C,h,d,q;var D=this.layout.setNames;var w=D.length;this.colors={};for(J=0;J=0;J--){E=D[J];s=this.colors[E];q=this.dygraph_.axisPropertiesForSeries(E);var e=1+q.minyval*q.yscale;if(e<0){e=0}else{if(e>1){e=1}}e=this.area.h*e+this.area.y;p.save();b=NaN;f=[-1,-1];d=q.yscale;h=new RGBColor(s);C="rgba("+h.r+","+h.g+","+h.b+","+l+")";p.fillStyle=C;p.beginPath();for(H=0;Hi){d=f[c];if(this._dashedLineToHistory[1]){i+=this._dashedLineToHistory[1]}else{i+=d}if(i>e){this._dashedLineToHistory=[c,i-e];i=e}else{this._dashedLineToHistory=[(c+1)%f.length,0]}if(c%2===0){j.lineTo(i,0)}else{j.moveTo(i,0)}c=(c+1)%f.length}j.restore()};"use strict";var Dygraph=function(c,b,a){if(arguments.length>0){if(arguments.length==4){this.warn("Using deprecated four-argument dygraph constructor");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME="Dygraph";Dygraph.VERSION="1.2dev";Dygraph.__repr__=function(){return"["+this.NAME+" "+this.VERSION+"]"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.ANIMATION_STEPS=10;Dygraph.ANIMATION_DURATION=200;Dygraph.numberValueFormatter=function(a,e,h,d){var b=e("sigFigs");if(b!==null){return Dygraph.floatFormat(a,b)}var f=e("digitsAfterDecimal");var c=e("maxNumberWidth");if(a!==0&&(Math.abs(a)>=Math.pow(10,c)||Math.abs(a)=Dygraph.DECADAL){return b.strftime("%Y")}else{if(c>=Dygraph.MONTHLY){return b.strftime("%b %y")}else{var a=b.getHours()*3600+b.getMinutes()*60+b.getSeconds()+b.getMilliseconds();if(a===0||c>=Dygraph.DAILY){return new Date(b.getTime()+3600*1000).strftime("%d%b")}else{return Dygraph.hmsString_(b.getTime())}}}};Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsShowZeroValues:true,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,digitsAfterDecimal:2,maxNumberWidth:6,sigFigs:null,strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,rightGap:5,showRoller:false,xValueParser:Dygraph.dateParser,delimiter:",",sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true,legend:"onmouseover",stepPlot:false,avoidMinZero:false,titleHeight:28,xLabelHeight:18,yLabelWidth:18,drawXAxis:true,drawYAxis:true,axisLineColor:"black",axisLineWidth:0.3,gridLineWidth:0.3,axisLabelColor:"black",axisLabelFont:"Arial",axisLabelWidth:50,drawYGrid:true,drawXGrid:true,gridLineColor:"rgb(128,128,128)",interactionModel:null,animatedZooms:false,showRangeSelector:false,rangeSelectorHeight:40,rangeSelectorPlotStrokeColor:"#808FAB",rangeSelectorPlotFillColor:"#A7B1C4",axes:{x:{pixelsPerLabel:60,axisLabelFormatter:Dygraph.dateAxisFormatter,valueFormatter:Dygraph.dateString_,ticker:null},y:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null},y2:{pixelsPerLabel:30,valueFormatter:Dygraph.numberValueFormatter,axisLabelFormatter:Dygraph.numberAxisLabelFormatter,ticker:null}}};Dygraph.HORIZONTAL=1;Dygraph.VERTICAL=2;Dygraph.addedAnnotationCSS=false;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!==null){var a=["Date"];for(var c=0;c=this.axes_.length){return null}var b=this.axes_[a];return[b.computedValueRange[0],b.computedValueRange[1]]};Dygraph.prototype.yAxisRanges=function(){var a=[];for(var b=0;b0){return[this.rawData_[0][0],this.rawData_[this.numRows()-1][0]]}else{return[0,1]}};Dygraph.prototype.getValue=function(b,a){if(b<0||b>this.rawData_.length){return null}if(a<0||a>this.rawData_[b].length){return null}return this.rawData_[b][a]};Dygraph.prototype.createInterface_=function(){var a=this.maindiv_;this.graphDiv=document.createElement("div");this.graphDiv.style.width=this.width_+"px";this.graphDiv.style.height=this.height_+"px";a.appendChild(this.graphDiv);this.canvas_=Dygraph.createCanvas();this.canvas_.style.position="absolute";this.canvas_.width=this.width_;this.canvas_.height=this.height_;this.canvas_.style.width=this.width_+"px";this.canvas_.style.height=this.height_+"px";this.canvas_ctx_=Dygraph.getContext(this.canvas_);this.hidden_=this.createPlotKitCanvas_(this.canvas_);this.hidden_ctx_=Dygraph.getContext(this.hidden_);if(this.attr_("showRangeSelector")){this.rangeSelector_=new DygraphRangeSelector(this)}this.graphDiv.appendChild(this.hidden_);this.graphDiv.appendChild(this.canvas_);this.mouseEventElement_=this.createMouseEventElement_();this.layout_=new DygraphLayout(this);if(this.rangeSelector_){this.rangeSelector_.addToGraph(this.graphDiv,this.layout_)}var b=this;this.mouseMoveHandler=function(c){b.mouseMove_(c)};Dygraph.addEvent(this.mouseEventElement_,"mousemove",this.mouseMoveHandler);this.mouseOutHandler=function(c){b.mouseOut_(c)};Dygraph.addEvent(this.mouseEventElement_,"mouseout",this.mouseOutHandler);this.createStatusMessage_();this.createDragInterface_();this.resizeHandler=function(c){b.resize()};Dygraph.addEvent(window,"resize",this.resizeHandler)};Dygraph.prototype.destroy=function(){var a=function(c){while(c.hasChildNodes()){a(c.firstChild);c.removeChild(c.firstChild)}};Dygraph.removeEvent(this.mouseEventElement_,"mouseout",this.mouseOutHandler);Dygraph.removeEvent(this.mouseEventElement_,"mousemove",this.mouseMoveHandler);a(this.maindiv_);var b=function(c){for(var d in c){if(typeof(c[d])==="object"){c[d]=null}}};Dygraph.removeEvent(window,"resize",this.resizeHandler);this.resizeHandler=null;b(this.layout_);b(this.plotter_);b(this)};Dygraph.prototype.createPlotKitCanvas_=function(a){var b=Dygraph.createCanvas();b.style.position="absolute";b.style.top=a.style.top;b.style.left=a.style.left;b.width=this.width_;b.height=this.height_;b.style.width=this.width_+"px";b.style.height=this.height_+"px";return b};Dygraph.prototype.createMouseEventElement_=function(){if(this.isUsingExcanvas_){var a=document.createElement("div");a.style.position="absolute";a.style.backgroundColor="white";a.style.filter="alpha(opacity=0)";a.style.width=this.width_+"px";a.style.height=this.height_+"px";this.graphDiv.appendChild(a);return a}else{return this.canvas_}};Dygraph.prototype.setColors_=function(){var e=this.attr_("labels").length-1;this.colors_=[];var a=this.attr_("colors");var d;if(!a){var c=this.attr_("colorSaturation")||1;var b=this.attr_("colorValue")||0.5;var j=Math.ceil(e/2);for(d=1;d<=e;d++){if(!this.visibility()[d-1]){continue}var g=d%2?Math.ceil(d/2):(j+d/2);var f=(1*g/(1+e));this.colors_.push(Dygraph.hsvToRGB(f,c,b))}}else{for(d=0;dn){continue}n=h;o=f}if(o>=0){j=r[o].xval}this.selPoints_=[];var d=r.length;if(!this.attr_("stackedGraph")){for(f=0;f=0;f--){if(r[f].xval==j){var c={};for(var e in r[f]){c[e]=r[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}this.selPoints_.reverse()}if(this.attr_("highlightCallback")){var m=this.lastx_;if(m!==null&&j!=m){this.attr_("highlightCallback")(b,j,this.selPoints_,this.idxToRow_(o))}}this.lastx_=j;this.updateSelection_()};Dygraph.prototype.idxToRow_=function(a){if(a<0){return -1}var c=-1;for(var b=0;b'}else{for(f=0;f<=o.length;f++){c+=o[f%o.length]}g=Math.floor(n/(c-o[0]));if(g>1){for(f=0;f'}}}return h};Dygraph.prototype.generateLegendHTML_=function(k,f,b){var l,u,o,s,m,g;if(typeof(k)==="undefined"){if(this.attr_("legend")!="always"){return""}u=this.attr_("labelsSeparateLines");var r=this.attr_("labels");l="";for(o=1;o":" ")}g=this.attr_("strokePattern",r[o]);m=this.generateLegendDashHTML_(g,s,b);l+=""+m+" "+r[o]+""}return l}var t=this.optionsViewForAxis_("x");var h=t("valueFormatter");l=h(k,t,this.attr_("labels")[0],this)+":";var p=[];var d=this.numAxes();for(o=0;o"}var j=p[this.seriesToAxisMap_[n.name]];var q=j("valueFormatter");s=this.plotter_.colors[n.name];var a=q(n.yval,j,n.name,this);l+=" "+n.name+":"+a}return l};Dygraph.prototype.setLegendHTML_=function(b,e){var c=this.attr_("labelsDiv");var f=document.createElement("span");f.setAttribute("style","margin: 0; padding: 0 0 0 1em; border: 0;");c.appendChild(f);var a=f.offsetWidth;var d=this.generateLegendHTML_(b,e,a);if(c!==null){c.innerHTML=d}else{if(typeof(this.shown_legend_error_)=="undefined"){this.error("labelsDiv is set to something nonexistent; legend will not be shown.");this.shown_legend_error_=true}}};Dygraph.prototype.updateSelection_=function(){var d;var h=this.canvas_ctx_;if(this.previousVerticalX_>=0){var e=0;var f=this.attr_("labels");for(d=1;de){e=b}}var g=this.previousVerticalX_;h.clearRect(g-e-1,0,2*e+2,this.height_)}if(this.isUsingExcanvas_&&this.currentZoomRectArgs_){Dygraph.prototype.drawZoomRect_.apply(this,this.currentZoomRectArgs_)}if(this.selPoints_.length>0){if(this.attr_("showLabelsOnHighlight")){this.setLegendHTML_(this.lastx_,this.selPoints_)}var c=this.selPoints_[0].canvasx;h.save();for(d=0;d=0){for(var b=0;bg){a=g}if(ef){f=e}if(h===null||af){f=g}if(h===null||g=1;u--){if(!this.visibility()[u-1]){continue}var h=[];for(t=0;t=A&&d===null){d=r}if(h[r][0]<=f){z=r}}if(d===null){d=0}if(d>0){d--}if(z===null){z=h.length-1}if(zn[1]){n[1]=b[g]}if(b[g]0){this.setIndexByName_[g[0]]=0}for(var f=1;fc){c=a}}return 1+c};Dygraph.prototype.axisPropertiesForSeries=function(a){return this.axes_[this.seriesToAxisMap_[a]]};Dygraph.prototype.computeYAxisRanges_=function(a){var g=[],h;for(h in this.seriesToAxisMap_){if(!this.seriesToAxisMap_.hasOwnProperty(h)){continue}var p=this.seriesToAxisMap_[h];while(g.length<=p){g.push([])}g[p].push(h)}for(var u=0;u0){x=0}if(x==Infinity){x=0}if(w==-Infinity){w=1}var t=w-x;if(t===0){t=w}var d,z;if(b.logscale){d=w+0.1*t;z=x}else{d=w+0.1*t;z=x-0.1*t;if(!this.attr_("avoidMinZero")){if(z<0&&x>=0){z=0}if(d>0&&w<=0){d=0}}if(this.attr_("includeZero")){if(w<0){d=0}if(x>0){z=0}}}b.extremeRange=[z,d]}if(b.valueWindow){b.computedValueRange=[b.valueWindow[0],b.valueWindow[1]]}else{if(b.valueRange){b.computedValueRange=[b.valueRange[0],b.valueRange[1]]}else{b.computedValueRange=b.extremeRange}}var n=this.optionsViewForAxis_("y"+(u?"2":""));var y=n("ticker");if(u===0||b.independentTicks){b.ticks=y(b.computedValueRange[0],b.computedValueRange[1],this.height_,n,this)}else{var l=this.axes_[0];var e=l.ticks;var f=l.computedValueRange[1]-l.computedValueRange[0];var A=b.computedValueRange[1]-b.computedValueRange[0];var c=[];for(var r=0;r=0){k-=l[w-d][1][0];h-=l[w-d][1][1]}var A=l[w][0];var u=h?k/h:0;if(this.attr_("errorBars")){if(this.attr_("wilsonInterval")){if(h){var r=u<0?0:u,t=h;var z=s*Math.sqrt(r*(1-r)/t+s*s/(4*t*t));var a=1+s*s/h;E=(r+s*s/(2*h)-z)/a;o=(r+s*s/(2*h)+z)/a;b[w]=[A,[r*e,(r-E)*e,(o-r)*e]]}else{b[w]=[A,[0,0,0]]}}else{x=h?s*Math.sqrt(u*(1-u)/h):1;b[w]=[A,[e*u,e*x,e*x]]}}else{b[w]=[A,e*u]}}}else{if(this.attr_("customBars")){E=0;var B=0;o=0;var g=0;for(w=0;w=0){var q=l[w-d];if(q[1][1]!==null&&!isNaN(q[1][1])){E-=q[1][0];B-=q[1][1];o-=q[1][2];g-=1}}if(g){b[w]=[l[w][0],[1*B/g,1*(B-E)/g,1*(o-B)/g]]}else{b[w]=[l[w][0],[null,null,null]]}}}else{if(!this.attr_("errorBars")){if(d==1){return l}for(w=0;w0&&(b[c-1]!="e"&&b[c-1]!="E"))||b.indexOf("/")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>"19700101"&&b<"20371231"){a=true}}if(a){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{this.attrs_.xValueParser=function(d){return parseFloat(d)};this.attrs_.axes.x.valueFormatter=function(d){return d};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}};Dygraph.prototype.parseFloat_=function(a,c,b){var e=parseFloat(a);if(!isNaN(e)){return e}if(/^ *$/.test(a)){return null}if(/^ *nan *$/i.test(a)){return NaN}var d="Unable to parse '"+a+"' as a number";if(b!==null&&c!==null){d+=" on line "+(1+c)+" ('"+b+"') of CSV."}this.error(d);return null};Dygraph.prototype.parseCSV_=function(s){var r=[];var a=s.split("\n");var g,k;var p=this.attr_("delimiter");if(a[0].indexOf(p)==-1&&a[0].indexOf("\t")>=0){p="\t"}var b=0;if(!("labels" in this.user_attrs_)){b=1;this.attrs_.labels=a[0].split(p)}var o=0;var m;var q=false;var c=this.attr_("labels").length;var f=false;for(var l=b;l0&&h[0]0){j=String.fromCharCode(65+(i-1)%26)+j.toLowerCase();i=Math.floor((i-1)/26)}return j};var h=w.getNumberOfColumns();var g=w.getNumberOfRows();var f=w.getColumnType(0);if(f=="date"||f=="datetime"){this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.axes.x.valueFormatter=Dygraph.dateString_;this.attrs_.axes.x.ticker=Dygraph.dateTicker;this.attrs_.axes.x.axisLabelFormatter=Dygraph.dateAxisFormatter}else{if(f=="number"){this.attrs_.xValueParser=function(i){return parseFloat(i)};this.attrs_.axes.x.valueFormatter=function(i){return i};this.attrs_.axes.x.ticker=Dygraph.numericTicks;this.attrs_.axes.x.axisLabelFormatter=this.attrs_.axes.x.valueFormatter}else{this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '"+f+"')");return null}}var m=[];var t={};var s=false;var q,o;for(q=1;q0&&e[0]0){this.setAnnotations(a,true)}};Dygraph.prototype.start_=function(){var c=this.file_;if(typeof c=="function"){c=c()}if(Dygraph.isArrayLike(c)){this.rawData_=this.parseArray_(c);this.predraw_()}else{if(typeof c=="object"&&typeof c.getColumnRange=="function"){this.parseDataTable_(c);this.predraw_()}else{if(typeof c=="string"){if(c.indexOf("\n")>=0){this.loadedEvent_(c)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status===200||b.status===0){a.loadedEvent_(b.responseText)}}};b.open("GET",c,true);b.send(null)}}else{this.error("Unknown data format: "+(typeof c))}}}};Dygraph.prototype.updateOptions=function(e,b){if(typeof(b)=="undefined"){b=false}var d=e.file;var c=Dygraph.mapLegacyOptions_(e);if("rollPeriod" in c){this.rollPeriod_=c.rollPeriod}if("dateWindow" in c){this.dateWindow_=c.dateWindow;if(!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_x_=(c.dateWindow!==null)}}if("valueRange" in c&&!("isZoomedIgnoreProgrammaticZoom" in c)){this.zoomed_y_=(c.valueRange!==null)}var a=Dygraph.isPixelChangingOptionList(this.attr_("labels"),c);Dygraph.updateDeep(this.user_attrs_,c);if(d){this.file_=d;if(!b){this.start_()}}else{if(!b){if(a){this.predraw_()}else{this.renderGraph_(false,false)}}}};Dygraph.mapLegacyOptions_=function(c){var a={};for(var b in c){if(b=="file"){continue}if(c.hasOwnProperty(b)){a[b]=c[b]}}var e=function(g,f,h){if(!a.axes){a.axes={}}if(!a.axes[g]){a.axes[g]={}}a.axes[g][f]=h};var d=function(f,g,h){if(typeof(c[f])!="undefined"){e(g,h,c[f]);delete a[f]}};d("xValueFormatter","x","valueFormatter");d("pixelsPerXLabel","x","pixelsPerLabel");d("xAxisLabelFormatter","x","axisLabelFormatter");d("xTicker","x","ticker");d("yValueFormatter","y","valueFormatter");d("pixelsPerYLabel","y","pixelsPerLabel");d("yAxisLabelFormatter","y","axisLabelFormatter");d("yTicker","y","ticker");return a};Dygraph.prototype.resize=function(d,b){if(this.resize_lock){return}this.resize_lock=true;if((d===null)!=(b===null)){this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.");d=b=null}var a=this.width_;var c=this.height_;if(d){this.maindiv_.style.width=d+"px";this.maindiv_.style.height=b+"px";this.width_=d;this.height_=b}else{this.width_=this.maindiv_.clientWidth;this.height_=this.maindiv_.clientHeight}if(a!=this.width_||c!=this.height_){this.maindiv_.innerHTML="";this.roller_=null;this.attrs_.labelsDiv=null;this.createInterface_();if(this.annotations_.length){this.layout_.setAnnotations(this.annotations_)}this.predraw_()}this.resize_lock=false};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.predraw_()};Dygraph.prototype.visibility=function(){if(!this.attr_("visibility")){this.attrs_.visibility=[]}while(this.attr_("visibility").length=a.length){this.warn("invalid series number in setVisibility: "+b)}else{a[b]=c;this.predraw_()}};Dygraph.prototype.size=function(){return{width:this.width_,height:this.height_}};Dygraph.prototype.setAnnotations=function(b,a){Dygraph.addAnnotationRule();this.annotations_=b;this.layout_.setAnnotations(this.annotations_);if(!a){this.predraw_()}};Dygraph.prototype.annotations=function(){return this.annotations_};Dygraph.prototype.getLabels=function(a){return this.attr_("labels").slice()};Dygraph.prototype.indexFromSetName=function(a){return this.setIndexByName_[a]};Dygraph.addAnnotationRule=function(){if(Dygraph.addedAnnotationCSS){return}var f="border: 1px solid black; background-color: white; text-align: center;";var e=document.createElement("style");e.type="text/css";document.getElementsByTagName("head")[0].appendChild(e);for(var b=0;bb){return -1}if(i===null||i===undefined){i=0}var h=function(j){return j>=0&&ja){if(i>0){f=g-1;if(h(f)&&d[f]a){return g}}return Dygraph.binarySearch(a,d,i,g+1,b)}};Dygraph.dateParser=function(a){var b;var c;c=Dygraph.dateStrToMillis(a);if(c&&!isNaN(c)){return c}if(a.search("-")!=-1){b=a.replace("-","/","g");while(b.search("-")!=-1){b=b.replace("-","/")}c=Dygraph.dateStrToMillis(b)}else{if(a.length==8){b=a.substr(0,4)+"/"+a.substr(4,2)+"/"+a.substr(6,2);c=Dygraph.dateStrToMillis(b)}else{c=Dygraph.dateStrToMillis(a)}}if(!c||isNaN(c)){Dygraph.error("Couldn't parse "+a+" as a date")}return c};Dygraph.dateStrToMillis=function(a){return new Date(a).getTime()};Dygraph.update=function(b,c){if(typeof(c)!="undefined"&&c!==null){for(var a in c){if(c.hasOwnProperty(a)){b[a]=c[a]}}}return b};Dygraph.updateDeep=function(b,d){function c(e){return(typeof Node==="object"?e instanceof Node:typeof e==="object"&&typeof e.nodeType==="number"&&typeof e.nodeName==="string")}if(typeof(d)!="undefined"&&d!==null){for(var a in d){if(d.hasOwnProperty(a)){if(d[a]===null){b[a]=null}else{if(Dygraph.isArrayLike(d[a])){b[a]=d[a].slice()}else{if(c(d[a])){b[a]=d[a]}else{if(typeof(d[a])=="object"){if(typeof(b[a])!="object"){b[a]={}}Dygraph.updateDeep(b[a],d[a])}else{b[a]=d[a]}}}}}}}return b};Dygraph.isArrayLike=function(b){var a=typeof(b);if((a!="object"&&!(a=="function"&&typeof(b.item)=="function"))||b===null||typeof(b.length)!="number"||b.nodeType===3){return false}return true};Dygraph.isDateLike=function(a){if(typeof(a)!="object"||a===null||typeof(a.getTime)!="function"){return false}return true};Dygraph.clone=function(c){var b=[];for(var a=0;a=g){return}var h=d+(1+e)*f;setTimeout(function(){e++;b(e);if(e>=g-1){c()}else{a()}},h-new Date().getTime())})()};Dygraph.isPixelChangingOptionList=function(h,e){var d={annotationClickHandler:true,annotationDblClickHandler:true,annotationMouseOutHandler:true,annotationMouseOverHandler:true,axisLabelColor:true,axisLineColor:true,axisLineWidth:true,clickCallback:true,digitsAfterDecimal:true,drawCallback:true,drawPoints:true,drawXGrid:true,drawYGrid:true,fillAlpha:true,gridLineColor:true,gridLineWidth:true,hideOverlayOnMouseOut:true,highlightCallback:true,highlightCircleSize:true,interactionModel:true,isZoomedIgnoreProgrammaticZoom:true,labelsDiv:true,labelsDivStyles:true,labelsDivWidth:true,labelsKMB:true,labelsKMG2:true,labelsSeparateLines:true,labelsShowZeroValues:true,legend:true,maxNumberWidth:true,panEdgeFraction:true,pixelsPerYLabel:true,pointClickCallback:true,pointSize:true,rangeSelectorPlotFillColor:true,rangeSelectorPlotStrokeColor:true,showLabelsOnHighlight:true,showRoller:true,sigFigs:true,strokeWidth:true,underlayCallback:true,unhighlightCallback:true,xAxisLabelFormatter:true,xTicker:true,xValueFormatter:true,yAxisLabelFormatter:true,yValueFormatter:true,zoomCallback:true};var a=false;var b={};if(h){for(var f=1;fc.boundedDates[1]){h=h-(a-c.boundedDates[1]);a=h+c.dateRange}}k.dateWindow_=[h,a];if(c.is2DPan){for(var j=0;j=10&&a.dragDirection==Dygraph.HORIZONTAL){b.doZoomX_(Math.min(a.dragStartX,a.dragEndX),Math.max(a.dragStartX,a.dragEndX))}else{if(d>=10&&a.dragDirection==Dygraph.VERTICAL){b.doZoomY_(Math.min(a.dragStartY,a.dragEndY),Math.max(a.dragStartY,a.dragEndY))}else{b.clearZoomRect_()}}a.dragStartX=null;a.dragStartY=null};Dygraph.Interaction.defaultModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);if(c.altKey||c.shiftKey){Dygraph.startPan(c,b,a)}else{Dygraph.startZoom(c,b,a)}},mousemove:function(c,b,a){if(a.isZooming){Dygraph.moveZoom(c,b,a)}else{if(a.isPanning){Dygraph.movePan(c,b,a)}}},mouseup:function(c,b,a){if(a.isZooming){Dygraph.endZoom(c,b,a)}else{if(a.isPanning){Dygraph.endPan(c,b,a)}}},mouseout:function(c,b,a){if(a.isZooming){a.dragEndX=null;a.dragEndY=null}},dblclick:function(c,b,a){if(c.altKey||c.shiftKey){return}b.doUnzoom_()}};Dygraph.DEFAULT_ATTRS.interactionModel=Dygraph.Interaction.defaultModel;Dygraph.defaultInteractionModel=Dygraph.Interaction.defaultModel;Dygraph.endZoom=Dygraph.Interaction.endZoom;Dygraph.moveZoom=Dygraph.Interaction.moveZoom;Dygraph.startZoom=Dygraph.Interaction.startZoom;Dygraph.endPan=Dygraph.Interaction.endPan;Dygraph.movePan=Dygraph.Interaction.movePan;Dygraph.startPan=Dygraph.Interaction.startPan;Dygraph.Interaction.nonInteractiveModel_={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a)},mouseup:function(c,b,a){a.dragEndX=b.dragGetX_(c,a);a.dragEndY=b.dragGetY_(c,a);var e=Math.abs(a.dragEndX-a.dragStartX);var d=Math.abs(a.dragEndY-a.dragStartY);if(e<2&&d<2&&b.lastx_!==undefined&&b.lastx_!=-1){Dygraph.Interaction.treatMouseOpAsClick(b,c,a)}}};Dygraph.Interaction.dragIsPanInteractionModel={mousedown:function(c,b,a){a.initializeMouseDown(c,b,a);Dygraph.startPan(c,b,a)},mousemove:function(c,b,a){if(a.isPanning){Dygraph.movePan(c,b,a)}},mouseup:function(c,b,a){if(a.isPanning){Dygraph.endPan(c,b,a)}}};"use strict";var DygraphRangeSelector=function(a){this.isIE_=/MSIE/.test(navigator.userAgent)&&!window.opera;this.isUsingExcanvas_=a.isUsingExcanvas_;this.dygraph_=a;this.createCanvases_();if(this.isUsingExcanvas_){this.createIEPanOverlay_()}this.createZoomHandles_();this.initInteraction_()};DygraphRangeSelector.prototype.addToGraph=function(a,b){this.layout_=b;this.resize_();a.appendChild(this.bgcanvas_);a.appendChild(this.fgcanvas_);a.appendChild(this.leftZoomHandle_);a.appendChild(this.rightZoomHandle_)};DygraphRangeSelector.prototype.renderStaticLayer=function()