X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=555bae478e73b0d812c260217679bce13549a28f;hb=5f453f17a4e6cd8a588018ce54d9cbab45e0ffac;hp=375670289af7c3234bf39122769f2f285c4a521c;hpb=5cd7ac686768da92135a96a21d589b076269ffb0;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 3756702..555bae4 100644 --- a/dygraph.js +++ b/dygraph.js @@ -41,13 +41,18 @@ */ /** - * An interactive, zoomable graph - * @param {String | Function} file A file containing CSV data or a function that - * returns this data. The expected format for each line is - * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set, - * YYYYMMDD,val1,stddev1,val2,stddev2,... + * Creates an interactive, zoomable chart. + * + * @constructor + * @param {div | String} div A div or the id of a div into which to construct + * the chart. + * @param {String | Function} file A file containing CSV data or a function + * that returns this data. The most basic expected format for each line is + * "YYYY/MM/DD,val1,val2,...". For more information, see + * http://dygraphs.com/data.html. * @param {Object} attrs Various other attributes, e.g. errorBars determines - * whether the input data contains error ranges. + * whether the input data contains error ranges. For a complete list of + * options, see http://dygraphs.com/options.html. */ Dygraph = function(div, data, opts) { if (arguments.length > 0) { @@ -68,74 +73,18 @@ Dygraph.VERSION = "1.2"; Dygraph.__repr__ = function() { return "[" + this.NAME + " " + this.VERSION + "]"; }; -Dygraph.toString = function() { - return this.__repr__(); -}; /** - * Formatting to use for an integer number. - * - * @param {Number} x The number to format - * @param {Number} unused_precision The precision to use, ignored. - * @return {String} A string formatted like %g in printf. The max generated - * string length should be precision + 6 (e.g 1.123e+300). + * Returns information about the Dygraph class. */ -Dygraph.intFormat = function(x, unused_precision) { - return x.toString(); -} - -/** - * Number formatting function which mimicks the behavior of %g in printf, i.e. - * either exponential or fixed format (without trailing 0s) is used depending on - * the length of the generated string. The advantage of this format is that - * there is a predictable upper bound on the resulting string length, - * significant figures are not dropped, and normal numbers are not displayed in - * exponential notation. - * - * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g. - * It creates strings which are too long for absolute values between 10^-4 and - * 10^-6. See tests/number-format.html for output examples. - * - * @param {Number} x The number to format - * @param {Number} opt_precision The precision to use, default 2. - * @return {String} A string formatted like %g in printf. The max generated - * string length should be precision + 6 (e.g 1.123e+300). - */ -Dygraph.floatFormat = function(x, opt_precision) { - // Avoid invalid precision values; [1, 21] is the valid range. - var p = Math.min(Math.max(1, opt_precision || 2), 21); - - // This is deceptively simple. The actual algorithm comes from: - // - // Max allowed length = p + 4 - // where 4 comes from 'e+n' and '.'. - // - // Length of fixed format = 2 + y + p - // where 2 comes from '0.' and y = # of leading zeroes. - // - // Equating the two and solving for y yields y = 2, or 0.00xxxx which is - // 1.0e-3. - // - // Since the behavior of toPrecision() is identical for larger numbers, we - // don't have to worry about the other bound. - // - // Finally, the argument for toExponential() is the number of trailing digits, - // so we take off 1 for the value before the '.'. - return (Math.abs(x) < 1.0e-3 && x != 0.0) ? - x.toExponential(p - 1) : x.toPrecision(p); +Dygraph.toString = function() { + return this.__repr__(); }; // Various default values Dygraph.DEFAULT_ROLL_PERIOD = 1; Dygraph.DEFAULT_WIDTH = 480; Dygraph.DEFAULT_HEIGHT = 320; -Dygraph.AXIS_LINE_WIDTH = 0.3; - -Dygraph.LOG_SCALE = 10; -Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE); -Dygraph.log10 = function(x) { - return Math.log(x) / Dygraph.LN_TEN; -} // Default attribute values. Dygraph.DEFAULT_ATTRS = { @@ -153,11 +102,10 @@ Dygraph.DEFAULT_ATTRS = { labelsKMG2: false, showLabelsOnHighlight: true, - yValueFormatter: function(x, opt_precision) { - var s = Dygraph.floatFormat(x, opt_precision); - var s2 = Dygraph.intFormat(x); - return s.length < s2.length ? s : s2; - }, + yValueFormatter: function(a,b) { return Dygraph.numberFormatter(a,b); }, + digitsAfterDecimal: 2, + maxNumberWidth: 6, + sigFigs: null, strokeWidth: 1.0, @@ -187,17 +135,31 @@ Dygraph.DEFAULT_ATTRS = { stackedGraph: false, hideOverlayOnMouseOut: true, + // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms. + legend: 'onmouseover', // the only relevant value at the moment is 'always'. + stepPlot: false, avoidMinZero: false, - interactionModel: null // will be set to Dygraph.defaultInteractionModel. -}; + // Sizes of the various chart labels. + titleHeight: 28, + xLabelHeight: 18, + yLabelWidth: 18, + + drawXAxis: true, + drawYAxis: true, + axisLineColor: "black", + axisLineWidth: 0.3, + gridLineWidth: 0.3, + axisLabelColor: "black", + axisLabelFont: "Arial", // TODO(danvk): is this implemented? + axisLabelWidth: 50, + drawYGrid: true, + drawXGrid: true, + gridLineColor: "rgb(128,128,128)", -// Various logging levels. -Dygraph.DEBUG = 1; -Dygraph.INFO = 2; -Dygraph.WARNING = 3; -Dygraph.ERROR = 3; + interactionModel: null // will be set to Dygraph.Interaction.defaultModel +}; // Directions for panning and zooming. Use bit operations when combined // values are possible. @@ -255,42 +217,32 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.is_initial_draw_ = true; this.annotations_ = []; - // Number of digits to use when labeling the x (if numeric) and y axis - // ticks. - this.numXDigits_ = 2; - this.numYDigits_ = 2; - - // When labeling x (if numeric) or y values in the legend, there are - // numDigits + numExtraDigits of precision used. For axes labels with N - // digits of precision, the data should be displayed with at least N+1 digits - // of precision. The reason for this is to divide each interval between - // successive ticks into tenths (for 1) or hundredths (for 2), etc. For - // example, if the labels are [0, 1, 2], we want data to be displayed as - // 0.1, 1.3, etc. - this.numExtraDigits_ = 1; + // Zoomed indicators - These indicate when the graph has been zoomed and on what axis. + this.zoomed_x_ = false; + this.zoomed_y_ = false; // Clear the div. This ensure that, if multiple dygraphs are passed the same // 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 (div.offsetHeight == 0) { + div.style.height = Dygraph.DEFAULT_HEIGHT + "px"; + if (div.style.width == '') { + div.style.width = Dygraph.DEFAULT_WIDTH + "px"; + } } + this.width_ = div.offsetWidth; + this.height_ = div.offsetHeight; if (this.width_ == 0) { this.error("dygraph has zero width. Please specify a width in pixels."); @@ -322,22 +274,59 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.boundaryIds_ = []; - // Make a note of whether labels will be pulled from the CSV file. - this.labelsFromCSV_ = (this.attr_("labels") == null); - // Create the containing DIV and other interactive elements this.createInterface_(); this.start_(); }; +/** + * Returns the zoomed status of the chart for one or both axes. + * + * Axis is an optional parameter. Can be set to 'x' or 'y'. + * + * The zoomed status for an axis is set whenever a user zooms using the mouse + * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom + * 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_; + throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'."; +}; + +/** + * Returns information about the Dygraph object, including its containing ID. + */ Dygraph.prototype.toString = function() { var maindiv = this.maindiv_; var id = (maindiv && maindiv.id) ? maindiv.id : maindiv return "[Dygraph " + id + "]"; } +/** + * @private + * Returns the value of an option. This may be set by the user (either in the + * constructor or by calling updateOptions) or by dygraphs, and may be set to a + * per-series value. + * @param { String } name The name of the option, e.g. 'rollPeriod'. + * @param { String } [seriesName] The name of the series to which the option + * will be applied. If no per-series value of this option is available, then + * the global value is returned. This is optional. + * @return { ... } The value of the option. + */ Dygraph.prototype.attr_ = function(name, seriesName) { +// + if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') { + this.error('Must include options reference JS for testing'); + } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) { + this.error('Dygraphs is using property ' + name + ', which has no entry ' + + 'in the Dygraphs.OPTIONS_REFERENCE listing.'); + // Only log this error once. + Dygraph.OPTIONS_REFERENCE[name] = true; + } +// if (seriesName && typeof(this.user_attrs_[seriesName]) != 'undefined' && this.user_attrs_[seriesName] != null && @@ -352,35 +341,6 @@ Dygraph.prototype.attr_ = function(name, seriesName) { } }; -// TODO(danvk): any way I can get the line numbers to be this.warn call? -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.info = function(message) { - this.log(Dygraph.INFO, message); -} -Dygraph.prototype.warn = function(message) { - this.log(Dygraph.WARNING, message); -} -Dygraph.prototype.error = function(message) { - this.log(Dygraph.ERROR, message); -} - /** * Returns the current rolling period, as set by the user or an option. * @return {Number} The number of points in the rolling window @@ -396,9 +356,14 @@ Dygraph.prototype.rollPeriod = function() { * If the Dygraph has dates on the x-axis, these will be millis since epoch. */ Dygraph.prototype.xAxisRange = function() { - if (this.dateWindow_) return this.dateWindow_; + return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes(); +}; - // The entire chart is visible. +/** + * Returns the lower- and upper-bound x-axis values of the + * data set. + */ +Dygraph.prototype.xAxisExtremes = function() { var left = this.rawData_[0][0]; var right = this.rawData_[this.rawData_.length - 1][0]; return [left, right]; @@ -412,9 +377,11 @@ Dygraph.prototype.xAxisRange = 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] ]; }; /** @@ -520,7 +487,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { if (typeof(axis) == "undefined") axis = 0; if (!this.axes_[axis].logscale) { - return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]); + 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 @@ -551,7 +518,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { /** * Converts a y for an axis to a percentage from the top to the - * bottom of the div. + * bottom of the drawing area. * * If the coordinate represents a value visible on the canvas, then * the value will be between 0 and 1, where 0 is the top of the canvas. @@ -560,6 +527,10 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { * * If y is null, this returns null. * if axis is null, this uses the first axis. + * + * @param { Number } y The data y-coordinate. + * @param { Number } [axis] The axis number on which the data coordinate lives. + * @return { Number } A fraction in [0, 1] where 0 = the top edge. */ Dygraph.prototype.toPercentYCoord = function(y, axis) { if (y == null) { @@ -572,8 +543,8 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { var pct; if (!this.axes_[axis].logscale) { - // yrange[1] - y is unit distance from the bottom. - // yrange[1] - yrange[0] is the scale of the range. + // yRange[1] - y is unit distance from the bottom. + // yRange[1] - yRange[0] is the scale of the range. // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom. pct = (yRange[1] - y) / (yRange[1] - yRange[0]); } else { @@ -584,7 +555,30 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { } /** + * Converts an x value to a percentage from the left to the right of + * the drawing area. + * + * If the coordinate represents a value visible on the canvas, then + * the value will be between 0 and 1, where 0 is the left of the canvas. + * However, this method will return values outside the range, as + * values can fall outside the canvas. + * + * If x is null, this returns null. + * @param { Number } x The data x-coordinate. + * @return { Number } A fraction in [0, 1] where 0 = the left edge. + */ +Dygraph.prototype.toPercentXCoord = function(x) { + if (x == null) { + return null; + } + + var xRange = this.xAxisRange(); + return (x - xRange[0]) / (xRange[1] - xRange[0]); +}; + +/** * Returns the number of columns (including the independent variable). + * @return { Integer } The number of columns. */ Dygraph.prototype.numColumns = function() { return this.rawData_[0].length; @@ -592,6 +586,7 @@ Dygraph.prototype.numColumns = function() { /** * Returns the number of rows (excluding any header/label row). + * @return { Integer } The number of rows, less any header. */ Dygraph.prototype.numRows = function() { return this.rawData_.length; @@ -601,6 +596,11 @@ Dygraph.prototype.numRows = function() { * Returns the value in the given row and column. If the row and column exceed * the bounds on the data, returns null. Also returns null if the value is * missing. + * @param { Number} row The row number of the data (0-based). Row 0 is the + * first row of data, not a header row. + * @param { Number} col The column number of the data (0-based) + * @return { Number } The value in the specified cell or null if the row/col + * were out of range. */ Dygraph.prototype.getValue = function(row, col) { if (row < 0 || row > this.rawData_.length) return null; @@ -609,36 +609,6 @@ Dygraph.prototype.getValue = function(row, col) { return this.rawData_[row][col]; }; -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); - } -}; - - -// Based on the article at -// http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel -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 @@ -662,8 +632,11 @@ Dygraph.prototype.createInterface_ = function() { this.canvas_.style.width = this.width_ + "px"; // for IE this.canvas_.style.height = this.height_ + "px"; // for IE + this.canvas_ctx_ = Dygraph.getContext(this.canvas_); + // ... and for static parts of the chart. this.hidden_ = this.createPlotKitCanvas_(this.canvas_); + this.hidden_ctx_ = Dygraph.getContext(this.hidden_); // The interactive parts of the graph are drawn on top of the chart. this.graphDiv.appendChild(this.hidden_); @@ -679,21 +652,7 @@ Dygraph.prototype.createInterface_ = function() { }); // Create the grapher - // TODO(danvk): why does the Layout need its own set of options? - this.layoutOptions_ = { 'xOriginIsZero': false }; - Dygraph.update(this.layoutOptions_, this.attrs_); - Dygraph.update(this.layoutOptions_, this.user_attrs_); - Dygraph.update(this.layoutOptions_, { - 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) }); - - this.layout_ = new DygraphLayout(this, this.layoutOptions_); - - // TODO(danvk): why does the Renderer need its own set of options? - this.renderOptions_ = { colorScheme: this.colors_, - strokeColor: null, - axisLineWidth: Dygraph.AXIS_LINE_WIDTH }; - Dygraph.update(this.renderOptions_, this.attrs_); - Dygraph.update(this.renderOptions_, this.user_attrs_); + this.layout_ = new DygraphLayout(this); this.createStatusMessage_(); this.createDragInterface_(); @@ -728,8 +687,9 @@ Dygraph.prototype.destroy = function() { }; /** - * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on - * this particular canvas. All Dygraph work is done on this.canvas_. + * Creates the canvas on which the chart will be drawn. Only the Renderer ever + * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots + * or the zoom rectangles) is done on this.canvas_. * @param {Object} canvas The Dygraph canvas over which to overlay the plot * @return {Object} The newly-created canvas * @private @@ -749,38 +709,6 @@ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) { return h; }; -// Taken from MochiKit.Color -Dygraph.hsvToRGB = function (hue, saturation, value) { - var red; - var green; - var blue; - if (saturation === 0) { - red = value; - green = value; - blue = value; - } 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; - } - } - 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 @@ -789,8 +717,6 @@ Dygraph.hsvToRGB = function (hue, saturation, value) { * @private */ Dygraph.prototype.setColors_ = function() { - // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do - // away with this.renderOptions_. var num = this.attr_("labels").length - 1; this.colors_ = []; var colors = this.attr_('colors'); @@ -813,57 +739,18 @@ Dygraph.prototype.setColors_ = function() { } } - // TODO(danvk): update this w/r/t/ the new options system. - this.renderOptions_.colorScheme = this.colors_; - Dygraph.update(this.plotter_.options, this.renderOptions_); - Dygraph.update(this.layoutOptions_, this.user_attrs_); - Dygraph.update(this.layoutOptions_, this.attrs_); -} + this.plotter_.setColors(this.colors_); +}; /** * Return the list of colors. This is either the list of colors passed in the - * attributes, or the autogenerated list of rgb(r,g,b) strings. + * attributes or the autogenerated list of rgb(r,g,b) strings. * @return {Array} The list of colors. */ 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 -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; -}; - -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 @@ -901,8 +788,10 @@ Dygraph.prototype.createStatusMessage_ = function() { }; /** - * Position the labels div so that its right edge is flush with the right edge - * of the charting area. + * Position the labels div so that: + * - its right edge is flush with the right edge of the charting area + * - its top edge is flush with the top edge of the charting area + * @private */ Dygraph.prototype.positionLabelsDiv_ = function() { // Don't touch a user-specified labelsDiv. @@ -911,6 +800,7 @@ Dygraph.prototype.positionLabelsDiv_ = function() { var area = this.plotter_.area; var div = this.attr_("labelsDiv"); div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px"; + div.style.top = area.y + "px"; }; /** @@ -928,10 +818,11 @@ Dygraph.prototype.createRollInterface_ = function() { var display = this.attr_('showRoller') ? 'block' : 'none'; + var area = this.plotter_.area; var textAttr = { "position": "absolute", "zIndex": 10, - "top": (this.plotter_.area.h - 25) + "px", - "left": (this.plotter_.area.x + 1) + "px", + "top": (area.y + area.h - 25) + "px", + "left": (area.x + 1) + "px", "display": display }; this.roller_.size = "2"; @@ -946,284 +837,24 @@ Dygraph.prototype.createRollInterface_ = function() { this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); }; }; -// These functions are 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); - } -}; - -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 }; +/** + * @private + * Converts page the y-coordinate of the event to pixel y-coordinates on the + * canvas (i.e. DOM Coords). + */ Dygraph.prototype.dragGetY_ = function(e, context) { return Dygraph.pageY(e) - context.py }; -// 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. -// -Dygraph.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); - - // 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); - - // While calculating axes, set 2dpan. - if (axis.valueWindow || axis.valueRange) context.is2DPan = true; - } -}; - -// 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. -// -Dygraph.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; - var 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; - - // In log scale, maxValue and minValue are the logs of those values. - var maxValue = axis.initialTopValue + unitsDragged; - var 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 ]; - } - } - } - - g.drawGraph_(); -} - -// 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. -// -Dygraph.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; -} - -// 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. -// -Dygraph.startZoom = function(event, g, context) { - context.isZooming = true; -} - -// 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. -// -Dygraph.moveZoom = function(event, g, context) { - context.dragEndX = g.dragGetX_(event, context); - context.dragEndY = g.dragGetY_(event, context); - - var xDelta = Math.abs(context.dragStartX - context.dragEndX); - var yDelta = Math.abs(context.dragStartY - context.dragEndY); - - // drag direction threshold for y axis is twice as large as x axis - context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL; - - g.drawZoomRect_( - context.dragDirection, - context.dragStartX, - context.dragEndX, - context.dragStartY, - context.dragEndY, - context.prevDragDirection, - context.prevEndX, - context.prevEndY); - - 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. -// -Dygraph.endZoom = function(event, g, context) { - 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_.getContext("2d").clearRect(0, 0, - g.canvas_.width, - g.canvas_.height); - } - context.dragStartX = null; - context.dragStartY = null; -} - -Dygraph.defaultInteractionModel = { - // 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.defaultInteractionModel; - /** * Set up all the mouse handlers needed to capture dragging behavior for zoom * events. @@ -1235,13 +866,13 @@ Dygraph.prototype.createDragInterface_ = function() { 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, + dragStartX: null, // pixel coordinates + dragStartY: null, // pixel coordinates + dragEndX: null, // pixel coordinates + dragEndY: null, // pixel coordinates dragDirection: null, - prevEndX: null, - prevEndY: null, + prevEndX: null, // pixel coordinates + prevEndY: null, // pixel coordinates prevDragDirection: null, // The value on the left side of the graph when a pan operation starts. @@ -1256,10 +887,16 @@ Dygraph.prototype.createDragInterface_ = function() { // panning operation. dateRange: null, - // Utility function to convert page-wide coordinates to canvas coords + // Top-left corner of the canvas, in DOM coords + // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY. px: 0, py: 0, + // Values for use with panEdgeFraction, which limit how far outside the + // graph's data boundaries it can be panned. + boundedDates: null, // [minDate, maxDate] + boundedValues: null, // [[minValue, maxValue] ...] + initializeMouseDown: function(event, g, context) { // prevents mouse drags from selecting page text. if (event.preventDefault) { @@ -1341,7 +978,7 @@ Dygraph.prototype.createDragInterface_ = function() { Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY, prevDirection, prevEndX, prevEndY) { - var ctx = this.canvas_.getContext("2d"); + var ctx = this.canvas_ctx_; // Clean up from the previous rect if necessary if (prevDirection == Dygraph.HORIZONTAL) { @@ -1398,6 +1035,7 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { */ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { this.dateWindow_ = [minDate, maxDate]; + this.zoomed_x_ = true; this.drawGraph_(); if (this.attr_("zoomCallback")) { this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); @@ -1425,9 +1063,11 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { valueRanges.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()); } }; @@ -1452,9 +1092,14 @@ Dygraph.prototype.doUnzoom_ = function() { } } + // Clear any selection, since it's likely to be drawn in the wrong place. + 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]; @@ -1472,12 +1117,12 @@ Dygraph.prototype.doUnzoom_ = function() { * @private */ Dygraph.prototype.mouseMove_ = function(event) { - var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); - var points = this.layout_.points; - // This prevents JS errors when mousing over the canvas before data loads. + var points = this.layout_.points; if (points === undefined) return; + var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); + var lastx = -1; var lasty = -1; @@ -1554,13 +1199,84 @@ Dygraph.prototype.idxToRow_ = function(idx) { }; /** + * @private + * Generates HTML for the legend which is displayed when hovering over the + * chart. If no selected points are specified, a default legend is returned + * (this may just be the empty string). + * @param { Number } [x] The x-value of the selected points. + * @param { [Object] } [sel_points] List of selected points for the given + * x-value. Should have properties like 'name', 'yval' and 'canvasy'. + */ +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. + if (typeof(x) === 'undefined') { + if (this.attr_('legend') != 'always') return ''; + + var sepLines = this.attr_('labelsSeparateLines'); + var labels = this.attr_('labels'); + var html = ''; + for (var i = 1; i < labels.length; i++) { + if (!this.visibility()[i - 1]) continue; + var c = this.plotter_.colors[labels[i]]; + if (html != '') html += (sepLines ? '
' : ' '); + html += "—" + labels[i] + + ""; + } + return html; + } + + var html = this.attr_('xValueFormatter')(x) + ":"; + + var fmtFunc = this.attr_('yValueFormatter'); + var showZeros = this.attr_("labelsShowZeroValues"); + var sepLines = this.attr_("labelsSeparateLines"); + for (var i = 0; i < this.selPoints_.length; i++) { + var pt = this.selPoints_[i]; + if (pt.yval == 0 && !showZeros) continue; + if (!Dygraph.isOK(pt.canvasy)) continue; + if (sepLines) html += "
"; + + var c = this.plotter_.colors[pt.name]; + var yval = fmtFunc(pt.yval, this); + // TODO(danvk): use a template string here and make it an attribute. + html += " " + + pt.name + ":" + + yval; + } + return html; +}; + +/** + * @private + * Displays information about the selected points in the legend. If there is no + * selection, the legend will be cleared. + * @param { Number } [x] The x-value of the selected points. + * @param { [Object] } [sel_points] List of selected points for the given + * x-value. Should have properties like 'name', 'yval' and 'canvasy'. + */ +Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { + var html = this.generateLegendHTML_(x, sel_points); + var labelsDiv = this.attr_("labelsDiv"); + if (labelsDiv !== null) { + labelsDiv.innerHTML = html; + } else { + if (typeof(this.shown_legend_error_) == 'undefined') { + this.error('labelsDiv is set to something nonexistent; legend will not be shown.'); + this.shown_legend_error_ = true; + } + } +}; + +/** * Draw dots over the selectied points in the data series. This function * takes care of cleanup of previously-drawn dots. * @private */ Dygraph.prototype.updateSelection_ = function() { // Clear the previously drawn vertical, if there is one - var ctx = this.canvas_.getContext("2d"); + var ctx = this.canvas_ctx_; if (this.previousVerticalX_ >= 0) { // Determine the maximum highlight circle size. var maxCircleSize = 0; @@ -1574,46 +1290,23 @@ Dygraph.prototype.updateSelection_ = function() { 2 * maxCircleSize + 2, this.height_); } - var isOK = function(x) { return x && !isNaN(x); }; - if (this.selPoints_.length > 0) { - var canvasx = this.selPoints_[0].canvasx; - // Set the status message to indicate the selected point(s) - var replace = this.attr_('xValueFormatter')( - this.lastx_, this.numXDigits_ + this.numExtraDigits_) + ":"; - var fmtFunc = this.attr_('yValueFormatter'); - var clen = this.colors_.length; - if (this.attr_('showLabelsOnHighlight')) { - // Set the status message to indicate the selected point(s) - for (var i = 0; i < this.selPoints_.length; i++) { - if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue; - if (!isOK(this.selPoints_[i].canvasy)) continue; - if (this.attr_("labelsSeparateLines")) { - replace += "
"; - } - var point = this.selPoints_[i]; - var c = new RGBColor(this.plotter_.colors[point.name]); - var yval = fmtFunc(point.yval, this.numYDigits_ + this.numExtraDigits_); - replace += " " - + point.name + ":" - + yval; - } - - this.attr_("labelsDiv").innerHTML = replace; + this.setLegendHTML_(this.lastx_, this.selPoints_); } // Draw colored circles over the center of each selected point + var canvasx = this.selPoints_[0].canvasx; ctx.save(); for (var i = 0; i < this.selPoints_.length; i++) { - if (!isOK(this.selPoints_[i].canvasy)) continue; - var circleSize = - this.attr_('highlightCircleSize', this.selPoints_[i].name); + var pt = this.selPoints_[i]; + if (!Dygraph.isOK(pt.canvasy)) continue; + + var circleSize = this.attr_('highlightCircleSize', pt.name); ctx.beginPath(); - ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name]; - ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize, - 0, 2 * Math.PI, false); + ctx.fillStyle = this.plotter_.colors[pt.name]; + ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false); ctx.fill(); } ctx.restore(); @@ -1623,10 +1316,11 @@ Dygraph.prototype.updateSelection_ = function() { }; /** - * Set manually set selected dots, and display information about them - * @param int row number that should by highlighted - * false value clears the selection - * @public + * Manually set the selected points and display information about them in the + * legend. The selection can be cleared using clearSelection() and queried + * using getSelection(). + * @param { Integer } row number that should be highlighted (i.e. appear with + * hover dots on the chart). Set to false to clear any selection. */ Dygraph.prototype.setSelection = function(row) { // Extract the points we've selected @@ -1656,7 +1350,6 @@ Dygraph.prototype.setSelection = function(row) { this.lastx_ = this.selPoints_[0].xval; this.updateSelection_(); } else { - this.lastx_ = -1; this.clearSelection(); } @@ -1678,22 +1371,21 @@ Dygraph.prototype.mouseOut_ = function(event) { }; /** - * Remove all selection from the canvas - * @public + * 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 - var ctx = this.canvas_.getContext("2d"); - ctx.clearRect(0, 0, this.width_, this.height_); - this.attr_("labelsDiv").innerHTML = ""; + 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 - * @return int row number, of -1 if nothing is selected - * @public + * 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) { @@ -1706,29 +1398,35 @@ Dygraph.prototype.getSelection = function() { } } return -1; -} - -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 + * Return a string version of a number. This respects the digitsAfterDecimal + * and maxNumberWidth options. + * @param {Number} x The number to be formatted + * @param {Dygraph} g The dygraph object */ -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()); +Dygraph.numberFormatter = function(x, g) { + var sigFigs = g.attr_('sigFigs'); + + if (sigFigs !== null) { + // User has opted for a fixed number of significant figures. + return Dygraph.floatFormat(x, sigFigs); + } + + var digits = g.attr_('digitsAfterDecimal'); + var maxNumberWidth = g.attr_('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 zeropad(d.getHours()) + ":" + zeropad(d.getMinutes()); + return '' + Dygraph.round_(x, digits); } -} +}; /** * Convert a JS date to a string appropriate to display on an axis that @@ -1751,30 +1449,6 @@ Dygraph.dateAxisFormatter = function(date, granularity) { 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; }; /** @@ -1804,21 +1478,8 @@ Dygraph.prototype.addXTicks_ = function() { range = [this.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]]; } - var formatter = this.attr_('xTicker'); - var ret = formatter(range[0], range[1], this); - var xTicks = []; - - // Note: numericTicks() returns a {ticks: [...], numDigits: yy} dictionary, - // whereas dateTicker and user-defined tickers typically just return a ticks - // array. - if (ret.ticks !== undefined) { - xTicks = ret.ticks; - this.numXDigits_ = ret.numDigits; - } else { - xTicks = ret; - } - - this.layout_.updateOptions({xTicks: xTicks}); + var xTicks = this.attr_('xTicker')(range[0], range[1], this); + this.layout_.setXTicks(xTicks); }; // Time granularity enumeration @@ -1862,11 +1523,11 @@ Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6; Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400; Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800; -// NumXTicks() -// -// If we used this time granularity, how many ticks would there be? -// This is only an approximation, but it's generally good enough. -// +/** + * @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. @@ -1887,13 +1548,14 @@ Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) { } }; -// GetXAxis() -// -// 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. -// +/** + * @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 = []; @@ -1966,7 +1628,7 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { 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 = Date.parse(date_str); + var t = Dygraph.dateStrToMillis(date_str); if (t < start_time || t > end_time) continue; ticks.push({ v:t, label: formatter(new Date(t), granularity) }); } @@ -1981,10 +1643,12 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { * 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) - * @return {Array.} Array of {label, value} tuples. + * @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); @@ -2001,10 +1665,13 @@ Dygraph.dateTicker = function(startDate, endDate, self) { } }; -// 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. +/** + * @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. + */ Dygraph.PREFERRED_LOG_TICK_VALUES = function() { var vals = []; for (var power = -39; power <= 39; power++) { @@ -2017,100 +1684,15 @@ Dygraph.PREFERRED_LOG_TICK_VALUES = function() { return vals; }(); -// val is the value to search for -// arry is the value over which to search -// 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. -// Currently does not work when val is outside the range of arry's values. -Dygraph.binarySearch = function(val, arry, abs, low, high) { - if (low == null || high == null) { - low = 0; - high = arry.length - 1; - } - if (low > high) { - 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; - } - } - return Dygraph.binarySearch(val, arry, abs, mid + 1, high); - } -}; - -/** - * Determine the number of significant figures in a Number up to the specified - * precision. Note that there is no way to determine if a trailing '0' is - * significant or not, so by convention we return 1 for all of the following - * inputs: 1, 1.0, 1.00, 1.000 etc. - * @param {Number} x The input value. - * @param {Number} opt_maxPrecision Optional maximum precision to consider. - * Default and maximum allowed value is 13. - * @return {Number} The number of significant figures which is >= 1. - */ -Dygraph.significantFigures = function(x, opt_maxPrecision) { - var precision = Math.max(opt_maxPrecision || 13, 13); - - // Convert the number to its exponential notation form and work backwards, - // ignoring the 'e+xx' bit. This may seem like a hack, but doing a loop and - // dividing by 10 leads to roundoff errors. By using toExponential(), we let - // the JavaScript interpreter handle the low level bits of the Number for us. - var s = x.toExponential(precision); - var ePos = s.lastIndexOf('e'); // -1 case handled by return below. - - for (var i = ePos - 1; i >= 0; i--) { - if (s[i] == '.') { - // Got to the decimal place. We'll call this 1 digit of precision because - // we can't know for sure how many trailing 0s are significant. - return 1; - } else if (s[i] != '0') { - // Found the first non-zero digit. Return the number of characters - // except for the '.'. - return i; // This is i - 1 + 1 (-1 is for '.', +1 is for 0 based index). - } - } - - // Occurs if toExponential() doesn't return a string containing 'e', which - // should never happen. - return 1; -}; - +// TODO(konigsberg): Update comment. /** * Add ticks when the x axis has numbers on it (instead of dates) - * TODO(konigsberg): Update comment. * * @param {Number} minV minimum value * @param {Number} maxV maximum value * @param self * @param {function} attribute accessor function. - * @return {Array.} Array of {label, value} tuples. - * @public + * @return {[Object]} Array of {label, value} tuples. */ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { var attr = function(k) { @@ -2225,27 +1807,18 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { var formatter = attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter'); - // Determine the number of decimal places needed for the labels below by - // taking the maximum number of significant figures for any label. We must - // take the max because we can't tell if trailing 0s are significant. - var numDigits = 0; - for (var i = 0; i < ticks.length; i++) { - numDigits = Math.max(Dygraph.significantFigures(ticks[i].v), numDigits); - } - // 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 !== undefined) ? - formatter(tickV, numDigits) : tickV.toPrecision(numDigits); + 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 = formatter(tickV / n, numDigits) + k_labels[j]; + label = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j]; break; } } @@ -2253,13 +1826,16 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { ticks[i].label = label; } - return {ticks: ticks, numDigits: numDigits}; + return ticks; }; -// Computes the range of the data series (including confidence intervals). -// series is either [ [x1, y1], [x2, y2], ... ] or -// [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ... -// Returns [low, high] +/** + * @private + * Computes the range of the data series (including confidence intervals). + * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or + * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ... + * @return [low, high] + */ Dygraph.prototype.extremeValues_ = function(series) { var minY = null, maxY = null; @@ -2297,6 +1873,7 @@ Dygraph.prototype.extremeValues_ = function(series) { }; /** + * @private * This function is called once when the chart's data is changed or the options * dictionary is updated. It is _not_ called when the user pans or zooms. The * idea is that values derived from the chart's data can be computed here, @@ -2310,8 +1887,9 @@ Dygraph.prototype.predraw_ = function() { // Create a new plotter. if (this.plotter_) this.plotter_.clear(); this.plotter_ = new DygraphCanvasRenderer(this, - this.hidden_, this.layout_, - this.renderOptions_); + this.hidden_, + this.hidden_ctx_, + this.layout_); // The roller sits in the bottom left corner of the chart. We don't know where // this will be until the options are available, so it's positioned here. @@ -2330,9 +1908,19 @@ Dygraph.prototype.predraw_ = function() { * 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() { +Dygraph.prototype.drawGraph_ = function(clearSelection) { + if (typeof(clearSelection) === 'undefined') { + clearSelection = true; + } + var data = this.rawData_; // This is used to set the second parameter to drawCallback, below. @@ -2456,26 +2044,44 @@ Dygraph.prototype.drawGraph_ = function() { } this.computeYAxisRanges_(extremes); - this.layout_.updateOptions( { yAxes: this.axes_, - seriesToAxisMap: this.seriesToAxisMap_ - } ); + this.layout_.setYAxes(this.axes_); this.addXTicks_(); + // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously + var tmp_zoomed_x = this.zoomed_x_; // Tell PlotKit to use this new data and render itself - this.layout_.updateOptions({dateWindow: this.dateWindow_}); + this.layout_.setDateWindow(this.dateWindow_); + this.zoomed_x_ = tmp_zoomed_x; this.layout_.evaluateWithError(); this.plotter_.clear(); this.plotter_.render(); this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width, this.canvas_.height); + if (is_initial_draw) { + // Generate a static legend before any particular point is selected. + this.setLegendHTML_(); + } else { + 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.attr_("drawCallback") !== null) { this.attr_("drawCallback")(this, is_initial_draw); } }; /** + * @private * Determine properties of the y-axes which are independent of the data * currently being displayed. This includes things like the number of axes and * the style of the axes. It does not include the range of each axis and its @@ -2486,6 +2092,17 @@ 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 valueWindows; + if (this.axes_ != undefined && this.user_attrs_.hasOwnProperty("valueRange") == false) { + valueWindows = []; + for (var 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_ = {}; @@ -2562,6 +2179,13 @@ Dygraph.prototype.computeYAxes_ = function() { if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s]; } this.seriesToAxisMap_ = seriesToAxisFiltered; + + if (valueWindows != undefined) { + // Restore valueWindow settings. + for (var index = 0; index < valueWindows.length; index++) { + this.axes_[index].valueWindow = valueWindows[index]; + } + } }; /** @@ -2579,6 +2203,19 @@ Dygraph.prototype.numAxes = function() { }; /** + * @private + * Returns axis properties for the given series. + * @param { String } setName The name of the series for which to get axis + * properties, e.g. 'Y1'. + * @return { Object } The axis properties. + */ +Dygraph.prototype.axisPropertiesForSeries = function(series) { + // TODO(danvk): handle errors. + return this.axes_[this.seriesToAxisMap_[series]]; +}; + +/** + * @private * Determine the value range and tick marks for each axis. * @param {Object} extremes A mapping from seriesName -> [low, high] * This fills in the valueRange and ticks fields in each entry of this.axes_. @@ -2596,25 +2233,33 @@ 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 (axis.valueWindow) { - // This is only set if the user has zoomed on the y-axis. It is never set - // by a user. It takes precedence over axis.valueRange because, if you set - // valueRange, you'd still expect to be able to pan. - axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; - } else if (axis.valueRange) { - // This is a user-set value range for this axis. - axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]]; + + 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]; 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++) { - minY = Math.min(extremes[series[j]][0], minY); - maxY = Math.max(extremes[series[j]][1], maxY); + // Only use valid extremes to stop null data series' from corrupting the scale. + extremeMinY = extremes[series[j]][0]; + if (extremeMinY != null) { + minY = Math.min(extremeMinY, minY); + } + extremeMaxY = extremes[series[j]][1]; + 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. + if (minY == Infinity) minY = 0; + if (maxY == -Infinity) maxY = 0; + // 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. @@ -2640,44 +2285,51 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { if (minY > 0) minAxisY = 0; } } - - axis.computedValueRange = [minAxisY, maxAxisY]; + axis.extremeRange = [minAxisY, maxAxisY]; + } + if (axis.valueWindow) { + // This is only set if the user has zoomed on the y-axis. It is never set + // by a user. It takes precedence over axis.valueRange because, if you set + // valueRange, you'd still expect to be able to pan. + axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; + } else if (axis.valueRange) { + // This is a user-set value range for this axis. + axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]]; + } else { + axis.computedValueRange = axis.extremeRange; } // 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) { - var ret = + axis.ticks = Dygraph.numericTicks(axis.computedValueRange[0], axis.computedValueRange[1], this, axis); - axis.ticks = ret.ticks; - this.numYDigits_ = ret.numDigits; } 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); } - var ret = + axis.ticks = Dygraph.numericTicks(axis.computedValueRange[0], axis.computedValueRange[1], this, axis, tick_values); - axis.ticks = ret.ticks; - this.numYDigits_ = ret.numDigits; } } }; - + /** + * @private * Calculates the rolling average of a data set. * If originalData is [label, val], rolls the average of those. * If originalData is [label, [, it's interpreted as [value, stddev] @@ -2758,9 +2410,13 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { 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 @@ -2814,40 +2470,6 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { }; /** - * 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. - * @public - */ -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 = Date.parse(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 = Date.parse(dateStrSlashed); - } else { - // Any format that Date.parse will accept, e.g. "2009/07/12" or - // "2009/07/12 12:34:56" - d = Date.parse(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. @@ -2870,7 +2492,10 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else { - this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; + // 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; @@ -2912,15 +2537,15 @@ Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) { }; /** + * @private * Parses a string in a special csv format. We expect a csv file where each * line is a date point, and the first field in each line is the date string. * We also expect that all remaining fields represent series. * if the errorBars attribute is set, then interpret the fields as: * date, series1, stddev1, series2, stddev2, ... - * @param {Array.} data See above. - * @private + * @param {[Object]} data See above. * - * @return Array. An array with one entry for each row. These entries + * @return [Object] An array with one entry for each row. These entries * are an array of cells in that row. The first entry is the parsed x-value for * the row. The second, third, etc. are the y-values. These can take on one of * three forms, depending on the CSV and constructor parameters: @@ -2939,9 +2564,10 @@ Dygraph.prototype.parseCSV_ = function(data) { } var start = 0; - if (this.labelsFromCSV_) { + if (!('labels' in this.user_attrs_)) { + // User hasn't explicitly set labels, so they're (presumably) in the CSV. start = 1; - this.attrs_.labels = lines[0].split(delim); + this.attrs_.labels = lines[0].split(delim); // NOTE: _not_ user_attrs_. } var line_no = 0; @@ -2970,21 +2596,45 @@ Dygraph.prototype.parseCSV_ = function(data) { for (var j = 1; j < inFields.length; j++) { // TODO(danvk): figure out an appropriate way to flag parse errors. var vals = inFields[j].split("/"); - fields[j] = [this.parseFloat_(vals[0], i, line), - this.parseFloat_(vals[1], i, line)]; + if (vals.length != 2) { + this.error('Expected fractional "num/den" values in CSV data ' + + "but found a value '" + inFields[j] + "' on line " + + (1 + i) + " ('" + line + "') which is not of this form."); + fields[j] = [0, 0]; + } else { + fields[j] = [this.parseFloat_(vals[0], i, line), + this.parseFloat_(vals[1], i, line)]; + } } } else if (this.attr_("errorBars")) { // If there are error bars, values are (value, stddev) pairs - for (var j = 1; j < inFields.length; j += 2) + if (inFields.length % 2 != 1) { + this.error('Expected alternating (value, stdev.) pairs in CSV data ' + + 'but line ' + (1 + i) + ' has an odd number of values (' + + (inFields.length - 1) + "): '" + line + "'"); + } + for (var 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++) { - var vals = inFields[j].split(";"); - fields[j] = [ this.parseFloat_(vals[0], i, line), - this.parseFloat_(vals[1], i, line), - this.parseFloat_(vals[2], i, line) ]; + var val = inFields[j]; + if (/^ *$/.test(val)) { + fields[j] = [null, null, null]; + } else { + var vals = val.split(";"); + if (vals.length == 3) { + fields[j] = [ this.parseFloat_(vals[0], i, line), + this.parseFloat_(vals[1], i, line), + this.parseFloat_(vals[2], i, line) ]; + } else { + this.warning('When using customBars, values must be either blank ' + + 'or "low;center;high" tuples (got "' + val + + '" on line ' + (1+i)); + } + } } } else { // Values are just numbers @@ -3030,11 +2680,12 @@ Dygraph.prototype.parseCSV_ = function(data) { }; /** + * @private * The user has provided their data as a pre-packaged JS array. If the x values * are numeric, this is the same as dygraphs' internal format. If the x values * are dates, we need to convert them from Date objects to ms since epoch. - * @param {Array.} data - * @return {Array.} data with numeric x values. + * @param {[Object]} data + * @return {[Object]} data with numeric x values. */ Dygraph.prototype.parseArray_ = function(data) { // Peek at the first x value to see if it's numeric. @@ -3080,7 +2731,8 @@ Dygraph.prototype.parseArray_ = function(data) { return parsedData; } else { // Some intelligent defaults for a numeric x-axis. - this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; + /** @private (shut up, jsdoc!) */ + this.attrs_.xValueFormatter = function(x) { return x; }; this.attrs_.xTicker = Dygraph.numericTicks; return data; } @@ -3092,7 +2744,7 @@ Dygraph.prototype.parseArray_ = function(data) { * number. All subsequent columns must be numbers. If there is a clear mismatch * between this.xValueParser_ and the type of the first column, it will be * fixed. Fills out rawData_. - * @param {Array.} data See above. + * @param {[Object]} data See above. * @private */ Dygraph.prototype.parseDataTable_ = function(data) { @@ -3106,7 +2758,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else if (indepType == 'number') { - this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; + 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; @@ -3185,6 +2837,11 @@ Dygraph.prototype.parseDataTable_ = function(data) { annotations.push(ann); } } + + // Strip out infinities, which give dygraphs problems later on. + for (var j = 0; j < row.length; j++) { + if (!isFinite(row[j])) row[j] = null; + } } else { for (var j = 0; j < cols - 1; j++) { row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]); @@ -3193,11 +2850,6 @@ Dygraph.prototype.parseDataTable_ = function(data) { if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) { outOfOrder = true; } - - // Strip out infinities, which give dygraphs problems later on. - for (var j = 0; j < row.length; j++) { - if (!isFinite(row[j])) row[j] = null; - } ret.push(row); } @@ -3212,54 +2864,6 @@ Dygraph.prototype.parseDataTable_ = function(data) { } } -// These functions are all based on MochiKit. -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; -}; - -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; -}; - -Dygraph.isDateLike = function (o) { - if (typeof(o) != "object" || o === null || - typeof(o.getTime) != 'function') { - return false; - } - return true; -}; - -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. @@ -3286,7 +2890,8 @@ Dygraph.prototype.start_ = function() { var caller = this; req.onreadystatechange = function () { if (req.readyState == 4) { - if (req.status == 200) { + if (req.status == 200 || // Normal http + req.status == 0) { // Chrome w/ --allow-file-access-from-files caller.loadedEvent_(req.responseText); } } @@ -3306,15 +2911,32 @@ Dygraph.prototype.start_ = function() { *
  • file: changes the source data for the graph
  • *
  • errorBars: changes whether the data contains stddev
  • * + * + * There's a huge variety of options that can be passed to this method. For a + * full list, see http://dygraphs.com/options.html. + * * @param {Object} attrs The new properties and values + * @param {Boolean} [block_redraw] Usually the chart is redrawn after every + * call to updateOptions(). If you know better, you can pass true to explicitly + * block the redraw. This can be useful for chaining updateOptions() calls, + * avoiding the occasional infinite loop and preventing redraws when it's not + * necessary (e.g. when updating a callback). */ -Dygraph.prototype.updateOptions = function(attrs) { - // TODO(danvk): this is a mess. Rethink this function. +Dygraph.prototype.updateOptions = function(attrs, block_redraw) { + if (typeof(block_redraw) == 'undefined') block_redraw = false; + + // TODO(danvk): this is a mess. Move these options into attr_. if ('rollPeriod' in attrs) { this.rollPeriod_ = attrs.rollPeriod; } if ('dateWindow' in attrs) { this.dateWindow_ = attrs.dateWindow; + if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) { + this.zoomed_x_ = attrs.dateWindow != null; + } + } + if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) { + this.zoomed_y_ = attrs.valueRange != null; } // TODO(danvk): validate per-series options. @@ -3325,17 +2947,12 @@ Dygraph.prototype.updateOptions = function(attrs) { // highlightCircleSize Dygraph.update(this.user_attrs_, attrs); - Dygraph.update(this.renderOptions_, attrs); - - this.labelsFromCSV_ = (this.attr_("labels") == null); - // TODO(danvk): this doesn't match the constructor logic - this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") }); if (attrs['file']) { this.file_ = attrs['file']; - this.start_(); + if (!block_redraw) this.start_(); } else { - this.predraw_(); + if (!block_redraw) this.predraw_(); } }; @@ -3347,8 +2964,8 @@ Dygraph.prototype.updateOptions = function(attrs) { * This is far more efficient than destroying and re-instantiating a * Dygraph, since it doesn't have to reparse the underlying data. * - * @param {Number} width Width (in pixels) - * @param {Number} height Height (in pixels) + * @param {Number} [width] Width (in pixels) + * @param {Number} [height] Height (in pixels) */ Dygraph.prototype.resize = function(width, height) { if (this.resize_lock) { @@ -3421,6 +3038,16 @@ 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. */ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { @@ -3452,6 +3079,12 @@ Dygraph.prototype.indexFromSetName = function(name) { return null; }; +/** + * @private + * Adds a default style for the annotation CSS classes to the document. This is + * only executed when annotations are actually used. It is designed to only be + * called once -- all calls after the first will return immediately. + */ Dygraph.addAnnotationRule = function() { if (Dygraph.addedAnnotationCSS) return; @@ -3487,76 +3120,5 @@ Dygraph.addAnnotationRule = function() { this.warn("Unable to add default annotation CSS rule; display may be off."); } -/** - * 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; -}; - - -/** - * A wrapper around Dygraph that implements the gviz API. - * @param {Object} container The DOM object the visualization should live in. - */ -Dygraph.GVizChart = function(container) { - this.container = container; -} - -Dygraph.GVizChart.prototype.draw = function(data, options) { - // Clear out any existing dygraph. - // TODO(danvk): would it make more sense to simply redraw using the current - // date_graph object? - this.container.innerHTML = ''; - if (typeof(this.date_graph) != 'undefined') { - this.date_graph.destroy(); - } - - this.date_graph = new Dygraph(this.container, data, options); -} - -/** - * Google charts compatible setSelection - * Only row selection is supported, all points in the row will be highlighted - * @param {Array} array of the selected cells - * @public - */ -Dygraph.GVizChart.prototype.setSelection = function(selection_array) { - var row = false; - if (selection_array.length) { - row = selection_array[0].row; - } - this.date_graph.setSelection(row); -} - -/** - * Google charts compatible getSelection implementation - * @return {Array} array of the selected cells - * @public - */ -Dygraph.GVizChart.prototype.getSelection = function() { - var selection = []; - - var row = this.date_graph.getSelection(); - - if (row < 0) return selection; - - col = 1; - for (var i in this.date_graph.layout_.datasets) { - selection.push({row: row, column: col}); - col++; - } - - return selection; -} - // Older pages may still use this name. DateGraph = Dygraph;