From: Paul Felix Date: Mon, 7 Jan 2013 18:41:56 +0000 (-0500) Subject: Range selector is now a plugin X-Git-Tag: v1.0.0~128 X-Git-Url: https://adrianiainlam.tk/git/?a=commitdiff_plain;h=0d216a600817f49892b92eefe48077197509ea6b;p=dygraphs.git Range selector is now a plugin Conflicts: dygraph-combined.js --- diff --git a/dygraph-dev.js b/dygraph-dev.js index b90796c..923bebb 100644 --- a/dygraph-dev.js +++ b/dygraph-dev.js @@ -27,7 +27,6 @@ "dygraph-utils.js", "dygraph-gviz.js", "dygraph-interaction-model.js", - "dygraph-range-selector.js", "dygraph-tickers.js", "plugins/base.js", "plugins/annotations.js", @@ -35,6 +34,7 @@ "plugins/chart-labels.js", "plugins/grid.js", "plugins/legend.js", + "plugins/range-selector.js", "plugins/install.js", "dygraph-options-reference.js" // Shouldn't be included in generate-combined.sh ]; diff --git a/dygraph-layout.js b/dygraph-layout.js index b81a12c..6b9adaf 100644 --- a/dygraph-layout.js +++ b/dygraph-layout.js @@ -53,12 +53,12 @@ DygraphLayout.prototype.addDataset = function(setname, set_xy) { }; DygraphLayout.prototype.getPlotArea = function() { - return this.computePlotArea_(); + return this.area_; }; // Compute the box which the chart should be drawn in. This is the canvas's // box, less space needed for axis and chart labels. -DygraphLayout.prototype.computePlotArea_ = function() { +DygraphLayout.prototype.computePlotArea = function() { var area = { // TODO(danvk): per-axis setting. x: 0, @@ -119,12 +119,7 @@ DygraphLayout.prototype.computePlotArea_ = function() { }; this.dygraph_.cascadeEvents_('layout', e); - // Add space for range selector, if needed. - if (this.attr_('showRangeSelector')) { - area.h -= this.attr_('rangeSelectorHeight') + 4; - } - - return area; + this.area_ = area; }; DygraphLayout.prototype.setAnnotations = function(ann) { diff --git a/dygraph-options-reference.js b/dygraph-options-reference.js index c7f4417..98cf22f 100644 --- a/dygraph-options-reference.js +++ b/dygraph-options-reference.js @@ -728,7 +728,7 @@ Dygraph.OPTIONS_REFERENCE = // "default": "false", "labels": ["Interactive Elements"], "type": "boolean", - "description": "Show the range selector widget. This option can only be specified at Dygraph creation time." + "description": "Show or hide the range selector widget." }, "rangeSelectorHeight": { "default": "40", diff --git a/dygraph-range-selector.js b/dygraph-range-selector.js deleted file mode 100644 index 0a1ae13..0000000 --- a/dygraph-range-selector.js +++ /dev/null @@ -1,758 +0,0 @@ -/* - * @license - * Copyright 2011 Paul Felix (paul.eric.felix@gmail.com) - * MIT-licensed (http://opensource.org/licenses/MIT) - */ - -/** - * @fileoverview This file contains the DygraphRangeSelector class used to provide - * a timeline range selector widget for dygraphs. - */ - -/*jshint globalstrict: true */ -/*global Dygraph:false */ -"use strict"; - -/** - * The DygraphRangeSelector class provides a timeline range selector widget. - * @param {Dygraph} dygraph The dygraph object - * @constructor - */ -var DygraphRangeSelector = function(dygraph) { - this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera; - this.isUsingExcanvas_ = dygraph.isUsingExcanvas_; - this.dygraph_ = dygraph; - this.hasTouchInterface_ = typeof(TouchEvent) != 'undefined'; - this.isMobileDevice_ = /mobile|android/gi.test(navigator.appVersion); - this.createCanvases_(); - if (this.isUsingExcanvas_) { - this.createIEPanOverlay_(); - } - this.createZoomHandles_(); - this.initInteraction_(); -}; - -/** - * Adds the range selector to the dygraph. - * @param {Object} graphDiv The container div for the range selector. - * @param {DygraphLayout} layout The DygraphLayout object for this graph. - */ -DygraphRangeSelector.prototype.addToGraph = function(graphDiv, layout) { - this.layout_ = layout; - this.resize_(); - graphDiv.appendChild(this.bgcanvas_); - graphDiv.appendChild(this.fgcanvas_); - graphDiv.appendChild(this.leftZoomHandle_); - graphDiv.appendChild(this.rightZoomHandle_); -}; - -/** - * Renders the static background portion of the range selector. - */ -DygraphRangeSelector.prototype.renderStaticLayer = function() { - this.resize_(); - this.drawStaticLayer_(); -}; - -/** - * Renders the interactive foreground portion of the range selector. - */ -DygraphRangeSelector.prototype.renderInteractiveLayer = function() { - if (this.isChangingRange_) { - return; - } - this.placeZoomHandles_(); - this.drawInteractiveLayer_(); -}; - -/** - * @private - * Resizes the range selector. - */ -DygraphRangeSelector.prototype.resize_ = function() { - function setElementRect(canvas, rect) { - canvas.style.top = rect.y + 'px'; - canvas.style.left = rect.x + 'px'; - canvas.width = rect.w; - canvas.height = rect.h; - canvas.style.width = canvas.width + 'px'; // for IE - canvas.style.height = canvas.height + 'px'; // for IE - } - - var plotArea = this.layout_.getPlotArea(); - var xAxisLabelHeight = this.attr_('xAxisHeight') || (this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize')); - this.canvasRect_ = { - x: plotArea.x, - y: plotArea.y + plotArea.h + xAxisLabelHeight + 4, - w: plotArea.w, - h: this.attr_('rangeSelectorHeight') - }; - - setElementRect(this.bgcanvas_, this.canvasRect_); - setElementRect(this.fgcanvas_, this.canvasRect_); -}; - -DygraphRangeSelector.prototype.attr_ = function(name) { - return this.dygraph_.attr_(name); -}; - -/** - * @private - * Creates the background and foreground canvases. - */ -DygraphRangeSelector.prototype.createCanvases_ = function() { - this.bgcanvas_ = Dygraph.createCanvas(); - this.bgcanvas_.className = 'dygraph-rangesel-bgcanvas'; - this.bgcanvas_.style.position = 'absolute'; - this.bgcanvas_.style.zIndex = 9; - this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_); - - this.fgcanvas_ = Dygraph.createCanvas(); - this.fgcanvas_.className = 'dygraph-rangesel-fgcanvas'; - this.fgcanvas_.style.position = 'absolute'; - this.fgcanvas_.style.zIndex = 9; - this.fgcanvas_.style.cursor = 'default'; - this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_); -}; - -/** - * @private - * Creates overlay divs for IE/Excanvas so that mouse events are handled properly. - */ -DygraphRangeSelector.prototype.createIEPanOverlay_ = function() { - this.iePanOverlay_ = document.createElement("div"); - this.iePanOverlay_.style.position = 'absolute'; - this.iePanOverlay_.style.backgroundColor = 'white'; - this.iePanOverlay_.style.filter = 'alpha(opacity=0)'; - this.iePanOverlay_.style.display = 'none'; - this.iePanOverlay_.style.cursor = 'move'; - this.fgcanvas_.appendChild(this.iePanOverlay_); -}; - -/** - * @private - * Creates the zoom handle elements. - */ -DygraphRangeSelector.prototype.createZoomHandles_ = function() { - var img = new Image(); - img.className = 'dygraph-rangesel-zoomhandle'; - img.style.position = 'absolute'; - img.style.zIndex = 10; - img.style.visibility = 'hidden'; // Initially hidden so they don't show up in the wrong place. - img.style.cursor = 'col-resize'; - - if (/MSIE 7/.test(navigator.userAgent)) { // IE7 doesn't support embedded src data. - img.width = 7; - img.height = 14; - img.style.backgroundColor = 'white'; - img.style.border = '1px solid #333333'; // Just show box in IE7. - } else { - img.width = 9; - img.height = 16; - img.src = 'data:image/png;base64,' + -'iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA' + -'zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv' + -'bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl' + -'6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s' + -'qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII='; - } - - if (this.isMobileDevice_) { - img.width *= 2; - img.height *= 2; - } - - this.leftZoomHandle_ = img; - this.rightZoomHandle_ = img.cloneNode(false); -}; - -/** - * @private - * Sets up the interaction for the range selector. - */ -DygraphRangeSelector.prototype.initInteraction_ = function() { - var self = this; - var topElem = this.isIE_ ? document : window; - var xLast = 0; - var handle = null; - var isZooming = false; - var isPanning = false; - var dynamic = !this.isMobileDevice_ && !this.isUsingExcanvas_; - - // We cover iframes during mouse interactions. See comments in - // dygraph-utils.js for more info on why this is a good idea. - var tarp = new Dygraph.IFrameTarp(); - - // functions, defined below. Defining them this way (rather than with - // "function foo() {...}" makes JSHint happy. - var toXDataWindow, onZoomStart, onZoom, onZoomEnd, doZoom, isMouseInPanZone, - onPanStart, onPan, onPanEnd, doPan, onCanvasMouseMove, applyBrowserZoomLevel; - - // Touch event functions - var onZoomHandleTouchEvent, onCanvasTouchEvent, addTouchEvents; - - toXDataWindow = function(zoomHandleStatus) { - var xDataLimits = self.dygraph_.xAxisExtremes(); - var fact = (xDataLimits[1] - xDataLimits[0])/self.canvasRect_.w; - var xDataMin = xDataLimits[0] + (zoomHandleStatus.leftHandlePos - self.canvasRect_.x)*fact; - var xDataMax = xDataLimits[0] + (zoomHandleStatus.rightHandlePos - self.canvasRect_.x)*fact; - return [xDataMin, xDataMax]; - }; - - applyBrowserZoomLevel = function(delX) { - var zoom = window.outerWidth/document.documentElement.clientWidth; - if (!isNaN(zoom)) { - return delX/zoom; - } else { - return delX; - } - }; - - onZoomStart = function(e) { - Dygraph.cancelEvent(e); - isZooming = true; - xLast = e.screenX; - handle = e.target ? e.target : e.srcElement; - self.dygraph_.addEvent(topElem, 'mousemove', onZoom); - self.dygraph_.addEvent(topElem, 'mouseup', onZoomEnd); - self.fgcanvas_.style.cursor = 'col-resize'; - tarp.cover(); - return true; - }; - - onZoom = function(e) { - if (!isZooming) { - return false; - } - Dygraph.cancelEvent(e); - var delX = e.screenX - xLast; - if (Math.abs(delX) < 4 || e.screenX === 0) { - // First iPad move event seems to have screenX = 0 - return true; - } - xLast = e.screenX; - delX = applyBrowserZoomLevel(delX); - - // Move handle. - var zoomHandleStatus = self.getZoomHandleStatus_(); - var newPos; - if (handle == self.leftZoomHandle_) { - newPos = zoomHandleStatus.leftHandlePos + delX; - newPos = Math.min(newPos, zoomHandleStatus.rightHandlePos - handle.width - 3); - newPos = Math.max(newPos, self.canvasRect_.x); - } else { - newPos = zoomHandleStatus.rightHandlePos + delX; - newPos = Math.min(newPos, self.canvasRect_.x + self.canvasRect_.w); - newPos = Math.max(newPos, zoomHandleStatus.leftHandlePos + handle.width + 3); - } - var halfHandleWidth = handle.width/2; - handle.style.left = (newPos - halfHandleWidth) + 'px'; - self.drawInteractiveLayer_(); - - // Zoom on the fly (if not using excanvas). - if (dynamic) { - doZoom(); - } - return true; - }; - - onZoomEnd = function(e) { - if (!isZooming) { - return false; - } - isZooming = false; - tarp.uncover(); - Dygraph.removeEvent(topElem, 'mousemove', onZoom); - Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd); - self.fgcanvas_.style.cursor = 'default'; - - // If using excanvas, Zoom now. - if (!dynamic) { - doZoom(); - } - return true; - }; - - doZoom = function() { - try { - var zoomHandleStatus = self.getZoomHandleStatus_(); - self.isChangingRange_ = true; - if (!zoomHandleStatus.isZoomed) { - self.dygraph_.resetZoom(); - } else { - var xDataWindow = toXDataWindow(zoomHandleStatus); - self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]); - } - } finally { - self.isChangingRange_ = false; - } - }; - - isMouseInPanZone = function(e) { - if (self.isUsingExcanvas_) { - return e.srcElement == self.iePanOverlay_; - } else { - var rect = self.leftZoomHandle_.getBoundingClientRect(); - var leftHandleClientX = rect.left + rect.width/2; - rect = self.rightZoomHandle_.getBoundingClientRect(); - var rightHandleClientX = rect.left + rect.width/2; - return (e.clientX > leftHandleClientX && e.clientX < rightHandleClientX); - } - }; - - onPanStart = function(e) { - if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) { - Dygraph.cancelEvent(e); - isPanning = true; - xLast = e.screenX; - self.dygraph_.addEvent(topElem, 'mousemove', onPan); - self.dygraph_.addEvent(topElem, 'mouseup', onPanEnd); - return true; - } - return false; - }; - - onPan = function(e) { - if (!isPanning) { - return false; - } - Dygraph.cancelEvent(e); - - var delX = e.screenX - xLast; - if (Math.abs(delX) < 4) { - return true; - } - xLast = e.screenX; - delX = applyBrowserZoomLevel(delX); - - // Move range view - var zoomHandleStatus = self.getZoomHandleStatus_(); - var leftHandlePos = zoomHandleStatus.leftHandlePos; - var rightHandlePos = zoomHandleStatus.rightHandlePos; - var rangeSize = rightHandlePos - leftHandlePos; - if (leftHandlePos + delX <= self.canvasRect_.x) { - leftHandlePos = self.canvasRect_.x; - rightHandlePos = leftHandlePos + rangeSize; - } else if (rightHandlePos + delX >= self.canvasRect_.x + self.canvasRect_.w) { - rightHandlePos = self.canvasRect_.x + self.canvasRect_.w; - leftHandlePos = rightHandlePos - rangeSize; - } else { - leftHandlePos += delX; - rightHandlePos += delX; - } - var halfHandleWidth = self.leftZoomHandle_.width/2; - self.leftZoomHandle_.style.left = (leftHandlePos - halfHandleWidth) + 'px'; - self.rightZoomHandle_.style.left = (rightHandlePos - halfHandleWidth) + 'px'; - self.drawInteractiveLayer_(); - - // Do pan on the fly (if not using excanvas). - if (dynamic) { - doPan(); - } - return true; - }; - - onPanEnd = function(e) { - if (!isPanning) { - return false; - } - isPanning = false; - Dygraph.removeEvent(topElem, 'mousemove', onPan); - Dygraph.removeEvent(topElem, 'mouseup', onPanEnd); - // If using excanvas, do pan now. - if (!dynamic) { - doPan(); - } - return true; - }; - - doPan = function() { - try { - self.isChangingRange_ = true; - self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_()); - self.dygraph_.drawGraph_(false); - } finally { - self.isChangingRange_ = false; - } - }; - - onCanvasMouseMove = function(e) { - if (isZooming || isPanning) { - return; - } - var cursor = isMouseInPanZone(e) ? 'move' : 'default'; - if (cursor != self.fgcanvas_.style.cursor) { - self.fgcanvas_.style.cursor = cursor; - } - }; - - onZoomHandleTouchEvent = function(e) { - if (e.type == 'touchstart' && e.targetTouches.length == 1) { - if (onZoomStart(e.targetTouches[0])) { - Dygraph.cancelEvent(e); - } - } else if (e.type == 'touchmove' && e.targetTouches.length == 1) { - if (onZoom(e.targetTouches[0])) { - Dygraph.cancelEvent(e); - } - } else { - onZoomEnd(e); - } - }; - - onCanvasTouchEvent = function(e) { - if (e.type == 'touchstart' && e.targetTouches.length == 1) { - if (onPanStart(e.targetTouches[0])) { - Dygraph.cancelEvent(e); - } - } else if (e.type == 'touchmove' && e.targetTouches.length == 1) { - if (onPan(e.targetTouches[0])) { - Dygraph.cancelEvent(e); - } - } else { - onPanEnd(e); - } - }; - - addTouchEvents = function(elem, fn) { - var types = ['touchstart', 'touchend', 'touchmove', 'touchcancel']; - for (var i = 0; i < types.length; i++) { - self.dygraph_.addEvent(elem, types[i], fn); - } - }; - - this.dygraph_.attrs_.interactionModel = - Dygraph.Interaction.dragIsPanInteractionModel; - this.dygraph_.attrs_.panEdgeFraction = 0.0001; - - var dragStartEvent = window.opera ? 'mousedown' : 'dragstart'; - this.dygraph_.addEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart); - this.dygraph_.addEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart); - - if (this.isUsingExcanvas_) { - this.dygraph_.addEvent(this.iePanOverlay_, 'mousedown', onPanStart); - } else { - this.dygraph_.addEvent(this.fgcanvas_, 'mousedown', onPanStart); - this.dygraph_.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove); - } - - // Touch events - if (this.hasTouchInterface_) { - addTouchEvents(this.leftZoomHandle_, onZoomHandleTouchEvent); - addTouchEvents(this.rightZoomHandle_, onZoomHandleTouchEvent); - addTouchEvents(this.fgcanvas_, onCanvasTouchEvent); - } -}; - -/** - * @private - * Draws the static layer in the background canvas. - */ -DygraphRangeSelector.prototype.drawStaticLayer_ = function() { - var ctx = this.bgcanvas_ctx_; - ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h); - try { - this.drawMiniPlot_(); - } catch(ex) { - Dygraph.warn(ex); - } - - var margin = 0.5; - this.bgcanvas_ctx_.lineWidth = 1; - ctx.strokeStyle = 'gray'; - ctx.beginPath(); - ctx.moveTo(margin, margin); - ctx.lineTo(margin, this.canvasRect_.h-margin); - ctx.lineTo(this.canvasRect_.w-margin, this.canvasRect_.h-margin); - ctx.lineTo(this.canvasRect_.w-margin, margin); - ctx.stroke(); -}; - - -/** - * @private - * Draws the mini plot in the background canvas. - */ -DygraphRangeSelector.prototype.drawMiniPlot_ = function() { - var fillStyle = this.attr_('rangeSelectorPlotFillColor'); - var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor'); - if (!fillStyle && !strokeStyle) { - return; - } - - var stepPlot = this.attr_('stepPlot'); - - var combinedSeriesData = this.computeCombinedSeriesAndLimits_(); - var yRange = combinedSeriesData.yMax - combinedSeriesData.yMin; - - // Draw the mini plot. - var ctx = this.bgcanvas_ctx_; - var margin = 0.5; - - var xExtremes = this.dygraph_.xAxisExtremes(); - var xRange = Math.max(xExtremes[1] - xExtremes[0], 1.e-30); - var xFact = (this.canvasRect_.w - margin)/xRange; - var yFact = (this.canvasRect_.h - margin)/yRange; - var canvasWidth = this.canvasRect_.w - margin; - var canvasHeight = this.canvasRect_.h - margin; - - var prevX = null, prevY = null; - - ctx.beginPath(); - ctx.moveTo(margin, canvasHeight); - for (var i = 0; i < combinedSeriesData.data.length; i++) { - var dataPoint = combinedSeriesData.data[i]; - var x = ((dataPoint[0] !== null) ? ((dataPoint[0] - xExtremes[0])*xFact) : NaN); - var y = ((dataPoint[1] !== null) ? (canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact) : NaN); - if (isFinite(x) && isFinite(y)) { - if(prevX === null) { - ctx.lineTo(x, canvasHeight); - } - else if (stepPlot) { - ctx.lineTo(x, prevY); - } - ctx.lineTo(x, y); - prevX = x; - prevY = y; - } - else { - if(prevX !== null) { - if (stepPlot) { - ctx.lineTo(x, prevY); - ctx.lineTo(x, canvasHeight); - } - else { - ctx.lineTo(prevX, canvasHeight); - } - } - prevX = prevY = null; - } - } - ctx.lineTo(canvasWidth, canvasHeight); - ctx.closePath(); - - if (fillStyle) { - var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight); - lingrad.addColorStop(0, 'white'); - lingrad.addColorStop(1, fillStyle); - this.bgcanvas_ctx_.fillStyle = lingrad; - ctx.fill(); - } - - if (strokeStyle) { - this.bgcanvas_ctx_.strokeStyle = strokeStyle; - this.bgcanvas_ctx_.lineWidth = 1.5; - ctx.stroke(); - } -}; - -/** - * @private - * Computes and returns the combinded series data along with min/max for the mini plot. - * @return {Object} An object containing combinded series array, ymin, ymax. - */ -DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() { - var data = this.dygraph_.rawData_; - var logscale = this.attr_('logscale'); - - // Create a combined series (average of all series values). - var combinedSeries = []; - var sum; - var count; - var mutipleValues; - var i, j, k; - var xVal, yVal; - - // Find out if data has multiple values per datapoint. - // Go to first data point that actually has values (see http://code.google.com/p/dygraphs/issues/detail?id=246) - for (i = 0; i < data.length; i++) { - if (data[i].length > 1 && data[i][1] !== null) { - mutipleValues = typeof data[i][1] != 'number'; - if (mutipleValues) { - sum = []; - count = []; - for (k = 0; k < data[i][1].length; k++) { - sum.push(0); - count.push(0); - } - } - break; - } - } - - for (i = 0; i < data.length; i++) { - var dataPoint = data[i]; - xVal = dataPoint[0]; - - if (mutipleValues) { - for (k = 0; k < sum.length; k++) { - sum[k] = count[k] = 0; - } - } else { - sum = count = 0; - } - - for (j = 1; j < dataPoint.length; j++) { - if (this.dygraph_.visibility()[j-1]) { - var y; - if (mutipleValues) { - for (k = 0; k < sum.length; k++) { - y = dataPoint[j][k]; - if (y === null || isNaN(y)) continue; - sum[k] += y; - count[k]++; - } - } else { - y = dataPoint[j]; - if (y === null || isNaN(y)) continue; - sum += y; - count++; - } - } - } - - if (mutipleValues) { - for (k = 0; k < sum.length; k++) { - sum[k] /= count[k]; - } - yVal = sum.slice(0); - } else { - yVal = sum/count; - } - - combinedSeries.push([xVal, yVal]); - } - - // Account for roll period, fractions. - combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_); - - if (typeof combinedSeries[0][1] != 'number') { - for (i = 0; i < combinedSeries.length; i++) { - yVal = combinedSeries[i][1]; - combinedSeries[i][1] = yVal[0]; - } - } - - // Compute the y range. - var yMin = Number.MAX_VALUE; - var yMax = -Number.MAX_VALUE; - for (i = 0; i < combinedSeries.length; i++) { - yVal = combinedSeries[i][1]; - if (yVal !== null && isFinite(yVal) && (!logscale || yVal > 0)) { - yMin = Math.min(yMin, yVal); - yMax = Math.max(yMax, yVal); - } - } - - // Convert Y data to log scale if needed. - // Also, expand the Y range to compress the mini plot a little. - var extraPercent = 0.25; - if (logscale) { - yMax = Dygraph.log10(yMax); - yMax += yMax*extraPercent; - yMin = Dygraph.log10(yMin); - for (i = 0; i < combinedSeries.length; i++) { - combinedSeries[i][1] = Dygraph.log10(combinedSeries[i][1]); - } - } else { - var yExtra; - var yRange = yMax - yMin; - if (yRange <= Number.MIN_VALUE) { - yExtra = yMax*extraPercent; - } else { - yExtra = yRange*extraPercent; - } - yMax += yExtra; - yMin -= yExtra; - } - - return {data: combinedSeries, yMin: yMin, yMax: yMax}; -}; - -/** - * @private - * Places the zoom handles in the proper position based on the current X data window. - */ -DygraphRangeSelector.prototype.placeZoomHandles_ = function() { - var xExtremes = this.dygraph_.xAxisExtremes(); - var xWindowLimits = this.dygraph_.xAxisRange(); - var xRange = xExtremes[1] - xExtremes[0]; - var leftPercent = Math.max(0, (xWindowLimits[0] - xExtremes[0])/xRange); - var rightPercent = Math.max(0, (xExtremes[1] - xWindowLimits[1])/xRange); - var leftCoord = this.canvasRect_.x + this.canvasRect_.w*leftPercent; - var rightCoord = this.canvasRect_.x + this.canvasRect_.w*(1 - rightPercent); - var handleTop = Math.max(this.canvasRect_.y, this.canvasRect_.y + (this.canvasRect_.h - this.leftZoomHandle_.height)/2); - var halfHandleWidth = this.leftZoomHandle_.width/2; - this.leftZoomHandle_.style.left = (leftCoord - halfHandleWidth) + 'px'; - this.leftZoomHandle_.style.top = handleTop + 'px'; - this.rightZoomHandle_.style.left = (rightCoord - halfHandleWidth) + 'px'; - this.rightZoomHandle_.style.top = this.leftZoomHandle_.style.top; - - this.leftZoomHandle_.style.visibility = 'visible'; - this.rightZoomHandle_.style.visibility = 'visible'; -}; - -/** - * @private - * Draws the interactive layer in the foreground canvas. - */ -DygraphRangeSelector.prototype.drawInteractiveLayer_ = function() { - var ctx = this.fgcanvas_ctx_; - ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h); - var margin = 1; - var width = this.canvasRect_.w - margin; - var height = this.canvasRect_.h - margin; - var zoomHandleStatus = this.getZoomHandleStatus_(); - - ctx.strokeStyle = 'black'; - if (!zoomHandleStatus.isZoomed) { - ctx.beginPath(); - ctx.moveTo(margin, margin); - ctx.lineTo(margin, height); - ctx.lineTo(width, height); - ctx.lineTo(width, margin); - ctx.stroke(); - if (this.iePanOverlay_) { - this.iePanOverlay_.style.display = 'none'; - } - } else { - var leftHandleCanvasPos = Math.max(margin, zoomHandleStatus.leftHandlePos - this.canvasRect_.x); - var rightHandleCanvasPos = Math.min(width, zoomHandleStatus.rightHandlePos - this.canvasRect_.x); - - ctx.fillStyle = 'rgba(240, 240, 240, 0.6)'; - ctx.fillRect(0, 0, leftHandleCanvasPos, this.canvasRect_.h); - ctx.fillRect(rightHandleCanvasPos, 0, this.canvasRect_.w - rightHandleCanvasPos, this.canvasRect_.h); - - ctx.beginPath(); - ctx.moveTo(margin, margin); - ctx.lineTo(leftHandleCanvasPos, margin); - ctx.lineTo(leftHandleCanvasPos, height); - ctx.lineTo(rightHandleCanvasPos, height); - ctx.lineTo(rightHandleCanvasPos, margin); - ctx.lineTo(width, margin); - ctx.stroke(); - - if (this.isUsingExcanvas_) { - this.iePanOverlay_.style.width = (rightHandleCanvasPos - leftHandleCanvasPos) + 'px'; - this.iePanOverlay_.style.left = leftHandleCanvasPos + 'px'; - this.iePanOverlay_.style.height = height + 'px'; - this.iePanOverlay_.style.display = 'inline'; - } - } -}; - -/** - * @private - * Returns the current zoom handle position information. - * @return {Object} The zoom handle status. - */ -DygraphRangeSelector.prototype.getZoomHandleStatus_ = function() { - var halfHandleWidth = this.leftZoomHandle_.width/2; - var leftHandlePos = parseInt(this.leftZoomHandle_.style.left, 10) + halfHandleWidth; - var rightHandlePos = parseInt(this.rightZoomHandle_.style.left, 10) + halfHandleWidth; - return { - leftHandlePos: leftHandlePos, - rightHandlePos: rightHandlePos, - isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w) - }; -}; diff --git a/dygraph.js b/dygraph.js index 77f9ab4..092f511 100644 --- a/dygraph.js +++ b/dygraph.js @@ -44,7 +44,7 @@ */ /*jshint globalstrict: true */ -/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */ +/*global DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */ "use strict"; /** @@ -415,12 +415,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // TODO(nikhilk): Add any other stackedGraph checks here. } - // These two options have a bad interaction. See issue 359. - if (attrs.showRangeSelector && attrs.animatedZooms) { - this.warn('You should not set animatedZooms=true when using the range selector.'); - attrs.animatedZooms = false; - } - // DEPRECATION WARNING: All option processing should be moved from // attrs_ and user_attrs_ to options_, which holds all this information. // @@ -949,12 +943,6 @@ Dygraph.prototype.createInterface_ = function() { this.hidden_ = this.createPlotKitCanvas_(this.canvas_); this.hidden_ctx_ = Dygraph.getContext(this.hidden_); - if (this.attr_('showRangeSelector')) { - // The range selector must be created here so that its canvases and contexts get created here. - // For some reason, if the canvases and contexts don't get created here, things don't work in IE. - this.rangeSelector_ = new DygraphRangeSelector(this); - } - // The interactive parts of the graph are drawn on top of the chart. this.graphDiv.appendChild(this.hidden_); this.graphDiv.appendChild(this.canvas_); @@ -963,11 +951,6 @@ Dygraph.prototype.createInterface_ = function() { // Create the grapher this.layout_ = new DygraphLayout(this); - if (this.rangeSelector_) { - // This needs to happen after the graph canvases are added to the div and the layout object is created. - this.rangeSelector_.addToGraph(this.graphDiv, this.layout_); - } - var dygraph = this; this.mouseMoveHandler = function(e) { @@ -2151,6 +2134,8 @@ Dygraph.prototype.extremeValues_ = function(series) { Dygraph.prototype.predraw_ = function() { var start = new Date(); + this.layout_.computePlotArea(); + // TODO(danvk): move more computations out of drawGraph_ and into here. this.computeYAxes_(); @@ -2170,10 +2155,6 @@ Dygraph.prototype.predraw_ = function() { this.cascadeEvents_('predraw'); - if (this.rangeSelector_) { - this.rangeSelector_.renderStaticLayer(); - } - // Convert the raw data (a 2D array) into the internal format and compute // rolling averages. this.rolledSeries_ = [null]; // x-axis is the first series and it's special @@ -2412,11 +2393,6 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width, this.canvas_.height); - // Generate a static legend before any particular point is selected. - - if (this.rangeSelector_) { - this.rangeSelector_.renderInteractiveLayer(); - } if (this.attr_("drawCallback") !== null) { this.attr_("drawCallback")(this, is_initial_draw); } @@ -2448,7 +2424,7 @@ Dygraph.prototype.computeYAxes_ = function() { // data computation as well as options storage. // Go through once and add all the axes. this.axes_ = []; - + for (axis = 0; axis < this.attributes_.numAxes(); axis++) { // Add a new axis, making a copy of its per-axis options. opts = { g : this }; diff --git a/generate-combined.sh b/generate-combined.sh index 40c8019..a79c1d0 100755 --- a/generate-combined.sh +++ b/generate-combined.sh @@ -17,7 +17,6 @@ dygraph.js \ dygraph-utils.js \ dygraph-gviz.js \ dygraph-interaction-model.js \ -dygraph-range-selector.js \ dygraph-tickers.js \ plugins/base.js \ plugins/annotations.js \ @@ -25,6 +24,7 @@ plugins/axes.js \ plugins/chart-labels.js \ plugins/grid.js \ plugins/legend.js \ +plugins/range-selector.js \ plugins/install.js \ | perl -ne 'print unless m,REMOVE_FOR_COMBINED,..m,/REMOVE_FOR_COMBINED,' \ > /tmp/dygraph.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index 756ca07..2b411e2 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -8,12 +8,12 @@ load: - dashed-canvas.js - dygraph-layout.js - dygraph-canvas.js + - dygraph-options.js - dygraph.js - dygraph-utils.js - dygraph-gviz.js - dygraph-interaction-model.js - dygraph-options-reference.js - - dygraph-range-selector.js - dygraph-tickers.js - dygraph-dev.js - excanvas.js @@ -21,6 +21,7 @@ load: - plugins/base.js - plugins/annotations.js - plugins/axes.js + - plugins/range-selector.js - plugins/chart-labels.js - plugins/grid.js - plugins/legend.js diff --git a/plugins/install.js b/plugins/install.js index a8dd0e6..ecf7b3b 100644 --- a/plugins/install.js +++ b/plugins/install.js @@ -11,6 +11,7 @@ Dygraph.PLUGINS.push( Dygraph.Plugins.Legend, Dygraph.Plugins.Axes, + Dygraph.Plugins.RangeSelector, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks. Dygraph.Plugins.ChartLabels, Dygraph.Plugins.Annotations, Dygraph.Plugins.Grid diff --git a/plugins/range-selector.js b/plugins/range-selector.js new file mode 100644 index 0000000..fd5d791 --- /dev/null +++ b/plugins/range-selector.js @@ -0,0 +1,855 @@ +/** + * @license + * Copyright 2013 Paul Felix (paul.eric.felix@gmail.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview This file contains the RangeSelector plugin used to provide + * a timeline range selector widget for dygraphs. + */ + +Dygraph.Plugins.RangeSelector = (function() { + +/*jshint globalstrict: true */ +/*global Dygraph:false */ +"use strict"; + +var NOT_CREATED = 0; +var CREATED = 1<<0; +var ADDED_TO_GRAPH = 1<<1; + +var rangeSelector = function() { + this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera; + this.hasTouchInterface_ = typeof(TouchEvent) != 'undefined'; + this.isMobileDevice_ = /mobile|android/gi.test(navigator.appVersion); + this.status_ = NOT_CREATED; +}; + +rangeSelector.prototype.toString = function() { + return "RangeSelector Plugin"; +}; + +rangeSelector.prototype.activate = function(dygraph) { + this.dygraph_ = dygraph; + this.layout_ = this.dygraph_.layout_; + this.isUsingExcanvas_ = dygraph.isUsingExcanvas_; + if (this.attr_('showRangeSelector')) { + this.createInterface_(); + } + return { + layout: this.reserveSpace_, + predraw: this.renderStaticLayer_, + didDrawChart: this.renderInteractiveLayer_ + }; +}; + +rangeSelector.prototype.destroy = function() { + this.bgcanvas_ = null; + this.fgcanvas_ = null; + this.leftZoomHandle_ = null; + this.rightZoomHandle_ = null; + this.iePanOverlay_ = null; +}; + +//------------------------------------------------------------------ +// Private methods +//------------------------------------------------------------------ + +rangeSelector.prototype.attr_ = function(name) { + return this.dygraph_.attr_(name); +}; + +/** + * @private + * Creates the range selector elements and adds them to the graph. + */ +rangeSelector.prototype.createInterface_ = function() { + this.createCanvases_(); + if (this.isUsingExcanvas_) { + this.createIEPanOverlay_(); + } + this.createZoomHandles_(); + this.initInteraction_(); + + // Range selector and animatedZooms have a bad interaction. See issue 359. + if (this.attr_('animatedZooms')) { + this.dygraph_.warn('You should not set animatedZooms=true when using the range selector.'); + this.dygraph_.attrs_.animatedZooms = false; + } + + this.addToGraph_(); + this.status_ = CREATED; +}; + +/** + * @private + * Adds the range selector to the graph. + */ +rangeSelector.prototype.addToGraph_ = function() { + var graphDiv = this.dygraph_.graphDiv; + graphDiv.appendChild(this.bgcanvas_); + graphDiv.appendChild(this.fgcanvas_); + graphDiv.appendChild(this.leftZoomHandle_); + graphDiv.appendChild(this.rightZoomHandle_); + this.status_ |= ADDED_TO_GRAPH; +}; + +/** + * @private + * Removes the range selector from the graph. + */ +rangeSelector.prototype.removeFromGraph_ = function() { + var graphDiv = this.dygraph_.graphDiv; + graphDiv.removeChild(this.bgcanvas_); + graphDiv.removeChild(this.fgcanvas_); + graphDiv.removeChild(this.leftZoomHandle_); + graphDiv.removeChild(this.rightZoomHandle_); + this.status_ ^= ADDED_TO_GRAPH; +}; + +/** + * @private + * Called by Layout to allow range selector to reserve its space. + */ +rangeSelector.prototype.reserveSpace_ = function(e) { + if (this.attr_('showRangeSelector')) { + e.reserveSpaceBottom(this.attr_('rangeSelectorHeight') + 4); + } +}; + +/** + * @private + * Renders the static portion of the range selector at the predraw stage. + */ +rangeSelector.prototype.renderStaticLayer_ = function() { + if (!this.isEnabled_()) { + return; + } + this.resize_(); + this.drawStaticLayer_(); +}; + +/** + * @private + * Renders the interactive portion of the range selector after the chart has been drawn. + */ +rangeSelector.prototype.renderInteractiveLayer_ = function() { + if (!this.isEnabled_() || this.isChangingRange_) { + return; + } + this.placeZoomHandles_(); + this.drawInteractiveLayer_(); +}; + +/** + * @private + * Check to see if the range selector is enabled and needs to be created or added to graph. + */ +rangeSelector.prototype.isEnabled_ = function() { + var enabled = this.attr_('showRangeSelector'); + if (enabled) { + if (!(this.status_ & CREATED)) { + this.createInterface_(); + } else if (!(this.status_ & ADDED_TO_GRAPH)) { + this.addToGraph_(); + } + } else if (this.status_ & ADDED_TO_GRAPH) { + this.removeFromGraph_(); + var dygraph = this.dygraph_; + setTimeout(function() { dygraph.width_ = 0; dygraph.resize(); }, 1); + } + return enabled; +} + +/** + * @private + * Resizes the range selector. + */ +rangeSelector.prototype.resize_ = function() { + function setElementRect(canvas, rect) { + canvas.style.top = rect.y + 'px'; + canvas.style.left = rect.x + 'px'; + canvas.width = rect.w; + canvas.height = rect.h; + canvas.style.width = canvas.width + 'px'; // for IE + canvas.style.height = canvas.height + 'px'; // for IE + } + + var plotArea = this.layout_.getPlotArea(); + var xAxisLabelHeight = this.attr_('xAxisHeight') || (this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize')); + this.canvasRect_ = { + x: plotArea.x, + y: plotArea.y + plotArea.h + xAxisLabelHeight + 4, + w: plotArea.w, + h: this.attr_('rangeSelectorHeight') + }; + + setElementRect(this.bgcanvas_, this.canvasRect_); + setElementRect(this.fgcanvas_, this.canvasRect_); +}; + +/** + * @private + * Creates the background and foreground canvases. + */ +rangeSelector.prototype.createCanvases_ = function() { + this.bgcanvas_ = Dygraph.createCanvas(); + this.bgcanvas_.className = 'dygraph-rangesel-bgcanvas'; + this.bgcanvas_.style.position = 'absolute'; + this.bgcanvas_.style.zIndex = 9; + this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_); + + this.fgcanvas_ = Dygraph.createCanvas(); + this.fgcanvas_.className = 'dygraph-rangesel-fgcanvas'; + this.fgcanvas_.style.position = 'absolute'; + this.fgcanvas_.style.zIndex = 9; + this.fgcanvas_.style.cursor = 'default'; + this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_); +}; + +/** + * @private + * Creates overlay divs for IE/Excanvas so that mouse events are handled properly. + */ +rangeSelector.prototype.createIEPanOverlay_ = function() { + this.iePanOverlay_ = document.createElement("div"); + this.iePanOverlay_.style.position = 'absolute'; + this.iePanOverlay_.style.backgroundColor = 'white'; + this.iePanOverlay_.style.filter = 'alpha(opacity=0)'; + this.iePanOverlay_.style.display = 'none'; + this.iePanOverlay_.style.cursor = 'move'; + this.fgcanvas_.appendChild(this.iePanOverlay_); +}; + +/** + * @private + * Creates the zoom handle elements. + */ +rangeSelector.prototype.createZoomHandles_ = function() { + var img = new Image(); + img.className = 'dygraph-rangesel-zoomhandle'; + img.style.position = 'absolute'; + img.style.zIndex = 10; + img.style.visibility = 'hidden'; // Initially hidden so they don't show up in the wrong place. + img.style.cursor = 'col-resize'; + + if (/MSIE 7/.test(navigator.userAgent)) { // IE7 doesn't support embedded src data. + img.width = 7; + img.height = 14; + img.style.backgroundColor = 'white'; + img.style.border = '1px solid #333333'; // Just show box in IE7. + } else { + img.width = 9; + img.height = 16; + img.src = 'data:image/png;base64,' + +'iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA' + +'zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv' + +'bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl' + +'6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s' + +'qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII='; + } + + if (this.isMobileDevice_) { + img.width *= 2; + img.height *= 2; + } + + this.leftZoomHandle_ = img; + this.rightZoomHandle_ = img.cloneNode(false); +}; + +/** + * @private + * Sets up the interaction for the range selector. + */ +rangeSelector.prototype.initInteraction_ = function() { + var self = this; + var topElem = this.isIE_ ? document : window; + var xLast = 0; + var handle = null; + var isZooming = false; + var isPanning = false; + var dynamic = !this.isMobileDevice_ && !this.isUsingExcanvas_; + + // We cover iframes during mouse interactions. See comments in + // dygraph-utils.js for more info on why this is a good idea. + var tarp = new Dygraph.IFrameTarp(); + + // functions, defined below. Defining them this way (rather than with + // "function foo() {...}" makes JSHint happy. + var toXDataWindow, onZoomStart, onZoom, onZoomEnd, doZoom, isMouseInPanZone, + onPanStart, onPan, onPanEnd, doPan, onCanvasMouseMove, applyBrowserZoomLevel; + + // Touch event functions + var onZoomHandleTouchEvent, onCanvasTouchEvent, addTouchEvents; + + toXDataWindow = function(zoomHandleStatus) { + var xDataLimits = self.dygraph_.xAxisExtremes(); + var fact = (xDataLimits[1] - xDataLimits[0])/self.canvasRect_.w; + var xDataMin = xDataLimits[0] + (zoomHandleStatus.leftHandlePos - self.canvasRect_.x)*fact; + var xDataMax = xDataLimits[0] + (zoomHandleStatus.rightHandlePos - self.canvasRect_.x)*fact; + return [xDataMin, xDataMax]; + }; + + applyBrowserZoomLevel = function(delX) { + var zoom = window.outerWidth/document.documentElement.clientWidth; + if (!isNaN(zoom)) { + return delX/zoom; + } else { + return delX; + } + }; + + onZoomStart = function(e) { + Dygraph.cancelEvent(e); + isZooming = true; + xLast = e.screenX; + handle = e.target ? e.target : e.srcElement; + self.dygraph_.addEvent(topElem, 'mousemove', onZoom); + self.dygraph_.addEvent(topElem, 'mouseup', onZoomEnd); + self.fgcanvas_.style.cursor = 'col-resize'; + tarp.cover(); + return true; + }; + + onZoom = function(e) { + if (!isZooming) { + return false; + } + Dygraph.cancelEvent(e); + var delX = e.screenX - xLast; + if (Math.abs(delX) < 4 || e.screenX === 0) { + // First iPad move event seems to have screenX = 0 + return true; + } + xLast = e.screenX; + delX = applyBrowserZoomLevel(delX); + + // Move handle. + var zoomHandleStatus = self.getZoomHandleStatus_(); + var newPos; + if (handle == self.leftZoomHandle_) { + newPos = zoomHandleStatus.leftHandlePos + delX; + newPos = Math.min(newPos, zoomHandleStatus.rightHandlePos - handle.width - 3); + newPos = Math.max(newPos, self.canvasRect_.x); + } else { + newPos = zoomHandleStatus.rightHandlePos + delX; + newPos = Math.min(newPos, self.canvasRect_.x + self.canvasRect_.w); + newPos = Math.max(newPos, zoomHandleStatus.leftHandlePos + handle.width + 3); + } + var halfHandleWidth = handle.width/2; + handle.style.left = (newPos - halfHandleWidth) + 'px'; + self.drawInteractiveLayer_(); + + // Zoom on the fly (if not using excanvas). + if (dynamic) { + doZoom(); + } + return true; + }; + + onZoomEnd = function(e) { + if (!isZooming) { + return false; + } + isZooming = false; + tarp.uncover(); + Dygraph.removeEvent(topElem, 'mousemove', onZoom); + Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd); + self.fgcanvas_.style.cursor = 'default'; + + // If using excanvas, Zoom now. + if (!dynamic) { + doZoom(); + } + return true; + }; + + doZoom = function() { + try { + var zoomHandleStatus = self.getZoomHandleStatus_(); + self.isChangingRange_ = true; + if (!zoomHandleStatus.isZoomed) { + self.dygraph_.resetZoom(); + } else { + var xDataWindow = toXDataWindow(zoomHandleStatus); + self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]); + } + } finally { + self.isChangingRange_ = false; + } + }; + + isMouseInPanZone = function(e) { + if (self.isUsingExcanvas_) { + return e.srcElement == self.iePanOverlay_; + } else { + var rect = self.leftZoomHandle_.getBoundingClientRect(); + var leftHandleClientX = rect.left + rect.width/2; + rect = self.rightZoomHandle_.getBoundingClientRect(); + var rightHandleClientX = rect.left + rect.width/2; + return (e.clientX > leftHandleClientX && e.clientX < rightHandleClientX); + } + }; + + onPanStart = function(e) { + if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) { + Dygraph.cancelEvent(e); + isPanning = true; + xLast = e.screenX; + self.dygraph_.addEvent(topElem, 'mousemove', onPan); + self.dygraph_.addEvent(topElem, 'mouseup', onPanEnd); + return true; + } + return false; + }; + + onPan = function(e) { + if (!isPanning) { + return false; + } + Dygraph.cancelEvent(e); + + var delX = e.screenX - xLast; + if (Math.abs(delX) < 4) { + return true; + } + xLast = e.screenX; + delX = applyBrowserZoomLevel(delX); + + // Move range view + var zoomHandleStatus = self.getZoomHandleStatus_(); + var leftHandlePos = zoomHandleStatus.leftHandlePos; + var rightHandlePos = zoomHandleStatus.rightHandlePos; + var rangeSize = rightHandlePos - leftHandlePos; + if (leftHandlePos + delX <= self.canvasRect_.x) { + leftHandlePos = self.canvasRect_.x; + rightHandlePos = leftHandlePos + rangeSize; + } else if (rightHandlePos + delX >= self.canvasRect_.x + self.canvasRect_.w) { + rightHandlePos = self.canvasRect_.x + self.canvasRect_.w; + leftHandlePos = rightHandlePos - rangeSize; + } else { + leftHandlePos += delX; + rightHandlePos += delX; + } + var halfHandleWidth = self.leftZoomHandle_.width/2; + self.leftZoomHandle_.style.left = (leftHandlePos - halfHandleWidth) + 'px'; + self.rightZoomHandle_.style.left = (rightHandlePos - halfHandleWidth) + 'px'; + self.drawInteractiveLayer_(); + + // Do pan on the fly (if not using excanvas). + if (dynamic) { + doPan(); + } + return true; + }; + + onPanEnd = function(e) { + if (!isPanning) { + return false; + } + isPanning = false; + Dygraph.removeEvent(topElem, 'mousemove', onPan); + Dygraph.removeEvent(topElem, 'mouseup', onPanEnd); + // If using excanvas, do pan now. + if (!dynamic) { + doPan(); + } + return true; + }; + + doPan = function() { + try { + self.isChangingRange_ = true; + self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_()); + self.dygraph_.drawGraph_(false); + } finally { + self.isChangingRange_ = false; + } + }; + + onCanvasMouseMove = function(e) { + if (isZooming || isPanning) { + return; + } + var cursor = isMouseInPanZone(e) ? 'move' : 'default'; + if (cursor != self.fgcanvas_.style.cursor) { + self.fgcanvas_.style.cursor = cursor; + } + }; + + onZoomHandleTouchEvent = function(e) { + if (e.type == 'touchstart' && e.targetTouches.length == 1) { + if (onZoomStart(e.targetTouches[0])) { + Dygraph.cancelEvent(e); + } + } else if (e.type == 'touchmove' && e.targetTouches.length == 1) { + if (onZoom(e.targetTouches[0])) { + Dygraph.cancelEvent(e); + } + } else { + onZoomEnd(e); + } + }; + + onCanvasTouchEvent = function(e) { + if (e.type == 'touchstart' && e.targetTouches.length == 1) { + if (onPanStart(e.targetTouches[0])) { + Dygraph.cancelEvent(e); + } + } else if (e.type == 'touchmove' && e.targetTouches.length == 1) { + if (onPan(e.targetTouches[0])) { + Dygraph.cancelEvent(e); + } + } else { + onPanEnd(e); + } + }; + + addTouchEvents = function(elem, fn) { + var types = ['touchstart', 'touchend', 'touchmove', 'touchcancel']; + for (var i = 0; i < types.length; i++) { + self.dygraph_.addEvent(elem, types[i], fn); + } + }; + + this.dygraph_.attrs_.interactionModel = + Dygraph.Interaction.dragIsPanInteractionModel; + this.dygraph_.attrs_.panEdgeFraction = 0.0001; + + var dragStartEvent = window.opera ? 'mousedown' : 'dragstart'; + this.dygraph_.addEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart); + this.dygraph_.addEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart); + + if (this.isUsingExcanvas_) { + this.dygraph_.addEvent(this.iePanOverlay_, 'mousedown', onPanStart); + } else { + this.dygraph_.addEvent(this.fgcanvas_, 'mousedown', onPanStart); + this.dygraph_.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove); + } + + // Touch events + if (this.hasTouchInterface_) { + addTouchEvents(this.leftZoomHandle_, onZoomHandleTouchEvent); + addTouchEvents(this.rightZoomHandle_, onZoomHandleTouchEvent); + addTouchEvents(this.fgcanvas_, onCanvasTouchEvent); + } +}; + +/** + * @private + * Draws the static layer in the background canvas. + */ +rangeSelector.prototype.drawStaticLayer_ = function() { + var ctx = this.bgcanvas_ctx_; + ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h); + try { + this.drawMiniPlot_(); + } catch(ex) { + Dygraph.warn(ex); + } + + var margin = 0.5; + this.bgcanvas_ctx_.lineWidth = 1; + ctx.strokeStyle = 'gray'; + ctx.beginPath(); + ctx.moveTo(margin, margin); + ctx.lineTo(margin, this.canvasRect_.h-margin); + ctx.lineTo(this.canvasRect_.w-margin, this.canvasRect_.h-margin); + ctx.lineTo(this.canvasRect_.w-margin, margin); + ctx.stroke(); +}; + + +/** + * @private + * Draws the mini plot in the background canvas. + */ +rangeSelector.prototype.drawMiniPlot_ = function() { + var fillStyle = this.attr_('rangeSelectorPlotFillColor'); + var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor'); + if (!fillStyle && !strokeStyle) { + return; + } + + var stepPlot = this.attr_('stepPlot'); + + var combinedSeriesData = this.computeCombinedSeriesAndLimits_(); + var yRange = combinedSeriesData.yMax - combinedSeriesData.yMin; + + // Draw the mini plot. + var ctx = this.bgcanvas_ctx_; + var margin = 0.5; + + var xExtremes = this.dygraph_.xAxisExtremes(); + var xRange = Math.max(xExtremes[1] - xExtremes[0], 1.e-30); + var xFact = (this.canvasRect_.w - margin)/xRange; + var yFact = (this.canvasRect_.h - margin)/yRange; + var canvasWidth = this.canvasRect_.w - margin; + var canvasHeight = this.canvasRect_.h - margin; + + var prevX = null, prevY = null; + + ctx.beginPath(); + ctx.moveTo(margin, canvasHeight); + for (var i = 0; i < combinedSeriesData.data.length; i++) { + var dataPoint = combinedSeriesData.data[i]; + var x = ((dataPoint[0] !== null) ? ((dataPoint[0] - xExtremes[0])*xFact) : NaN); + var y = ((dataPoint[1] !== null) ? (canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact) : NaN); + if (isFinite(x) && isFinite(y)) { + if(prevX === null) { + ctx.lineTo(x, canvasHeight); + } + else if (stepPlot) { + ctx.lineTo(x, prevY); + } + ctx.lineTo(x, y); + prevX = x; + prevY = y; + } + else { + if(prevX !== null) { + if (stepPlot) { + ctx.lineTo(x, prevY); + ctx.lineTo(x, canvasHeight); + } + else { + ctx.lineTo(prevX, canvasHeight); + } + } + prevX = prevY = null; + } + } + ctx.lineTo(canvasWidth, canvasHeight); + ctx.closePath(); + + if (fillStyle) { + var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight); + lingrad.addColorStop(0, 'white'); + lingrad.addColorStop(1, fillStyle); + this.bgcanvas_ctx_.fillStyle = lingrad; + ctx.fill(); + } + + if (strokeStyle) { + this.bgcanvas_ctx_.strokeStyle = strokeStyle; + this.bgcanvas_ctx_.lineWidth = 1.5; + ctx.stroke(); + } +}; + +/** + * @private + * Computes and returns the combinded series data along with min/max for the mini plot. + * @return {Object} An object containing combinded series array, ymin, ymax. + */ +rangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() { + var data = this.dygraph_.rawData_; + var logscale = this.attr_('logscale'); + + // Create a combined series (average of all series values). + var combinedSeries = []; + var sum; + var count; + var mutipleValues; + var i, j, k; + var xVal, yVal; + + // Find out if data has multiple values per datapoint. + // Go to first data point that actually has values (see http://code.google.com/p/dygraphs/issues/detail?id=246) + for (i = 0; i < data.length; i++) { + if (data[i].length > 1 && data[i][1] !== null) { + mutipleValues = typeof data[i][1] != 'number'; + if (mutipleValues) { + sum = []; + count = []; + for (k = 0; k < data[i][1].length; k++) { + sum.push(0); + count.push(0); + } + } + break; + } + } + + for (i = 0; i < data.length; i++) { + var dataPoint = data[i]; + xVal = dataPoint[0]; + + if (mutipleValues) { + for (k = 0; k < sum.length; k++) { + sum[k] = count[k] = 0; + } + } else { + sum = count = 0; + } + + for (j = 1; j < dataPoint.length; j++) { + if (this.dygraph_.visibility()[j-1]) { + var y; + if (mutipleValues) { + for (k = 0; k < sum.length; k++) { + y = dataPoint[j][k]; + if (y === null || isNaN(y)) continue; + sum[k] += y; + count[k]++; + } + } else { + y = dataPoint[j]; + if (y === null || isNaN(y)) continue; + sum += y; + count++; + } + } + } + + if (mutipleValues) { + for (k = 0; k < sum.length; k++) { + sum[k] /= count[k]; + } + yVal = sum.slice(0); + } else { + yVal = sum/count; + } + + combinedSeries.push([xVal, yVal]); + } + + // Account for roll period, fractions. + combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_); + + if (typeof combinedSeries[0][1] != 'number') { + for (i = 0; i < combinedSeries.length; i++) { + yVal = combinedSeries[i][1]; + combinedSeries[i][1] = yVal[0]; + } + } + + // Compute the y range. + var yMin = Number.MAX_VALUE; + var yMax = -Number.MAX_VALUE; + for (i = 0; i < combinedSeries.length; i++) { + yVal = combinedSeries[i][1]; + if (yVal !== null && isFinite(yVal) && (!logscale || yVal > 0)) { + yMin = Math.min(yMin, yVal); + yMax = Math.max(yMax, yVal); + } + } + + // Convert Y data to log scale if needed. + // Also, expand the Y range to compress the mini plot a little. + var extraPercent = 0.25; + if (logscale) { + yMax = Dygraph.log10(yMax); + yMax += yMax*extraPercent; + yMin = Dygraph.log10(yMin); + for (i = 0; i < combinedSeries.length; i++) { + combinedSeries[i][1] = Dygraph.log10(combinedSeries[i][1]); + } + } else { + var yExtra; + var yRange = yMax - yMin; + if (yRange <= Number.MIN_VALUE) { + yExtra = yMax*extraPercent; + } else { + yExtra = yRange*extraPercent; + } + yMax += yExtra; + yMin -= yExtra; + } + + return {data: combinedSeries, yMin: yMin, yMax: yMax}; +}; + +/** + * @private + * Places the zoom handles in the proper position based on the current X data window. + */ +rangeSelector.prototype.placeZoomHandles_ = function() { + var xExtremes = this.dygraph_.xAxisExtremes(); + var xWindowLimits = this.dygraph_.xAxisRange(); + var xRange = xExtremes[1] - xExtremes[0]; + var leftPercent = Math.max(0, (xWindowLimits[0] - xExtremes[0])/xRange); + var rightPercent = Math.max(0, (xExtremes[1] - xWindowLimits[1])/xRange); + var leftCoord = this.canvasRect_.x + this.canvasRect_.w*leftPercent; + var rightCoord = this.canvasRect_.x + this.canvasRect_.w*(1 - rightPercent); + var handleTop = Math.max(this.canvasRect_.y, this.canvasRect_.y + (this.canvasRect_.h - this.leftZoomHandle_.height)/2); + var halfHandleWidth = this.leftZoomHandle_.width/2; + this.leftZoomHandle_.style.left = (leftCoord - halfHandleWidth) + 'px'; + this.leftZoomHandle_.style.top = handleTop + 'px'; + this.rightZoomHandle_.style.left = (rightCoord - halfHandleWidth) + 'px'; + this.rightZoomHandle_.style.top = this.leftZoomHandle_.style.top; + + this.leftZoomHandle_.style.visibility = 'visible'; + this.rightZoomHandle_.style.visibility = 'visible'; +}; + +/** + * @private + * Draws the interactive layer in the foreground canvas. + */ +rangeSelector.prototype.drawInteractiveLayer_ = function() { + var ctx = this.fgcanvas_ctx_; + ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h); + var margin = 1; + var width = this.canvasRect_.w - margin; + var height = this.canvasRect_.h - margin; + var zoomHandleStatus = this.getZoomHandleStatus_(); + + ctx.strokeStyle = 'black'; + if (!zoomHandleStatus.isZoomed) { + ctx.beginPath(); + ctx.moveTo(margin, margin); + ctx.lineTo(margin, height); + ctx.lineTo(width, height); + ctx.lineTo(width, margin); + ctx.stroke(); + if (this.iePanOverlay_) { + this.iePanOverlay_.style.display = 'none'; + } + } else { + var leftHandleCanvasPos = Math.max(margin, zoomHandleStatus.leftHandlePos - this.canvasRect_.x); + var rightHandleCanvasPos = Math.min(width, zoomHandleStatus.rightHandlePos - this.canvasRect_.x); + + ctx.fillStyle = 'rgba(240, 240, 240, 0.6)'; + ctx.fillRect(0, 0, leftHandleCanvasPos, this.canvasRect_.h); + ctx.fillRect(rightHandleCanvasPos, 0, this.canvasRect_.w - rightHandleCanvasPos, this.canvasRect_.h); + + ctx.beginPath(); + ctx.moveTo(margin, margin); + ctx.lineTo(leftHandleCanvasPos, margin); + ctx.lineTo(leftHandleCanvasPos, height); + ctx.lineTo(rightHandleCanvasPos, height); + ctx.lineTo(rightHandleCanvasPos, margin); + ctx.lineTo(width, margin); + ctx.stroke(); + + if (this.isUsingExcanvas_) { + this.iePanOverlay_.style.width = (rightHandleCanvasPos - leftHandleCanvasPos) + 'px'; + this.iePanOverlay_.style.left = leftHandleCanvasPos + 'px'; + this.iePanOverlay_.style.height = height + 'px'; + this.iePanOverlay_.style.display = 'inline'; + } + } +}; + +/** + * @private + * Returns the current zoom handle position information. + * @return {Object} The zoom handle status. + */ +rangeSelector.prototype.getZoomHandleStatus_ = function() { + var halfHandleWidth = this.leftZoomHandle_.width/2; + var leftHandlePos = parseInt(this.leftZoomHandle_.style.left, 10) + halfHandleWidth; + var rightHandlePos = parseInt(this.rightZoomHandle_.style.left, 10) + halfHandleWidth; + return { + leftHandlePos: leftHandlePos, + rightHandlePos: rightHandlePos, + isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w) + }; +}; + +return rangeSelector; + +})();