+ * Converts a y for an axis to a percentage from the top to the
+ * bottom of the drawing area.
+ *
+ * If the coordinate represents a value visible on the canvas, then
+ * the value will be between 0 and 1, where 0 is the top of the canvas.
+ * However, this method will return values outside the range, as
+ * values can fall outside the canvas.
+ *
+ * If y is null, this returns null.
+ * if axis is null, this uses the first axis.
+ *
+ * @param { Number } y The data y-coordinate.
+ * @param { Number } [axis] The axis number on which the data coordinate lives.
+ * @return { Number } A fraction in [0, 1] where 0 = the top edge.
+ */
+Dygraph.prototype.toPercentYCoord = function(y, axis) {
+ if (y === null) {
+ return null;
+ }
+ if (typeof(axis) == "undefined") axis = 0;
+
+ var yRange = this.yAxisRange(axis);
+
+ var pct;
+ if (!this.axes_[axis].logscale) {
+ // yRange[1] - y is unit distance from the bottom.
+ // yRange[1] - yRange[0] is the scale of the range.
+ // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
+ pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
+ } else {
+ var logr1 = Dygraph.log10(yRange[1]);
+ pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
+ }
+ return pct;
+};
+
+/**
+ * Converts an x value to a percentage from the left to the right of
+ * the drawing area.
+ *
+ * If the coordinate represents a value visible on the canvas, then
+ * the value will be between 0 and 1, where 0 is the left of the canvas.
+ * However, this method will return values outside the range, as
+ * values can fall outside the canvas.
+ *
+ * If x is null, this returns null.
+ * @param { Number } x The data x-coordinate.
+ * @return { Number } A fraction in [0, 1] where 0 = the left edge.
+ */
+Dygraph.prototype.toPercentXCoord = function(x) {
+ if (x === null) {
+ return null;
+ }
+
+ var xRange = this.xAxisRange();
+ return (x - xRange[0]) / (xRange[1] - xRange[0]);
+};
+
+/**
+ * Returns the number of columns (including the independent variable).
+ * @return { Integer } The number of columns.
+ */
+Dygraph.prototype.numColumns = function() {
+ return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
+};
+
+/**
+ * Returns the number of rows (excluding any header/label row).
+ * @return { Integer } The number of rows, less any header.
+ */
+Dygraph.prototype.numRows = function() {
+ return this.rawData_.length;
+};
+
+/**
+ * Returns the full range of the x-axis, as determined by the most extreme
+ * values in the data set. Not affected by zooming, visibility, etc.
+ * TODO(danvk): merge w/ xAxisExtremes
+ * @return { Array<Number> } A [low, high] pair
+ * @private
+ */
+Dygraph.prototype.fullXRange_ = function() {
+ if (this.numRows() > 0) {
+ return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]];
+ } else {
+ return [0, 1];
+ }
+};
+
+/**
+ * Returns the value in the given row and column. If the row and column exceed
+ * the bounds on the data, returns null. Also returns null if the value is
+ * missing.
+ * @param { Number} row The row number of the data (0-based). Row 0 is the
+ * first row of data, not a header row.
+ * @param { Number} col The column number of the data (0-based)
+ * @return { Number } The value in the specified cell or null if the row/col
+ * were out of range.
+ */
+Dygraph.prototype.getValue = function(row, col) {
+ if (row < 0 || row > this.rawData_.length) return null;
+ if (col < 0 || col > this.rawData_[row].length) return null;
+
+ return this.rawData_[row][col];
+};
+
+/**
+ * Generates interface elements for the Dygraph: a containing div, a div to
+ * display the current point, and a textbox to adjust the rolling average
+ * period. Also creates the Renderer/Layout elements.
+ * @private
+ */
+Dygraph.prototype.createInterface_ = function() {
+ // Create the all-enclosing graph div
+ var enclosing = this.maindiv_;
+
+ this.graphDiv = document.createElement("div");
+ this.graphDiv.style.width = this.width_ + "px";
+ this.graphDiv.style.height = this.height_ + "px";
+ enclosing.appendChild(this.graphDiv);
+
+ // Create the canvas for interactive parts of the chart.
+ this.canvas_ = Dygraph.createCanvas();
+ this.canvas_.style.position = "absolute";
+ this.canvas_.width = this.width_;
+ this.canvas_.height = this.height_;
+ this.canvas_.style.width = this.width_ + "px"; // for IE
+ this.canvas_.style.height = this.height_ + "px"; // for IE
+
+ this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
+
+ // ... and for static parts of the chart.
+ this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
+ this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
+
+ if (this.attr_('showRangeSelector')) {
+ // The range selector must be created here so that its canvases and contexts get created here.
+ // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
+ // The range selector also sets xAxisHeight in order to reserve space.
+ this.rangeSelector_ = new DygraphRangeSelector(this);
+ }
+
+ // The interactive parts of the graph are drawn on top of the chart.
+ this.graphDiv.appendChild(this.hidden_);
+ this.graphDiv.appendChild(this.canvas_);
+ this.mouseEventElement_ = this.createMouseEventElement_();
+
+ // Create the grapher
+ this.layout_ = new DygraphLayout(this);
+
+ if (this.rangeSelector_) {
+ // This needs to happen after the graph canvases are added to the div and the layout object is created.
+ this.rangeSelector_.addToGraph(this.graphDiv, this.layout_);
+ }
+
+ var dygraph = this;
+
+ this.mouseMoveHandler = function(e) {
+ dygraph.mouseMove_(e);
+ };
+ Dygraph.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
+
+ this.mouseOutHandler = function(e) {
+ dygraph.mouseOut_(e);
+ };
+ Dygraph.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
+
+ this.createDragInterface_();
+
+ this.resizeHandler = function(e) {
+ dygraph.resize();
+ };
+
+ // Update when the window is resized.
+ // TODO(danvk): drop frames depending on complexity of the chart.
+ Dygraph.addEvent(window, 'resize', this.resizeHandler);
+};
+
+/**
+ * Detach DOM elements in the dygraph and null out all data references.
+ * Calling this when you're done with a dygraph can dramatically reduce memory
+ * usage. See, e.g., the tests/perf.html example.
+ */
+Dygraph.prototype.destroy = function() {
+ var removeRecursive = function(node) {
+ while (node.hasChildNodes()) {
+ removeRecursive(node.firstChild);
+ node.removeChild(node.firstChild);
+ }
+ };
+
+ // remove mouse event handlers
+ Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler);
+ Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler);
+ Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseUpHandler_);
+ removeRecursive(this.maindiv_);
+
+ var nullOut = function(obj) {
+ for (var n in obj) {
+ if (typeof(obj[n]) === 'object') {
+ obj[n] = null;
+ }
+ }
+ };
+ // remove event handlers
+ Dygraph.removeEvent(window,'resize',this.resizeHandler);
+ this.resizeHandler = null;
+ // These may not all be necessary, but it can't hurt...
+ nullOut(this.layout_);
+ nullOut(this.plotter_);
+ nullOut(this);
+};
+
+/**
+ * Creates the canvas on which the chart will be drawn. Only the Renderer ever
+ * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
+ * or the zoom rectangles) is done on this.canvas_.
+ * @param {Object} canvas The Dygraph canvas over which to overlay the plot
+ * @return {Object} The newly-created canvas
+ * @private
+ */
+Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
+ var h = Dygraph.createCanvas();
+ h.style.position = "absolute";
+ // TODO(danvk): h should be offset from canvas. canvas needs to include
+ // some extra area to make it easier to zoom in on the far left and far
+ // right. h needs to be precisely the plot area, so that clipping occurs.
+ h.style.top = canvas.style.top;
+ h.style.left = canvas.style.left;
+ h.width = this.width_;
+ h.height = this.height_;
+ h.style.width = this.width_ + "px"; // for IE
+ h.style.height = this.height_ + "px"; // for IE
+ return h;
+};
+
+/**
+ * Creates an overlay element used to handle mouse events.
+ * @return {Object} The mouse event element.
+ * @private
+ */
+Dygraph.prototype.createMouseEventElement_ = function() {
+ if (this.isUsingExcanvas_) {
+ var elem = document.createElement("div");
+ elem.style.position = 'absolute';
+ elem.style.backgroundColor = 'white';
+ elem.style.filter = 'alpha(opacity=0)';
+ elem.style.width = this.width_ + "px";
+ elem.style.height = this.height_ + "px";
+ this.graphDiv.appendChild(elem);
+ return elem;
+ } else {
+ return this.canvas_;
+ }
+};
+
+/**
+ * Generate a set of distinct colors for the data series. This is done with a
+ * color wheel. Saturation/Value are customizable, and the hue is
+ * equally-spaced around the color wheel. If a custom set of colors is
+ * specified, that is used instead.
+ * @private
+ */
+Dygraph.prototype.setColors_ = function() {
+ var num = this.attr_("labels").length - 1;
+ this.colors_ = [];
+ var colors = this.attr_('colors');
+ var i;
+ if (!colors) {
+ var sat = this.attr_('colorSaturation') || 1.0;
+ var val = this.attr_('colorValue') || 0.5;
+ var half = Math.ceil(num / 2);
+ for (i = 1; i <= num; i++) {
+ if (!this.visibility()[i-1]) continue;
+ // alternate colors for high contrast.
+ var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
+ var hue = (1.0 * idx/ (1 + num));
+ this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
+ }
+ } else {
+ for (i = 0; i < num; i++) {
+ if (!this.visibility()[i]) continue;
+ var colorStr = colors[i % colors.length];
+ this.colors_.push(colorStr);
+ }
+ }
+
+ this.plotter_.setColors(this.colors_);
+};
+
+/**
+ * Return the list of colors. This is either the list of colors passed in the
+ * attributes or the autogenerated list of rgb(r,g,b) strings.
+ * This does not return colors for invisible series.
+ * @return {Array<string>} The list of colors.
+ */
+Dygraph.prototype.getColors = function() {
+ return this.colors_;
+};
+
+/**
+ * Returns a few attributes of a series, i.e. its color, its visibility, which
+ * axis it's assigned to, and its column in the original data.
+ * Returns null if the series does not exist.
+ * Otherwise, returns an object with column, visibility, color and axis properties.
+ * The "axis" property will be set to 1 for y1 and 2 for y2.
+ * The "column" property can be fed back into getValue(row, column) to get
+ * values for this series.
+ */
+Dygraph.prototype.getPropertiesForSeries = function(series_name) {
+ var idx = -1;
+ var labels = this.getLabels();
+ for (var i = 1; i < labels.length; i++) {
+ if (labels[i] == series_name) {
+ idx = i;
+ break;
+ }
+ }
+ if (idx == -1) return null;
+
+ return {
+ name: series_name,
+ column: idx,
+ visible: this.visibility()[idx - 1],
+ color: this.plotter_.colors[series_name],
+ axis: 1 + this.seriesToAxisMap_[series_name]
+ };
+};
+
+/**
+ * Create the text box to adjust the averaging period
+ * @private
+ */
+Dygraph.prototype.createRollInterface_ = function() {
+ // 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 area = this.plotter_.area;
+ var textAttr = { "position": "absolute",
+ "zIndex": 10,
+ "top": (area.y + area.h - 25) + "px",
+ "left": (area.x + 1) + "px",
+ "display": display
+ };
+ this.roller_.size = "2";
+ this.roller_.value = this.rollPeriod_;
+ for (var name in textAttr) {
+ if (textAttr.hasOwnProperty(name)) {
+ this.roller_.style[name] = textAttr[name];
+ }
+ }
+
+ var dygraph = this;
+ this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
+};
+
+/**
+ * @private
+ * Converts page the x-coordinate of the event to pixel x-coordinates on the
+ * canvas (i.e. DOM Coords).
+ */
+Dygraph.prototype.dragGetX_ = function(e, context) {
+ return Dygraph.pageX(e) - context.px;
+};
+
+/**
+ * @private
+ * Converts page the y-coordinate of the event to pixel y-coordinates on the
+ * canvas (i.e. DOM Coords).
+ */
+Dygraph.prototype.dragGetY_ = function(e, context) {
+ return Dygraph.pageY(e) - context.py;
+};
+
+/**
+ * Set up all the mouse handlers needed to capture dragging behavior for zoom
+ * events.
+ * @private
+ */
+Dygraph.prototype.createDragInterface_ = function() {
+ var context = {
+ // Tracks whether the mouse is down right now
+ isZooming: false,
+ isPanning: false, // is this drag part of a pan?
+ is2DPan: false, // if so, is that pan 1- or 2-dimensional?
+ dragStartX: null, // pixel coordinates
+ dragStartY: null, // pixel coordinates
+ dragEndX: null, // pixel coordinates
+ dragEndY: null, // pixel coordinates
+ dragDirection: null,
+ prevEndX: null, // pixel coordinates
+ prevEndY: null, // pixel coordinates
+ prevDragDirection: null,
+ cancelNextDblclick: false, // see comment in dygraph-interaction-model.js
+
+ // The value on the left side of the graph when a pan operation starts.
+ initialLeftmostDate: null,
+
+ // The number of units each pixel spans. (This won't be valid for log
+ // scales)
+ xUnitsPerPixel: null,
+
+ // TODO(danvk): update this comment
+ // The range in second/value units that the viewport encompasses during a
+ // panning operation.
+ dateRange: null,
+
+ // Top-left corner of the canvas, in DOM coords
+ // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
+ px: 0,
+ py: 0,
+
+ // Values for use with panEdgeFraction, which limit how far outside the
+ // graph's data boundaries it can be panned.
+ boundedDates: null, // [minDate, maxDate]
+ boundedValues: null, // [[minValue, maxValue] ...]
+
+ initializeMouseDown: function(event, g, context) {
+ // prevents mouse drags from selecting page text.
+ if (event.preventDefault) {
+ event.preventDefault(); // Firefox, Chrome, etc.
+ } else {
+ event.returnValue = false; // IE
+ event.cancelBubble = true;
+ }
+
+ context.px = Dygraph.findPosX(g.canvas_);
+ context.py = Dygraph.findPosY(g.canvas_);
+ context.dragStartX = g.dragGetX_(event, context);
+ context.dragStartY = g.dragGetY_(event, context);
+ context.cancelNextDblclick = false;
+ }
+ };
+
+ var interactionModel = this.attr_("interactionModel");
+
+ // Self is the graph.
+ var self = this;
+
+ // Function that binds the graph and context to the handler.
+ var bindHandler = function(handler) {
+ return function(event) {
+ handler(event, self, context);
+ };
+ };
+
+ for (var eventName in interactionModel) {
+ if (!interactionModel.hasOwnProperty(eventName)) continue;
+ Dygraph.addEvent(this.mouseEventElement_, eventName,
+ bindHandler(interactionModel[eventName]));
+ }
+
+ // If the user releases the mouse button during a drag, but not over the
+ // canvas, then it doesn't count as a zooming action.
+ this.mouseUpHandler_ = function(event) {
+ if (context.isZooming || context.isPanning) {
+ context.isZooming = false;
+ context.dragStartX = null;
+ context.dragStartY = null;
+ }
+
+ if (context.isPanning) {
+ context.isPanning = false;
+ context.draggingDate = null;
+ context.dateRange = null;
+ for (var i = 0; i < self.axes_.length; i++) {
+ delete self.axes_[i].draggingValue;
+ delete self.axes_[i].dragValueRange;
+ }
+ }
+ };
+
+ Dygraph.addEvent(document, 'mouseup', this.mouseUpHandler_);
+};
+
+/**
+ * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
+ * 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(direction, startX, endX, startY,
+ endY, prevDirection, prevEndX,
+ prevEndY) {
+ var ctx = this.canvas_ctx_;
+
+ // Clean up from the previous rect if necessary
+ if (prevDirection == Dygraph.HORIZONTAL) {
+ ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y,
+ Math.abs(startX - prevEndX), this.layout_.getPlotArea().h);
+ } else if (prevDirection == Dygraph.VERTICAL){
+ ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY),
+ this.layout_.getPlotArea().w, Math.abs(startY - prevEndY));
+ }
+
+ // Draw a light-grey rectangle to show the new viewing area
+ if (direction == Dygraph.HORIZONTAL) {
+ if (endX && startX) {
+ ctx.fillStyle = "rgba(128,128,128,0.33)";
+ ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y,
+ Math.abs(endX - startX), this.layout_.getPlotArea().h);
+ }
+ } else if (direction == Dygraph.VERTICAL) {
+ if (endY && startY) {
+ ctx.fillStyle = "rgba(128,128,128,0.33)";
+ ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY),
+ this.layout_.getPlotArea().w, Math.abs(endY - startY));
+ }
+ }
+
+ if (this.isUsingExcanvas_) {
+ this.currentZoomRectArgs_ = [direction, startX, endX, startY, endY, 0, 0, 0];
+ }
+};
+
+/**
+ * Clear the zoom rectangle (and perform no zoom).
+ * @private
+ */
+Dygraph.prototype.clearZoomRect_ = function() {
+ this.currentZoomRectArgs_ = null;
+ this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+};
+
+/**
+ * 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.doZoomX_ = function(lowX, highX) {
+ this.currentZoomRectArgs_ = null;
+ // Find the earliest and latest dates contained in this canvasx range.
+ // Convert the call to date ranges of the raw data.
+ var minDate = this.toDataXCoord(lowX);
+ var maxDate = this.toDataXCoord(highX);
+ this.doZoomXDates_(minDate, maxDate);
+};
+
+/**
+ * Transition function to use in animations. Returns values between 0.0
+ * (totally old values) and 1.0 (totally new values) for each frame.
+ * @private
+ */
+Dygraph.zoomAnimationFunction = function(frame, numFrames) {
+ var k = 1.5;
+ return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames));
+};
+
+/**
+ * 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) {
+ // TODO(danvk): when yAxisRange is null (i.e. "fit to data", the animation
+ // can produce strange effects. Rather than the y-axis transitioning slowly
+ // between values, it can jerk around.)
+ var old_window = this.xAxisRange();
+ var new_window = [minDate, maxDate];
+ this.zoomed_x_ = true;
+ var that = this;
+ this.doAnimatedZoom(old_window, new_window, null, null, function() {
+ if (that.attr_("zoomCallback")) {
+ that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges());
+ }
+ });
+};
+
+/**
+ * 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) {
+ this.currentZoomRectArgs_ = null;
+ // 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 oldValueRanges = this.yAxisRanges();
+ var newValueRanges = [];
+ for (var i = 0; i < this.axes_.length; i++) {
+ var hi = this.toDataYCoord(lowY, i);
+ var low = this.toDataYCoord(highY, i);
+ newValueRanges.push([low, hi]);
+ }
+
+ this.zoomed_y_ = true;
+ var that = this;
+ this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, function() {
+ if (that.attr_("zoomCallback")) {
+ var xRange = that.xAxisRange();
+ that.attr_("zoomCallback")(xRange[0], xRange[1], that.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, dirtyX = false, dirtyY = false;
+ if (this.dateWindow_ !== null) {
+ dirty = true;
+ dirtyX = true;
+ }
+
+ 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