X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=74d6ce55330fc01f09853d0b64a0910a254f3457;hb=758a629f806fa73483f730fb343013acd0ace078;hp=d1052b3ce3414d645d011bdfccbb033d40eee56e;hpb=2d9596f2fd2a3c8d8d064c0f68a22c69ef82626b;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index d1052b3..74d6ce5 100644 --- a/dygraph.js +++ b/dygraph.js @@ -1,5 +1,8 @@ -// Copyright 2006 Dan Vanderkam (danvdk@gmail.com) -// All Rights Reserved. +/** + * @license + * Copyright 2006 Dan Vanderkam (danvdk@gmail.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ /** * @fileoverview Creates an interactive, zoomable graph based on a CSV file or @@ -40,6 +43,10 @@ */ +/*jshint globalstrict: true */ +/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */ +"use strict"; + /** * Creates an interactive, zoomable chart. * @@ -54,7 +61,7 @@ * whether the input data contains error ranges. For a complete list of * options, see http://dygraphs.com/options.html. */ -Dygraph = function(div, data, opts) { +var Dygraph = function(div, data, opts) { if (arguments.length > 0) { if (arguments.length == 4) { // Old versions of dygraphs took in the series labels as a constructor @@ -86,18 +93,99 @@ Dygraph.DEFAULT_ROLL_PERIOD = 1; Dygraph.DEFAULT_WIDTH = 480; Dygraph.DEFAULT_HEIGHT = 320; -Dygraph.LOG_SCALE = 10; -Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE); -/** @private */ -Dygraph.log10 = function(x) { - return Math.log(x) / Dygraph.LN_TEN; -} +Dygraph.ANIMATION_STEPS = 10; +Dygraph.ANIMATION_DURATION = 200; + +// These are defined before DEFAULT_ATTRS so that it can refer to them. +/** + * @private + * Return a string version of a number. This respects the digitsAfterDecimal + * and maxNumberWidth options. + * @param {Number} x The number to be formatted + * @param {Dygraph} opts An options view + * @param {String} name The name of the point's data series + * @param {Dygraph} g The dygraph object + */ +Dygraph.numberValueFormatter = function(x, opts, pt, g) { + var sigFigs = opts('sigFigs'); + + if (sigFigs !== null) { + // User has opted for a fixed number of significant figures. + return Dygraph.floatFormat(x, sigFigs); + } + + var digits = opts('digitsAfterDecimal'); + var maxNumberWidth = opts('maxNumberWidth'); + + // switch to scientific notation if we underflow or overflow fixed display. + if (x !== 0.0 && + (Math.abs(x) >= Math.pow(10, maxNumberWidth) || + Math.abs(x) < Math.pow(10, -digits))) { + return x.toExponential(digits); + } else { + return '' + Dygraph.round_(x, digits); + } +}; + +/** + * variant for use as an axisLabelFormatter. + * @private + */ +Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) { + return Dygraph.numberValueFormatter(x, opts, g); +}; + +/** + * Convert a JS date (millis since epoch) to YYYY/MM/DD + * @param {Number} date The JavaScript date (ms since epoch) + * @return {String} A date of the form "YYYY/MM/DD" + * @private + */ +Dygraph.dateString_ = function(date) { + var zeropad = Dygraph.zeropad; + var d = new Date(date); + + // Get the year: + var year = "" + d.getFullYear(); + // Get a 0 padded month string + var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh + // Get a 0 padded day string + var day = zeropad(d.getDate()); + + var ret = ""; + var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); + if (frac) ret = " " + Dygraph.hmsString_(date); + + return year + "/" + month + "/" + day + ret; +}; + +/** + * Convert a JS date to a string appropriate to display on an axis that + * is displaying values at the stated granularity. + * @param {Date} date The date to format + * @param {Number} granularity One of the Dygraph granularity constants + * @return {String} The formatted date + * @private + */ +Dygraph.dateAxisFormatter = function(date, granularity) { + if (granularity >= Dygraph.DECADAL) { + return date.strftime('%Y'); + } else if (granularity >= Dygraph.MONTHLY) { + return date.strftime('%b %y'); + } else { + var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds(); + if (frac === 0 || granularity >= Dygraph.DAILY) { + return new Date(date.getTime() + 3600*1000).strftime('%d%b'); + } else { + return Dygraph.hmsString_(date.getTime()); + } + } +}; + // Default attribute values. Dygraph.DEFAULT_ATTRS = { highlightCircleSize: 3, - pixelsPerXLabel: 60, - pixelsPerYLabel: 30, labelsDivWidth: 250, labelsDivStyles: { @@ -109,7 +197,6 @@ Dygraph.DEFAULT_ATTRS = { labelsKMG2: false, showLabelsOnHighlight: true, - yValueFormatter: function(a,b) { return Dygraph.numberFormatter(a,b); }, digitsAfterDecimal: 2, maxNumberWidth: 6, sigFigs: null, @@ -120,13 +207,10 @@ Dygraph.DEFAULT_ATTRS = { axisLabelFontSize: 14, xAxisLabelWidth: 50, yAxisLabelWidth: 50, - xAxisLabelFormatter: Dygraph.dateAxisFormatter, rightGap: 5, showRoller: false, - xValueFormatter: Dygraph.dateString_, xValueParser: Dygraph.dateParser, - xTicker: Dygraph.dateTicker, delimiter: ',', @@ -165,15 +249,38 @@ Dygraph.DEFAULT_ATTRS = { drawXGrid: true, gridLineColor: "rgb(128,128,128)", - interactionModel: null // will be set to Dygraph.defaultInteractionModel. + interactionModel: null, // will be set to Dygraph.Interaction.defaultModel + animatedZooms: false, // (for now) + + // Range selector options + showRangeSelector: false, + rangeSelectorHeight: 40, + rangeSelectorPlotStrokeColor: "#808FAB", + rangeSelectorPlotFillColor: "#A7B1C4", + + // per-axis options + axes: { + x: { + pixelsPerLabel: 60, + axisLabelFormatter: Dygraph.dateAxisFormatter, + valueFormatter: Dygraph.dateString_, + ticker: null // will be set in dygraph-tickers.js + }, + y: { + pixelsPerLabel: 30, + valueFormatter: Dygraph.numberValueFormatter, + axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + ticker: null // will be set in dygraph-tickers.js + }, + y2: { + pixelsPerLabel: 30, + valueFormatter: Dygraph.numberValueFormatter, + axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + ticker: null // will be set in dygraph-tickers.js + } + } }; -// Various logging levels. -Dygraph.DEBUG = 1; -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; @@ -182,28 +289,11 @@ Dygraph.VERTICAL = 2; // Used for initializing annotation CSS rules only once. Dygraph.addedAnnotationCSS = false; -/** - * @private - * Return the 2d context for a dygraph canvas. - * - * This method is only exposed for the sake of replacing the function in - * automated tests, e.g. - * - * var oldFunc = Dygraph.getContext(); - * Dygraph.getContext = function(canvas) { - * var realContext = oldFunc(canvas); - * return new Proxy(realContext); - * }; - */ -Dygraph.getContext = function(canvas) { - return canvas.getContext("2d"); -}; - Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) { // Labels is no longer a constructor parameter, since it's typically set // directly from the data source. It also conains a name for the x-axis, // which the previous constructor form did not. - if (labels != null) { + if (labels !== null) { var new_labels = ["Date"]; for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]); Dygraph.update(attrs, { 'labels': new_labels }); @@ -228,11 +318,21 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { typeof(G_vmlCanvasManager) != 'undefined' && document.readyState != 'complete') { var self = this; - setTimeout(function() { self.__init__(div, file, attrs) }, 100); + setTimeout(function() { self.__init__(div, file, attrs); }, 100); + return; } // Support two-argument constructor - if (attrs == null) { attrs = {}; } + if (attrs === null || attrs === undefined) { attrs = {}; } + + attrs = Dygraph.mapLegacyOptions_(attrs); + + if (!div) { + Dygraph.error("Constructing dygraph with a non-existent div!"); + return; + } + + this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined'; // Copy the important bits into the object // TODO(danvk): most of these should just stay in the attrs_ dictionary. @@ -243,7 +343,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; - this.wilsonInterval_ = attrs.wilsonInterval || true; this.is_initial_draw_ = true; this.annotations_ = []; @@ -255,35 +354,29 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // div, then only one will be drawn. div.innerHTML = ""; - // If the div isn't already sized then inherit from our attrs or - // give it a default size. - if (div.style.width == '') { - div.style.width = (attrs.width || Dygraph.DEFAULT_WIDTH) + "px"; - } - if (div.style.height == '') { - div.style.height = (attrs.height || Dygraph.DEFAULT_HEIGHT) + "px"; + // For historical reasons, the 'width' and 'height' options trump all CSS + // rules _except_ for an explicit 'width' or 'height' on the div. + // As an added convenience, if the div has zero height (like
does + // without any styles), then we use a default height/width. + if (div.style.width === '' && attrs.width) { + div.style.width = attrs.width + "px"; } - this.width_ = parseInt(div.style.width, 10); - this.height_ = parseInt(div.style.height, 10); - // The div might have been specified as percent of the current window size, - // convert that to an appropriate number of pixels. - if (div.style.width.indexOf("%") == div.style.width.length - 1) { - this.width_ = div.offsetWidth; + if (div.style.height === '' && attrs.height) { + div.style.height = attrs.height + "px"; } - if (div.style.height.indexOf("%") == div.style.height.length - 1) { - this.height_ = div.offsetHeight; - } - - if (this.width_ == 0) { - this.error("dygraph has zero width. Please specify a width in pixels."); - } - if (this.height_ == 0) { - this.error("dygraph has zero height. Please specify a height in pixels."); + if (div.style.height === '' && div.clientHeight === 0) { + div.style.height = Dygraph.DEFAULT_HEIGHT + "px"; + if (div.style.width === '') { + div.style.width = Dygraph.DEFAULT_WIDTH + "px"; + } } + // these will be zero if the dygraph's div is hidden. + this.width_ = div.clientWidth; + this.height_ = div.clientHeight; // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. - if (attrs['stackedGraph']) { - attrs['fillGraph'] = true; + if (attrs.stackedGraph) { + attrs.fillGraph = true; // TODO(nikhilk): Add any other stackedGraph checks here. } @@ -299,8 +392,9 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.user_attrs_ = {}; Dygraph.update(this.user_attrs_, attrs); + // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified. this.attrs_ = {}; - Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS); + Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS); this.boundaryIds_ = []; @@ -320,9 +414,9 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { * option is also specified). */ Dygraph.prototype.isZoomed = function(axis) { - if (axis == null) return this.zoomed_x_ || this.zoomed_y_; - if (axis == 'x') return this.zoomed_x_; - if (axis == 'y') return this.zoomed_y_; + if (axis === null) return this.zoomed_x_ || this.zoomed_y_; + if (axis === 'x') return this.zoomed_x_; + if (axis === 'y') return this.zoomed_y_; throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'."; }; @@ -331,9 +425,9 @@ Dygraph.prototype.isZoomed = function(axis) { */ Dygraph.prototype.toString = function() { var maindiv = this.maindiv_; - var id = (maindiv && maindiv.id) ? maindiv.id : maindiv + var id = (maindiv && maindiv.id) ? maindiv.id : maindiv; return "[Dygraph " + id + "]"; -} +}; /** * @private @@ -359,7 +453,7 @@ Dygraph.prototype.attr_ = function(name, seriesName) { // if (seriesName && typeof(this.user_attrs_[seriesName]) != 'undefined' && - this.user_attrs_[seriesName] != null && + 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') { @@ -371,45 +465,37 @@ Dygraph.prototype.attr_ = function(name, seriesName) { } }; -// TODO(danvk): any way I can get the line numbers to be this.warn call? /** * @private - * Log an error on the JS console at the given severity. - * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR} - * @param { String } The message to log. + * @param String} axis The name of the axis (i.e. 'x', 'y' or 'y2') + * @return { ... } A function mapping string -> option value */ -Dygraph.prototype.log = function(severity, message) { - if (typeof(console) != 'undefined') { - switch (severity) { - case Dygraph.DEBUG: - console.debug('dygraphs: ' + message); - break; - case Dygraph.INFO: - console.info('dygraphs: ' + message); - break; - case Dygraph.WARNING: - console.warn('dygraphs: ' + message); - break; - case Dygraph.ERROR: - console.error('dygraphs: ' + message); - break; +Dygraph.prototype.optionsViewForAxis_ = function(axis) { + var self = this; + return function(opt) { + var axis_opts = self.user_attrs_.axes; + if (axis_opts && axis_opts[axis] && axis_opts[axis][opt]) { + return axis_opts[axis][opt]; + } + // user-specified attributes always trump defaults, even if they're less + // specific. + if (typeof(self.user_attrs_[opt]) != 'undefined') { + return self.user_attrs_[opt]; } - } -}; - -/** @private */ -Dygraph.prototype.info = function(message) { - this.log(Dygraph.INFO, message); -}; - -/** @private */ -Dygraph.prototype.warn = function(message) { - this.log(Dygraph.WARNING, message); -}; -/** @private */ -Dygraph.prototype.error = function(message) { - this.log(Dygraph.ERROR, message); + axis_opts = self.attrs_.axes; + if (axis_opts && axis_opts[axis] && axis_opts[axis][opt]) { + return axis_opts[axis][opt]; + } + // check old-style axis options + // TODO(danvk): add a deprecation warning if either of these match. + if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) { + return self.axes_[0][opt]; + } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) { + return self.axes_[1][opt]; + } + return self.attr_(opt); + }; }; /** @@ -448,9 +534,11 @@ Dygraph.prototype.xAxisExtremes = function() { */ Dygraph.prototype.yAxisRange = function(idx) { if (typeof(idx) == "undefined") idx = 0; - if (idx < 0 || idx >= this.axes_.length) return null; - return [ this.axes_[idx].computedValueRange[0], - this.axes_[idx].computedValueRange[1] ]; + if (idx < 0 || idx >= this.axes_.length) { + return null; + } + var axis = this.axes_[idx]; + return [ axis.computedValueRange[0], axis.computedValueRange[1] ]; }; /** @@ -487,14 +575,14 @@ Dygraph.prototype.toDomCoords = function(x, y, axis) { * Returns a single value or null if x is null. */ Dygraph.prototype.toDomXCoord = function(x) { - if (x == null) { + if (x === null) { return null; - }; + } var area = this.plotter_.area; var xRange = this.xAxisRange(); return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w; -} +}; /** * Convert from data x coordinates to canvas/div Y coordinate and optional @@ -505,12 +593,12 @@ Dygraph.prototype.toDomXCoord = function(x) { Dygraph.prototype.toDomYCoord = function(y, axis) { var pct = this.toPercentYCoord(y, axis); - if (pct == null) { + if (pct === null) { return null; } var area = this.plotter_.area; return area.y + pct * area.h; -} +}; /** * Convert from canvas/div coords to data coordinates. @@ -531,7 +619,7 @@ Dygraph.prototype.toDataCoords = function(x, y, axis) { * If x is null, this returns null. */ Dygraph.prototype.toDataXCoord = function(x) { - if (x == null) { + if (x === null) { return null; } @@ -547,7 +635,7 @@ Dygraph.prototype.toDataXCoord = function(x) { * if axis is null, this uses the first axis. */ Dygraph.prototype.toDataYCoord = function(y, axis) { - if (y == null) { + if (y === null) { return null; } @@ -559,7 +647,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]); } else { // Computing the inverse of toDomCoord. - var pct = (y - area.y) / area.h + var pct = (y - area.y) / area.h; // Computing the inverse of toPercentYCoord. The function was arrived at with // the following steps: @@ -602,12 +690,11 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { * @return { Number } A fraction in [0, 1] where 0 = the top edge. */ Dygraph.prototype.toPercentYCoord = function(y, axis) { - if (y == null) { + if (y === null) { return null; } if (typeof(axis) == "undefined") axis = 0; - var area = this.plotter_.area; var yRange = this.yAxisRange(axis); var pct; @@ -621,7 +708,7 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { 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 @@ -637,7 +724,7 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { * @return { Number } A fraction in [0, 1] where 0 = the left edge. */ Dygraph.prototype.toPercentXCoord = function(x) { - if (x == null) { + if (x === null) { return null; } @@ -650,7 +737,7 @@ Dygraph.prototype.toPercentXCoord = function(x) { * @return { Integer } The number of columns. */ Dygraph.prototype.numColumns = function() { - return this.rawData_[0].length; + return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length; }; /** @@ -662,6 +749,21 @@ Dygraph.prototype.numRows = function() { }; /** + * 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 } 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. @@ -679,51 +781,6 @@ Dygraph.prototype.getValue = function(row, col) { }; /** - * @private - * Add an event handler. This smooths a difference between IE and the rest of - * the world. - * @param { DOM element } el The element to add the event to. - * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'. - * @param { Function } fn The function to call on the event. The function takes - * one parameter: the event object. - */ -Dygraph.addEvent = function(el, evt, fn) { - var normed_fn = function(e) { - if (!e) var e = window.event; - fn(e); - }; - if (window.addEventListener) { // Mozilla, Netscape, Firefox - el.addEventListener(evt, normed_fn, false); - } else { // IE - el.attachEvent('on' + evt, normed_fn); - } -}; - - -/** - * @private - * Cancels further processing of an event. This is useful to prevent default - * browser actions, e.g. highlighting text on a double-click. - * Based on the article at - * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel - * @param { Event } e The event whose normal behavior should be canceled. - */ -Dygraph.cancelEvent = function(e) { - e = e ? e : window.event; - if (e.stopPropagation) { - e.stopPropagation(); - } - if (e.preventDefault) { - e.preventDefault(); - } - e.cancelBubble = true; - e.cancel = true; - e.returnValue = false; - return false; -}; - - -/** * 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. @@ -752,10 +809,25 @@ Dygraph.prototype.createInterface_ = function() { 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.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) { @@ -765,11 +837,14 @@ Dygraph.prototype.createInterface_ = function() { dygraph.mouseOut_(e); }); - // Create the grapher - this.layout_ = new DygraphLayout(this); - this.createStatusMessage_(); this.createDragInterface_(); + + // Update when the window is resized. + // TODO(danvk): drop frames depending on complexity of the chart. + Dygraph.addEvent(window, 'resize', function(e) { + dygraph.resize(); + }); }; /** @@ -824,46 +899,25 @@ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) { }; /** - * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This - * is used to generate default series colors which are evenly spaced on the - * color wheel. - * @param { Number } hue Range is 0.0-1.0. - * @param { Number } saturation Range is 0.0-1.0. - * @param { Number } value Range is 0.0-1.0. - * @return { String } "rgb(r,g,b)" where r, g and b range from 0-255. + * Creates an overlay element used to handle mouse events. + * @return {Object} The mouse event element. * @private */ -Dygraph.hsvToRGB = function (hue, saturation, value) { - var red; - var green; - var blue; - if (saturation === 0) { - red = value; - green = value; - blue = value; +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 { - var i = Math.floor(hue * 6); - var f = (hue * 6) - i; - var p = value * (1 - saturation); - var q = value * (1 - (saturation * f)); - var t = value * (1 - (saturation * (1 - f))); - switch (i) { - case 1: red = q; green = value; blue = p; break; - case 2: red = p; green = value; blue = t; break; - case 3: red = p; green = q; blue = value; break; - case 4: red = t; green = p; blue = value; break; - case 5: red = value; green = p; blue = q; break; - case 6: // fall through - case 0: red = value; green = t; blue = p; break; - } + return this.canvas_; } - red = Math.floor(255 * red + 0.5); - green = Math.floor(255 * green + 0.5); - blue = Math.floor(255 * blue + 0.5); - return 'rgb(' + red + ',' + green + ',' + blue + ')'; }; - /** * 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 @@ -875,11 +929,12 @@ 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 (var i = 1; i <= num; i++) { + 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); @@ -887,7 +942,7 @@ Dygraph.prototype.setColors_ = function() { this.colors_.push(Dygraph.hsvToRGB(hue, sat, val)); } } else { - for (var i = 0; i < num; i++) { + for (i = 0; i < num; i++) { if (!this.visibility()[i]) continue; var colorStr = colors[i % colors.length]; this.colors_.push(colorStr); @@ -906,44 +961,6 @@ Dygraph.prototype.getColors = function() { return this.colors_; }; -// The following functions are from quirksmode.org with a modification for Safari from -// http://blog.firetree.net/2005/07/04/javascript-find-position/ -// http://www.quirksmode.org/js/findpos.html - -/** @private */ -Dygraph.findPosX = function(obj) { - var curleft = 0; - if(obj.offsetParent) - while(1) - { - curleft += obj.offsetLeft; - if(!obj.offsetParent) - break; - obj = obj.offsetParent; - } - else if(obj.x) - curleft += obj.x; - return curleft; -}; - - -/** @private */ -Dygraph.findPosY = function(obj) { - var curtop = 0; - if(obj.offsetParent) - while(1) - { - curtop += obj.offsetTop; - if(!obj.offsetParent) - break; - obj = obj.offsetParent; - } - else if(obj.y) - curtop += obj.y; - return curtop; -}; - - /** * 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 @@ -951,10 +968,10 @@ Dygraph.findPosY = function(obj) { * @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); + 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'); @@ -970,6 +987,7 @@ Dygraph.prototype.createStatusMessage_ = function() { "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]; @@ -1032,47 +1050,11 @@ Dygraph.prototype.createRollInterface_ = function() { /** * @private - * Returns the x-coordinate of the event in a coordinate system where the - * top-left corner of the page (not the window) is (0,0). - * Taken from MochiKit.Signal - */ -Dygraph.pageX = function(e) { - if (e.pageX) { - return (!e.pageX || e.pageX < 0) ? 0 : e.pageX; - } else { - var de = document; - var b = document.body; - return e.clientX + - (de.scrollLeft || b.scrollLeft) - - (de.clientLeft || 0); - } -}; - -/** - * @private - * Returns the y-coordinate of the event in a coordinate system where the - * top-left corner of the page (not the window) is (0,0). - * Taken from MochiKit.Signal - */ -Dygraph.pageY = function(e) { - if (e.pageY) { - return (!e.pageY || e.pageY < 0) ? 0 : e.pageY; - } else { - var de = document; - var b = document.body; - return e.clientY + - (de.scrollTop || b.scrollTop) - - (de.clientTop || 0); - } -}; - -/** - * @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 + return Dygraph.pageX(e) - context.px; }; /** @@ -1081,428 +1063,71 @@ Dygraph.prototype.dragGetX_ = function(e, context) { * canvas (i.e. DOM Coords). */ Dygraph.prototype.dragGetY_ = function(e, context) { - return Dygraph.pageY(e) - context.py + return Dygraph.pageY(e) - context.py; }; /** - * A collection of functions to facilitate build custom interaction models. - * @class + * Set up all the mouse handlers needed to capture dragging behavior for zoom + * events. + * @private */ -Dygraph.Interaction = {}; - -/** - * Called in response to an interaction model operation that - * should start the default panning behavior. - * - * It's used in the default callback for "mousedown" operations. - * Custom interaction model builders can use it to provide the default - * panning behavior. - * - * @param { Event } event the event object which led to the startPan call. - * @param { Dygraph} g The dygraph on which to act. - * @param { Object} context The dragging context object (with - * dragStartX/dragStartY/etc. properties). This function modifies the context. - */ -Dygraph.Interaction.startPan = function(event, g, context) { - context.isPanning = true; - var xRange = g.xAxisRange(); - context.dateRange = xRange[1] - xRange[0]; - context.initialLeftmostDate = xRange[0]; - context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1); - - if (g.attr_("panEdgeFraction")) { - var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction"); - var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes! - - var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw; - var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw; - - var boundedLeftDate = g.toDataXCoord(boundedLeftX); - var boundedRightDate = g.toDataXCoord(boundedRightX); - context.boundedDates = [boundedLeftDate, boundedRightDate]; - - var boundedValues = []; - var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction"); - - for (var i = 0; i < g.axes_.length; i++) { - var axis = g.axes_[i]; - var yExtremes = axis.extremeRange; - - var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw; - var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw; - - var boundedTopValue = g.toDataYCoord(boundedTopY); - var boundedBottomValue = g.toDataYCoord(boundedBottomY); - - boundedValues[i] = [boundedTopValue, boundedBottomValue]; - } - context.boundedValues = boundedValues; - } - - // Record the range of each y-axis at the start of the drag. - // If any axis has a valueRange or valueWindow, then we want a 2D pan. - context.is2DPan = false; - for (var i = 0; i < g.axes_.length; i++) { - var axis = g.axes_[i]; - var yRange = g.yAxisRange(i); - // TODO(konigsberg): These values should be in |context|. - // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale. - if (axis.logscale) { - axis.initialTopValue = Dygraph.log10(yRange[1]); - axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]); - } else { - axis.initialTopValue = yRange[1]; - axis.dragValueRange = yRange[1] - yRange[0]; - } - axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1); +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, - // While calculating axes, set 2dpan. - if (axis.valueWindow || axis.valueRange) context.is2DPan = true; - } -}; + // The value on the left side of the graph when a pan operation starts. + initialLeftmostDate: null, -/** - * Called in response to an interaction model operation that - * responds to an event that pans the view. - * - * It's used in the default callback for "mousemove" operations. - * Custom interaction model builders can use it to provide the default - * panning behavior. - * - * @param { Event } event the event object which led to the movePan call. - * @param { Dygraph} g The dygraph on which to act. - * @param { Object} context The dragging context object (with - * dragStartX/dragStartY/etc. properties). This function modifies the context. - */ -Dygraph.Interaction.movePan = function(event, g, context) { - context.dragEndX = g.dragGetX_(event, context); - context.dragEndY = g.dragGetY_(event, context); - - var minDate = context.initialLeftmostDate - - (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel; - if (context.boundedDates) { - minDate = Math.max(minDate, context.boundedDates[0]); - } - var maxDate = minDate + context.dateRange; - if (context.boundedDates) { - if (maxDate > context.boundedDates[1]) { - // Adjust minDate, and recompute maxDate. - minDate = minDate - (maxDate - context.boundedDates[1]); - maxDate = minDate + context.dateRange; - } - } - - g.dateWindow_ = [minDate, maxDate]; - - // y-axis scaling is automatic unless this is a full 2D pan. - if (context.is2DPan) { - // Adjust each axis appropriately. - for (var i = 0; i < g.axes_.length; i++) { - var axis = g.axes_[i]; - - var pixelsDragged = context.dragEndY - context.dragStartY; - var unitsDragged = pixelsDragged * axis.unitsPerPixel; - - var boundedValue = context.boundedValues ? context.boundedValues[i] : null; - - // In log scale, maxValue and minValue are the logs of those values. - var maxValue = axis.initialTopValue + unitsDragged; - if (boundedValue) { - maxValue = Math.min(maxValue, boundedValue[1]); - } - var minValue = maxValue - axis.dragValueRange; - if (boundedValue) { - if (minValue < boundedValue[0]) { - // Adjust maxValue, and recompute minValue. - maxValue = maxValue - (minValue - boundedValue[0]); - minValue = maxValue - axis.dragValueRange; - } - } - if (axis.logscale) { - axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue), - Math.pow(Dygraph.LOG_SCALE, maxValue) ]; - } else { - axis.valueWindow = [ minValue, maxValue ]; - } - } - } + // The number of units each pixel spans. (This won't be valid for log + // scales) + xUnitsPerPixel: null, - g.drawGraph_(); -}; + // TODO(danvk): update this comment + // The range in second/value units that the viewport encompasses during a + // panning operation. + dateRange: null, -/** - * Called in response to an interaction model operation that - * responds to an event that ends panning. - * - * It's used in the default callback for "mouseup" operations. - * Custom interaction model builders can use it to provide the default - * panning behavior. - * - * @param { Event } event the event object which led to the startZoom call. - * @param { Dygraph} g The dygraph on which to act. - * @param { Object} context The dragging context object (with - * dragStartX/dragStartY/etc. properties). This function modifies the context. - */ -Dygraph.Interaction.endPan = function(event, g, context) { - // TODO(konigsberg): Clear the context data from the axis. - // TODO(konigsberg): mouseup should just delete the - // context object, and mousedown should create a new one. - context.isPanning = false; - context.is2DPan = false; - context.initialLeftmostDate = null; - context.dateRange = null; - context.valueRange = null; - context.boundedDates = null; - context.boundedValues = null; -}; - -/** - * Called in response to an interaction model operation that - * responds to an event that starts zooming. - * - * It's used in the default callback for "mousedown" operations. - * Custom interaction model builders can use it to provide the default - * zooming behavior. - * - * @param { Event } event the event object which led to the startZoom call. - * @param { Dygraph} g The dygraph on which to act. - * @param { Object} context The dragging context object (with - * dragStartX/dragStartY/etc. properties). This function modifies the context. - */ -Dygraph.Interaction.startZoom = function(event, g, context) { - context.isZooming = true; -}; + // Top-left corner of the canvas, in DOM coords + // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY. + px: 0, + py: 0, -/** - * Called in response to an interaction model operation that - * responds to an event that defines zoom boundaries. - * - * It's used in the default callback for "mousemove" operations. - * Custom interaction model builders can use it to provide the default - * zooming behavior. - * - * @param { Event } event the event object which led to the moveZoom call. - * @param { Dygraph} g The dygraph on which to act. - * @param { Object} context The dragging context object (with - * dragStartX/dragStartY/etc. properties). This function modifies the context. - */ -Dygraph.Interaction.moveZoom = function(event, g, context) { - context.dragEndX = g.dragGetX_(event, context); - context.dragEndY = g.dragGetY_(event, context); + // 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] ...] - var xDelta = Math.abs(context.dragStartX - context.dragEndX); - var yDelta = Math.abs(context.dragStartY - context.dragEndY); + 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; + } - // drag direction threshold for y axis is twice as large as x axis - context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL; + context.px = Dygraph.findPosX(g.canvas_); + context.py = Dygraph.findPosY(g.canvas_); + context.dragStartX = g.dragGetX_(event, context); + context.dragStartY = g.dragGetY_(event, context); + } + }; - g.drawZoomRect_( - context.dragDirection, - context.dragStartX, - context.dragEndX, - context.dragStartY, - context.dragEndY, - context.prevDragDirection, - context.prevEndX, - context.prevEndY); + var interactionModel = this.attr_("interactionModel"); - context.prevEndX = context.dragEndX; - context.prevEndY = context.dragEndY; - context.prevDragDirection = context.dragDirection; -}; - -/** - * Called in response to an interaction model operation that - * responds to an event that performs a zoom based on previously defined - * bounds.. - * - * It's used in the default callback for "mouseup" operations. - * Custom interaction model builders can use it to provide the default - * zooming behavior. - * - * @param { Event } event the event object which led to the endZoom call. - * @param { Dygraph} g The dygraph on which to end the zoom. - * @param { Object} context The dragging context object (with - * dragStartX/dragStartY/etc. properties). This function modifies the context. - */ -Dygraph.Interaction.endZoom = function(event, g, context) { - // TODO(konigsberg): Refactor or rename this fn -- it deals with clicks, too. - context.isZooming = false; - context.dragEndX = g.dragGetX_(event, context); - context.dragEndY = g.dragGetY_(event, context); - var regionWidth = Math.abs(context.dragEndX - context.dragStartX); - var regionHeight = Math.abs(context.dragEndY - context.dragStartY); - - if (regionWidth < 2 && regionHeight < 2 && - g.lastx_ != undefined && g.lastx_ != -1) { - // TODO(danvk): pass along more info about the points, e.g. 'x' - if (g.attr_('clickCallback') != null) { - g.attr_('clickCallback')(event, g.lastx_, g.selPoints_); - } - if (g.attr_('pointClickCallback')) { - // check if the click was on a particular point. - var closestIdx = -1; - var closestDistance = 0; - for (var i = 0; i < g.selPoints_.length; i++) { - var p = g.selPoints_[i]; - var distance = Math.pow(p.canvasx - context.dragEndX, 2) + - Math.pow(p.canvasy - context.dragEndY, 2); - if (closestIdx == -1 || distance < closestDistance) { - closestDistance = distance; - closestIdx = i; - } - } - - // Allow any click within two pixels of the dot. - var radius = g.attr_('highlightCircleSize') + 2; - if (closestDistance <= 5 * 5) { - g.attr_('pointClickCallback')(event, g.selPoints_[closestIdx]); - } - } - } - - if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) { - g.doZoomX_(Math.min(context.dragStartX, context.dragEndX), - Math.max(context.dragStartX, context.dragEndX)); - } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) { - g.doZoomY_(Math.min(context.dragStartY, context.dragEndY), - Math.max(context.dragStartY, context.dragEndY)); - } else { - g.canvas_ctx_.clearRect(0, 0, g.canvas_.width, g.canvas_.height); - } - context.dragStartX = null; - context.dragStartY = null; -}; - -/** - * Default interation model for dygraphs. You can refer to specific elements of - * this when constructing your own interaction model, e.g.: - * g.updateOptions( { - * interactionModel: { - * mousedown: Dygraph.defaultInteractionModel.mousedown - * } - * } ); - */ -Dygraph.Interaction.defaultModel = { - // Track the beginning of drag events - mousedown: function(event, g, context) { - context.initializeMouseDown(event, g, context); - - if (event.altKey || event.shiftKey) { - Dygraph.startPan(event, g, context); - } else { - Dygraph.startZoom(event, g, context); - } - }, - - // Draw zoom rectangles when the mouse is down and the user moves around - mousemove: function(event, g, context) { - if (context.isZooming) { - Dygraph.moveZoom(event, g, context); - } else if (context.isPanning) { - Dygraph.movePan(event, g, context); - } - }, - - mouseup: function(event, g, context) { - if (context.isZooming) { - Dygraph.endZoom(event, g, context); - } else if (context.isPanning) { - Dygraph.endPan(event, g, context); - } - }, - - // Temporarily cancel the dragging event when the mouse leaves the graph - mouseout: function(event, g, context) { - if (context.isZooming) { - context.dragEndX = null; - context.dragEndY = null; - } - }, - - // Disable zooming out if panning. - dblclick: function(event, g, context) { - if (event.altKey || event.shiftKey) { - return; - } - // TODO(konigsberg): replace g.doUnzoom()_ with something that is - // friendlier to public use. - g.doUnzoom_(); - } -}; - -Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel; - -// old ways of accessing these methods/properties -Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel; -Dygraph.endZoom = Dygraph.Interaction.endZoom; -Dygraph.moveZoom = Dygraph.Interaction.moveZoom; -Dygraph.startZoom = Dygraph.Interaction.startZoom; -Dygraph.endPan = Dygraph.Interaction.endPan; -Dygraph.movePan = Dygraph.Interaction.movePan; -Dygraph.startPan = Dygraph.Interaction.startPan; - -/** - * 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, - dragStartY: null, - dragEndX: null, - dragEndY: null, - dragDirection: null, - prevEndX: null, - prevEndY: null, - 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, - - // Utility function to convert page-wide coordinates to canvas coords - 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; + // Self is the graph. + var self = this; // Function that binds the graph and context to the handler. var bindHandler = function(handler) { @@ -1538,13 +1163,12 @@ Dygraph.prototype.createDragInterface_ = function() { }); }; - /** * 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 @@ -1568,28 +1192,40 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, // Clean up from the previous rect if necessary if (prevDirection == Dygraph.HORIZONTAL) { - ctx.clearRect(Math.min(startX, prevEndX), 0, - Math.abs(startX - prevEndX), this.height_); + 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(0, Math.min(startY, prevEndY), - this.width_, Math.abs(startY - prevEndY)); + 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), 0, - Math.abs(endX - startX), this.height_); + ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y, + Math.abs(endX - startX), this.layout_.getPlotArea().h); } - } - if (direction == Dygraph.VERTICAL) { + } else 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)); + 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); }; /** @@ -1603,6 +1239,7 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, * @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); @@ -1611,6 +1248,16 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { }; /** + * 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. @@ -1620,12 +1267,18 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { * @private */ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { - this.dateWindow_ = [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; - this.drawGraph_(); - if (this.attr_("zoomCallback")) { - this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); - } + var that = this; + this.doAnimatedZoom(old_window, new_window, null, null, function() { + if (that.attr_("zoomCallback")) { + that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges()); + } + }); }; /** @@ -1637,25 +1290,27 @@ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { * @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 valueRanges = []; + 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); - this.axes_[i].valueWindow = [low, hi]; - valueRanges.push([low, hi]); + newValueRanges.push([low, hi]); } this.zoomed_y_ = true; - this.drawGraph_(); - if (this.attr_("zoomCallback")) { - var xRange = this.xAxisRange(); - var yRange = this.yAxisRange(); - this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges()); - } + 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()); + } + }); }; /** @@ -1665,16 +1320,16 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { * @private */ Dygraph.prototype.doUnzoom_ = function() { - var dirty = false; - if (this.dateWindow_ != null) { + var dirty = false, dirtyX = false, dirtyY = false; + if (this.dateWindow_ !== null) { dirty = true; - this.dateWindow_ = null; + dirtyX = true; } for (var i = 0; i < this.axes_.length; i++) { - if (this.axes_[i].valueWindow != null) { + if (this.axes_[i].valueWindow !== null) { dirty = true; - delete this.axes_[i].valueWindow; + dirtyY = true; } } @@ -1682,17 +1337,113 @@ Dygraph.prototype.doUnzoom_ = function() { this.clearSelection(); if (dirty) { - // Putting the drawing operation before the callback because it resets - // yAxisRange. this.zoomed_x_ = false; this.zoomed_y_ = false; - this.drawGraph_(); - if (this.attr_("zoomCallback")) { - var minDate = this.rawData_[0][0]; - var maxDate = this.rawData_[this.rawData_.length - 1][0]; - this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); + + 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++) { + newValueRanges.push(this.axes_[i].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); }; /** @@ -1710,15 +1461,15 @@ Dygraph.prototype.mouseMove_ = function(event) { var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); var lastx = -1; - var lasty = -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 (var i = 0; i < points.length; i++) { + for (i = 0; i < points.length; i++) { var point = points[i]; - if (point == null) continue; + if (point === null) continue; var dist = Math.abs(point.canvasx - canvasx); if (dist > minDist) continue; minDist = dist; @@ -1730,7 +1481,7 @@ Dygraph.prototype.mouseMove_ = function(event) { this.selPoints_ = []; var l = points.length; if (!this.attr_("stackedGraph")) { - for (var i = 0; i < l; i++) { + for (i = 0; i < l; i++) { if (points[i].xval == lastx) { this.selPoints_.push(points[i]); } @@ -1738,7 +1489,7 @@ Dygraph.prototype.mouseMove_ = function(event) { } else { // Need to 'unstack' points starting from the bottom var cumulative_sum = 0; - for (var i = l - 1; i >= 0; i--) { + 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]) { @@ -1786,16 +1537,6 @@ Dygraph.prototype.idxToRow_ = function(idx) { /** * @private - * @param { Number } x The number to consider. - * @return { Boolean } Whether the number is zero or NaN. - */ -// TODO(danvk): rename this function to something like 'isNonZeroNan'. -Dygraph.isOK = function(x) { - return x && !isNaN(x); -}; - -/** - * @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). @@ -1807,39 +1548,48 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { // 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; if (typeof(x) === 'undefined') { if (this.attr_('legend') != 'always') return ''; - var sepLines = this.attr_('labelsSeparateLines'); + sepLines = this.attr_('labelsSeparateLines'); var labels = this.attr_('labels'); - var html = ''; - for (var i = 1; i < labels.length; i++) { + html = ''; + for (i = 1; i < labels.length; i++) { if (!this.visibility()[i - 1]) continue; - var c = this.plotter_.colors[labels[i]]; - if (html != '') html += (sepLines ? '
' : ' '); + c = this.plotter_.colors[labels[i]]; + if (html !== '') html += (sepLines ? '
' : ' '); html += "—" + labels[i] + ""; } return html; } - var html = this.attr_('xValueFormatter')(x) + ":"; + var xOptView = this.optionsViewForAxis_('x'); + var xvf = xOptView('valueFormatter'); + html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":"; - var fmtFunc = this.attr_('yValueFormatter'); + 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"); - var sepLines = this.attr_("labelsSeparateLines"); - for (var i = 0; i < this.selPoints_.length; i++) { + sepLines = this.attr_("labelsSeparateLines"); + for (i = 0; i < this.selPoints_.length; i++) { var pt = this.selPoints_[i]; - if (pt.yval == 0 && !showZeros) continue; + if (pt.yval === 0 && !showZeros) continue; if (!Dygraph.isOK(pt.canvasy)) continue; if (sepLines) html += "
"; - var c = this.plotter_.colors[pt.name]; - var yval = fmtFunc(pt.yval, this); + 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 += " " - + pt.name + ":" - + yval; + html += " " + pt.name + + ":" + yval; } return html; }; @@ -1872,12 +1622,13 @@ Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { */ 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 (var i = 1; i < labels.length; i++) { + for (i = 1; i < labels.length; i++) { var r = this.attr_('highlightCircleSize', labels[i]); if (r > maxCircleSize) maxCircleSize = r; } @@ -1886,6 +1637,10 @@ Dygraph.prototype.updateSelection_ = function() { 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')) { @@ -1895,7 +1650,7 @@ Dygraph.prototype.updateSelection_ = function() { // Draw colored circles over the center of each selected point var canvasx = this.selPoints_[0].canvasx; ctx.save(); - for (var i = 0; i < this.selPoints_.length; i++) { + for (i = 0; i < this.selPoints_.length; i++) { var pt = this.selPoints_[i]; if (!Dygraph.isOK(pt.canvasy)) continue; @@ -1924,18 +1679,18 @@ Dygraph.prototype.setSelection = function(row) { var pos = 0; if (row !== false) { - row = row-this.boundaryIds_[0][0]; + row = row - this.boundaryIds_[0][0]; } if (row !== false && row >= 0) { for (var i in this.layout_.datasets) { if (row < this.layout_.datasets[i].length) { 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; @@ -1958,628 +1713,77 @@ Dygraph.prototype.setSelection = function(row) { */ 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= Math.pow(10, maxNumberWidth) || - Math.abs(x) < Math.pow(10, -digits))) { - return x.toExponential(digits); - } else { - return '' + Dygraph.round_(x, digits); - } -}; - -/** - * @private - * Converts '9' to '09' (useful for dates) - */ -Dygraph.zeropad = function(x) { - if (x < 10) return "0" + x; else return "" + x; -}; - -/** - * Return a string version of the hours, minutes and seconds portion of a date. - * @param {Number} date The JavaScript date (ms since epoch) - * @return {String} A time of the form "HH:MM:SS" - * @private - */ -Dygraph.hmsString_ = function(date) { - var zeropad = Dygraph.zeropad; - var d = new Date(date); - if (d.getSeconds()) { - return zeropad(d.getHours()) + ":" + - zeropad(d.getMinutes()) + ":" + - zeropad(d.getSeconds()); - } else { - return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes()); - } -}; - -/** - * Convert a JS date to a string appropriate to display on an axis that - * is displaying values at the stated granularity. - * @param {Date} date The date to format - * @param {Number} granularity One of the Dygraph granularity constants - * @return {String} The formatted date - * @private - */ -Dygraph.dateAxisFormatter = function(date, granularity) { - if (granularity >= Dygraph.DECADAL) { - return date.strftime('%Y'); - } else if (granularity >= Dygraph.MONTHLY) { - return date.strftime('%b %y'); - } else { - var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds(); - if (frac == 0 || granularity >= Dygraph.DAILY) { - return new Date(date.getTime() + 3600*1000).strftime('%d%b'); - } else { - return Dygraph.hmsString_(date.getTime()); - } - } -}; - -/** - * Convert a JS date (millis since epoch) to YYYY/MM/DD - * @param {Number} date The JavaScript date (ms since epoch) - * @return {String} A date of the form "YYYY/MM/DD" - * @private - */ -Dygraph.dateString_ = function(date) { - var zeropad = Dygraph.zeropad; - var d = new Date(date); - - // Get the year: - var year = "" + d.getFullYear(); - // Get a 0 padded month string - var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh - // Get a 0 padded day string - var day = zeropad(d.getDate()); - - var ret = ""; - var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); - if (frac) ret = " " + Dygraph.hmsString_(date); - - return year + "/" + month + "/" + day + ret; -}; - -/** - * Round a number to the specified number of digits past the decimal point. - * @param {Number} num The number to round - * @param {Number} places The number of decimals to which to round - * @return {Number} The rounded number - * @private - */ -Dygraph.round_ = function(num, places) { - var shift = Math.pow(10, places); - return Math.round(num * shift)/shift; -}; - -/** - * 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_(); -}; - -Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; -Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"]; - -/** - * 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.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]]; - } - - var xTicks = this.attr_('xTicker')(range[0], range[1], this); - this.layout_.setXTicks(xTicks); -}; - -// Time granularity enumeration -Dygraph.SECONDLY = 0; -Dygraph.TWO_SECONDLY = 1; -Dygraph.FIVE_SECONDLY = 2; -Dygraph.TEN_SECONDLY = 3; -Dygraph.THIRTY_SECONDLY = 4; -Dygraph.MINUTELY = 5; -Dygraph.TWO_MINUTELY = 6; -Dygraph.FIVE_MINUTELY = 7; -Dygraph.TEN_MINUTELY = 8; -Dygraph.THIRTY_MINUTELY = 9; -Dygraph.HOURLY = 10; -Dygraph.TWO_HOURLY = 11; -Dygraph.SIX_HOURLY = 12; -Dygraph.DAILY = 13; -Dygraph.WEEKLY = 14; -Dygraph.MONTHLY = 15; -Dygraph.QUARTERLY = 16; -Dygraph.BIANNUAL = 17; -Dygraph.ANNUAL = 18; -Dygraph.DECADAL = 19; -Dygraph.CENTENNIAL = 20; -Dygraph.NUM_GRANULARITIES = 21; - -Dygraph.SHORT_SPACINGS = []; -Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1; -Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2; -Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5; -Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10; -Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30; -Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60; -Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2; -Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5; -Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10; -Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30; -Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600; -Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2; -Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6; -Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400; -Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800; - -/** - * @private - * If we used this time granularity, how many ticks would there be? - * This is only an approximation, but it's generally good enough. - */ -Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) { - if (granularity < Dygraph.MONTHLY) { - // Generate one tick mark for every fixed interval of time. - var spacing = Dygraph.SHORT_SPACINGS[granularity]; - return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing); - } else { - var year_mod = 1; // e.g. to only print one point every 10 years. - var num_months = 12; - if (granularity == Dygraph.QUARTERLY) num_months = 3; - if (granularity == Dygraph.BIANNUAL) num_months = 2; - if (granularity == Dygraph.ANNUAL) num_months = 1; - if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; } - if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; } - - var msInYear = 365.2524 * 24 * 3600 * 1000; - var num_years = 1.0 * (end_time - start_time) / msInYear; - return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod); - } -}; - -/** - * @private - * - * Construct an x-axis of nicely-formatted times on meaningful boundaries - * (e.g. 'Jan 09' rather than 'Jan 22, 2009'). - * - * Returns an array containing {v: millis, label: label} dictionaries. - */ -Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { - var formatter = this.attr_("xAxisLabelFormatter"); - var ticks = []; - if (granularity < Dygraph.MONTHLY) { - // Generate one tick mark for every fixed interval of time. - var spacing = Dygraph.SHORT_SPACINGS[granularity]; - var format = '%d%b'; // e.g. "1Jan" - - // Find a time less than start_time which occurs on a "nice" time boundary - // for this granularity. - var g = spacing / 1000; - var d = new Date(start_time); - if (g <= 60) { // seconds - var x = d.getSeconds(); d.setSeconds(x - x % g); - } else { - d.setSeconds(0); - g /= 60; - if (g <= 60) { // minutes - var x = d.getMinutes(); d.setMinutes(x - x % g); - } else { - d.setMinutes(0); - g /= 60; - - if (g <= 24) { // days - var x = d.getHours(); d.setHours(x - x % g); - } else { - d.setHours(0); - g /= 24; - - if (g == 7) { // one week - d.setDate(d.getDate() - d.getDay()); - } - } - } - } - start_time = d.getTime(); - - for (var t = start_time; t <= end_time; t += spacing) { - ticks.push({ v:t, label: formatter(new Date(t), granularity) }); - } - } else { - // Display a tick mark on the first of a set of months of each year. - // Years get a tick mark iff y % year_mod == 0. This is useful for - // displaying a tick mark once every 10 years, say, on long time scales. - var months; - var year_mod = 1; // e.g. to only print one point every 10 years. - - if (granularity == Dygraph.MONTHLY) { - months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]; - } else if (granularity == Dygraph.QUARTERLY) { - months = [ 0, 3, 6, 9 ]; - } else if (granularity == Dygraph.BIANNUAL) { - months = [ 0, 6 ]; - } else if (granularity == Dygraph.ANNUAL) { - months = [ 0 ]; - } else if (granularity == Dygraph.DECADAL) { - months = [ 0 ]; - year_mod = 10; - } else if (granularity == Dygraph.CENTENNIAL) { - months = [ 0 ]; - year_mod = 100; - } else { - this.warn("Span of dates is too long"); - } - - var start_year = new Date(start_time).getFullYear(); - var end_year = new Date(end_time).getFullYear(); - var zeropad = Dygraph.zeropad; - for (var i = start_year; i <= end_year; i++) { - if (i % year_mod != 0) continue; - for (var j = 0; j < months.length; j++) { - var date_str = i + "/" + zeropad(1 + months[j]) + "/01"; - var t = Dygraph.dateStrToMillis(date_str); - if (t < start_time || t > end_time) continue; - ticks.push({ v:t, label: formatter(new Date(t), granularity) }); - } - } - } - - return ticks; -}; - - -/** - * Add ticks to the x-axis based on a date range. - * @param {Number} startDate Start of the date window (millis since epoch) - * @param {Number} endDate End of the date window (millis since epoch) - * @param {Dygraph} self The dygraph object - * @return { [Object] } Array of {label, value} tuples. - * @public - */ -Dygraph.dateTicker = function(startDate, endDate, self) { - // TODO(danvk): why does this take 'self' as a param? - var chosen = -1; - for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) { - var num_ticks = self.NumXTicks(startDate, endDate, i); - if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) { - chosen = i; - break; - } + this.attr_("unhighlightCallback")(event); } - if (chosen >= 0) { - return self.GetXAxis(startDate, endDate, chosen); - } else { - // TODO(danvk): signal error. + if (this.attr_("hideOverlayOnMouseOut")) { + this.clearSelection(); } }; /** - * @private - * This is a list of human-friendly values at which to show tick marks on a log - * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so: - * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ... - * NOTE: this assumes that Dygraph.LOG_SCALE = 10. + * Clears the current selection (i.e. points that were highlighted by moving + * the mouse over the chart). */ -Dygraph.PREFERRED_LOG_TICK_VALUES = function() { - var vals = []; - for (var power = -39; power <= 39; power++) { - var range = Math.pow(10, power); - for (var mult = 1; mult <= 9; mult++) { - var val = range * mult; - vals.push(val); - } - } - return vals; -}(); +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; +}; /** - * @private - * Implementation of binary search over an array. - * Currently does not work when val is outside the range of arry's values. - * @param { Integer } val the value to search for - * @param { Integer[] } arry is the value over which to search - * @param { Integer } abs If abs > 0, find the lowest entry greater than val - * If abs < 0, find the highest entry less than val. - * if abs == 0, find the entry that equals val. - * @param { Integer } [low] The first index in arry to consider (optional) - * @param { Integer } [high] The last index in arry to consider (optional) - */ -Dygraph.binarySearch = function(val, arry, abs, low, high) { - if (low == null || high == null) { - low = 0; - high = arry.length - 1; - } - if (low > high) { + * 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; } - if (abs == null) { - abs = 0; - } - var validIndex = function(idx) { - return idx >= 0 && idx < arry.length; - } - var mid = parseInt((low + high) / 2); - var element = arry[mid]; - if (element == val) { - return mid; - } - if (element > val) { - if (abs > 0) { - // Accept if element > val, but also if prior element < val. - var idx = mid - 1; - if (validIndex(idx) && arry[idx] < val) { - return mid; - } - } - return Dygraph.binarySearch(val, arry, abs, low, mid - 1); - } - if (element < val) { - if (abs < 0) { - // Accept if element < val, but also if prior element > val. - var idx = mid + 1; - if (validIndex(idx) && arry[idx] > val) { - return mid; - } + + for (var row=0; row= nTicks / 4) { - var axisId = axis_props.yAxisId; - for (var idx = maxIdx; idx >= minIdx; idx--) { - var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx]; - var domCoord = axis_props.g.toDomYCoord(tickValue, axisId); - var tick = { v: tickValue }; - if (lastDisplayed == null) { - lastDisplayed = { - tickValue : tickValue, - domCoord : domCoord - }; - } else { - if (domCoord - lastDisplayed.domCoord >= pixelsPerTick) { - lastDisplayed = { - tickValue : tickValue, - domCoord : domCoord - }; - } else { - tick.label = ""; - } - } - ticks.push(tick); - } - // Since we went in backwards order. - ticks.reverse(); - } - } - - // ticks.length won't be 0 if the log scale function finds values to insert. - if (ticks.length == 0) { - // Basic idea: - // Try labels every 1, 2, 5, 10, 20, 50, 100, etc. - // Calculate the resulting tick spacing (i.e. this.height_ / nTicks). - // The first spacing greater than pixelsPerYLabel is what we use. - // TODO(danvk): version that works on a log scale. - if (attr("labelsKMG2")) { - var mults = [1, 2, 4, 8]; - } else { - var mults = [1, 2, 5]; - } - var scale, low_val, high_val, nTicks; - // TODO(danvk): make it possible to set this for x- and y-axes independently. - var pixelsPerTick = attr('pixelsPerYLabel'); - for (var i = -10; i < 50; i++) { - if (attr("labelsKMG2")) { - var base_scale = Math.pow(16, i); - } else { - var base_scale = Math.pow(10, i); - } - for (var j = 0; j < mults.length; j++) { - scale = base_scale * mults[j]; - low_val = Math.floor(minV / scale) * scale; - high_val = Math.ceil(maxV / scale) * scale; - nTicks = Math.abs(high_val - low_val) / scale; - var spacing = self.height_ / nTicks; - // wish I could break out of both loops at once... - if (spacing > pixelsPerTick) break; - } - if (spacing > pixelsPerTick) break; - } - - // Construct the set of ticks. - // Allow reverse y-axis if it's explicitly requested. - if (low_val > high_val) scale *= -1; - for (var i = 0; i < nTicks; i++) { - var tickV = low_val + i * scale; - ticks.push( {v: tickV} ); - } - } - } - - // Add formatted labels to the ticks. - var k; - var k_labels = []; - if (attr("labelsKMB")) { - k = 1000; - k_labels = [ "K", "M", "B", "T" ]; - } - if (attr("labelsKMG2")) { - if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!"); - k = 1024; - k_labels = [ "k", "M", "G", "T" ]; - } - var formatter = attr('yAxisLabelFormatter') ? - attr('yAxisLabelFormatter') : attr('yValueFormatter'); - - // Add labels to the ticks. - for (var i = 0; i < ticks.length; i++) { - if (ticks[i].label !== undefined) continue; // Use current label. - var tickV = ticks[i].v; - var absTickV = Math.abs(tickV); - var label = formatter(tickV, self); - if (k_labels.length > 0) { - // Round up to an appropriate unit. - var n = k*k*k*k; - for (var j = 3; j >= 0; j--, n /= k) { - if (absTickV >= n) { - label = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j]; - break; - } - } - } - ticks[i].label = label; - } - - return ticks; + 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); }; /** @@ -2590,33 +1794,33 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { * @return [low, high] */ Dygraph.prototype.extremeValues_ = function(series) { - var minY = null, maxY = null; + 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 (var j = 0; j < series.length; j++) { - var y = series[j][1][0]; + 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) { + if (maxY === null || high > maxY) { maxY = high; } - if (minY == null || low < minY) { + if (minY === null || low < minY) { minY = low; } } } else { - for (var j = 0; j < series.length; j++) { - var y = series[j][1]; + for (j = 0; j < series.length; j++) { + y = series[j][1]; if (y === null || isNaN(y)) continue; - if (maxY == null || y > maxY) { + if (maxY === null || y > maxY) { maxY = y; } - if (minY == null || y < minY) { + if (minY === null || y < minY) { minY = y; } } @@ -2634,6 +1838,8 @@ Dygraph.prototype.extremeValues_ = function(series) { * 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_(); @@ -2653,78 +1859,72 @@ Dygraph.prototype.predraw_ = function() { // 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); }; /** - * 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. + * 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.drawGraph_ = function() { - var data = this.rawData_; - - // This is used to set the second parameter to drawCallback, below. - var is_initial_draw = this.is_initial_draw_; - this.is_initial_draw_ = false; - - var minY = null, maxY = null; - this.layout_.removeAllDatasets(); - this.setColors_(); - this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize'); - - // Loop over the fields (series). Go from the last to the first, - // because if they're stacked that's how we accumulate the values. - +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 all fields and create datasets - for (var i = data[0].length - 1; i >= 1; i--) { + // 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; - var seriesName = this.attr_("labels")[i]; - var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); - var logScale = this.attr_('logscale', i); - + // TODO(danvk): is this copy really necessary? var series = []; - for (var j = 0; j < data.length; j++) { - var date = data[j][0]; - var point = data[j][i]; - if (logScale) { - // On the log scale, points less than zero do not exist. - // This will create a gap in the chart. Note that this ignores - // connectSeparatedPoints. - if (point <= 0) { - point = null; - } - series.push([date, point]); - } else { - if (point != null || !connectSeparatedPoints) { - series.push([date, point]); - } - } + for (j = 0; j < rolledSeries[i].length; j++) { + series.push(rolledSeries[i][j]); } - // TODO(danvk): move this into predraw_. It's insane to do it here. - series = this.rollingAverage(series, this.rollPeriod_); - // 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 (this.dateWindow_) { - var low = this.dateWindow_[0]; - var high= this.dateWindow_[1]; + 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 (var k = 0; k < series.length; k++) { + for (k = 0; k < series.length; k++) { if (series[k][0] >= low && firstIdx === null) { firstIdx = k; } @@ -2736,26 +1936,28 @@ Dygraph.prototype.drawGraph_ = function() { if (firstIdx > 0) firstIdx--; if (lastIdx === null) lastIdx = series.length - 1; if (lastIdx < series.length - 1) lastIdx++; - this.boundaryIds_[i-1] = [firstIdx, lastIdx]; - for (var k = firstIdx; k <= lastIdx; k++) { + boundaryIds[i-1] = [firstIdx, lastIdx]; + for (k = firstIdx; k <= lastIdx; k++) { pruned.push(series[k]); } series = pruned; } else { - this.boundaryIds_[i-1] = [0, series.length-1]; + boundaryIds[i-1] = [0, series.length-1]; } var seriesExtremes = this.extremeValues_(series); if (bars) { - for (var j=0; j seriesExtremes[1]) { seriesExtremes[1] = cumulative_y[x]; @@ -2776,11 +1978,47 @@ Dygraph.prototype.drawGraph_ = function() { } } } - extremes[seriesName] = seriesExtremes; + 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]; + for (var i = 1; i < datasets.length; i++) { if (!this.visibility()[i - 1]) continue; this.layout_.addDataset(this.attr_("labels")[i], datasets[i]); @@ -2797,6 +2035,17 @@ Dygraph.prototype.drawGraph_ = function() { 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, @@ -2806,16 +2055,22 @@ Dygraph.prototype.drawGraph_ = function() { // Generate a static legend before any particular point is selected. this.setLegendHTML_(); } else { - 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 (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); } @@ -2833,13 +2088,23 @@ Dygraph.prototype.drawGraph_ = function() { * 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; + 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 (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1); + for (i = 1; i < labels.length; i++) series[labels[i]] = (i - 1); // all options which could be applied per-axis: var axisOptions = [ @@ -2855,17 +2120,17 @@ Dygraph.prototype.computeYAxes_ = function() { ]; // Copy global axis options over to the first axis. - for (var i = 0; i < axisOptions.length; i++) { + for (i = 0; i < axisOptions.length; i++) { var k = axisOptions[i]; var v = this.attr_(k); if (v) this.axes_[0][k] = v; } // Go through once and add all the axes. - for (var seriesName in series) { + for (seriesName in series) { if (!series.hasOwnProperty(seriesName)) continue; - var axis = this.attr_("axis", seriesName); - if (axis == null) { + axis = this.attr_("axis", seriesName); + if (axis === null) { this.seriesToAxisMap_[seriesName] = 0; continue; } @@ -2885,9 +2150,9 @@ Dygraph.prototype.computeYAxes_ = function() { // Go through one more time and assign series to an axis defined by another // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } } - for (var seriesName in series) { + for (seriesName in series) { if (!series.hasOwnProperty(seriesName)) continue; - var axis = this.attr_("axis", seriesName); + 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 " + @@ -2899,16 +2164,12 @@ Dygraph.prototype.computeYAxes_ = function() { } } - // Now we remove series from seriesToAxisMap_ which are not visible. We do - // this last so that hiding the first series doesn't destroy the axis - // properties of the primary axis. - var seriesToAxisFiltered = {}; - var vis = this.visibility(); - for (var i = 1; i < labels.length; i++) { - var s = labels[i]; - if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s]; + if (valueWindows !== undefined) { + // Restore valueWindow settings. + for (index = 0; index < valueWindows.length; index++) { + this.axes_[index].valueWindow = valueWindows[index]; + } } - this.seriesToAxisMap_ = seriesToAxisFiltered; }; /** @@ -2945,8 +2206,8 @@ Dygraph.prototype.axisPropertiesForSeries = function(series) { */ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // Build a map from axis number -> [list of series names] - var seriesForAxis = []; - for (var series in this.seriesToAxisMap_) { + var seriesForAxis = [], series; + for (series in this.seriesToAxisMap_) { if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue; var idx = this.seriesToAxisMap_[series]; while (seriesForAxis.length <= idx) seriesForAxis.push([]); @@ -2956,46 +2217,49 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // Compute extreme values, a span and tick marks for each axis. for (var i = 0; i < this.axes_.length; i++) { var axis = this.axes_[i]; - + if (!seriesForAxis[i]) { // If no series are defined or visible then use a reasonable default axis.extremeRange = [0, 1]; } else { // Calculate the extremes of extremes. - var series = seriesForAxis[i]; + series = seriesForAxis[i]; var minY = Infinity; // extremes[series[0]][0]; var maxY = -Infinity; // extremes[series[0]][1]; var extremeMinY, extremeMaxY; + for (var j = 0; j < series.length; j++) { + // this skips invisible series + if (!extremes.hasOwnProperty(series[j])) continue; + // Only use valid extremes to stop null data series' from corrupting the scale. extremeMinY = extremes[series[j]][0]; - if (extremeMinY != null) { + if (extremeMinY !== null) { minY = Math.min(extremeMinY, minY); } extremeMaxY = extremes[series[j]][1]; - if (extremeMaxY != null) { + if (extremeMaxY !== null) { maxY = Math.max(extremeMaxY, maxY); } } if (axis.includeZero && minY > 0) minY = 0; - // Ensure we have a valid scale, otherwise defualt to zero for safety. + // Ensure we have a valid scale, otherwise default to [0, 1] for safety. if (minY == Infinity) minY = 0; - if (maxY == -Infinity) maxY = 0; + if (maxY == -Infinity) maxY = 1; // Add some padding and round up to an integer to be human-friendly. var span = maxY - minY; // special case: if we have no sense of scale, use +/-10% of the sole value. - if (span == 0) { span = maxY; } + if (span === 0) { span = maxY; } - var maxAxisY; - var minAxisY; + var maxAxisY, minAxisY; if (axis.logscale) { - var maxAxisY = maxY + 0.1 * span; - var minAxisY = minY; + maxAxisY = maxY + 0.1 * span; + minAxisY = minY; } else { - var maxAxisY = maxY + 0.1 * span; - var minAxisY = minY - 0.1 * span; + maxAxisY = maxY + 0.1 * span; + minAxisY = minY - 0.1 * span; // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense. if (!this.attr_("avoidMinZero")) { @@ -3025,32 +2289,67 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // Add ticks. By default, all axes inherit the tick positions of the // primary axis. However, if an axis is specifically marked as having // independent ticks, then that is permissible as well. - if (i == 0 || axis.independentTicks) { - axis.ticks = - Dygraph.numericTicks(axis.computedValueRange[0], - axis.computedValueRange[1], - this, - axis); + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); + if (i === 0 || axis.independentTicks) { + axis.ticks = ticker(axis.computedValueRange[0], + axis.computedValueRange[1], + this.height_, // TODO(danvk): should be area.height + opts, + this); } else { var p_axis = this.axes_[0]; var p_ticks = p_axis.ticks; var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0]; var scale = axis.computedValueRange[1] - axis.computedValueRange[0]; var tick_values = []; - for (var i = 0; i < p_ticks.length; i++) { - var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale; + for (var k = 0; k < p_ticks.length; k++) { + var y_frac = (p_ticks[k].v - p_axis.computedValueRange[0]) / p_scale; var y_val = axis.computedValueRange[0] + y_frac * scale; tick_values.push(y_val); } - axis.ticks = - Dygraph.numericTicks(axis.computedValueRange[0], - axis.computedValueRange[1], - this, axis, tick_values); + axis.ticks = ticker(axis.computedValueRange[0], + axis.computedValueRange[1], + this.height_, // TODO(danvk): should be area.height + opts, + this, + tick_values); + } + } +}; + +/** + * Extracts one series from the raw data (a 2D array) into an array of (date, + * value) tuples. + * + * This is where undesirable points (i.e. negative values on log scales and + * missing values through which we wish to connect lines) are dropped. + * + * @private + */ +Dygraph.prototype.extractSeries_ = function(rawData, i, logScale, connectSeparatedPoints) { + var series = []; + for (var j = 0; j < rawData.length; j++) { + var x = rawData[j][0]; + var point = rawData[j][i]; + if (logScale) { + // On the log scale, points less than zero do not exist. + // This will create a gap in the chart. Note that this ignores + // connectSeparatedPoints. + if (point <= 0) { + point = null; + } + series.push([x, point]); + } else { + if (point !== null || !connectSeparatedPoints) { + series.push([x, point]); + } } } + return series; }; - + /** * @private * Calculates the rolling average of a data set. @@ -3067,15 +2366,16 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { if (originalData.length < 2) return originalData; - var rollPeriod = Math.min(rollPeriod, originalData.length - 1); + rollPeriod = Math.min(rollPeriod, originalData.length); var rollingData = []; var sigma = this.attr_("sigma"); + var low, high, i, j, y, sum, num_ok, stddev; if (this.fractions_) { var num = 0; var den = 0; // numerator/denominator var mult = 100.0; - for (var i = 0; i < originalData.length; i++) { + for (i = 0; i < originalData.length; i++) { num += originalData[i][1][0]; den += originalData[i][1][1]; if (i - rollPeriod >= 0) { @@ -3086,22 +2386,22 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { var date = originalData[i][0]; var value = den ? num / den : 0.0; if (this.attr_("errorBars")) { - if (this.wilsonInterval_) { + if (this.attr_("wilsonInterval")) { // For more details on this confidence interval, see: // http://en.wikipedia.org/wiki/Binomial_confidence_interval if (den) { var p = value < 0 ? 0 : value, n = den; var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n)); var denom = 1 + sigma * sigma / den; - var low = (p + sigma * sigma / (2 * den) - pm) / denom; - var high = (p + sigma * sigma / (2 * den) + pm) / denom; + low = (p + sigma * sigma / (2 * den) - pm) / denom; + high = (p + sigma * sigma / (2 * den) + pm) / denom; rollingData[i] = [date, [p * mult, (p - low) * mult, (high - p) * mult]]; } else { rollingData[i] = [date, [0, 0, 0]]; } } else { - var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0; + stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0; rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]]; } } else { @@ -3109,16 +2409,16 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { } } } else if (this.attr_("customBars")) { - var low = 0; + low = 0; var mid = 0; - var high = 0; + high = 0; var count = 0; - for (var i = 0; i < originalData.length; i++) { + for (i = 0; i < originalData.length; i++) { var data = originalData[i][1]; - var y = data[1]; + y = data[1]; rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]]; - if (y != null && !isNaN(y)) { + if (y !== null && !isNaN(y)) { low += data[0]; mid += y; high += data[2]; @@ -3126,32 +2426,35 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { } if (i - rollPeriod >= 0) { var prev = originalData[i - rollPeriod]; - if (prev[1][1] != null && !isNaN(prev[1][1])) { + if (prev[1][1] !== null && !isNaN(prev[1][1])) { low -= prev[1][0]; mid -= prev[1][1]; high -= prev[1][2]; count -= 1; } } - rollingData[i] = [originalData[i][0], [ 1.0 * mid / count, - 1.0 * (mid - low) / count, - 1.0 * (high - mid) / count ]]; + if (count) { + rollingData[i] = [originalData[i][0], [ 1.0 * mid / count, + 1.0 * (mid - low) / count, + 1.0 * (high - mid) / count ]]; + } else { + rollingData[i] = [originalData[i][0], [null, null, null]]; + } } } else { // Calculate the rolling average for the first rollPeriod - 1 points where // there is not enough data to roll over the full number of points - var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2); if (!this.attr_("errorBars")){ if (rollPeriod == 1) { return originalData; } - for (var i = 0; i < originalData.length; i++) { - var sum = 0; - var num_ok = 0; - for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { - var y = originalData[j][1]; - if (y == null || isNaN(y)) continue; + for (i = 0; i < originalData.length; i++) { + sum = 0; + num_ok = 0; + for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { + y = originalData[j][1]; + if (y === null || isNaN(y)) continue; num_ok++; sum += originalData[j][1]; } @@ -3163,19 +2466,19 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { } } else { - for (var i = 0; i < originalData.length; i++) { - var sum = 0; + for (i = 0; i < originalData.length; i++) { + sum = 0; var variance = 0; - var num_ok = 0; - for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { - var y = originalData[j][1][0]; - if (y == null || isNaN(y)) continue; + num_ok = 0; + for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { + y = originalData[j][1][0]; + if (y === null || isNaN(y)) continue; num_ok++; sum += originalData[j][1][0]; variance += Math.pow(originalData[j][1][1], 2); } if (num_ok) { - var stddev = Math.sqrt(variance) / num_ok; + stddev = Math.sqrt(variance) / num_ok; rollingData[i] = [originalData[i][0], [sum / num_ok, sigma * stddev, sigma * stddev]]; } else { @@ -3189,40 +2492,6 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { }; /** - * @private - * Parses a date, returning the number of milliseconds since epoch. This can be - * passed in as an xValueParser in the Dygraph constructor. - * TODO(danvk): enumerate formats that this understands. - * @param {String} A date in YYYYMMDD format. - * @return {Number} Milliseconds since epoch. - */ -Dygraph.dateParser = function(dateStr, self) { - var dateStrSlashed; - var d; - if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12' - dateStrSlashed = dateStr.replace("-", "/", "g"); - while (dateStrSlashed.search("-") != -1) { - dateStrSlashed = dateStrSlashed.replace("-", "/"); - } - d = Dygraph.dateStrToMillis(dateStrSlashed); - } else if (dateStr.length == 8) { // e.g. '20090712' - // TODO(danvk): remove support for this format. It's confusing. - dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) - + "/" + dateStr.substr(6,2); - d = Dygraph.dateStrToMillis(dateStrSlashed); - } else { - // Any format that Date.parse will accept, e.g. "2009/07/12" or - // "2009/07/12 12:34:56" - d = Dygraph.dateStrToMillis(dateStr); - } - - if (!d || isNaN(d)) { - self.error("Couldn't parse " + dateStr + " as a date"); - } - return d; -}; - -/** * Detects the type of the str (date or numeric) and sets the various * formatting attributes in this.attrs_ based on this type. * @param {String} str An x value. @@ -3230,7 +2499,8 @@ Dygraph.dateParser = function(dateStr, self) { */ Dygraph.prototype.detectTypeFromString_ = function(str) { var isDate = false; - if (str.indexOf('-') > 0 || + var dashPos = str.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2 + if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) || str.indexOf('/') >= 0 || isNaN(parseFloat(str))) { isDate = true; @@ -3240,18 +2510,18 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { } if (isDate) { - this.attrs_.xValueFormatter = Dygraph.dateString_; this.attrs_.xValueParser = Dygraph.dateParser; - this.attrs_.xTicker = Dygraph.dateTicker; - this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; + this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; + this.attrs_.axes.x.ticker = Dygraph.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter; } else { - // TODO(danvk): use Dygraph.numberFormatter here? - /** @private (shut up, jsdoc!) */ - this.attrs_.xValueFormatter = function(x) { return x; }; /** @private (shut up, jsdoc!) */ this.attrs_.xValueParser = function(x) { return parseFloat(x); }; - this.attrs_.xTicker = Dygraph.numericTicks; - this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; + // TODO(danvk): use Dygraph.numberValueFormatter here? + /** @private (shut up, jsdoc!) */ + this.attrs_.axes.x.valueFormatter = function(x) { return x; }; + this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } }; @@ -3309,6 +2579,7 @@ Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) { Dygraph.prototype.parseCSV_ = function(data) { var ret = []; var lines = data.split("\n"); + var vals, j; // Use the default delimiter or fall back to a tab if that makes sense. var delim = this.attr_('delimiter'); @@ -3331,7 +2602,7 @@ Dygraph.prototype.parseCSV_ = function(data) { for (var i = start; i < lines.length; i++) { var line = lines[i]; line_no = i; - if (line.length == 0) continue; // skip blank lines + if (line.length === 0) continue; // skip blank lines if (line[0] == '#') continue; // skip comment lines var inFields = line.split(delim); if (inFields.length < 2) continue; @@ -3346,9 +2617,9 @@ Dygraph.prototype.parseCSV_ = function(data) { // If fractions are expected, parse the numbers as "A/B" if (this.fractions_) { - for (var j = 1; j < inFields.length; j++) { + for (j = 1; j < inFields.length; j++) { // TODO(danvk): figure out an appropriate way to flag parse errors. - var vals = inFields[j].split("/"); + vals = inFields[j].split("/"); if (vals.length != 2) { this.error('Expected fractional "num/den" values in CSV data ' + "but found a value '" + inFields[j] + "' on line " + @@ -3366,18 +2637,18 @@ Dygraph.prototype.parseCSV_ = function(data) { 'but line ' + (1 + i) + ' has an odd number of values (' + (inFields.length - 1) + "): '" + line + "'"); } - for (var j = 1; j < inFields.length; j += 2) { + for (j = 1; j < inFields.length; j += 2) { fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line), this.parseFloat_(inFields[j + 1], i, line)]; } } else if (this.attr_("customBars")) { // Bars are a low;center;high tuple - for (var j = 1; j < inFields.length; j++) { + for (j = 1; j < inFields.length; j++) { var val = inFields[j]; if (/^ *$/.test(val)) { fields[j] = [null, null, null]; } else { - var vals = val.split(";"); + vals = val.split(";"); if (vals.length == 3) { fields[j] = [ this.parseFloat_(vals[0], i, line), this.parseFloat_(vals[1], i, line), @@ -3391,7 +2662,7 @@ Dygraph.prototype.parseCSV_ = function(data) { } } else { // Values are just numbers - for (var j = 1; j < inFields.length; j++) { + for (j = 1; j < inFields.length; j++) { fields[j] = this.parseFloat_(inFields[j], i, line); } } @@ -3409,9 +2680,9 @@ Dygraph.prototype.parseCSV_ = function(data) { // first row parsed correctly, then they probably double-specified the // labels. We go with the values set in the option, discard this row and // log a warning to the JS console. - if (i == 0 && this.attr_('labels')) { + if (i === 0 && this.attr_('labels')) { var all_null = true; - for (var j = 0; all_null && j < fields.length; j++) { + for (j = 0; all_null && j < fields.length; j++) { if (fields[j]) all_null = false; } if (all_null) { @@ -3426,7 +2697,7 @@ Dygraph.prototype.parseCSV_ = function(data) { if (outOfOrder) { this.warn("CSV is out of order; order it correctly to speed loading."); - ret.sort(function(a,b) { return a[0] - b[0] }); + ret.sort(function(a,b) { return a[0] - b[0]; }); } return ret; @@ -3442,40 +2713,41 @@ Dygraph.prototype.parseCSV_ = function(data) { */ Dygraph.prototype.parseArray_ = function(data) { // Peek at the first x value to see if it's numeric. - if (data.length == 0) { + if (data.length === 0) { this.error("Can't plot empty data set"); return null; } - if (data[0].length == 0) { + if (data[0].length === 0) { this.error("Data set cannot contain an empty row"); return null; } - if (this.attr_("labels") == null) { + var i; + if (this.attr_("labels") === null) { this.warn("Using default labels. Set labels explicitly via 'labels' " + "in the options parameter"); this.attrs_.labels = [ "X" ]; - for (var i = 1; i < data[0].length; i++) { + for (i = 1; i < data[0].length; i++) { this.attrs_.labels.push("Y" + i); } } if (Dygraph.isDateLike(data[0][0])) { // Some intelligent defaults for a date x-axis. - this.attrs_.xValueFormatter = Dygraph.dateString_; - this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; - this.attrs_.xTicker = Dygraph.dateTicker; + this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter; + this.attrs_.axes.x.ticker = Dygraph.dateTicker; // Assume they're all dates. var parsedData = Dygraph.clone(data); - for (var i = 0; i < data.length; i++) { - if (parsedData[i].length == 0) { + for (i = 0; i < data.length; i++) { + if (parsedData[i].length === 0) { this.error("Row " + (1 + i) + " of data is empty"); return null; } - if (parsedData[i][0] == null - || typeof(parsedData[i][0].getTime) != 'function' - || isNaN(parsedData[i][0].getTime())) { + if (parsedData[i][0] === null || + typeof(parsedData[i][0].getTime) != 'function' || + isNaN(parsedData[i][0].getTime())) { this.error("x value in row " + (1 + i) + " is not a Date"); return null; } @@ -3485,8 +2757,9 @@ Dygraph.prototype.parseArray_ = function(data) { } else { // Some intelligent defaults for a numeric x-axis. /** @private (shut up, jsdoc!) */ - this.attrs_.xValueFormatter = function(x) { return x; }; - this.attrs_.xTicker = Dygraph.numericTicks; + this.attrs_.axes.x.valueFormatter = function(x) { return x; }; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter; + this.attrs_.axes.x.ticker = Dygraph.numericTicks; return data; } }; @@ -3506,15 +2779,15 @@ Dygraph.prototype.parseDataTable_ = function(data) { var indepType = data.getColumnType(0); if (indepType == 'date' || indepType == 'datetime') { - this.attrs_.xValueFormatter = Dygraph.dateString_; this.attrs_.xValueParser = Dygraph.dateParser; - this.attrs_.xTicker = Dygraph.dateTicker; - this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; + this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; + this.attrs_.axes.x.ticker = Dygraph.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter; } else if (indepType == 'number') { - this.attrs_.xValueFormatter = function(x) { return x; }; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; - this.attrs_.xTicker = Dygraph.numericTicks; - this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; + this.attrs_.axes.x.valueFormatter = function(x) { return x; }; + this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } else { this.error("only 'date', 'datetime' and 'number' types are supported for " + "column 1 of DataTable input (Got '" + indepType + "')"); @@ -3525,7 +2798,8 @@ Dygraph.prototype.parseDataTable_ = function(data) { var colIdx = []; var annotationCols = {}; // data index -> [annotation cols] var hasAnnotations = false; - for (var i = 1; i < cols; i++) { + var i, j; + for (i = 1; i < cols; i++) { var type = data.getColumnType(i); if (type == 'number') { colIdx.push(i); @@ -3547,7 +2821,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { // Read column labels // TODO(danvk): add support back for errorBars var labels = [data.getColumnLabel(0)]; - for (var i = 0; i < colIdx.length; i++) { + for (i = 0; i < colIdx.length; i++) { labels.push(data.getColumnLabel(colIdx[i])); if (this.attr_("errorBars")) i += 1; } @@ -3557,7 +2831,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { var ret = []; var outOfOrder = false; var annotations = []; - for (var i = 0; i < rows; i++) { + for (i = 0; i < rows; i++) { var row = []; if (typeof(data.getValue(i, 0)) === 'undefined' || data.getValue(i, 0) === null) { @@ -3572,16 +2846,16 @@ Dygraph.prototype.parseDataTable_ = function(data) { row.push(data.getValue(i, 0)); } if (!this.attr_("errorBars")) { - for (var j = 0; j < colIdx.length; j++) { + for (j = 0; j < colIdx.length; j++) { var col = colIdx[j]; row.push(data.getValue(i, col)); if (hasAnnotations && annotationCols.hasOwnProperty(col) && - data.getValue(i, annotationCols[col][0]) != null) { + data.getValue(i, annotationCols[col][0]) !== null) { var ann = {}; ann.series = data.getColumnLabel(col); ann.xval = row[0]; - ann.shortText = String.fromCharCode(65 /* A */ + annotations.length) + ann.shortText = String.fromCharCode(65 /* A */ + annotations.length); ann.text = ''; for (var k = 0; k < annotationCols[col].length; k++) { if (k) ann.text += "\n"; @@ -3592,11 +2866,11 @@ Dygraph.prototype.parseDataTable_ = function(data) { } // Strip out infinities, which give dygraphs problems later on. - for (var j = 0; j < row.length; j++) { + for (j = 0; j < row.length; j++) { if (!isFinite(row[j])) row[j] = null; } } else { - for (var j = 0; j < cols - 1; j++) { + for (j = 0; j < cols - 1; j++) { row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]); } } @@ -3608,87 +2882,15 @@ Dygraph.prototype.parseDataTable_ = function(data) { if (outOfOrder) { this.warn("DataTable is out of order; order it correctly to speed loading."); - ret.sort(function(a,b) { return a[0] - b[0] }); + ret.sort(function(a,b) { return a[0] - b[0]; }); } this.rawData_ = ret; if (annotations.length > 0) { this.setAnnotations(annotations, true); } -} - -/** - * @private - * This is identical to JavaScript's built-in Date.parse() method, except that - * it doesn't get replaced with an incompatible method by aggressive JS - * libraries like MooTools or Joomla. - * @param { String } str The date string, e.g. "2011/05/06" - * @return { Integer } millis since epoch - */ -Dygraph.dateStrToMillis = function(str) { - return new Date(str).getTime(); -}; - -// These functions are all based on MochiKit. -/** - * @private - */ -Dygraph.update = function (self, o) { - if (typeof(o) != 'undefined' && o !== null) { - for (var k in o) { - if (o.hasOwnProperty(k)) { - self[k] = o[k]; - } - } - } - return self; -}; - -/** - * @private - */ -Dygraph.isArrayLike = function (o) { - var typ = typeof(o); - if ( - (typ != 'object' && !(typ == 'function' && - typeof(o.item) == 'function')) || - o === null || - typeof(o.length) != 'number' || - o.nodeType === 3 - ) { - return false; - } - return true; -}; - -/** - * @private - */ -Dygraph.isDateLike = function (o) { - if (typeof(o) != "object" || o === null || - typeof(o.getTime) != 'function') { - return false; - } - return true; -}; - -/** - * @private - */ -Dygraph.clone = function(o) { - // TODO(danvk): figure out how MochiKit's version works - var r = []; - for (var i = 0; i < o.length; i++) { - if (Dygraph.isArrayLike(o[i])) { - r.push(Dygraph.clone(o[i])); - } else { - r.push(o[i]); - } - } - return r; }; - /** * Get the CSV data. If it's in a function, call that function. If it's in a * file, do an XMLHttpRequest to get it. @@ -3715,8 +2917,8 @@ Dygraph.prototype.start_ = function() { var caller = this; req.onreadystatechange = function () { if (req.readyState == 4) { - if (req.status == 200 || // Normal http - req.status == 0) { // Chrome w/ --allow-file-access-from-files + if (req.status === 200 || // Normal http + req.status === 0) { // Chrome w/ --allow-file-access-from-files caller.loadedEvent_(req.responseText); } } @@ -3747,9 +2949,13 @@ Dygraph.prototype.start_ = function() { * avoiding the occasional infinite loop and preventing redraws when it's not * necessary (e.g. when updating a callback). */ -Dygraph.prototype.updateOptions = function(attrs, block_redraw) { +Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { if (typeof(block_redraw) == 'undefined') block_redraw = false; + // mapLegacyOptions_ drops the "file" parameter as a convenience to us. + var file = input_attrs.file; + var attrs = Dygraph.mapLegacyOptions_(input_attrs); + // TODO(danvk): this is a mess. Move these options into attr_. if ('rollPeriod' in attrs) { this.rollPeriod_ = attrs.rollPeriod; @@ -3757,11 +2963,11 @@ Dygraph.prototype.updateOptions = function(attrs, block_redraw) { if ('dateWindow' in attrs) { this.dateWindow_ = attrs.dateWindow; if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) { - this.zoomed_x_ = attrs.dateWindow != null; + this.zoomed_x_ = (attrs.dateWindow !== null); } } if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) { - this.zoomed_y_ = attrs.valueRange != null; + this.zoomed_y_ = (attrs.valueRange !== null); } // TODO(danvk): validate per-series options. @@ -3771,14 +2977,60 @@ Dygraph.prototype.updateOptions = function(attrs, block_redraw) { // drawPoints // highlightCircleSize - Dygraph.update(this.user_attrs_, attrs); + // Check if this set options will require new points. + var requiresNewPoints = Dygraph.isPixelChangingOptionList(this.attr_("labels"), attrs); - if (attrs['file']) { - this.file_ = attrs['file']; + Dygraph.updateDeep(this.user_attrs_, attrs); + + if (file) { + this.file_ = file; if (!block_redraw) this.start_(); } else { - if (!block_redraw) this.predraw_(); + if (!block_redraw) { + if (requiresNewPoints) { + this.predraw_(); + } else { + this.renderGraph_(false, false); + } + } + } +}; + +/** + * Returns a copy of the options with deprecated names converted into current + * names. Also drops the (potentially-large) 'file' attribute. If the caller is + * interested in that, they should save a copy before calling this. + * @private + */ +Dygraph.mapLegacyOptions_ = function(attrs) { + var my_attrs = {}; + for (var k in attrs) { + if (k == 'file') continue; + if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k]; } + + var set = function(axis, opt, value) { + if (!my_attrs.axes) my_attrs.axes = {}; + if (!my_attrs.axes[axis]) my_attrs.axes[axis] = {}; + my_attrs.axes[axis][opt] = value; + }; + var map = function(opt, axis, new_opt) { + if (typeof(attrs[opt]) != 'undefined') { + set(axis, new_opt, attrs[opt]); + delete my_attrs[opt]; + } + }; + + // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } } + map('xValueFormatter', 'x', 'valueFormatter'); + map('pixelsPerXLabel', 'x', 'pixelsPerLabel'); + map('xAxisLabelFormatter', 'x', 'axisLabelFormatter'); + map('xTicker', 'x', 'ticker'); + map('yValueFormatter', 'y', 'valueFormatter'); + map('pixelsPerYLabel', 'y', 'pixelsPerLabel'); + map('yAxisLabelFormatter', 'y', 'axisLabelFormatter'); + map('yTicker', 'y', 'ticker'); + return my_attrs; }; /** @@ -3804,9 +3056,8 @@ Dygraph.prototype.resize = function(width, height) { width = height = null; } - // TODO(danvk): there should be a clear() method. - this.maindiv_.innerHTML = ""; - this.attrs_.labelsDiv = null; + var old_width = this.width_; + var old_height = this.height_; if (width) { this.maindiv_.style.width = width + "px"; @@ -3814,12 +3065,22 @@ Dygraph.prototype.resize = function(width, height) { this.width_ = width; this.height_ = height; } else { - this.width_ = this.maindiv_.offsetWidth; - this.height_ = this.maindiv_.offsetHeight; + this.width_ = this.maindiv_.clientWidth; + this.height_ = this.maindiv_.clientHeight; } - this.createInterface_(); - this.predraw_(); + if (old_width != this.width_ || old_height != this.height_) { + // TODO(danvk): there should be a clear() method. + this.maindiv_.innerHTML = ""; + this.roller_ = null; + this.attrs_.labelsDiv = null; + this.createInterface_(); + if (this.annotations_.length) { + // createInterface_ reset the layout, so we need to do this. + this.layout_.setAnnotations(this.annotations_); + } + this.predraw_(); + } this.resize_lock = false; }; @@ -3841,10 +3102,11 @@ Dygraph.prototype.visibility = function() { // Do lazy-initialization, so that this happens after we know the number of // data series. if (!this.attr_("visibility")) { - this.attrs_["visibility"] = []; + this.attrs_.visibility = []; } - while (this.attr_("visibility").length < this.rawData_[0].length - 1) { - this.attr_("visibility").push(true); + // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs. + while (this.attr_("visibility").length < this.numColumns() - 1) { + this.attrs_.visibility.push(true); } return this.attr_("visibility"); }; @@ -3863,7 +3125,20 @@ Dygraph.prototype.setVisibility = function(num, value) { }; /** + * How large of an area will the dygraph render itself in? + * This is used for testing. + * @return A {width: w, height: h} object. + * @private + */ +Dygraph.prototype.size = function() { + return { width: this.width_, height: this.height_ }; +}; + +/** * Update the list of annotations and redraw the chart. + * See dygraphs.com/annotations.html for more info on how to use annotations. + * @param ann {Array} An array of annotation objects. + * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional). */ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { // Only add the annotation CSS rule once we know it will be used. @@ -3933,23 +3208,7 @@ Dygraph.addAnnotationRule = function() { } this.warn("Unable to add default annotation CSS rule; display may be off."); -} - -/** - * @private - * Create a new canvas element. This is more complex than a simple - * document.createElement("canvas") because of IE and excanvas. - */ -Dygraph.createCanvas = function() { - var canvas = document.createElement("canvas"); - - isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); - if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) { - canvas = G_vmlCanvasManager.initElement(canvas); - } - - return canvas; }; // Older pages may still use this name. -DateGraph = Dygraph; +var DateGraph = Dygraph;