+ 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;
+ Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
+ dygraph.mouseMove_(e);
+ });
+ Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
+ dygraph.mouseOut_(e);
+ });
+
+ this.createStatusMessage_();
+ 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);
+ }
+ };
+ 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.
+ * @return {Array<string>} The list of colors.
+ */
+Dygraph.prototype.getColors = function() {
+ return this.colors_;
+};
+
+/**
+ * Create the div that contains information on the selected point(s)
+ * This goes in the top right of the canvas, unless an external div has already
+ * been specified.
+ * @private
+ */
+Dygraph.prototype.createStatusMessage_ = function() {
+ var userLabelsDiv = this.user_attrs_.labelsDiv;
+ if (userLabelsDiv && null !== userLabelsDiv &&
+ (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
+ this.user_attrs_.labelsDiv = document.getElementById(userLabelsDiv);
+ }
+ if (!this.attr_("labelsDiv")) {
+ var divWidth = this.attr_('labelsDivWidth');
+ var messagestyle = {
+ "position": "absolute",
+ "fontSize": "14px",
+ "zIndex": 10,
+ "width": divWidth + "px",
+ "top": "0px",
+ "left": (this.width_ - divWidth - 2) + "px",
+ "background": "white",
+ "textAlign": "left",
+ "overflow": "hidden"};
+ Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
+ var div = document.createElement("div");
+ div.className = "dygraph-legend";
+ for (var name in messagestyle) {
+ if (messagestyle.hasOwnProperty(name)) {
+ div.style[name] = messagestyle[name];
+ }
+ }
+ this.graphDiv.appendChild(div);
+ this.attrs_.labelsDiv = div;
+ }
+};
+
+/**
+ * Position the labels div so that:
+ * - its right edge is flush with the right edge of the charting area
+ * - its top edge is flush with the top edge of the charting area
+ * @private
+ */
+Dygraph.prototype.positionLabelsDiv_ = function() {
+ // Don't touch a user-specified labelsDiv.
+ if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
+
+ var area = this.plotter_.area;
+ var div = this.attr_("labelsDiv");
+ div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
+ div.style.top = area.y + "px";
+};
+
+/**
+ * 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,
+
+ // 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);
+ }
+ };
+
+ 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.
+ Dygraph.addEvent(document, 'mouseup', 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;
+ }
+ }
+ });
+};
+
+/**
+ * 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);
+};
+
+/**
+ * When the mouse moves in the canvas, display information about a nearby data
+ * point and draw dots over those points in the data series. This function
+ * takes care of cleanup of previously-drawn dots.
+ * @param {Object} event The mousemove event from the browser.
+ * @private
+ */
+Dygraph.prototype.mouseMove_ = function(event) {
+ // This prevents JS errors when mousing over the canvas before data loads.
+ var points = this.layout_.points;
+ if (points === undefined) return;
+
+ var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
+
+ var lastx = -1;
+ var i;
+
+ // Loop through all the points and find the date nearest to our current
+ // location.
+ var minDist = 1e+100;
+ var idx = -1;
+ for (i = 0; i < points.length; i++) {
+ var point = points[i];
+ if (point === null) continue;
+ var dist = Math.abs(point.canvasx - canvasx);
+ if (dist > minDist) continue;
+ minDist = dist;
+ idx = i;
+ }
+ if (idx >= 0) lastx = points[idx].xval;
+
+ // Extract the points we've selected
+ this.selPoints_ = [];
+ var l = points.length;
+ if (!this.attr_("stackedGraph")) {
+ for (i = 0; i < l; i++) {
+ if (points[i].xval == lastx) {
+ this.selPoints_.push(points[i]);
+ }
+ }
+ } else {
+ // Need to 'unstack' points starting from the bottom
+ var cumulative_sum = 0;
+ for (i = l - 1; i >= 0; i--) {
+ if (points[i].xval == lastx) {
+ var p = {}; // Clone the point since we modify it
+ for (var k in points[i]) {
+ p[k] = points[i][k];
+ }
+ p.yval -= cumulative_sum;
+ cumulative_sum += p.yval;
+ this.selPoints_.push(p);
+ }
+ }
+ this.selPoints_.reverse();
+ }
+
+ if (this.attr_("highlightCallback")) {
+ var px = this.lastx_;
+ if (px !== null && lastx != px) {
+ // only fire if the selected point has changed.
+ this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
+ }
+ }
+
+ // Save last x position for callbacks.
+ this.lastx_ = lastx;
+
+ this.updateSelection_();
+};
+
+/**
+ * Transforms layout_.points index into data row number.
+ * @param int layout_.points index
+ * @return int row number, or -1 if none could be found.
+ * @private
+ */
+Dygraph.prototype.idxToRow_ = function(idx) {
+ if (idx < 0) return -1;
+
+ // make sure that you get the boundaryIds record which is also defined (see bug #236)
+ var boundaryIdx = -1;
+ for (var i = 0; i < this.boundaryIds_.length; i++) {
+ if (this.boundaryIds_[i] !== undefined) {
+ boundaryIdx = i;
+ break;
+ }
+ }
+ if (boundaryIdx < 0) return -1;
+ for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+ var set = this.layout_.datasets[setIdx];
+ if (idx < set.length) {
+ return this.boundaryIds_[boundaryIdx][0] + idx;
+ }
+ idx -= set.length;
+ }
+ return -1;
+};
+
+/**
+ * @private
+ * Generates legend html dash for any stroke pattern. It will try to scale the
+ * pattern to fit in 1em width. Or if small enough repeat the partern for 1em
+ * width.
+ * @param strokePattern The pattern
+ * @param color The color of the series.
+ * @param oneEmWidth The width in pixels of 1em in the legend.
+ */
+Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) {
+ var dash = "";
+ var i, j, paddingLeft, marginRight;
+ var strokePixelLength = 0, segmentLoop = 0;
+ var normalizedPattern = [];
+ var loop;
+ // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
+ var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+ if(isIE) {
+ return "—";
+ }
+ if (!strokePattern || strokePattern.length <= 1) {
+ // Solid line
+ dash = "<div style=\"display: inline-block; position: relative; " +
+ "bottom: .5ex; padding-left: 1em; height: 1px; " +
+ "border-bottom: 2px solid " + color + ";\"></div>";
+ } else {
+ // Compute the length of the pixels including the first segment twice,
+ // since we repeat it.
+ for (i = 0; i <= strokePattern.length; i++) {
+ strokePixelLength += strokePattern[i%strokePattern.length];
+ }
+
+ // See if we can loop the pattern by itself at least twice.
+ loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0]));
+ if (loop > 1) {
+ // This pattern fits at least two times, no scaling just convert to em;
+ for (i = 0; i < strokePattern.length; i++) {
+ normalizedPattern[i] = strokePattern[i]/oneEmWidth;
+ }
+ // Since we are repeating the pattern, we don't worry about repeating the
+ // first segment in one draw.
+ segmentLoop = normalizedPattern.length;
+ } else {
+ // If the pattern doesn't fit in the legend we scale it to fit.
+ loop = 1;
+ for (i = 0; i < strokePattern.length; i++) {
+ normalizedPattern[i] = strokePattern[i]/strokePixelLength;
+ }
+ // For the scaled patterns we do redraw the first segment.
+ segmentLoop = normalizedPattern.length+1;
+ }
+ // Now make the pattern.
+ for (j = 0; j < loop; j++) {
+ for (i = 0; i < segmentLoop; i+=2) {
+ // The padding is the drawn segment.
+ paddingLeft = normalizedPattern[i%normalizedPattern.length];
+ if (i < strokePattern.length) {
+ // The margin is the space segment.
+ marginRight = normalizedPattern[(i+1)%normalizedPattern.length];
+ } else {
+ // The repeated first segment has no right margin.
+ marginRight = 0;
+ }
+ dash += "<div style=\"display: inline-block; position: relative; " +
+ "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
+ paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
+ ";\"></div>";
+ }
+ }
+ }
+ return dash;
+};
+
+/**
+ * @private
+ * Generates HTML for the legend which is displayed when hovering over the
+ * chart. If no selected points are specified, a default legend is returned
+ * (this may just be the empty string).
+ * @param { Number } [x] The x-value of the selected points.
+ * @param { [Object] } [sel_points] List of selected points for the given
+ * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
+ * @param { Number } [oneEmWidth] The pixel width for 1em in the legend.
+ */
+Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) {
+ // If no points are selected, we display a default legend. Traditionally,
+ // this has been blank. But a better default would be a conventional legend,
+ // which provides essential information for a non-interactive chart.
+ var html, sepLines, i, c, dash, strokePattern;
+ if (typeof(x) === 'undefined') {
+ if (this.attr_('legend') != 'always') return '';
+
+ sepLines = this.attr_('labelsSeparateLines');
+ var labels = this.attr_('labels');
+ html = '';
+ for (i = 1; i < labels.length; i++) {
+ if (!this.visibility()[i - 1]) continue;
+ c = this.plotter_.colors[labels[i]];
+ if (html !== '') html += (sepLines ? '<br/>' : ' ');
+ strokePattern = this.attr_("strokePattern", labels[i]);
+ dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
+ html += "<span style='font-weight: bold; color: " + c + ";'>" + dash +
+ " " + labels[i] + "</span>";
+ }
+ return html;
+ }
+
+ var xOptView = this.optionsViewForAxis_('x');
+ var xvf = xOptView('valueFormatter');
+ html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":";
+
+ var yOptViews = [];
+ var num_axes = this.numAxes();
+ for (i = 0; i < num_axes; i++) {
+ yOptViews[i] = this.optionsViewForAxis_('y' + (i ? 1 + i : ''));
+ }
+ var showZeros = this.attr_("labelsShowZeroValues");
+ sepLines = this.attr_("labelsSeparateLines");
+ for (i = 0; i < this.selPoints_.length; i++) {
+ var pt = this.selPoints_[i];
+ if (pt.yval === 0 && !showZeros) continue;
+ if (!Dygraph.isOK(pt.canvasy)) continue;
+ if (sepLines) html += "<br/>";
+
+ var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]];
+ var fmtFunc = yOptView('valueFormatter');
+ c = this.plotter_.colors[pt.name];
+ var yval = fmtFunc(pt.yval, yOptView, pt.name, this);
+
+ // TODO(danvk): use a template string here and make it an attribute.
+ html += " <b><span style='color: " + c + ";'>" + pt.name +
+ "</span></b>:" + yval;
+ }
+ return html;
+};
+
+/**
+ * @private
+ * Displays information about the selected points in the legend. If there is no
+ * selection, the legend will be cleared.
+ * @param { Number } [x] The x-value of the selected points.
+ * @param { [Object] } [sel_points] List of selected points for the given
+ * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
+ */
+Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
+ var labelsDiv = this.attr_("labelsDiv");
+ var sizeSpan = document.createElement('span');
+ // Calculates the width of 1em in pixels for the legend.
+ sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
+ labelsDiv.appendChild(sizeSpan);
+ var oneEmWidth=sizeSpan.offsetWidth;
+
+ var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
+ if (labelsDiv !== null) {
+ labelsDiv.innerHTML = html;
+ } else {
+ if (typeof(this.shown_legend_error_) == 'undefined') {
+ this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
+ this.shown_legend_error_ = true;
+ }
+ }
+};
+
+/**
+ * Draw dots over the selectied points in the data series. This function
+ * takes care of cleanup of previously-drawn dots.
+ * @private
+ */
+Dygraph.prototype.updateSelection_ = function() {
+ // Clear the previously drawn vertical, if there is one
+ var i;
+ var ctx = this.canvas_ctx_;
+ if (this.previousVerticalX_ >= 0) {
+ // Determine the maximum highlight circle size.
+ var maxCircleSize = 0;
+ var labels = this.attr_('labels');
+ for (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 - maxCircleSize - 1, 0,
+ 2 * maxCircleSize + 2, this.height_);
+ }
+
+ if (this.isUsingExcanvas_ && this.currentZoomRectArgs_) {
+ Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_);
+ }
+
+ if (this.selPoints_.length > 0) {
+ // Set the status message to indicate the selected point(s)
+ if (this.attr_('showLabelsOnHighlight')) {
+ this.setLegendHTML_(this.lastx_, this.selPoints_);
+ }
+
+ // Draw colored circles over the center of each selected point
+ var canvasx = this.selPoints_[0].canvasx;
+ ctx.save();
+ for (i = 0; i < this.selPoints_.length; i++) {
+ var pt = this.selPoints_[i];
+ if (!Dygraph.isOK(pt.canvasy)) continue;
+
+ var circleSize = this.attr_('highlightCircleSize', pt.name);
+ var callback = this.attr_("drawHighlightCallback", pt.name);
+ if (!callback) {
+ callback = Dygraph.Circles.DEFAULT;
+ }
+ callback(this.g, pt.name, ctx, canvasx, pt.canvasy,
+ this.plotter_.colors[pt.name], circleSize);
+ }
+ ctx.restore();
+
+ this.previousVerticalX_ = canvasx;
+ }
+};
+
+/**
+ * Manually set the selected points and display information about them in the
+ * legend. The selection can be cleared using clearSelection() and queried
+ * using getSelection().
+ * @param { Integer } row number that should be highlighted (i.e. appear with
+ * hover dots on the chart). Set to false to clear any selection.
+ */
+Dygraph.prototype.setSelection = function(row) {
+ // Extract the points we've selected
+ this.selPoints_ = [];
+ var pos = 0;
+
+ if (row !== false) {
+ row = row - this.boundaryIds_[0][0];
+ }
+
+ if (row !== false && row >= 0) {
+ for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+ var set = this.layout_.datasets[setIdx];
+ if (row < set.length) {
+ var point = this.layout_.points[pos+row];
+
+ if (this.attr_("stackedGraph")) {
+ point = this.layout_.unstackPointAtIndex(pos+row);
+ }
+
+ this.selPoints_.push(point);
+ }
+ pos += set.length;
+ }
+ }
+
+ if (this.selPoints_.length) {
+ this.lastx_ = this.selPoints_[0].xval;
+ this.updateSelection_();
+ } else {
+ this.clearSelection();
+ }
+
+};
+
+/**
+ * The mouse has left the canvas. Clear out whatever artifacts remain
+ * @param {Object} event the mouseout event from the browser.
+ * @private
+ */
+Dygraph.prototype.mouseOut_ = function(event) {
+ if (this.attr_("unhighlightCallback")) {
+ this.attr_("unhighlightCallback")(event);
+ }
+
+ if (this.attr_("hideOverlayOnMouseOut")) {
+ this.clearSelection();
+ }
+};
+
+/**
+ * Clears the current selection (i.e. points that were highlighted by moving
+ * the mouse over the chart).
+ */
+Dygraph.prototype.clearSelection = function() {
+ // Get rid of the overlay data
+ this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
+ this.setLegendHTML_();
+ this.selPoints_ = [];
+ this.lastx_ = -1;
+};
+
+/**
+ * Returns the number of the currently selected row. To get data for this row,
+ * you can use the getValue method.
+ * @return { Integer } row number, or -1 if nothing is selected
+ */
+Dygraph.prototype.getSelection = function() {
+ if (!this.selPoints_ || this.selPoints_.length < 1) {
+ return -1;
+ }
+
+ for (var row=0; row<this.layout_.points.length; row++ ) {
+ if (this.layout_.points[row].x == this.selPoints_[0].x) {
+ return row + this.boundaryIds_[0][0];
+ }
+ }
+ return -1;
+};
+
+/**
+ * Fires when there's data available to be graphed.
+ * @param {String} data Raw CSV data to be plotted
+ * @private
+ */
+Dygraph.prototype.loadedEvent_ = function(data) {
+ this.rawData_ = this.parseCSV_(data);
+ this.predraw_();
+};
+
+/**
+ * Add ticks on the x-axis representing years, months, quarters, weeks, or days
+ * @private
+ */
+Dygraph.prototype.addXTicks_ = function() {
+ // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
+ var range;
+ if (this.dateWindow_) {
+ range = [this.dateWindow_[0], this.dateWindow_[1]];
+ } else {
+ range = this.fullXRange_();
+ }
+
+ var xAxisOptionsView = this.optionsViewForAxis_('x');
+ var xTicks = xAxisOptionsView('ticker')(
+ range[0],
+ range[1],
+ this.width_, // TODO(danvk): should be area.width
+ xAxisOptionsView,
+ this);
+ // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
+ // console.log(msg);
+ this.layout_.setXTicks(xTicks);
+};
+
+/**
+ * @private
+ * Computes the range of the data series (including confidence intervals).
+ * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
+ * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
+ * @return [low, high]
+ */
+Dygraph.prototype.extremeValues_ = function(series) {
+ var minY = null, maxY = null, j, y;
+
+ var bars = this.attr_("errorBars") || this.attr_("customBars");
+ if (bars) {
+ // With custom bars, maxY is the max of the high values.
+ for (j = 0; j < series.length; j++) {
+ y = series[j][1][0];
+ if (!y) continue;
+ var low = y - series[j][1][1];
+ var high = y + series[j][1][2];
+ if (low > y) low = y; // this can happen with custom bars,
+ if (high < y) high = y; // e.g. in tests/custom-bars.html
+ if (maxY === null || high > maxY) {
+ maxY = high;
+ }
+ if (minY === null || low < minY) {
+ minY = low;
+ }
+ }
+ } else {
+ for (j = 0; j < series.length; j++) {
+ y = series[j][1];
+ if (y === null || isNaN(y)) continue;
+ if (maxY === null || y > maxY) {
+ maxY = y;
+ }
+ if (minY === null || y < minY) {
+ minY = y;
+ }
+ }
+ }
+
+ return [minY, maxY];
+};
+
+/**
+ * @private
+ * This function is called once when the chart's data is changed or the options
+ * dictionary is updated. It is _not_ called when the user pans or zooms. The
+ * idea is that values derived from the chart's data can be computed here,
+ * rather than every time the chart is drawn. This includes things like the
+ * number of axes, rolling averages, etc.
+ */
+Dygraph.prototype.predraw_ = function() {
+ var start = new Date();
+
+ // TODO(danvk): move more computations out of drawGraph_ and into here.
+ this.computeYAxes_();
+
+ // Create a new plotter.
+ if (this.plotter_) this.plotter_.clear();
+ this.plotter_ = new DygraphCanvasRenderer(this,
+ this.hidden_,
+ this.hidden_ctx_,
+ this.layout_);
+
+ // 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.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
+ // edge of the div, if we have two y-axes.
+ this.positionLabelsDiv_();
+
+ if (this.rangeSelector_) {
+ this.rangeSelector_.renderStaticLayer();
+ }
+
+ // Convert the raw data (a 2D array) into the internal format and compute
+ // rolling averages.
+ this.rolledSeries_ = [null]; // x-axis is the first series and it's special
+ for (var i = 1; i < this.numColumns(); i++) {
+ var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
+ var logScale = this.attr_('logscale', i);
+ var series = this.extractSeries_(this.rawData_, i, logScale, connectSeparatedPoints);
+ series = this.rollingAverage(series, this.rollPeriod_);
+ this.rolledSeries_.push(series);
+ }
+
+ // If the data or options have changed, then we'd better redraw.
+ this.drawGraph_();
+
+ // This is used to determine whether to do various animations.
+ var end = new Date();
+ this.drawingTimeMs_ = (end - start);
+};
+
+/**
+ * Loop over all fields and create datasets, calculating extreme y-values for
+ * each series and extreme x-indices as we go.
+ *
+ * dateWindow is passed in as an explicit parameter so that we can compute
+ * extreme values "speculatively", i.e. without actually setting state on the
+ * dygraph.
+ *
+ * TODO(danvk): make this more of a true function
+ * @return [ datasets, seriesExtremes, boundaryIds ]
+ * @private
+ */
+Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
+ var boundaryIds = [];
+ var cumulative_y = []; // For stacked series.
+ var datasets = [];
+ var extremes = {}; // series name -> [low, high]
+ var i, j, k;
+
+ // Loop over the fields (series). Go from the last to the first,
+ // because if they're stacked that's how we accumulate the values.
+ var num_series = rolledSeries.length - 1;
+ for (i = num_series; i >= 1; i--) {
+ if (!this.visibility()[i - 1]) continue;
+
+ // TODO(danvk): is this copy really necessary?
+ var series = [];
+ for (j = 0; j < rolledSeries[i].length; j++) {
+ series.push(rolledSeries[i][j]);
+ }
+
+ // Prune down to the desired range, if necessary (for zooming)
+ // Because there can be lines going to points outside of the visible area,
+ // we actually prune to visible points, plus one on either side.
+ var bars = this.attr_("errorBars") || this.attr_("customBars");
+ if (dateWindow) {
+ var low = dateWindow[0];
+ var high = dateWindow[1];
+ var pruned = [];
+ // TODO(danvk): do binary search instead of linear search.
+ // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
+ var firstIdx = null, lastIdx = null;
+ for (k = 0; k < series.length; k++) {
+ if (series[k][0] >= low && firstIdx === null) {
+ firstIdx = k;
+ }
+ if (series[k][0] <= high) {
+ lastIdx = k;
+ }
+ }
+ if (firstIdx === null) firstIdx = 0;
+ if (firstIdx > 0) firstIdx--;
+ if (lastIdx === null) lastIdx = series.length - 1;
+ if (lastIdx < series.length - 1) lastIdx++;
+ boundaryIds[i-1] = [firstIdx, lastIdx];
+ for (k = firstIdx; k <= lastIdx; k++) {
+ pruned.push(series[k]);
+ }
+ series = pruned;
+ } else {
+ boundaryIds[i-1] = [0, series.length-1];
+ }
+
+ var seriesExtremes = this.extremeValues_(series);
+
+ if (bars) {
+ for (j=0; j<series.length; j++) {
+ series[j] = [series[j][0],
+ series[j][1][0],
+ series[j][1][1],
+ series[j][1][2]];
+ }
+ } else if (this.attr_("stackedGraph")) {
+ var l = series.length;
+ var actual_y;
+ for (j = 0; j < l; j++) {
+ // If one data set has a NaN, let all subsequent stacked
+ // sets inherit the NaN -- only start at 0 for the first set.
+ var x = series[j][0];
+ if (cumulative_y[x] === undefined) {
+ cumulative_y[x] = 0;
+ }
+
+ actual_y = series[j][1];
+ cumulative_y[x] += actual_y;
+
+ series[j] = [x, 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];
+ }
+ }
+ }
+
+ var seriesName = this.attr_("labels")[i];
+ extremes[seriesName] = seriesExtremes;
+ datasets[i] = series;
+ }
+
+ return [ datasets, extremes, boundaryIds ];
+};
+
+/**
+ * Update the graph with new data. This method is called when the viewing area
+ * has changed. If the underlying data or options have changed, predraw_ will
+ * be called before drawGraph_ is called.
+ *
+ * clearSelection, when undefined or true, causes this.clearSelection to be
+ * called at the end of the draw operation. This should rarely be defined,
+ * and never true (that is it should be undefined most of the time, and
+ * rarely false.)
+ *
+ * @private
+ */
+Dygraph.prototype.drawGraph_ = function(clearSelection) {
+ var start = new Date();
+
+ if (typeof(clearSelection) === 'undefined') {
+ clearSelection = true;
+ }
+
+ // This is used to set the second parameter to drawCallback, below.
+ var is_initial_draw = this.is_initial_draw_;
+ this.is_initial_draw_ = false;
+
+ this.layout_.removeAllDatasets();
+ this.setColors_();
+ this.attrs_.pointSize = 0.5 * this.attr_('highlightCircleSize');
+
+ var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
+ var datasets = packed[0];
+ var extremes = packed[1];
+ this.boundaryIds_ = packed[2];
+
+ this.setIndexByName_ = {};
+ var labels = this.attr_("labels");
+ if (labels.length > 0) {
+ this.setIndexByName_[labels[0]] = 0;
+ }
+ for (var i = 1; i < datasets.length; i++) {
+ this.setIndexByName_[labels[i]] = i;
+ if (!this.visibility()[i - 1]) continue;
+ this.layout_.addDataset(labels[i], datasets[i]);
+ }
+
+ this.computeYAxisRanges_(extremes);
+ this.layout_.setYAxes(this.axes_);
+
+ this.addXTicks_();
+
+ // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
+ var tmp_zoomed_x = this.zoomed_x_;
+ // Tell PlotKit to use this new data and render itself
+ this.layout_.setDateWindow(this.dateWindow_);
+ this.zoomed_x_ = tmp_zoomed_x;
+ this.layout_.evaluateWithError();
+ this.renderGraph_(is_initial_draw, false);
+
+ if (this.attr_("timingName")) {
+ var end = new Date();
+ if (console) {
+ console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms");
+ }
+ }
+};
+
+Dygraph.prototype.renderGraph_ = function(is_initial_draw, clearSelection) {
+ this.plotter_.clear();
+ this.plotter_.render();
+ this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
+ this.canvas_.height);
+
+ // Generate a static legend before any particular point is selected.
+ this.setLegendHTML_();
+
+ if (!is_initial_draw) {
+ if (clearSelection) {
+ if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
+ // We should select the point nearest the page x/y here, but it's easier
+ // to just clear the selection. This prevents erroneous hover dots from
+ // being displayed.
+ this.clearSelection();
+ } else {
+ this.clearSelection();
+ }
+ }
+ }
+
+ if (this.rangeSelector_) {
+ this.rangeSelector_.renderInteractiveLayer();
+ }
+
+ if (this.attr_("drawCallback") !== null) {
+ this.attr_("drawCallback")(this, is_initial_draw);
+ }
+};
+
+/**
+ * @private
+ * Determine properties of the y-axes which are independent of the data
+ * currently being displayed. This includes things like the number of axes and
+ * the style of the axes. It does not include the range of each axis and its
+ * tick marks.
+ * This fills in this.axes_ and this.seriesToAxisMap_.
+ * axes_ = [ { options } ]
+ * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
+ * indices are into the axes_ array.
+ */
+Dygraph.prototype.computeYAxes_ = function() {
+ // Preserve valueWindow settings if they exist, and if the user hasn't
+ // specified a new valueRange.
+ var i, valueWindows, seriesName, axis, index, opts, v;
+ if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) {
+ valueWindows = [];
+ for (index = 0; index < this.axes_.length; index++) {
+ valueWindows.push(this.axes_[index].valueWindow);
+ }
+ }
+
+ this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis.
+ this.seriesToAxisMap_ = {};
+
+ // Get a list of series names.
+ var labels = this.attr_("labels");
+ var series = {};
+ for (i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
+
+ // all options which could be applied per-axis:
+ var axisOptions = [
+ 'includeZero',
+ 'valueRange',
+ 'labelsKMB',
+ 'labelsKMG2',
+ 'pixelsPerYLabel',
+ 'yAxisLabelWidth',
+ 'axisLabelFontSize',
+ 'axisTickSize',
+ 'logscale'
+ ];
+
+ // Copy global axis options over to the first axis.
+ for (i = 0; i < axisOptions.length; i++) {
+ var k = axisOptions[i];
+ v = this.attr_(k);
+ if (v) this.axes_[0][k] = v;
+ }
+
+ // Go through once and add all the axes.
+ for (seriesName in series) {
+ if (!series.hasOwnProperty(seriesName)) continue;
+ axis = this.attr_("axis", seriesName);
+ if (axis === null) {
+ this.seriesToAxisMap_[seriesName] = 0;
+ continue;
+ }
+ if (typeof(axis) == 'object') {
+ // Add a new axis, making a copy of its per-axis options.
+ opts = {};
+ Dygraph.update(opts, this.axes_[0]);
+ Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
+ var yAxisId = this.axes_.length;
+ opts.yAxisId = yAxisId;
+ opts.g = this;
+ Dygraph.update(opts, axis);
+ this.axes_.push(opts);
+ this.seriesToAxisMap_[seriesName] = yAxisId;
+ }
+ }
+
+ // Go through one more time and assign series to an axis defined by another
+ // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
+ for (seriesName in series) {
+ if (!series.hasOwnProperty(seriesName)) continue;
+ axis = this.attr_("axis", seriesName);
+ if (typeof(axis) == 'string') {
+ if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
+ this.error("Series " + seriesName + " wants to share a y-axis with " +
+ "series " + axis + ", which does not define its own axis.");
+ return null;