X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph-utils.js;h=8b9dfd9940e2d113b2f5d5096e0b47f5215424ba;hb=758a629f806fa73483f730fb343013acd0ace078;hp=27196d8c22ea90efcbd5beca7b42bd0ccef3aa7a;hpb=8442269f2a0a809ece4bf150d61ed8a15bf84788;p=dygraphs.git diff --git a/dygraph-utils.js b/dygraph-utils.js index 27196d8..8b9dfd9 100644 --- a/dygraph-utils.js +++ b/dygraph-utils.js @@ -1,5 +1,8 @@ -// Copyright 2011 Dan Vanderkam (danvdk@gmail.com) -// All Rights Reserved. +/** + * @license + * Copyright 2011 Dan Vanderkam (danvdk@gmail.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ /** * @fileoverview This file contains utility functions used by dygraphs. These @@ -8,13 +11,17 @@ * search) and generic DOM-manipulation functions. */ +/*jshint globalstrict: true */ +/*global Dygraph:false, G_vmlCanvasManager:false, Node:false, printStackTrace: false */ +"use strict"; + Dygraph.LOG_SCALE = 10; Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE); /** @private */ Dygraph.log10 = function(x) { return Math.log(x) / Dygraph.LN_TEN; -} +}; // Various logging levels. Dygraph.DEBUG = 1; @@ -22,7 +29,12 @@ Dygraph.INFO = 2; Dygraph.WARNING = 3; Dygraph.ERROR = 3; -// TODO(danvk): any way I can get the line numbers to be this.warn call? +// Set this to log stack traces on warnings, etc. +// This requires stacktrace.js, which is up to you to provide. +// A copy can be found in the dygraphs repo, or at +// https://github.com/eriwen/javascript-stacktrace +Dygraph.LOG_STACK_TRACES = false; + /** * @private * Log an error on the JS console at the given severity. @@ -30,6 +42,24 @@ Dygraph.ERROR = 3; * @param { String } The message to log. */ Dygraph.log = function(severity, message) { + var st; + if (typeof(printStackTrace) != 'undefined') { + // Remove uninteresting bits: logging functions and paths. + st = printStackTrace({guess:false}); + while (st[0].indexOf("stacktrace") != -1) { + st.splice(0, 1); + } + + st.splice(0, 2); + for (var i = 0; i < st.length; i++) { + st[i] = st[i].replace(/\([^)]*\/(.*)\)/, '@$1') + .replace(/\@.*\/([^\/]*)/, '@$1') + .replace('[object Object].', ''); + } + var top_msg = st.splice(0, 1)[0]; + message += ' (' + top_msg.replace(/^.*@ ?/, '') + ')'; + } + if (typeof(console) != 'undefined') { switch (severity) { case Dygraph.DEBUG: @@ -46,6 +76,10 @@ Dygraph.log = function(severity, message) { break; } } + + if (Dygraph.LOG_STACK_TRACES) { + console.log(st.join('\n')); + } }; /** @private */ @@ -90,20 +124,35 @@ Dygraph.getContext = function(canvas) { * @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 { DOM element } elem The element to add the event to. + * @param { String } type The type 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); +Dygraph.addEvent = function addEvent(elem, type, fn) { + if (elem.addEventListener) { + elem.addEventListener(type, fn, false); + } else { + elem[type+fn] = function(){fn(window.event);}; + elem.attachEvent('on'+type, elem[type+fn]); + } +}; + +/** + * @private + * Remove an event handler. This smooths a difference between IE and the rest of + * the world. + * @param { DOM element } elem The element to add the event to. + * @param { String } type The type 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.removeEvent = function addEvent(elem, type, fn) { + if (elem.removeEventListener) { + elem.removeEventListener(type, fn, false); + } else { + elem.detachEvent('on'+type, elem[type+fn]); + elem[type+fn] = null; } }; @@ -172,6 +221,7 @@ Dygraph.hsvToRGB = function (hue, saturation, value) { // 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 +// ... and modifications to support scrolling divs. /** * Find the x-coordinate of the supplied object relative to the left side @@ -269,6 +319,7 @@ Dygraph.pageY = function(e) { * @return { Boolean } Whether the number is zero or NaN. */ // TODO(danvk): rename this function to something like 'isNonZeroNan'. +// TODO(danvk): determine when else this returns false (e.g. for undefined or null) Dygraph.isOK = function(x) { return x && !isNaN(x); }; @@ -311,7 +362,7 @@ Dygraph.floatFormat = function(x, opt_precision) { // // 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) ? + return (Math.abs(x) < 1.0e-3 && x !== 0.0) ? x.toExponential(p - 1) : x.toPrecision(p); }; @@ -342,30 +393,6 @@ Dygraph.hmsString_ = function(date) { }; /** - * 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 @@ -390,28 +417,31 @@ Dygraph.round_ = function(num, places) { * @param { Integer } [high] The last index in arry to consider (optional) */ Dygraph.binarySearch = function(val, arry, abs, low, high) { - if (low == null || high == null) { + if (low === null || low === undefined || + high === null || high === undefined) { low = 0; high = arry.length - 1; } if (low > high) { return -1; } - if (abs == null) { + if (abs === null || abs === undefined) { abs = 0; } var validIndex = function(idx) { return idx >= 0 && idx < arry.length; - } - var mid = parseInt((low + high) / 2); + }; + var mid = parseInt((low + high) / 2, 10); var element = arry[mid]; if (element == val) { return mid; } + + var idx; if (element > val) { if (abs > 0) { // Accept if element > val, but also if prior element < val. - var idx = mid - 1; + idx = mid - 1; if (validIndex(idx) && arry[idx] < val) { return mid; } @@ -421,7 +451,7 @@ Dygraph.binarySearch = function(val, arry, abs, low, high) { if (element < val) { if (abs < 0) { // Accept if element < val, but also if prior element > val. - var idx = mid + 1; + idx = mid + 1; if (validIndex(idx) && arry[idx] > val) { return mid; } @@ -441,6 +471,11 @@ Dygraph.binarySearch = function(val, arry, abs, low, high) { Dygraph.dateParser = function(dateStr) { var dateStrSlashed; var d; + + // Let the system try the format first. + d = Dygraph.dateStrToMillis(dateStr); + if (d && !isNaN(d)) return d; + if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12' dateStrSlashed = dateStr.replace("-", "/", "g"); while (dateStrSlashed.search("-") != -1) { @@ -449,8 +484,8 @@ Dygraph.dateParser = function(dateStr) { 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); + 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 @@ -494,6 +529,44 @@ Dygraph.update = function (self, o) { }; /** + * Copies all the properties from o to self. + * + * @private + */ +Dygraph.updateDeep = function (self, o) { + // Taken from http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object + function isNode(o) { + return ( + typeof Node === "object" ? o instanceof Node : + typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string" + ); + } + + if (typeof(o) != 'undefined' && o !== null) { + for (var k in o) { + if (o.hasOwnProperty(k)) { + if (o[k] === null) { + self[k] = null; + } else if (Dygraph.isArrayLike(o[k])) { + self[k] = o[k].slice(); + } else if (isNode(o[k])) { + // DOM objects are shallowly-copied. + self[k] = o[k]; + } else if (typeof(o[k]) == 'object') { + if (typeof(self[k]) != 'object') { + self[k] = {}; + } + Dygraph.updateDeep(self[k], o[k]); + } else { + self[k] = o[k]; + } + } + } + } + return self; +}; + +/** * @private */ Dygraph.isArrayLike = function (o) { @@ -522,6 +595,7 @@ Dygraph.isDateLike = function (o) { }; /** + * Note: this only seems to work for arrays. * @private */ Dygraph.clone = function(o) { @@ -545,10 +619,159 @@ Dygraph.clone = function(o) { Dygraph.createCanvas = function() { var canvas = document.createElement("canvas"); - isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); + var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) { canvas = G_vmlCanvasManager.initElement(canvas); } return canvas; }; + +/** + * @private + * Checks whether the user is on an Android browser. + * Android does not fully support the tag, e.g. w/r/t/ clipping. + */ +Dygraph.isAndroid = function() { + return (/Android/).test(navigator.userAgent); +}; + +/** + * @private + * Call a function N times at a given interval, then call a cleanup function + * once. repeat_fn is called once immediately, then (times - 1) times + * asynchronously. If times=1, then cleanup_fn() is also called synchronously. + * @param repeat_fn {Function} Called repeatedly -- takes the number of calls + * (from 0 to times-1) as an argument. + * @param times {number} The number of times to call repeat_fn + * @param every_ms {number} Milliseconds between calls + * @param cleanup_fn {Function} A function to call after all repeat_fn calls. + * @private + */ +Dygraph.repeatAndCleanup = function(repeat_fn, times, every_ms, cleanup_fn) { + var count = 0; + var start_time = new Date().getTime(); + repeat_fn(count); + if (times == 1) { + cleanup_fn(); + return; + } + + (function loop() { + if (count >= times) return; + var target_time = start_time + (1 + count) * every_ms; + setTimeout(function() { + count++; + repeat_fn(count); + if (count >= times - 1) { + cleanup_fn(); + } else { + loop(); + } + }, target_time - new Date().getTime()); + // TODO(danvk): adjust every_ms to produce evenly-timed function calls. + })(); +}; + +/** + * @private + * This function will scan the option list and determine if they + * require us to recalculate the pixel positions of each point. + * @param { List } a list of options to check. + * @return { Boolean } true if the graph needs new points else false. + */ +Dygraph.isPixelChangingOptionList = function(labels, attrs) { + // A whitelist of options that do not change pixel positions. + var pixelSafeOptions = { + 'annotationClickHandler': true, + 'annotationDblClickHandler': true, + 'annotationMouseOutHandler': true, + 'annotationMouseOverHandler': true, + 'axisLabelColor': true, + 'axisLineColor': true, + 'axisLineWidth': true, + 'clickCallback': true, + 'digitsAfterDecimal': true, + 'drawCallback': true, + 'drawPoints': true, + 'drawXGrid': true, + 'drawYGrid': true, + 'fillAlpha': true, + 'gridLineColor': true, + 'gridLineWidth': true, + 'hideOverlayOnMouseOut': true, + 'highlightCallback': true, + 'highlightCircleSize': true, + 'interactionModel': true, + 'isZoomedIgnoreProgrammaticZoom': true, + 'labelsDiv': true, + 'labelsDivStyles': true, + 'labelsDivWidth': true, + 'labelsKMB': true, + 'labelsKMG2': true, + 'labelsSeparateLines': true, + 'labelsShowZeroValues': true, + 'legend': true, + 'maxNumberWidth': true, + 'panEdgeFraction': true, + 'pixelsPerYLabel': true, + 'pointClickCallback': true, + 'pointSize': true, + 'rangeSelectorPlotFillColor': true, + 'rangeSelectorPlotStrokeColor': true, + 'showLabelsOnHighlight': true, + 'showRoller': true, + 'sigFigs': true, + 'strokeWidth': true, + 'underlayCallback': true, + 'unhighlightCallback': true, + 'xAxisLabelFormatter': true, + 'xTicker': true, + 'xValueFormatter': true, + 'yAxisLabelFormatter': true, + 'yValueFormatter': true, + 'zoomCallback': true + }; + + // Assume that we do not require new points. + // This will change to true if we actually do need new points. + var requiresNewPoints = false; + + // Create a dictionary of series names for faster lookup. + // If there are no labels, then the dictionary stays empty. + var seriesNamesDictionary = { }; + if (labels) { + for (var i = 1; i < labels.length; i++) { + seriesNamesDictionary[labels[i]] = true; + } + } + + // Iterate through the list of updated options. + for (var property in attrs) { + // Break early if we already know we need new points from a previous option. + if (requiresNewPoints) { + break; + } + if (attrs.hasOwnProperty(property)) { + // Find out of this field is actually a series specific options list. + if (seriesNamesDictionary[property]) { + // This property value is a list of options for this series. + // If any of these sub properties are not pixel safe, set the flag. + for (var subProperty in attrs[property]) { + // Break early if we already know we need new points from a previous option. + if (requiresNewPoints) { + break; + } + if (attrs[property].hasOwnProperty(subProperty) && !pixelSafeOptions[subProperty]) { + requiresNewPoints = true; + } + } + // If this was not a series specific option list, check if its a pixel changing property. + } else if (!pixelSafeOptions[property]) { + requiresNewPoints = true; + } + } + } + + return requiresNewPoints; +};