X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;ds=sidebyside;f=dygraph.js;h=b909aa7cc9dd9923ab151f5c40082596870f436c;hb=79b3ee420f46d964b796adc06b4cd64e74a44cb8;hp=330aa77c60cb725008283f1e22f78612b30109fe;hpb=f97785a9ed74007f6480305a358fec57591b77ef;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 330aa77..b909aa7 100644 --- a/dygraph.js +++ b/dygraph.js @@ -312,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) { @@ -333,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) { @@ -354,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]); } @@ -670,40 +691,40 @@ Dygraph.prototype.positionLabelsDiv_ = function() { var area = this.plotter_.area; var div = this.attr_("labelsDiv"); - div.style.left = area.x + area.w - this.attr_("labelsDivWidth") + "px"; + div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px"; }; /** * Create the text box to adjust the averaging period - * @return {Object} The newly-created text box * @private */ Dygraph.prototype.createRollInterface_ = function() { - // Destroy any existing roller. - if (this.roller_) this.graphDiv.removeChild(this.roller_); + // Create a roller if one doesn't exist already. + if (!this.roller_) { + this.roller_ = document.createElement("input"); + this.roller_.type = "text"; + this.roller_.style.display = "none"; + this.graphDiv.appendChild(this.roller_); + } + + var display = this.attr_('showRoller') ? 'block' : 'none'; - var display = this.attr_('showRoller') ? "block" : "none"; var textAttr = { "position": "absolute", "zIndex": 10, "top": (this.plotter_.area.h - 25) + "px", "left": (this.plotter_.area.x + 1) + "px", "display": display }; - var roller = document.createElement("input"); - roller.type = "text"; - roller.size = "2"; - roller.value = this.rollPeriod_; + this.roller_.size = "2"; + this.roller_.value = this.rollPeriod_; for (var name in textAttr) { if (textAttr.hasOwnProperty(name)) { - roller.style[name] = textAttr[name]; + this.roller_.style[name] = textAttr[name]; } } - var pa = this.graphDiv; - pa.appendChild(roller); var dygraph = this; - roller.onchange = function() { dygraph.adjustRoll(roller.value); }; - return roller; + this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); }; }; // These functions are taken from MochiKit.Signal @@ -741,7 +762,8 @@ 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; @@ -751,17 +773,17 @@ Dygraph.prototype.createDragInterface_ = function() { 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; - var draggingValue = null; + // TODO(danvk): update this comment // The range in second/value units that the viewport encompasses during a // panning operation. var dateRange = null; - var valueRange = null; // Utility function to convert page-wide coordinates to canvas coords var px = 0; @@ -791,6 +813,7 @@ Dygraph.prototype.createDragInterface_ = function() { dragEndX = getX(event); dragEndY = getY(event); + // TODO(danvk): update this comment // Want to have it so that: // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY. // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered. @@ -802,14 +825,16 @@ Dygraph.prototype.createDragInterface_ = function() { self.dateWindow_ = [minDate, maxDate]; - // MERGE - // y-axis scaling is automatic unless a valueRange is defined or - // if the user zooms in on the y-axis. If neither is true, valueWindow_ - // will be null. - if (self.valueWindow_) { - var maxValue = draggingValue + (dragEndY / self.height_) * valueRange; - var minValue = maxValue - valueRange; - self.valueWindow_ = [ minValue, maxValue ]; + // 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.drawGraph_(); @@ -818,6 +843,14 @@ Dygraph.prototype.createDragInterface_ = function() { // Track the beginning of drag events Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) { + // prevents mouse drags from selecting page text. + if (event.preventDefault) { + event.preventDefault(); // Firefox, Chrome, etc. + } else { + event.returnValue = false; // IE + event.cancelBubble = true; + } + px = Dygraph.findPosX(self.canvas_); py = Dygraph.findPosY(self.canvas_); dragStartX = getX(event); @@ -825,20 +858,34 @@ Dygraph.prototype.createDragInterface_ = function() { if (event.altKey || event.shiftKey) { // have to be zoomed in to pan. - if (!self.dateWindow_ && !self.valueWindow_) return; + 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; var xRange = self.xAxisRange(); dateRange = xRange[1] - xRange[0]; - var yRange = self.yAxisRange(); - valueRange = yRange[1] - yRange[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]; - var r = self.toDataCoords(null, dragStartY); - draggingValue = r[1]; + draggingDate = (dragStartX / self.width_) * dateRange + xRange[0]; } else { isZooming = true; } @@ -856,9 +903,11 @@ Dygraph.prototype.createDragInterface_ = function() { if (isPanning) { isPanning = false; draggingDate = null; - draggingValue = null; dateRange = null; - valueRange = null; + for (var i = 0; i < self.axes_.length; i++) { + delete self.axes_[i].draggingValue; + delete self.axes_[i].dragValueRange; + } } }); @@ -926,8 +975,8 @@ Dygraph.prototype.createDragInterface_ = function() { if (isPanning) { isPanning = false; + is2DPan = false; draggingDate = null; - draggingValue = null; dateRange = null; valueRange = null; } @@ -999,7 +1048,7 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY * 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 @@ -1018,7 +1067,7 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { * 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 @@ -1034,59 +1083,50 @@ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { /** * Zoom to something containing [lowY, highY]. These are pixel coordinates in - * the canvas. The exact zoom window may be slightly larger if there are no - * data points near lowY or highY. Don't confuse this function with - * doZoomYValues, which accepts parameters that match the raw data. This - * function redraws the graph. - * + * 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. - var r = this.toDataCoords(null, lowY); - var maxValue = r[1]; - r = this.toDataCoords(null, highY); - var minValue = r[1]; - - this.doZoomYValues_(minValue, maxValue); -}; + // 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]]); + } -/** - * Zoom to something containing [minValue, maxValue] values. Don't confuse this - * method with doZoomY which accepts pixel coordinates. This function redraws - * the graph. - * - * @param {Number} minValue The minimum Value that should be visible. - * @param {Number} maxValue The maximum value that should be visible. - * @private - */ -// MERGE: this doesn't make sense anymore. -Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) { - this.valueWindow_ = [minValue, maxValue]; this.drawGraph_(); if (this.attr_("zoomCallback")) { - var xRange = this.xAxisRange(); - this.attr_("zoomCallback")(xRange[0], xRange[1], minValue, maxValue); + 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 = null; + var dirty = false; if (this.dateWindow_ != null) { - dirty = 1; + dirty = true; this.dateWindow_ = null; } - if (this.valueWindow_ != null) { - dirty = 1; - this.valueWindow_ = this.valueRange_; + + for (var i = 0; i < this.axes_.length; i++) { + if (this.axes_[i].valueWindow != null) { + dirty = true; + delete this.axes_[i].valueWindow; + } } if (dirty) { @@ -1096,9 +1136,7 @@ Dygraph.prototype.doUnzoom_ = function() { if (this.attr_("zoomCallback")) { var minDate = this.rawData_[0][0]; var maxDate = this.rawData_[this.rawData_.length - 1][0]; - var minValue = this.yAxisRange()[0]; - var maxValue = this.yAxisRange()[1]; - this.attr_("zoomCallback")(minDate, maxDate, minValue, maxValue); + this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); } } }; @@ -1769,7 +1807,7 @@ Dygraph.prototype.predraw_ = function() { // The roller sits in the bottom left corner of the chart. We don't know where // this will be until the options are available, so it's positioned here. - this.roller_ = this.createRollInterface_(); + this.createRollInterface_(); // Same thing applies for the labelsDiv. It's right edge should be flush with // the right edge of the charting area (which may not be the same as the right @@ -1858,11 +1896,6 @@ Dygraph.prototype.drawGraph_ = function() { } var seriesExtremes = this.extremeValues_(series); - extremes[seriesName] = seriesExtremes; - var thisMinY = seriesExtremes[0]; - var thisMaxY = seriesExtremes[1]; - if (minY === null || (thisMinY != null && thisMinY < minY)) minY = thisMinY; - if (maxY === null || (thisMaxY != null && thisMaxY > maxY)) maxY = thisMaxY; if (bars) { for (var j=0; j maxY) - maxY = cumulative_y[x]; + if (cumulative_y[x] > seriesExtremes[1]) { + seriesExtremes[1] = cumulative_y[x]; + } + if (cumulative_y[x] < seriesExtremes[0]) { + seriesExtremes[0] = cumulative_y[x]; + } } } + extremes[seriesName] = seriesExtremes; datasets[i] = series; } @@ -1901,7 +1940,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 } ); @@ -1937,7 +1975,7 @@ Dygraph.prototype.computeYAxes_ = function() { // Get a list of series names. var labels = this.attr_("labels"); - var series = []; + var series = {}; for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1); // all options which could be applied per-axis: @@ -1993,6 +2031,17 @@ Dygraph.prototype.computeYAxes_ = function() { this.seriesToAxisMap_[seriesName] = idx; } } + + // Now we remove series from seriesToAxisMap_ which are not visible. We do + // this last so that hiding the first series doesn't destroy the axis + // properties of the primary axis. + var seriesToAxisFiltered = {}; + var vis = this.visibility(); + for (var i = 1; i < labels.length; i++) { + var s = labels[i]; + if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s]; + } + this.seriesToAxisMap_ = seriesToAxisFiltered; }; /** @@ -2027,10 +2076,16 @@ 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. + // Calculate the extremes of extremes. var series = seriesForAxis[i]; var minY = Infinity; // extremes[series[0]][0]; var maxY = -Infinity; // extremes[series[0]][1]; @@ -2773,7 +2828,7 @@ Dygraph.prototype.visibility = function() { */ Dygraph.prototype.setVisibility = function(num, value) { var x = this.visibility(); - if (num < 0 && num >= x.length) { + if (num < 0 || num >= x.length) { this.warn("invalid series number in setVisibility: " + num); } else { x[num] = value;