record Dygraph in independent-series.html
[dygraphs.git] / dygraph.js
index 3e32297..3756702 100644 (file)
@@ -156,7 +156,7 @@ Dygraph.DEFAULT_ATTRS = {
   yValueFormatter: function(x, opt_precision) {
     var s = Dygraph.floatFormat(x, opt_precision);
     var s2 = Dygraph.intFormat(x);
-    return s.length <= s2.length ? s : s2;
+    return s.length < s2.length ? s : s2;
   },
 
   strokeWidth: 1.0,
@@ -221,7 +221,7 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
 
 /**
  * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
- * and context &lt;canvas&gt; inside of it. See the constructor for details
+ * and context &lt;canvas&gt; inside of it. See the constructor for details.
  * on the parameters.
  * @param {Element} div the Element to render the graph into.
  * @param {String | Function} file Source data
@@ -254,7 +254,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.wilsonInterval_ = attrs.wilsonInterval || true;
   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;
@@ -331,6 +331,12 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.start_();
 };
 
+Dygraph.prototype.toString = function() {
+  var maindiv = this.maindiv_;
+  var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
+  return "[Dygraph " + id + "]";
+}
+
 Dygraph.prototype.attr_ = function(name, seriesName) {
   if (seriesName &&
       typeof(this.user_attrs_[seriesName]) != 'undefined' &&
@@ -1469,6 +1475,9 @@ 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.
+  if (points === undefined) return;
+
   var lastx = -1;
   var lasty = -1;
 
@@ -1799,8 +1808,10 @@ Dygraph.prototype.addXTicks_ = function() {
   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) {
-    // numericTicks() returns multiple values.
     xTicks = ret.ticks;
     this.numXDigits_ = ret.numDigits;
   } else {
@@ -2110,7 +2121,7 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
   var ticks = [];
   if (vals) {
     for (var i = 0; i < vals.length; i++) {
-      ticks[i].push({v: vals[i]});
+      ticks.push({v: vals[i]});
     }
   } else {
     if (axis_props && attr("logscale")) {
@@ -2224,6 +2235,7 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
 
   // 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) ?
@@ -2233,13 +2245,14 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
       var n = k*k*k*k;
       for (var j = 3; j >= 0; j--, n /= k) {
         if (absTickV >= n) {
-          label = (tickV / n).toPrecision(numDigits) + k_labels[j];
+          label = formatter(tickV / n, numDigits) + k_labels[j];
           break;
         }
       }
-      ticks[i].label = label;
     }
+    ticks[i].label = label;
   }
+
   return {ticks: ticks, numDigits: numDigits};
 };
 
@@ -2355,7 +2368,7 @@ Dygraph.prototype.drawGraph_ = function() {
         // On the log scale, points less than zero do not exist.
         // This will create a gap in the chart. Note that this ignores
         // connectSeparatedPoints.
-        if (point < 0) {
+        if (point <= 0) {
           point = null;
         }
         series.push([date, point]);
@@ -2857,7 +2870,7 @@ Dygraph.prototype.detectTypeFromString_ = function(str) {
     this.attrs_.xTicker = Dygraph.dateTicker;
     this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
   } else {
-    this.attrs_.xValueFormatter = this.attrs_.xValueFormatter;
+    this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
     this.attrs_.xTicker = Dygraph.numericTicks;
     this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
@@ -2865,6 +2878,40 @@ Dygraph.prototype.detectTypeFromString_ = function(str) {
 };
 
 /**
+ * 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.
+ * @private
+ */
+
+// Parse the x as a float or return null if it's not a number.
+Dygraph.prototype.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 !== null && opt_line_no !== null) {
+    msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
+  }
+  this.error(msg);
+
+  return null;
+};
+
+/**
  * 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.
@@ -2896,13 +2943,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
     start = 1;
     this.attrs_.labels = lines[0].split(delim);
   }
-
-  // Parse the x as a float or return null if it's not a number.
-  var parseFloatOrNull = function(x) {
-    var val = parseFloat(x);
-    // isFinite() returns false for NaN and +/-Infinity.
-    return isFinite(val) ? val : null;
-  };
+  var line_no = 0;
 
   var xParser;
   var defaultParserSet = false;  // attempt to auto-detect x value type
@@ -2910,6 +2951,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
   var outOfOrder = false;
   for (var i = start; i < lines.length; i++) {
     var line = lines[i];
+    line_no = i;
     if (line.length == 0) continue;  // skip blank lines
     if (line[0] == '#') continue;    // skip comment lines
     var inFields = line.split(delim);
@@ -2928,37 +2970,55 @@ 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] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
+        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)
-        fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
-                               parseFloatOrNull(inFields[j + 1])];
+        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] = [ parseFloatOrNull(vals[0]),
-                      parseFloatOrNull(vals[1]),
-                      parseFloatOrNull(vals[2]) ];
+        fields[j] = [ this.parseFloat_(vals[0], i, line),
+                      this.parseFloat_(vals[1], i, line),
+                      this.parseFloat_(vals[2], i, line) ];
       }
     } else {
       // Values are just numbers
       for (var j = 1; j < inFields.length; j++) {
-        fields[j] = parseFloatOrNull(inFields[j]);
+        fields[j] = this.parseFloat_(inFields[j], i, line);
       }
     }
     if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
       outOfOrder = true;
     }
-    ret.push(fields);
 
     if (fields.length != expectedCols) {
       this.error("Number of columns in line " + i + " (" + fields.length +
                  ") does not agree with number of labels (" + expectedCols +
                  ") " + line);
     }
+
+    // If the user specified the 'labels' option and none of the cells of the
+    // first row parsed correctly, then they probably double-specified the
+    // labels. We go with the values set in the option, discard this row and
+    // log a warning to the JS console.
+    if (i == 0 && this.attr_('labels')) {
+      var all_null = true;
+      for (var j = 0; all_null && j < fields.length; j++) {
+        if (fields[j]) all_null = false;
+      }
+      if (all_null) {
+        this.warn("The dygraphs 'labels' option is set, but the first row of " +
+                  "CSV data ('" + line + "') appears to also contain labels. " +
+                  "Will drop the CSV labels and use the option labels.");
+        continue;
+      }
+    }
+    ret.push(fields);
   }
 
   if (outOfOrder) {