From: Robert Konigsberg Date: Fri, 8 Oct 2010 14:21:30 +0000 (-0400) Subject: Merge branch 'master' of git://github.com/danvk/dygraphs X-Git-Tag: v1.0.0~613^2~9 X-Git-Url: https://adrianiainlam.tk/git/?a=commitdiff_plain;ds=inline;h=848b47c9adc94820b3e8110b2b1624bb0adbcc7d;hp=-c;p=dygraphs.git Merge branch 'master' of git://github.com/danvk/dygraphs --- 848b47c9adc94820b3e8110b2b1624bb0adbcc7d diff --combined dygraph-canvas.js index 182f29d,bf04e13..ac0e741 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@@ -19,7 -19,7 +19,7 @@@ DygraphLayout = function(dygraph, optio this.options = {}; // TODO(danvk): remove, use attr_ instead. Dygraph.update(this.options, options ? options : {}); this.datasets = new Array(); - this.annotations = new Array() + this.annotations = new Array(); }; DygraphLayout.prototype.attr_ = function(name) { @@@ -103,6 -103,13 +103,6 @@@ DygraphLayout.prototype._evaluateLineCh name: setName }; - // limit the x, y values so they do not overdraw - if (point.y <= 0.0) { - point.y = 0.0; - } - if (point.y >= 1.0) { - point.y = 1.0; - } this.points.push(point); } } @@@ -195,6 -202,35 +195,35 @@@ DygraphLayout.prototype.updateOptions Dygraph.update(this.options, new_options ? new_options : {}); }; + /** + * Return a copy of the point at the indicated index, with its yval unstacked. + * @param int index of point in layout_.points + */ + DygraphLayout.prototype.unstackPointAtIndex = function(idx) { + var point = this.points[idx]; + + // Clone the point since we modify it + var unstackedPoint = {}; + for (var i in point) { + unstackedPoint[i] = point[i]; + } + + if (!this.attr_("stackedGraph")) { + return unstackedPoint; + } + + // The unstacked yval is equal to the current yval minus the yval of the + // next point at the same xval. + for (var i = idx+1; i < this.points.length; i++) { + if (this.points[i].xval == point.xval) { + unstackedPoint.yval -= this.points[i].yval; + break; + } + } + + return unstackedPoint; + } + // Subclass PlotKit.CanvasRenderer to add: // 1. X/Y grid overlay // 2. Ability to draw error bars (if required) @@@ -748,13 -784,14 +777,14 @@@ DygraphCanvasRenderer.prototype._render for (var i = 0; i < setCount; i++) { var setName = setNames[i]; var color = this.colors[setName]; + var strokeWidth = this.dygraph_.attr_("strokeWidth", setName); // setup graphics context context.save(); var point = this.layout.points[0]; - var pointSize = this.dygraph_.attr_("pointSize"); + var pointSize = this.dygraph_.attr_("pointSize", setName); var prevX = null, prevY = null; - var drawPoints = this.dygraph_.attr_("drawPoints"); + var drawPoints = this.dygraph_.attr_("drawPoints", setName); var points = this.layout.points; for (var j = 0; j < points.length; j++) { var point = points[j]; @@@ -772,17 -809,20 +802,20 @@@ prevX = point.canvasx; prevY = point.canvasy; } else { - ctx.beginPath(); - ctx.strokeStyle = color; - ctx.lineWidth = this.options.strokeWidth; - ctx.moveTo(prevX, prevY); - if (stepPlot) { - ctx.lineTo(point.canvasx, prevY); + // TODO(danvk): figure out why this conditional is necessary. + if (strokeWidth) { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = strokeWidth; + ctx.moveTo(prevX, prevY); + if (stepPlot) { + ctx.lineTo(point.canvasx, prevY); + } + prevX = point.canvasx; + prevY = point.canvasy; + ctx.lineTo(prevX, prevY); + ctx.stroke(); } - prevX = point.canvasx; - prevY = point.canvasy; - ctx.lineTo(prevX, prevY); - ctx.stroke(); } if (drawPoints || isIsolated) { diff --combined dygraph.js index aea73ea,4cdc196..958f94b --- a/dygraph.js +++ b/dygraph.js @@@ -135,11 -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; @@@ -176,13 -171,7 +176,13 @@@ Dygraph.prototype.__init__ = function(d 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_ = []; @@@ -251,8 -240,13 +251,13 @@@ 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]; @@@ -757,20 -751,8 +762,20 @@@ Dygraph.prototype.createDragInterface_ 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; @@@ -784,41 -766,18 +789,41 @@@ 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 defiend 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_); } }); @@@ -831,21 -790,11 +836,21 @@@ 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; } @@@ -863,9 -812,7 +868,9 @@@ if (isPanning) { isPanning = false; draggingDate = null; + draggingValue = null; dateRange = null; + valueRange = null; } }); @@@ -915,12 -862,9 +920,12 @@@ } } - 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, @@@ -934,18 -878,20 +939,18 @@@ 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_(); }); }; @@@ -954,158 -900,49 +959,158 @@@ * 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); + } } }; @@@ -1185,11 -1022,18 +1190,18 @@@ Dygraph.prototype.mouseMove_ = function */ 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); }; @@@ -1205,7 -1049,7 +1217,7 @@@ 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 += "
"; @@@ -1225,6 -1069,8 +1237,8 @@@ 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, @@@ -1255,7 -1101,13 +1269,13 @@@ Dygraph.prototype.setSelection = functi 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; } @@@ -1749,8 -1601,6 +1769,6 @@@ Dygraph.prototype.drawGraph_ = function 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. @@@ -1761,6 -1611,8 +1779,8 @@@ 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) { @@@ -1842,10 -1694,10 +1862,10 @@@ } // 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) { @@@ -2305,6 -2157,7 +2325,7 @@@ Dygraph.prototype.parseDataTable_ = fun 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; @@@ -2316,8 -2169,8 +2337,8 @@@ 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; } @@@ -2474,6 -2327,14 +2495,14 @@@ Dygraph.prototype.updateOptions = funct if (attrs.valueRange) { this.valueRange_ = attrs.valueRange; } + + // TODO(danvk): validate per-series options. + // Supported: + // strokeWidth + // pointSize + // drawPoints + // highlightCircleSize + Dygraph.update(this.user_attrs_, attrs); Dygraph.update(this.renderOptions_, attrs); @@@ -2588,6 -2449,18 +2617,18 @@@ Dygraph.prototype.annotations = functio 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;