X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=ae27c7c4b9c0adca8111d1523d4f49f503a79417;hb=d63e67995e52dde3b735aaa0b426d4dfc2337126;hp=3d46dc43b0baf2a5e973fd6601d072290b5a0f20;hpb=0abfbd7e563fc0e53015ec875c324890761071ba;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 3d46dc4..ae27c7c 100644 --- a/dygraph.js +++ b/dygraph.js @@ -136,6 +136,11 @@ Dygraph.INFO = 2; Dygraph.WARNING = 3; Dygraph.ERROR = 3; +// Directions for panning and zooming. Use bit operations when combined +// values are possible. +Dygraph.HORIZONTAL = 1; +Dygraph.VERTICAL = 2; + // Used for initializing annotation CSS rules only once. Dygraph.addedAnnotationCSS = false; @@ -172,6 +177,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.previousVerticalX_ = -1; this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; + this.wilsonInterval_ = attrs.wilsonInterval || true; this.is_initial_draw_ = true; this.annotations_ = []; @@ -306,19 +312,39 @@ Dygraph.prototype.xAxisRange = function() { }; /** - * Returns the currently-visible y-range. This can be affected by zooming, - * panning or a call to updateOptions. + * Returns the currently-visible y-range for an axis. This can be affected by + * zooming, panning or a call to updateOptions. Axis indices are zero-based. If + * called with no arguments, returns the range of the first axis. * Returns a two-element array: [bottom, top]. */ -Dygraph.prototype.yAxisRange = function() { - return this.displayedYRange_; +Dygraph.prototype.yAxisRange = function(idx) { + if (typeof(idx) == "undefined") idx = 0; + if (idx < 0 || idx >= this.axes_.length) return null; + return [ this.axes_[idx].computedValueRange[0], + this.axes_[idx].computedValueRange[1] ]; +}; + +/** + * Returns the currently-visible y-ranges for each axis. This can be affected by + * zooming, panning, calls to updateOptions, etc. + * Returns an array of [bottom, top] pairs, one for each y-axis. + */ +Dygraph.prototype.yAxisRanges = function() { + var ret = []; + for (var i = 0; i < this.axes_.length; i++) { + ret.push(this.yAxisRange(i)); + } + return ret; }; +// TODO(danvk): use these functions throughout dygraphs. /** * Convert from data coordinates to canvas/div X/Y coordinates. + * If specified, do this conversion for the coordinate system of a particular + * axis. Uses the first axis by default. * Returns a two-element array: [X, Y] */ -Dygraph.prototype.toDomCoords = function(x, y) { +Dygraph.prototype.toDomCoords = function(x, y, axis) { var ret = [null, null]; var area = this.plotter_.area; if (x !== null) { @@ -327,19 +353,20 @@ Dygraph.prototype.toDomCoords = function(x, y) { } if (y !== null) { - var yRange = this.yAxisRange(); + var yRange = this.yAxisRange(axis); ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h; } return ret; }; -// TODO(danvk): use these functions throughout dygraphs. /** * Convert from canvas/div coords to data coordinates. + * If specified, do this conversion for the coordinate system of a particular + * axis. Uses the first axis by default. * Returns a two-element array: [X, Y] */ -Dygraph.prototype.toDataCoords = function(x, y) { +Dygraph.prototype.toDataCoords = function(x, y, axis) { var ret = [null, null]; var area = this.plotter_.area; if (x !== null) { @@ -348,7 +375,7 @@ Dygraph.prototype.toDataCoords = function(x, y) { } if (y !== null) { - var yRange = this.yAxisRange(); + var yRange = this.yAxisRange(axis); ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]); } @@ -735,13 +762,27 @@ Dygraph.prototype.createDragInterface_ = function() { // Tracks whether the mouse is down right now var isZooming = false; - var isPanning = false; + var isPanning = false; // is this drag part of a pan? + var is2DPan = false; // if so, is that pan 1- or 2-dimensional? var dragStartX = null; var dragStartY = null; var dragEndX = null; var dragEndY = null; + var dragDirection = null; var prevEndX = null; + var prevEndY = null; + var prevDragDirection = null; + + // TODO(danvk): update this comment + // draggingDate and draggingValue represent the [date,value] point on the + // graph at which the mouse was pressed. As the mouse moves while panning, + // the viewport must pan so that the mouse position points to + // [draggingDate, draggingValue] var draggingDate = null; + + // TODO(danvk): update this comment + // The range in second/value units that the viewport encompasses during a + // panning operation. var dateRange = null; // Utility function to convert page-wide coordinates to canvas coords @@ -756,18 +797,46 @@ Dygraph.prototype.createDragInterface_ = function() { dragEndX = getX(event); dragEndY = getY(event); - self.drawZoomRect_(dragStartX, dragEndX, prevEndX); + var xDelta = Math.abs(dragStartX - dragEndX); + var yDelta = Math.abs(dragStartY - dragEndY); + + // drag direction threshold for y axis is twice as large as x axis + dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL; + + self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY, + prevDragDirection, prevEndX, prevEndY); + prevEndX = dragEndX; + prevEndY = dragEndY; + prevDragDirection = dragDirection; } else if (isPanning) { dragEndX = getX(event); dragEndY = getY(event); + // TODO(danvk): update this comment // Want to have it so that: - // 1. draggingDate appears at dragEndX + // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY. // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered. + // 3. draggingValue appears at dragEndY. + // 4. valueRange is unaltered. + + var minDate = draggingDate - (dragEndX / self.width_) * dateRange; + var maxDate = minDate + dateRange; + self.dateWindow_ = [minDate, maxDate]; + + + // y-axis scaling is automatic unless this is a full 2D pan. + if (is2DPan) { + // Adjust each axis appropriately. + var y_frac = dragEndY / self.height_; + for (var i = 0; i < self.axes_.length; i++) { + var axis = self.axes_[i]; + var maxValue = axis.draggingValue + y_frac * axis.dragValueRange; + var minValue = maxValue - axis.dragValueRange; + axis.valueWindow = [ minValue, maxValue ]; + } + } - self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange; - self.dateWindow_[1] = self.dateWindow_[0] + dateRange; self.drawGraph_(); } }); @@ -780,11 +849,35 @@ Dygraph.prototype.createDragInterface_ = function() { dragStartY = getY(event); if (event.altKey || event.shiftKey) { - if (!self.dateWindow_) return; // have to be zoomed in to pan. + // have to be zoomed in to pan. + var zoomedY = false; + for (var i = 0; i < self.axes_.length; i++) { + if (self.axes_[i].valueWindow || self.axes_[i].valueRange) { + zoomedY = true; + break; + } + } + if (!self.dateWindow_ && !zoomedY) return; + isPanning = true; - dateRange = self.dateWindow_[1] - self.dateWindow_[0]; - draggingDate = (dragStartX / self.width_) * dateRange + - self.dateWindow_[0]; + var xRange = self.xAxisRange(); + dateRange = xRange[1] - xRange[0]; + + // 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. + is2DPan = false; + for (var i = 0; i < self.axes_.length; i++) { + var axis = self.axes_[i]; + var yRange = self.yAxisRange(i); + axis.dragValueRange = yRange[1] - yRange[0]; + var r = self.toDataCoords(null, dragStartY, i); + axis.draggingValue = r[1]; + if (axis.valueWindow || axis.valueRange) is2DPan = true; + } + + // TODO(konigsberg): Switch from all this math to toDataCoords? + // Seems to work for the dragging value. + draggingDate = (dragStartX / self.width_) * dateRange + xRange[0]; } else { isZooming = true; } @@ -803,6 +896,10 @@ Dygraph.prototype.createDragInterface_ = function() { isPanning = false; draggingDate = null; dateRange = null; + for (var i = 0; i < self.axes_.length; i++) { + delete self.axes_[i].draggingValue; + delete self.axes_[i].dragValueRange; + } } }); @@ -852,9 +949,12 @@ Dygraph.prototype.createDragInterface_ = function() { } } - if (regionWidth >= 10) { - self.doZoom_(Math.min(dragStartX, dragEndX), + if (regionWidth >= 10 && dragDirection == Dygraph.HORIZONTAL) { + self.doZoomX_(Math.min(dragStartX, dragEndX), Math.max(dragStartX, dragEndX)); + } else if (regionHeight >= 10 && dragDirection == Dygraph.VERTICAL){ + self.doZoomY_(Math.min(dragStartY, dragEndY), + Math.max(dragStartY, dragEndY)); } else { self.canvas_.getContext("2d").clearRect(0, 0, self.canvas_.width, @@ -867,21 +967,19 @@ Dygraph.prototype.createDragInterface_ = function() { if (isPanning) { isPanning = false; + is2DPan = false; draggingDate = null; dateRange = null; + valueRange = null; } }); // Double-clicking zooms back out Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) { - if (self.dateWindow_ == null) return; - self.dateWindow_ = null; - self.drawGraph_(); - var minDate = self.rawData_[0][0]; - var maxDate = self.rawData_[self.rawData_.length - 1][0]; - if (self.attr_("zoomCallback")) { - self.attr_("zoomCallback")(minDate, maxDate); - } + // Disable zooming out if panning. + if (event.altKey || event.shiftKey) return; + + self.doUnzoom_(); }); }; @@ -890,49 +988,148 @@ Dygraph.prototype.createDragInterface_ = function() { * up any previous zoom rectangles that were drawn. This could be optimized to * avoid extra redrawing, but it's tricky to avoid interactions with the status * dots. + * + * @param {Number} direction the direction of the zoom rectangle. Acceptable + * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL. * @param {Number} startX The X position where the drag started, in canvas * coordinates. * @param {Number} endX The current X position of the drag, in canvas coords. + * @param {Number} startY The Y position where the drag started, in canvas + * coordinates. + * @param {Number} endY The current Y position of the drag, in canvas coords. + * @param {Number} prevDirection the value of direction on the previous call to + * this function. Used to avoid excess redrawing * @param {Number} prevEndX The value of endX on the previous call to this * function. Used to avoid excess redrawing + * @param {Number} prevEndY The value of endY on the previous call to this + * function. Used to avoid excess redrawing * @private */ -Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) { +Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY, + prevDirection, prevEndX, prevEndY) { var ctx = this.canvas_.getContext("2d"); // Clean up from the previous rect if necessary - if (prevEndX) { + if (prevDirection == Dygraph.HORIZONTAL) { ctx.clearRect(Math.min(startX, prevEndX), 0, Math.abs(startX - prevEndX), this.height_); + } else if (prevDirection == Dygraph.VERTICAL){ + ctx.clearRect(0, Math.min(startY, prevEndY), + this.width_, Math.abs(startY - prevEndY)); } // Draw a light-grey rectangle to show the new viewing area - if (endX && startX) { - ctx.fillStyle = "rgba(128,128,128,0.33)"; - ctx.fillRect(Math.min(startX, endX), 0, - Math.abs(endX - startX), this.height_); + if (direction == Dygraph.HORIZONTAL) { + if (endX && startX) { + ctx.fillStyle = "rgba(128,128,128,0.33)"; + ctx.fillRect(Math.min(startX, endX), 0, + Math.abs(endX - startX), this.height_); + } + } + if (direction == Dygraph.VERTICAL) { + if (endY && startY) { + ctx.fillStyle = "rgba(128,128,128,0.33)"; + ctx.fillRect(0, Math.min(startY, endY), + this.width_, Math.abs(endY - startY)); + } } }; /** - * Zoom to something containing [lowX, highX]. These are pixel coordinates - * in the canvas. The exact zoom window may be slightly larger if there are no - * data points near lowX or highX. This function redraws the graph. + * Zoom to something containing [lowX, highX]. These are pixel coordinates in + * the canvas. The exact zoom window may be slightly larger if there are no data + * points near lowX or highX. Don't confuse this function with doZoomXDates, + * which accepts dates that match the raw data. This function redraws the graph. + * * @param {Number} lowX The leftmost pixel value that should be visible. * @param {Number} highX The rightmost pixel value that should be visible. * @private */ -Dygraph.prototype.doZoom_ = function(lowX, highX) { +Dygraph.prototype.doZoomX_ = function(lowX, highX) { // Find the earliest and latest dates contained in this canvasx range. + // Convert the call to date ranges of the raw data. var r = this.toDataCoords(lowX, null); var minDate = r[0]; r = this.toDataCoords(highX, null); var maxDate = r[0]; + this.doZoomXDates_(minDate, maxDate); +}; +/** + * Zoom to something containing [minDate, maxDate] values. Don't confuse this + * method with doZoomX which accepts pixel coordinates. This function redraws + * the graph. + * + * @param {Number} minDate The minimum date that should be visible. + * @param {Number} maxDate The maximum date that should be visible. + * @private + */ +Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { this.dateWindow_ = [minDate, maxDate]; this.drawGraph_(); if (this.attr_("zoomCallback")) { - this.attr_("zoomCallback")(minDate, maxDate); + var yRange = this.yAxisRange(); + this.attr_("zoomCallback")(minDate, maxDate, yRange[0], yRange[1]); + } +}; + +/** + * Zoom to something containing [lowY, highY]. These are pixel coordinates in + * the canvas. This function redraws the graph. + * + * @param {Number} lowY The topmost pixel value that should be visible. + * @param {Number} highY The lowest pixel value that should be visible. + * @private + */ +Dygraph.prototype.doZoomY_ = function(lowY, highY) { + // Find the highest and lowest values in pixel range for each axis. + // Note that lowY (in pixels) corresponds to the max Value (in data coords). + // This is because pixels increase as you go down on the screen, whereas data + // coordinates increase as you go up the screen. + var valueRanges = []; + for (var i = 0; i < this.axes_.length; i++) { + var hi = this.toDataCoords(null, lowY, i); + var low = this.toDataCoords(null, highY, i); + this.axes_[i].valueWindow = [low[1], hi[1]]; + valueRanges.push([low[1], hi[1]]); + } + + this.drawGraph_(); + if (this.attr_("zoomCallback")) { + var xRange = this.xAxisRange(); + this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges()); + } +}; + +/** + * Reset the zoom to the original view coordinates. This is the same as + * double-clicking on the graph. + * + * @private + */ +Dygraph.prototype.doUnzoom_ = function() { + var dirty = false; + if (this.dateWindow_ != null) { + dirty = true; + this.dateWindow_ = null; + } + + for (var i = 0; i < this.axes_.length; i++) { + if (this.axes_[i].valueWindow != null) { + dirty = true; + delete this.axes_[i].valueWindow; + } + } + + if (dirty) { + // Putting the drawing operation before the callback because it resets + // yAxisRange. + this.drawGraph_(); + if (this.attr_("zoomCallback")) { + var minDate = this.rawData_[0][0]; + var maxDate = this.rawData_[this.rawData_.length - 1][0]; + this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); + } } }; @@ -1614,6 +1811,7 @@ Dygraph.prototype.predraw_ = function() { }; /** +======= * Update the graph with new data. This method is called when the viewing area * has changed. If the underlying data or options have changed, predraw_ will * be called before drawGraph_ is called. @@ -1653,6 +1851,8 @@ Dygraph.prototype.drawGraph_ = function() { series.push([date, data[j][i]]); } } + + // TODO(danvk): move this into predraw_. It's insane to do it here. series = this.rollingAverage(series, this.rollPeriod_); // Prune down to the desired range, if necessary (for zooming) @@ -1731,7 +1931,6 @@ Dygraph.prototype.drawGraph_ = function() { var out = this.computeYAxisRanges_(extremes); var axes = out[0]; var seriesToAxisMap = out[1]; - this.displayedYRange_ = axes[0].valueRange; this.layout_.updateOptions( { yAxes: axes, seriesToAxisMap: seriesToAxisMap } ); @@ -1744,7 +1943,7 @@ Dygraph.prototype.drawGraph_ = function() { this.plotter_.clear(); this.plotter_.render(); this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width, - this.canvas_.height); + this.canvas_.height); if (this.attr_("drawCallback") !== null) { this.attr_("drawCallback")(this, is_initial_draw); @@ -1857,7 +2056,13 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // Compute extreme values, a span and tick marks for each axis. for (var i = 0; i < this.axes_.length; i++) { var axis = this.axes_[i]; - if (axis.valueRange) { + if (axis.valueWindow) { + // This is only set if the user has zoomed on the y-axis. It is never set + // by a user. It takes precedence over axis.valueRange because, if you set + // valueRange, you'd still expect to be able to pan. + axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; + } else if (axis.valueRange) { + // This is a user-set value range for this axis. axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]]; } else { // Calcuate the extremes of extremes. @@ -1878,8 +2083,10 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { var minAxisY = minY - 0.1 * span; // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense. - if (minAxisY < 0 && minY >= 0) minAxisY = 0; - if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; + if (!this.attr_("avoidMinZero")) { + if (minAxisY < 0 && minY >= 0) minAxisY = 0; + if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; + } if (this.attr_("includeZero")) { if (maxY < 0) maxAxisY = 0; @@ -2499,10 +2706,10 @@ Dygraph.prototype.start_ = function() { */ Dygraph.prototype.updateOptions = function(attrs) { // TODO(danvk): this is a mess. Rethink this function. - if (attrs.rollPeriod) { + if ('rollPeriod' in attrs) { this.rollPeriod_ = attrs.rollPeriod; } - if (attrs.dateWindow) { + if ('dateWindow' in attrs) { this.dateWindow_ = attrs.dateWindow; }