X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=b8ce5e2347ddfbb6a7c12e7d4cbf0add63c3b702;hb=c65f2303664330f03787d4e0bc7da5f2d9b4bc5f;hp=4adb7164760aca3d0a0d540d4e6cfb9572c21f74;hpb=e99fde0585bfc07ff26c9dc3a5ca30ea339cc81f;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 4adb716..b8ce5e2 100644 --- a/dygraph.js +++ b/dygraph.js @@ -135,6 +135,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; @@ -171,7 +176,13 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.previousVerticalX_ = -1; this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; + // valueRange and valueWindow are similar, but not the same. valueRange is a + // locally-stored copy of the attribute. valueWindow starts off the same as + // valueRange but is impacted by zoom or pan effects. valueRange is kept + // around to restore the original value back to valueRange. this.valueRange_ = attrs.valueRange || null; + this.valueWindow_ = this.valueRange_; + this.wilsonInterval_ = attrs.wilsonInterval || true; this.is_initial_draw_ = true; this.annotations_ = []; @@ -240,8 +251,13 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.start_(); }; -Dygraph.prototype.attr_ = function(name) { - if (typeof(this.user_attrs_[name]) != 'undefined') { +Dygraph.prototype.attr_ = function(name, seriesName) { + if (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 (typeof(this.user_attrs_[name]) != 'undefined') { return this.user_attrs_[name]; } else if (typeof(this.attrs_[name]) != 'undefined') { return this.attrs_[name]; @@ -746,8 +762,20 @@ Dygraph.prototype.createDragInterface_ = function() { var dragEndX = null; var dragEndY = null; var prevEndX = null; + var prevEndY = null; + var prevDragDirection = null; + + // 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; + + // 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; @@ -761,18 +789,41 @@ 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 + var 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); // 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]; - self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange; - self.dateWindow_[1] = self.dateWindow_[0] + dateRange; + + // 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 ]; + } self.drawGraph_(self.rawData_); } }); @@ -785,11 +836,21 @@ 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. + if (!self.dateWindow_ && !self.valueWindow_) return; + isPanning = true; - dateRange = self.dateWindow_[1] - self.dateWindow_[0]; + var xRange = self.xAxisRange(); + dateRange = xRange[1] - xRange[0]; + var yRange = self.yAxisRange(); + valueRange = yRange[1] - yRange[0]; + + // TODO(konigsberg): Switch from all this math to toDataCoords? + // Seems to work for the dragging value. draggingDate = (dragStartX / self.width_) * dateRange + - self.dateWindow_[0]; + xRange[0]; + var r = self.toDataCoords(null, dragStartY); + draggingValue = r[1]; } else { isZooming = true; } @@ -807,7 +868,9 @@ Dygraph.prototype.createDragInterface_ = function() { if (isPanning) { isPanning = false; draggingDate = null; + draggingValue = null; dateRange = null; + valueRange = null; } }); @@ -857,9 +920,12 @@ Dygraph.prototype.createDragInterface_ = function() { } } - if (regionWidth >= 10) { - self.doZoom_(Math.min(dragStartX, dragEndX), + if (regionWidth >= 10 && regionWidth > regionHeight) { + self.doZoomX_(Math.min(dragStartX, dragEndX), Math.max(dragStartX, dragEndX)); + } else if (regionHeight >= 10 && regionHeight > regionWidth){ + self.doZoomY_(Math.min(dragStartY, dragEndY), + Math.max(dragStartY, dragEndY)); } else { self.canvas_.getContext("2d").clearRect(0, 0, self.canvas_.width, @@ -873,20 +939,18 @@ Dygraph.prototype.createDragInterface_ = function() { if (isPanning) { isPanning = false; draggingDate = null; + draggingValue = 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_(self.rawData_); - 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_(); }); }; @@ -895,49 +959,158 @@ 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_(this.rawData_); 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. 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. + * + * @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); +}; + +/** + * 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 + */ +Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) { + this.valueWindow_ = [minValue, maxValue]; + this.drawGraph_(this.rawData_); + if (this.attr_("zoomCallback")) { + var xRange = this.xAxisRange(); + this.attr_("zoomCallback")(xRange[0], xRange[1], minValue, maxValue); + } +}; + +/** + * 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; + if (this.dateWindow_ != null) { + dirty = 1; + this.dateWindow_ = null; + } + if (this.valueWindow_ != null) { + dirty = 1; + this.valueWindow_ = this.valueRange_; + } + + if (dirty) { + // Putting the drawing operation before the callback because it resets + // yAxisRange. + this.drawGraph_(this.rawData_); + 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); + } } }; @@ -1017,11 +1190,18 @@ Dygraph.prototype.mouseMove_ = function(event) { */ Dygraph.prototype.updateSelection_ = function() { // Clear the previously drawn vertical, if there is one - var circleSize = this.attr_('highlightCircleSize'); var ctx = this.canvas_.getContext("2d"); if (this.previousVerticalX_ >= 0) { + // Determine the maximum highlight circle size. + var maxCircleSize = 0; + var labels = this.attr_('labels'); + for (var i = 1; i < labels.length; i++) { + var r = this.attr_('highlightCircleSize', labels[i]); + if (r > maxCircleSize) maxCircleSize = r; + } var px = this.previousVerticalX_; - ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_); + ctx.clearRect(px - maxCircleSize - 1, 0, + 2 * maxCircleSize + 2, this.height_); } var isOK = function(x) { return x && !isNaN(x); }; @@ -1037,7 +1217,7 @@ Dygraph.prototype.updateSelection_ = function() { if (this.attr_('showLabelsOnHighlight')) { // Set the status message to indicate the selected point(s) for (var i = 0; i < this.selPoints_.length; i++) { - if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue; + if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue; if (!isOK(this.selPoints_[i].canvasy)) continue; if (this.attr_("labelsSeparateLines")) { replace += "
"; @@ -1057,6 +1237,8 @@ Dygraph.prototype.updateSelection_ = function() { ctx.save(); for (var i = 0; i < this.selPoints_.length; i++) { if (!isOK(this.selPoints_[i].canvasy)) continue; + var circleSize = + this.attr_('highlightCircleSize', this.selPoints_[i].name); ctx.beginPath(); ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name]; ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize, @@ -1087,7 +1269,13 @@ Dygraph.prototype.setSelection = function(row) { if (row !== false && row >= 0) { for (var i in this.layout_.datasets) { if (row < this.layout_.datasets[i].length) { - this.selPoints_.push(this.layout_.points[pos+row]); + var point = this.layout_.points[pos+row]; + + if (this.attr_("stackedGraph")) { + point = this.layout_.unstackPointAtIndex(pos+row); + } + + this.selPoints_.push(point); } pos += this.layout_.datasets[i].length; } @@ -1581,8 +1769,6 @@ Dygraph.prototype.drawGraph_ = function(data) { this.setColors_(); this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize'); - var connectSeparatedPoints = this.attr_('connectSeparatedPoints'); - // Loop over the fields (series). Go from the last to the first, // because if they're stacked that's how we accumulate the values. @@ -1593,6 +1779,8 @@ Dygraph.prototype.drawGraph_ = function(data) { for (var i = data[0].length - 1; i >= 1; i--) { if (!this.visibility()[i - 1]) continue; + var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); + var series = []; for (var j = 0; j < data.length; j++) { if (data[j][i] != null || !connectSeparatedPoints) { @@ -1674,10 +1862,10 @@ Dygraph.prototype.drawGraph_ = function(data) { } // Use some heuristics to come up with a good maxY value, unless it's been - // set explicitly by the user. - if (this.valueRange_ != null) { - this.addYTicks_(this.valueRange_[0], this.valueRange_[1]); - this.displayedYRange_ = this.valueRange_; + // set explicitly by the developer or end-user (via drag) + if (this.valueWindow_ != null) { + this.addYTicks_(this.valueWindow_[0], this.valueWindow_[1]); + this.displayedYRange_ = this.valueWindow_; } else { // This affects the calculation of span, below. if (this.attr_("includeZero") && minY > 0) { @@ -2137,6 +2325,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { var labels = [data.getColumnLabel(0)]; for (var i = 0; i < colIdx.length; i++) { labels.push(data.getColumnLabel(colIdx[i])); + if (this.attr_("errorBars")) i += 1; } this.attrs_.labels = labels; cols = labels.length; @@ -2148,8 +2337,8 @@ Dygraph.prototype.parseDataTable_ = function(data) { var row = []; if (typeof(data.getValue(i, 0)) === 'undefined' || data.getValue(i, 0) === null) { - this.warning("Ignoring row " + i + - " of DataTable because of undefined or null first column."); + this.warn("Ignoring row " + i + + " of DataTable because of undefined or null first column."); continue; } @@ -2297,15 +2486,24 @@ 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; } - if (attrs.valueRange) { + if ('valueRange' in attrs) { this.valueRange_ = attrs.valueRange; + this.valueWindow_ = attrs.valueRange; } + + // TODO(danvk): validate per-series options. + // Supported: + // strokeWidth + // pointSize + // drawPoints + // highlightCircleSize + Dygraph.update(this.user_attrs_, attrs); Dygraph.update(this.renderOptions_, attrs); @@ -2420,6 +2618,18 @@ Dygraph.prototype.annotations = function() { return this.annotations_; }; +/** + * Get the index of a series (column) given its name. The first column is the + * x-axis, so the data series start with index 1. + */ +Dygraph.prototype.indexFromSetName = function(name) { + var labels = this.attr_("labels"); + for (var i = 0; i < labels.length; i++) { + if (labels[i] == name) return i; + } + return null; +}; + Dygraph.addAnnotationRule = function() { if (Dygraph.addedAnnotationCSS) return;