Check for visible viewport width, not the plotter width
[dygraphs.git] / dygraph-utils.js
index 7282c9c..63fc1e0 100644 (file)
@@ -33,11 +33,13 @@ Dygraph.INFO = 2;
 Dygraph.WARNING = 3;
 Dygraph.ERROR = 3;
 
+// <REMOVE_FOR_COMBINED>
 // 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;
+// </REMOVE_FOR_COMBINED>
 
 /** A dotted line stroke pattern. */
 Dygraph.DOTTED_LINE = [2, 2];
@@ -53,6 +55,7 @@ Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
  * @private
  */
 Dygraph.log = function(severity, message) {
+  // <REMOVE_FOR_COMBINED>
   var st;
   if (typeof(printStackTrace) != 'undefined') {
     try {
@@ -74,6 +77,7 @@ Dygraph.log = function(severity, message) {
       // Oh well, it was worth a shot!
     }
   }
+  // </REMOVE_FOR_COMBINED>
 
   if (typeof(window.console) != 'undefined') {
     // In older versions of Firefox, only console.log is defined.
@@ -102,9 +106,11 @@ Dygraph.log = function(severity, message) {
     }
   }
 
+  // <REMOVE_FOR_COMBINED>
   if (Dygraph.LOG_STACK_TRACES) {
     window.console.log(st.join('\n'));
   }
+  // </REMOVE_FOR_COMBINED>
 };
 
 /**
@@ -114,11 +120,6 @@ Dygraph.log = function(severity, message) {
 Dygraph.info = function(message) {
   Dygraph.log(Dygraph.INFO, message);
 };
-/**
- * @param {string} message
- * @private
- */
-Dygraph.prototype.info = Dygraph.info;
 
 /**
  * @param {string} message
@@ -127,11 +128,6 @@ Dygraph.prototype.info = Dygraph.info;
 Dygraph.warn = function(message) {
   Dygraph.log(Dygraph.WARNING, message);
 };
-/**
- * @param {string} message
- * @private
- */
-Dygraph.prototype.warn = Dygraph.warn;
 
 /**
  * @param {string} message
@@ -139,11 +135,6 @@ Dygraph.prototype.warn = Dygraph.warn;
 Dygraph.error = function(message) {
   Dygraph.log(Dygraph.ERROR, message);
 };
-/**
- * @param {string} message
- * @private
- */
-Dygraph.prototype.error = Dygraph.error;
 
 /**
  * Return the 2d context for a dygraph canvas.
@@ -167,9 +158,9 @@ Dygraph.getContext = function(canvas) {
 /**
  * Add an event handler. This smooths a difference between IE and the rest of
  * the world.
- * @param { !Element } elem The element to add the event to.
- * @param { string } type The type of the event, e.g. 'click' or 'mousemove'.
- * @param { function(Event):(boolean|undefined) } fn The function to call
+ * @param {!Node} elem The element to add the event to.
+ * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param {function(Event):(boolean|undefined)} fn The function to call
  *     on the event. The function takes one parameter: the event object.
  * @private
  */
@@ -186,9 +177,9 @@ Dygraph.addEvent = function addEvent(elem, type, fn) {
  * Add an event handler. This event handler is kept until the graph is
  * destroyed with a call to graph.destroy().
  *
- * @param { !Element } elem The element to add the event to.
- * @param { string } type The type of the event, e.g. 'click' or 'mousemove'.
- * @param { function(Event):(boolean|undefined) } fn The function to call
+ * @param {!Node} elem The element to add the event to.
+ * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param {function(Event):(boolean|undefined)} fn The function to call
  *     on the event. The function takes one parameter: the event object.
  * @private
  */
@@ -200,7 +191,7 @@ Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) {
 /**
  * Remove an event handler. This smooths a difference between IE and the rest
  * of the world.
- * @param {!Element} elem The element to add the event to.
+ * @param {!Node} elem The element to remove the event from.
  * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
  * @param {function(Event):(boolean|undefined)} fn The function to call
  *     on the event. The function takes one parameter: the event object.
@@ -229,14 +220,14 @@ Dygraph.prototype.removeTrackedEvents_ = function() {
   }
 
   this.registeredEvents_ = [];
-}
+};
 
 /**
  * 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.
+ * @param {!Event} e The event whose normal behavior should be canceled.
  * @private
  */
 Dygraph.cancelEvent = function(e) {
@@ -299,76 +290,47 @@ Dygraph.hsvToRGB = function (hue, saturation, value) {
 // ... and modifications to support scrolling divs.
 
 /**
- * Find the x-coordinate of the supplied object relative to the left side
- * of the page.
+ * Find the coordinates of an object relative to the top left of the page.
+ *
  * TODO(danvk): change obj type from Node -&gt; !Node
  * @param {Node} obj
- * @return {number}
+ * @return {{x:number,y:number}}
  * @private
  */
-Dygraph.findPosX = function(obj) {
-  var curleft = 0;
-  if(obj.offsetParent) {
+Dygraph.findPos = function(obj) {
+  var curleft = 0, curtop = 0;
+  if (obj.offsetParent) {
     var copyObj = obj;
-    while(1) {
+    while (1) {
       // NOTE: the if statement here is for IE8.
-      var borderLeft = "0";
+      var borderLeft = "0", borderTop = "0";
       if (window.getComputedStyle) {
-        borderLeft = window.getComputedStyle(copyObj, null).borderLeft || "0";
+        var computedStyle = window.getComputedStyle(copyObj, null);
+        borderLeft = computedStyle.borderLeft || "0";
+        borderTop = computedStyle.borderTop || "0";
       }
       curleft += parseInt(borderLeft, 10) ;
-      curleft += copyObj.offsetLeft;
-      if(!copyObj.offsetParent) {
-        break;
-      }
-      copyObj = copyObj.offsetParent;
-    }
-  } else if(obj.x) {
-    curleft += obj.x;
-  }
-  // This handles the case where the object is inside a scrolled div.
-  while(obj && obj != document.body) {
-    curleft -= obj.scrollLeft;
-    obj = obj.parentNode;
-  }
-  return curleft;
-};
-
-/**
- * Find the y-coordinate of the supplied object relative to the top of the
- * page.
- * TODO(danvk): change obj type from Node -&gt; !Node
- * TODO(danvk): consolidate with findPosX and return an {x, y} object.
- * @param {Node} obj
- * @return {number}
- * @private
- */
-Dygraph.findPosY = function(obj) {
-  var curtop = 0;
-  if(obj.offsetParent) {
-    var copyObj = obj;
-    while(1) {
-      // NOTE: the if statement here is for IE8.
-      var borderTop = "0";
-      if (window.getComputedStyle) {
-        borderTop = window.getComputedStyle(copyObj, null).borderTop || "0";
-      }
       curtop += parseInt(borderTop, 10) ;
+      curleft += copyObj.offsetLeft;
       curtop += copyObj.offsetTop;
-      if(!copyObj.offsetParent) {
+      if (!copyObj.offsetParent) {
         break;
       }
       copyObj = copyObj.offsetParent;
     }
-  } else if(obj.y) {
-    curtop += obj.y;
+  } else {
+    // TODO(danvk): why would obj ever have these properties?
+    if (obj.x) curleft += obj.x;
+    if (obj.y) curtop += obj.y;
   }
+
   // This handles the case where the object is inside a scrolled div.
-  while(obj && obj != document.body) {
+  while (obj && obj != document.body) {
+    curleft -= obj.scrollLeft;
     curtop -= obj.scrollTop;
     obj = obj.parentNode;
   }
-  return curtop;
+  return {x: curleft, y: curtop};
 };
 
 /**
@@ -412,6 +374,28 @@ Dygraph.pageY = function(e) {
 };
 
 /**
+ * Converts page the x-coordinate of the event to pixel x-coordinates on the
+ * canvas (i.e. DOM Coords).
+ * @param {!Event} e Drag event.
+ * @param {!DygraphInteractionContext} context Interaction context object.
+ * @return {number} The amount by which the drag has moved to the right.
+ */
+Dygraph.dragGetX_ = function(e, context) {
+  return Dygraph.pageX(e) - context.px;
+};
+
+/**
+ * Converts page the y-coordinate of the event to pixel y-coordinates on the
+ * canvas (i.e. DOM Coords).
+ * @param {!Event} e Drag event.
+ * @param {!DygraphInteractionContext} context Interaction context object.
+ * @return {number} The amount by which the drag has moved down.
+ */
+Dygraph.dragGetY_ = function(e, context) {
+  return Dygraph.pageY(e) - context.py;
+};
+
+/**
  * This returns true unless the parameter is 0, null, undefined or NaN.
  * TODO(danvk): rename this function to something like 'isNonZeroNan'.
  *
@@ -424,18 +408,18 @@ Dygraph.isOK = function(x) {
 };
 
 /**
- * @param { {x:?number,y:?number,yval:?number} } p The point to consider, valid
+ * @param {{x:?number,y:?number,yval:?number}} p The point to consider, valid
  *     points are {x, y} objects
- * @param { boolean } allowNaNY Treat point with y=NaN as valid
- * @return { boolean } Whether the point has numeric x and y.
+ * @param {boolean=} opt_allowNaNY Treat point with y=NaN as valid
+ * @return {boolean} Whether the point has numeric x and y.
  * @private
  */
-Dygraph.isValidPoint = function(p, allowNaNY) {
+Dygraph.isValidPoint = function(p, opt_allowNaNY) {
   if (!p) return false;  // null or undefined object
   if (p.yval === null) return false;  // missing point
   if (p.x === null || p.x === undefined) return false;
   if (p.y === null || p.y === undefined) return false;
-  if (isNaN(p.x) || (!allowNaNY && isNaN(p.y))) return false;
+  if (isNaN(p.x) || (!opt_allowNaNY && isNaN(p.y))) return false;
   return true;
 };
 
@@ -511,6 +495,30 @@ 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
@@ -698,7 +706,7 @@ Dygraph.updateDeep = function (self, o) {
 };
 
 /**
- * @param {Object} o
+ * @param {*} o
  * @return {boolean}
  * @private
  */
@@ -768,6 +776,30 @@ Dygraph.createCanvas = function() {
 };
 
 /**
+ * Returns the context's pixel ratio, which is the ratio between the device
+ * pixel ratio and the backing store ratio. Typically this is 1 for conventional
+ * displays, and > 1 for HiDPI displays (such as the Retina MBP).
+ * See http://www.html5rocks.com/en/tutorials/canvas/hidpi/ for more details.
+ *
+ * @param {!CanvasRenderingContext2D} context The canvas's 2d context.
+ * @return {number} The ratio of the device pixel ratio and the backing store
+ * ratio for the specified context.
+ */
+Dygraph.getContextPixelRatio = function(context) {
+  try {
+    var devicePixelRatio = window.devicePixelRatio || 1,
+        backingStoreRatio = context.webkitBackingStorePixelRatio ||
+                            context.mozBackingStorePixelRatio ||
+                            context.msBackingStorePixelRatio ||
+                            context.oBackingStorePixelRatio ||
+                            context.backingStorePixelRatio || 1;
+    return devicePixelRatio / backingStoreRatio;
+  } catch (e) {
+    return 1;
+  }
+};
+
+/**
  * Checks whether the user is on an Android browser.
  * Android does not fully support the <canvas> tag, e.g. w/r/t/ clipping.
  * @return {boolean}
@@ -1019,137 +1051,14 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
   return requiresNewPoints;
 };
 
-/**
- * Compares two arrays to see if they are equal. If either parameter is not an
- * array it will return false. Does a shallow compare
- * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false.
- * @param {!Array.<T>} array1 first array
- * @param {!Array.<T>} array2 second array
- * @return {boolean} True if both parameters are arrays, and contents are equal.
- * @template T
- */
-Dygraph.compareArrays = function(array1, array2) {
-  if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) {
-    return false;
-  }
-  if (array1.length !== array2.length) {
-    return false;
-  }
-  for (var i = 0; i < array1.length; i++) {
-    if (array1[i] !== array2[i]) {
-      return false;
-    }
-  }
-  return true;
-};
-
-/**
- * @param {!CanvasRenderingContext2D} ctx the canvas context
- * @param {number} sides the number of sides in the shape.
- * @param {number} radius the radius of the image.
- * @param {number} cx center x coordate
- * @param {number} cy center y coordinate
- * @param {number=} rotationRadians the shift of the initial angle, in radians.
- * @param {number=} delta the angle shift for each line. If missing, creates a
- *     regular polygon.
- * @private
- */
-Dygraph.regularShape_ = function(
-    ctx, sides, radius, cx, cy, rotationRadians, delta) {
-  rotationRadians = rotationRadians || 0;
-  delta = delta || Math.PI * 2 / sides;
-
-  ctx.beginPath();
-  var initialAngle = rotationRadians;
-  var angle = initialAngle;
-
-  var computeCoordinates = function() {
-    var x = cx + (Math.sin(angle) * radius);
-    var y = cy + (-Math.cos(angle) * radius);
-    return [x, y];
-  };
-
-  var initialCoordinates = computeCoordinates();
-  var x = initialCoordinates[0];
-  var y = initialCoordinates[1];
-  ctx.moveTo(x, y);
-
-  for (var idx = 0; idx < sides; idx++) {
-    angle = (idx == sides - 1) ? initialAngle : (angle + delta);
-    var coords = computeCoordinates();
-    ctx.lineTo(coords[0], coords[1]);
-  }
-  ctx.fill();
-  ctx.stroke();
-};
-
-/**
- * TODO(danvk): be more specific on the return type.
- * @param {number} sides
- * @param {number=} rotationRadians
- * @param {number=} delta
- * @return {Function}
- * @private
- */
-Dygraph.shapeFunction_ = function(sides, rotationRadians, delta) {
-  return function(g, name, ctx, cx, cy, color, radius) {
-    ctx.strokeStyle = color;
-    ctx.fillStyle = "white";
-    Dygraph.regularShape_(ctx, sides, radius, cx, cy, rotationRadians, delta);
-  };
-};
-
 Dygraph.Circles = {
   DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
     ctx.beginPath();
     ctx.fillStyle = color;
     ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false);
     ctx.fill();
-  },
-  TRIANGLE : Dygraph.shapeFunction_(3),
-  SQUARE : Dygraph.shapeFunction_(4, Math.PI / 4),
-  DIAMOND : Dygraph.shapeFunction_(4),
-  PENTAGON : Dygraph.shapeFunction_(5),
-  HEXAGON : Dygraph.shapeFunction_(6),
-  CIRCLE : function(g, name, ctx, cx, cy, color, radius) {
-    ctx.beginPath();
-    ctx.strokeStyle = color;
-    ctx.fillStyle = "white";
-    ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false);
-    ctx.fill();
-    ctx.stroke();
-  },
-  STAR : Dygraph.shapeFunction_(5, 0, 4 * Math.PI / 5),
-  PLUS : function(g, name, ctx, cx, cy, color, radius) {
-    ctx.strokeStyle = color;
-
-    ctx.beginPath();
-    ctx.moveTo(cx + radius, cy);
-    ctx.lineTo(cx - radius, cy);
-    ctx.closePath();
-    ctx.stroke();
-
-    ctx.beginPath();
-    ctx.moveTo(cx, cy + radius);
-    ctx.lineTo(cx, cy - radius);
-    ctx.closePath();
-    ctx.stroke();
-  },
-  EX : function(g, name, ctx, cx, cy, color, radius) {
-    ctx.strokeStyle = color;
-
-    ctx.beginPath();
-    ctx.moveTo(cx + radius, cy + radius);
-    ctx.lineTo(cx - radius, cy - radius);
-    ctx.closePath();
-    ctx.stroke();
-
-    ctx.beginPath();
-    ctx.moveTo(cx + radius, cy - radius);
-    ctx.lineTo(cx - radius, cy + radius);
-    ctx.closePath();
-    ctx.stroke();
   }
+  // For more shapes, include extras/shapes.js
 };
 
 /**
@@ -1190,8 +1099,9 @@ Dygraph.IFrameTarp.prototype.cover = function() {
   var iframes = document.getElementsByTagName("iframe");
   for (var i = 0; i < iframes.length; i++) {
     var iframe = iframes[i];
-    var x = Dygraph.findPosX(iframe),
-        y = Dygraph.findPosY(iframe),
+    var pos = Dygraph.findPos(iframe),
+        x = pos.x,
+        y = pos.y,
         width = iframe.offsetWidth,
         height = iframe.offsetHeight;
 
@@ -1303,3 +1213,79 @@ Dygraph.setDateSameTZ = function(d, parts) {
     }
   }
 };
+
+/**
+ * Converts any valid CSS color (hex, rgb(), named color) to an RGB tuple.
+ *
+ * @param {!string} colorStr Any valid CSS color string.
+ * @return {{r:number,g:number,b:number}} Parsed RGB tuple.
+ * @private
+ */
+Dygraph.toRGB_ = function(colorStr) {
+  // TODO(danvk): cache color parses to avoid repeated DOM manipulation.
+  var div = document.createElement('div');
+  div.style.backgroundColor = colorStr;
+  div.style.visibility = 'hidden';
+  document.body.appendChild(div);
+  var rgbStr = window.getComputedStyle(div, null).backgroundColor;
+  document.body.removeChild(div);
+  var bits = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(rgbStr);
+  return {
+    r: parseInt(bits[1], 10),
+    g: parseInt(bits[2], 10),
+    b: parseInt(bits[3], 10)
+  };
+};
+
+/**
+ * Checks whether the browser supports the &lt;canvas&gt; tag.
+ * @param {HTMLCanvasElement=} opt_canvasElement Pass a canvas element as an
+ *     optimization if you have one.
+ * @return {boolean} Whether the browser supports canvas.
+ */
+Dygraph.isCanvasSupported = function(opt_canvasElement) {
+  var canvas;
+  try {
+    canvas = opt_canvasElement || document.createElement("canvas");
+    canvas.getContext("2d");
+  }
+  catch (e) {
+    var ie = navigator.appVersion.match(/MSIE (\d\.\d)/);
+    var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
+    if ((!ie) || (ie[1] < 6) || (opera))
+      return false;
+    return true;
+  }
+  return true;
+};
+
+/**
+ * Parses the value as a floating point number. This is like the parseFloat()
+ * built-in, but with a few differences:
+ * - the empty string is parsed as null, rather than NaN.
+ * - if the string cannot be parsed at all, an error is logged.
+ * If the string can't be parsed, this method returns null.
+ * @param {string} x The string to be parsed
+ * @param {number=} opt_line_no The line number from which the string comes.
+ * @param {string=} opt_line The text of the line from which the string comes.
+ */
+Dygraph.parseFloat_ = function(x, opt_line_no, opt_line) {
+  var val = parseFloat(x);
+  if (!isNaN(val)) return val;
+
+  // Try to figure out what happeend.
+  // If the value is the empty string, parse it as null.
+  if (/^ *$/.test(x)) return null;
+
+  // If it was actually "NaN", return it as NaN.
+  if (/^ *nan *$/i.test(x)) return NaN;
+
+  // Looks like a parsing error.
+  var msg = "Unable to parse '" + x + "' as a number";
+  if (opt_line !== undefined && opt_line_no !== undefined) {
+    msg += " on line " + (1+(opt_line_no||0)) + " ('" + opt_line + "') of CSV.";
+  }
+  Dygraph.error(msg);
+
+  return null;
+};