parseFloatOrNull
[dygraphs.git] / dygraph.js
index ee5306c..fe38657 100644 (file)
@@ -134,6 +134,9 @@ Dygraph.INFO = 2;
 Dygraph.WARNING = 3;
 Dygraph.ERROR = 3;
 
+// Used for initializing annotation CSS rules only once.
+Dygraph.addedAnnotationCSS = false;
+
 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,
@@ -170,6 +173,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   this.valueRange_ = attrs.valueRange || null;
   this.wilsonInterval_ = attrs.wilsonInterval || true;
   this.is_initial_draw_ = true;
+  this.annotations_ = [];
 
   // Clear the div. This ensure that, if multiple dygraphs are passed the same
   // div, then only one will be drawn.
@@ -227,6 +231,8 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   // Make a note of whether labels will be pulled from the CSV file.
   this.labelsFromCSV_ = (this.attr_("labels") == null);
 
+  Dygraph.addAnnotationRule();
+
   // Create the containing DIV and other interactive elements
   this.createInterface_();
 
@@ -1609,6 +1615,7 @@ Dygraph.prototype.drawGraph_ = function(data) {
   }
 
   for (var i = 1; i < datasets.length; i++) {
+    if (!this.visibility()[i - 1]) continue;
     this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
   }
 
@@ -1890,6 +1897,12 @@ Dygraph.prototype.parseCSV_ = function(data) {
     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) {
+    if (x.length == 0) return null;
+    return parseFloat(x);
+  };
+
   var xParser;
   var defaultParserSet = false;  // attempt to auto-detect x value type
   var expectedCols = this.attr_("labels").length;
@@ -1914,25 +1927,25 @@ 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] = [parseFloat(vals[0]), parseFloat(vals[1])];
+        fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
       }
     } 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] = [parseFloat(inFields[j]),
-                               parseFloat(inFields[j + 1])];
+        fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
+                               parseFloatOrNull(inFields[j + 1])];
     } 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] = [ parseFloat(vals[0]),
-                      parseFloat(vals[1]),
-                      parseFloat(vals[2]) ];
+        fields[j] = [ parseFloatOrNull(vals[0]),
+                      parseFloatOrNull(vals[1]),
+                      parseFloatOrNull(vals[2]) ];
       }
     } else {
       // Values are just numbers
       for (var j = 1; j < inFields.length; j++) {
-        fields[j] = parseFloat(inFields[j]);
+        fields[j] = parseFloatOrNull(inFields[j]);
       }
     }
     if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
@@ -2017,7 +2030,7 @@ Dygraph.prototype.parseArray_ = function(data) {
  * The data is expected to have a first column that is either a date or a
  * number. All subsequent columns must be numbers. If there is a clear mismatch
  * between this.xValueParser_ and the type of the first column, it will be
- * fixed. Returned value is in the same format as return value of parseCSV_.
+ * fixed. Fills out rawData_.
  * @param {Array.<Object>} data See above.
  * @private
  */
@@ -2025,15 +2038,6 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   var cols = data.getNumberOfColumns();
   var rows = data.getNumberOfRows();
 
-  // Read column labels
-  var labels = [];
-  for (var i = 0; i < cols; i++) {
-    labels.push(data.getColumnLabel(i));
-    if (i != 0 && this.attr_("errorBars")) i += 1;
-  }
-  this.attrs_.labels = labels;
-  cols = labels.length;
-
   var indepType = data.getColumnType(0);
   if (indepType == 'date' || indepType == 'datetime') {
     this.attrs_.xValueFormatter = Dygraph.dateString_;
@@ -2051,8 +2055,41 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     return null;
   }
 
+  // Array of the column indices which contain data (and not annotations).
+  var colIdx = [];
+  var annotationCols = {};  // data index -> [annotation cols]
+  var hasAnnotations = false;
+  for (var i = 1; i < cols; i++) {
+    var type = data.getColumnType(i);
+    if (type == 'number') {
+      colIdx.push(i);
+    } else if (type == 'string' && this.attr_('displayAnnotations')) {
+      // This is OK -- it's an annotation column.
+      var dataIdx = colIdx[colIdx.length - 1];
+      if (!annotationCols.hasOwnProperty(dataIdx)) {
+        annotationCols[dataIdx] = [i];
+      } else {
+        annotationCols[dataIdx].push(i);
+      }
+      hasAnnotations = true;
+    } else {
+      this.error("Only 'number' is supported as a dependent type with Gviz." +
+                 " 'string' is only supported if displayAnnotations is true");
+    }
+  }
+
+  // Read column labels
+  // TODO(danvk): add support back for errorBars
+  var labels = [data.getColumnLabel(0)];
+  for (var i = 0; i < colIdx.length; i++) {
+    labels.push(data.getColumnLabel(colIdx[i]));
+  }
+  this.attrs_.labels = labels;
+  cols = labels.length;
+
   var ret = [];
   var outOfOrder = false;
+  var annotations = [];
   for (var i = 0; i < rows; i++) {
     var row = [];
     if (typeof(data.getValue(i, 0)) === 'undefined' ||
@@ -2068,8 +2105,23 @@ Dygraph.prototype.parseDataTable_ = function(data) {
       row.push(data.getValue(i, 0));
     }
     if (!this.attr_("errorBars")) {
-      for (var j = 1; j < cols; j++) {
-        row.push(data.getValue(i, j));
+      for (var j = 0; j < colIdx.length; j++) {
+        var col = colIdx[j];
+        row.push(data.getValue(i, col));
+        if (hasAnnotations &&
+            annotationCols.hasOwnProperty(col) &&
+            data.getValue(i, annotationCols[col][0]) != null) {
+          var ann = {};
+          ann.series = data.getColumnLabel(col);
+          ann.xval = row[0];
+          ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
+          ann.text = '';
+          for (var k = 0; k < annotationCols[col].length; k++) {
+            if (k) ann.text += "\n";
+            ann.text += data.getValue(i, annotationCols[col][k]);
+          }
+          annotations.push(ann);
+        }
       }
     } else {
       for (var j = 0; j < cols - 1; j++) {
@@ -2086,7 +2138,11 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     this.warn("DataTable is out of order; order it correctly to speed loading.");
     ret.sort(function(a,b) { return a[0] - b[0] });
   }
-  return ret;
+  this.rawData_ = ret;
+
+  if (annotations.length > 0) {
+    this.setAnnotations(annotations, true);
+  }
 }
 
 // These functions are all based on MochiKit.
@@ -2152,7 +2208,7 @@ Dygraph.prototype.start_ = function() {
   } else if (typeof this.file_ == 'object' &&
              typeof this.file_.getColumnRange == 'function') {
     // must be a DataTable from gviz.
-    this.rawData_ = this.parseDataTable_(this.file_);
+    this.parseDataTable_(this.file_);
     this.drawGraph_(this.rawData_);
   } else if (typeof this.file_ == 'string') {
     // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
@@ -2197,6 +2253,7 @@ Dygraph.prototype.updateOptions = function(attrs) {
     this.valueRange_ = attrs.valueRange;
   }
   Dygraph.update(this.user_attrs_, attrs);
+  Dygraph.update(this.renderOptions_, attrs);
 
   this.labelsFromCSV_ = (this.attr_("labels") == null);
 
@@ -2292,6 +2349,52 @@ Dygraph.prototype.setVisibility = function(num, value) {
 };
 
 /**
+ * Update the list of annotations and redraw the chart.
+ */
+Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
+  this.annotations_ = ann;
+  this.layout_.setAnnotations(this.annotations_);
+  if (!suppressDraw) {
+    this.drawGraph_(this.rawData_);
+  }
+};
+
+/**
+ * Return the list of annotations.
+ */
+Dygraph.prototype.annotations = function() {
+  return this.annotations_;
+};
+
+Dygraph.addAnnotationRule = function() {
+  if (Dygraph.addedAnnotationCSS) return;
+
+  var mysheet;
+  if (document.styleSheets.length > 0) {
+    mysheet = document.styleSheets[0];
+  } else {
+    var styleSheetElement = document.createElement("style");
+    styleSheetElement.type = "text/css";
+    document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
+    for(i = 0; i < document.styleSheets.length; i++) {
+      if (document.styleSheets[i].disabled) continue;
+      mysheet = document.styleSheets[i];
+    }
+  }
+
+  var rule = "border: 1px solid black; " +
+             "background-color: white; " +
+             "text-align: center;";
+  if (mysheet.insertRule) {  // Firefox
+    mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }");
+  } else if (mysheet.addRule) {  // IE
+    mysheet.addRule(".dygraphDefaultAnnotation", rule);
+  }
+
+  Dygraph.addedAnnotationCSS = true;
+}
+
+/**
  * Create a new canvas element. This is more complex than a simple
  * document.createElement("canvas") because of IE and excanvas.
  */