+
+ for (var i = 0; i < this.axes_.length; i++) {
+ if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) {
+ dirty = true;
+ dirtyY = true;
+ }
+ }
+
+ // Clear any selection, since it's likely to be drawn in the wrong place.
+ this.clearSelection();
+
+ if (dirty) {
+ this.zoomed_x_ = false;
+ this.zoomed_y_ = false;
+
+ var minDate = this.rawData_[0][0];
+ var maxDate = this.rawData_[this.rawData_.length - 1][0];
+
+ // With only one frame, don't bother calculating extreme ranges.
+ // TODO(danvk): merge this block w/ the code below.
+ if (!this.attr_("animatedZooms")) {
+ this.dateWindow_ = null;
+ for (i = 0; i < this.axes_.length; i++) {
+ if (this.axes_[i].valueWindow !== null) {
+ delete this.axes_[i].valueWindow;
+ }
+ }
+ this.drawGraph_();
+ if (this.attr_("zoomCallback")) {
+ this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
+ }
+ return;
+ }
+
+ var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null;
+ if (dirtyX) {
+ oldWindow = this.xAxisRange();
+ newWindow = [minDate, maxDate];
+ }
+
+ if (dirtyY) {
+ oldValueRanges = this.yAxisRanges();
+ // TODO(danvk): this is pretty inefficient
+ var packed = this.gatherDatasets_(this.rolledSeries_, null);
+ var extremes = packed[1];
+
+ // this has the side-effect of modifying this.axes_.
+ // this doesn't make much sense in this context, but it's convenient (we
+ // need this.axes_[*].extremeValues) and not harmful since we'll be
+ // calling drawGraph_ shortly, which clobbers these values.
+ this.computeYAxisRanges_(extremes);
+
+ newValueRanges = [];
+ for (i = 0; i < this.axes_.length; i++) {
+ var axis = this.axes_[i];
+ newValueRanges.push(axis.valueRange != null ? axis.valueRange : axis.extremeRange);
+ }
+ }
+
+ var that = this;
+ this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges,
+ function() {
+ that.dateWindow_ = null;
+ for (var i = 0; i < that.axes_.length; i++) {
+ if (that.axes_[i].valueWindow !== null) {
+ delete that.axes_[i].valueWindow;
+ }
+ }
+ if (that.attr_("zoomCallback")) {
+ that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges());
+ }
+ });
+ }
+};
+
+/**
+ * Combined animation logic for all zoom functions.
+ * either the x parameters or y parameters may be null.
+ * @private
+ */
+Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) {
+ var steps = this.attr_("animatedZooms") ? Dygraph.ANIMATION_STEPS : 1;
+
+ var windows = [];
+ var valueRanges = [];
+ var step, frac;
+
+ if (oldXRange !== null && newXRange !== null) {
+ for (step = 1; step <= steps; step++) {
+ frac = Dygraph.zoomAnimationFunction(step, steps);
+ windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0],
+ oldXRange[1]*(1-frac) + frac*newXRange[1]];
+ }
+ }
+
+ if (oldYRanges !== null && newYRanges !== null) {
+ for (step = 1; step <= steps; step++) {
+ frac = Dygraph.zoomAnimationFunction(step, steps);
+ var thisRange = [];
+ for (var j = 0; j < this.axes_.length; j++) {
+ thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0],
+ oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]);
+ }
+ valueRanges[step-1] = thisRange;
+ }
+ }
+
+ var that = this;
+ Dygraph.repeatAndCleanup(function(step) {
+ if (valueRanges.length) {
+ for (var i = 0; i < that.axes_.length; i++) {
+ var w = valueRanges[step][i];
+ that.axes_[i].valueWindow = [w[0], w[1]];
+ }
+ }
+ if (windows.length) {
+ that.dateWindow_ = windows[step];
+ }
+ that.drawGraph_();
+ }, steps, Dygraph.ANIMATION_DURATION / steps, callback);
+};
+
+/**
+ * Get the current graph's area object.
+ *
+ * Returns: {x, y, w, h}
+ */
+Dygraph.prototype.getArea = function() {
+ return this.plotter_.area;
+};
+
+/**
+ * Convert a mouse event to DOM coordinates relative to the graph origin.
+ *
+ * Returns a two-element array: [X, Y].
+ */
+Dygraph.prototype.eventToDomCoords = function(event) {
+ var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
+ var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_);
+ return [canvasx, canvasy];
+};
+
+/**
+ * Given a canvas X coordinate, find the closest row.
+ * @param {Number} domX graph-relative DOM X coordinate
+ * Returns: row number, integer
+ * @private
+ */
+Dygraph.prototype.findClosestRow = function(domX) {
+ var minDistX = Infinity;
+ var idx = -1;
+ var points = this.layout_.points;
+ var l = points.length;
+ for (var i = 0; i < l; i++) {
+ var point = points[i];
+ if (!Dygraph.isValidPoint(point, true)) continue;
+ var dist = Math.abs(point.canvasx - domX);
+ if (dist < minDistX) {
+ minDistX = dist;
+ idx = i;
+ }
+ }
+ return this.idxToRow_(idx);
+};
+
+/**
+ * Given canvas X,Y coordinates, find the closest point.
+ *
+ * This finds the individual data point across all visible series
+ * that's closest to the supplied DOM coordinates using the standard
+ * Euclidean X,Y distance.
+ *
+ * @param {Number} domX graph-relative DOM X coordinate
+ * @param {Number} domY graph-relative DOM Y coordinate
+ * Returns: {row, seriesName, point}
+ * @private
+ */
+Dygraph.prototype.findClosestPoint = function(domX, domY) {
+ var minDist = Infinity;
+ var idx = -1;
+ var points = this.layout_.points;
+ var dist, dx, dy, point, closestPoint, closestSeries;
+ for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+ var first = this.layout_.setPointsOffsets[setIdx];
+ var len = this.layout_.setPointsLengths[setIdx];
+ for (var i = 0; i < len; ++i) {
+ var point = points[first + i];
+ if (!Dygraph.isValidPoint(point)) continue;
+ dx = point.canvasx - domX;
+ dy = point.canvasy - domY;
+ dist = dx * dx + dy * dy;
+ if (dist < minDist) {
+ minDist = dist;
+ closestPoint = point;
+ closestSeries = setIdx;
+ idx = i;
+ }
+ }
+ }
+ var name = this.layout_.setNames[closestSeries];
+ return {
+ row: idx + this.getLeftBoundary_(),
+ seriesName: name,
+ point: closestPoint
+ };
+};
+
+/**
+ * Given canvas X,Y coordinates, find the touched area in a stacked graph.
+ *
+ * This first finds the X data point closest to the supplied DOM X coordinate,
+ * then finds the series which puts the Y coordinate on top of its filled area,
+ * using linear interpolation between adjacent point pairs.
+ *
+ * @param {Number} domX graph-relative DOM X coordinate
+ * @param {Number} domY graph-relative DOM Y coordinate
+ * Returns: {row, seriesName, point}
+ * @private
+ */
+Dygraph.prototype.findStackedPoint = function(domX, domY) {
+ var row = this.findClosestRow(domX);
+ var boundary = this.getLeftBoundary_();
+ var rowIdx = row - boundary;
+ var points = this.layout_.points;
+ var closestPoint, closestSeries;
+ for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+ var first = this.layout_.setPointsOffsets[setIdx];
+ var len = this.layout_.setPointsLengths[setIdx];
+ if (rowIdx >= len) continue;
+ var p1 = points[first + rowIdx];
+ if (!Dygraph.isValidPoint(p1)) continue;
+ var py = p1.canvasy;
+ if (domX > p1.canvasx && rowIdx + 1 < len) {
+ // interpolate series Y value using next point
+ var p2 = points[first + rowIdx + 1];
+ if (Dygraph.isValidPoint(p2)) {
+ var dx = p2.canvasx - p1.canvasx;
+ if (dx > 0) {
+ var r = (domX - p1.canvasx) / dx;
+ py += r * (p2.canvasy - p1.canvasy);
+ }
+ }
+ } else if (domX < p1.canvasx && rowIdx > 0) {
+ // interpolate series Y value using previous point
+ var p0 = points[first + rowIdx - 1];
+ if (Dygraph.isValidPoint(p0)) {
+ var dx = p1.canvasx - p0.canvasx;
+ if (dx > 0) {
+ var r = (p1.canvasx - domX) / dx;
+ py += r * (p0.canvasy - p1.canvasy);
+ }
+ }
+ }
+ // Stop if the point (domX, py) is above this series' upper edge
+ if (setIdx == 0 || py < domY) {
+ closestPoint = p1;
+ closestSeries = setIdx;
+ }
+ }
+ var name = this.layout_.setNames[closestSeries];
+ return {
+ row: row,
+ seriesName: name,
+ point: closestPoint
+ };