X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=c04bccc724284eb6cb88306db3007f681731496c;hb=48e614acf3084b9836803f8b014bf0e65f1ca119;hp=560d5d3a8e68987f84b645e690af307ea6ed6c15;hpb=846f3d2dcf876af22f00b7ba65fb1cf8f23685c7;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 560d5d3..c04bccc 100644 --- a/dygraph.js +++ b/dygraph.js @@ -86,18 +86,96 @@ 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; -} +// 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 +187,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 +197,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,14 +239,30 @@ Dygraph.DEFAULT_ATTRS = { drawXGrid: true, gridLineColor: "rgb(128,128,128)", - interactionModel: null // will be set to Dygraph.Interaction.defaultModel -}; + interactionModel: null, // will be set to Dygraph.Interaction.defaultModel -// Various logging levels. -Dygraph.DEBUG = 1; -Dygraph.INFO = 2; -Dygraph.WARNING = 3; -Dygraph.ERROR = 3; + // 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 + } + } +}; // Directions for panning and zooming. Use bit operations when combined // values are possible. @@ -182,23 +272,6 @@ 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, @@ -234,6 +307,13 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // Support two-argument constructor if (attrs == null) { attrs = {}; } + attrs = Dygraph.mapLegacyOptions_(attrs); + + if (!div) { + Dygraph.error("Constructing dygraph with a non-existent div!"); + return; + } + // Copy the important bits into the object // TODO(danvk): most of these should just stay in the attrs_ dictionary. this.maindiv_ = div; @@ -255,31 +335,25 @@ 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.offsetHeight == 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.offsetWidth; + this.height_ = div.offsetHeight; // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. if (attrs['stackedGraph']) { @@ -299,8 +373,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_ = []; @@ -371,45 +446,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); + }; }; /** @@ -681,51 +748,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. @@ -772,6 +794,12 @@ Dygraph.prototype.createInterface_ = function() { 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(); + }); }; /** @@ -826,47 +854,6 @@ 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. - * @private - */ -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 * equally-spaced around the color wheel. If a custom set of colors is @@ -908,44 +895,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 @@ -972,6 +921,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]; @@ -1034,42 +984,6 @@ 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). */ @@ -1097,13 +1011,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. @@ -1118,7 +1032,8 @@ 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, @@ -1430,16 +1345,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). @@ -1467,9 +1372,15 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { return html; } - var html = this.attr_('xValueFormatter')(x) + ":"; + var xOptView = this.optionsViewForAxis_('x'); + var xvf = xOptView('valueFormatter'); + var html = xvf(x, xOptView, this.attr_('labels')[0], this) + ":"; - var fmtFunc = this.attr_('yValueFormatter'); + var yOptViews = []; + var num_axes = this.numAxes(); + for (var 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++) { @@ -1478,8 +1389,11 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { if (!Dygraph.isOK(pt.canvasy)) continue; if (sepLines) html += "
"; + var yOptView = yOptViews[this.seriesToAxisMap_[pt.name]]; + var fmtFunc = yOptView('valueFormatter'); var c = this.plotter_.colors[pt.name]; - var yval = fmtFunc(pt.yval, this); + var yval = fmtFunc(pt.yval, yOptView, pt.name, this); + // TODO(danvk): use a template string here and make it an attribute. html += " " + pt.name + ":" @@ -1641,161 +1555,6 @@ Dygraph.prototype.getSelection = function() { }; /** - * 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, e.g. '0.00001' instead of '1e-5'. 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); -}; - -/** - * @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.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 '' + 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 @@ -1805,10 +1564,6 @@ Dygraph.prototype.loadedEvent_ = function(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 @@ -1822,410 +1577,18 @@ Dygraph.prototype.addXTicks_ = function() { range = [this.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]]; } - var xTicks = this.attr_('xTicker')(range[0], range[1], this); + 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); }; -// 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; - } - } - - if (chosen >= 0) { - return self.GetXAxis(startDate, endDate, chosen); - } else { - // TODO(danvk): signal error. - } -}; - -/** - * @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++) { - var range = Math.pow(10, power); - for (var mult = 1; mult <= 9; mult++) { - var val = range * mult; - vals.push(val); - } - } - return vals; -}(); - -/** - * @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) { - 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); - } -}; - -// TODO(konigsberg): Update comment. -/** - * Add ticks when the x axis has numbers on it (instead of dates) - * - * @param {Number} minV minimum value - * @param {Number} maxV maximum value - * @param self - * @param {function} attribute accessor function. - * @return {[Object]} Array of {label, value} tuples. - */ -Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { - var attr = function(k) { - if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k]; - return self.attr_(k); - }; - - var ticks = []; - if (vals) { - for (var i = 0; i < vals.length; i++) { - ticks.push({v: vals[i]}); - } - } else { - if (axis_props && attr("logscale")) { - var pixelsPerTick = attr('pixelsPerYLabel'); - // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h? - var nTicks = Math.floor(self.height_ / pixelsPerTick); - var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1); - var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1); - if (minIdx == -1) { - minIdx = 0; - } - if (maxIdx == -1) { - maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1; - } - // Count the number of tick values would appear, if we can get at least - // nTicks / 4 accept them. - var lastDisplayed = null; - if (maxIdx - minIdx >= 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; -}; - /** * @private * Computes the range of the data series (including confidence intervals). @@ -2278,6 +1641,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_(); @@ -2299,6 +1664,10 @@ Dygraph.prototype.predraw_ = function() { // 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); }; /** @@ -2314,6 +1683,8 @@ Dygraph.prototype.predraw_ = function() { * @private */ Dygraph.prototype.drawGraph_ = function(clearSelection) { + var start = new Date(); + if (typeof(clearSelection) === 'undefined') { clearSelection = true; } @@ -2451,6 +1822,17 @@ Dygraph.prototype.drawGraph_ = function(clearSelection) { 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, @@ -2499,7 +1881,6 @@ Dygraph.prototype.computeYAxes_ = function() { } } - this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis. this.seriesToAxisMap_ = {}; @@ -2630,7 +2011,7 @@ 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]; @@ -2699,32 +2080,36 @@ 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. + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); if (i == 0 || axis.independentTicks) { - axis.ticks = - Dygraph.numericTicks(axis.computedValueRange[0], - axis.computedValueRange[1], - this, - axis); + 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); } } }; - + /** * @private * Calculates the rolling average of a data set. @@ -2741,7 +2126,7 @@ 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); + var rollPeriod = Math.min(rollPeriod, originalData.length); var rollingData = []; var sigma = this.attr_("sigma"); @@ -2807,9 +2192,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 @@ -2863,40 +2252,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. @@ -2914,18 +2269,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; } }; @@ -3136,9 +2491,9 @@ Dygraph.prototype.parseArray_ = function(data) { 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); @@ -3159,8 +2514,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; } }; @@ -3180,15 +2536,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 + "')"); @@ -3292,80 +2648,6 @@ Dygraph.prototype.parseDataTable_ = function(data) { } /** - * @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. -/** - * Copies all the properties from o to self. - * - * @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. * @private @@ -3423,9 +2705,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; @@ -3447,17 +2733,63 @@ 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); + + Dygraph.updateDeep(this.user_attrs_, attrs); - if (attrs['file']) { - this.file_ = attrs['file']; + 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; +}; + +/** * Resizes the dygraph. If no parameters are specified, resizes to fill the * containing div (which has presumably changed size since the dygraph was * instantiated. If the width/height are specified, the div will be resized. @@ -3480,9 +2812,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"; @@ -3494,8 +2825,14 @@ Dygraph.prototype.resize = function(width, height) { this.height_ = this.maindiv_.offsetHeight; } - 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_(); + this.predraw_(); + } this.resize_lock = false; }; @@ -3539,6 +2876,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) { @@ -3611,21 +2958,5 @@ 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;