data access API
[dygraphs.git] / dygraph.js
index ba266ab..4adb716 100644 (file)
@@ -135,6 +135,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,
@@ -171,6 +174,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.
@@ -228,6 +232,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_();
 
@@ -346,6 +352,32 @@ Dygraph.prototype.toDataCoords = function(x, y) {
   return ret;
 };
 
+/**
+ * Returns the number of columns (including the independent variable).
+ */
+Dygraph.prototype.numColumns = function() {
+  return this.rawData_[0].length;
+};
+
+/**
+ * Returns the number of rows (excluding any header/label row).
+ */
+Dygraph.prototype.numRows = function() {
+  return this.rawData_.length;
+};
+
+/**
+ * Returns the value in the given row and column. If the row and column exceed
+ * the bounds on the data, returns null. Also returns null if the value is
+ * missing.
+ */
+Dygraph.prototype.getValue = function(row, col) {
+  if (row < 0 || row > this.rawData_.length) return null;
+  if (col < 0 || col > this.rawData_[row].length) return null;
+
+  return this.rawData_[row][col];
+};
+
 Dygraph.addEvent = function(el, evt, fn) {
   var normed_fn = function(e) {
     if (!e) var e = window.event;
@@ -721,7 +753,7 @@ Dygraph.prototype.createDragInterface_ = function() {
   var px = 0;
   var py = 0;
   var getX = function(e) { return Dygraph.pageX(e) - px };
-  var getY = function(e) { return Dygraph.pageX(e) - py };
+  var getY = function(e) { return Dygraph.pageY(e) - py };
 
   // Draw zoom rectangles when the mouse is down and the user moves around
   Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
@@ -798,10 +830,31 @@ Dygraph.prototype.createDragInterface_ = function() {
       var regionHeight = Math.abs(dragEndY - dragStartY);
 
       if (regionWidth < 2 && regionHeight < 2 &&
-          self.attr_('clickCallback') != null &&
-          self.lastx_ != undefined) {
-        // TODO(danvk): pass along more info about the points.
-        self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
+          self.lastx_ != undefined && self.lastx_ != -1) {
+        // TODO(danvk): pass along more info about the points, e.g. 'x'
+        if (self.attr_('clickCallback') != null) {
+          self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
+        }
+        if (self.attr_('pointClickCallback')) {
+          // check if the click was on a particular point.
+          var closestIdx = -1;
+          var closestDistance = 0;
+          for (var i = 0; i < self.selPoints_.length; i++) {
+            var p = self.selPoints_[i];
+            var distance = Math.pow(p.canvasx - dragEndX, 2) +
+                           Math.pow(p.canvasy - dragEndY, 2);
+            if (closestIdx == -1 || distance < closestDistance) {
+              closestDistance = distance;
+              closestIdx = i;
+            }
+          }
+
+          // Allow any click within two pixels of the dot.
+          var radius = self.attr_('highlightCircleSize') + 2;
+          if (closestDistance <= 5 * 5) {
+            self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
+          }
+        }
       }
 
       if (regionWidth >= 10) {
@@ -1900,8 +1953,8 @@ Dygraph.prototype.parseCSV_ = function(data) {
 
   // 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 val = parseFloat(x);
+    return isNaN(val) ? null : val;
   };
 
   var xParser;
@@ -2006,7 +2059,7 @@ Dygraph.prototype.parseArray_ = function(data) {
     var parsedData = Dygraph.clone(data);
     for (var i = 0; i < data.length; i++) {
       if (parsedData[i].length == 0) {
-        this.error("Row " << (1 + i) << " of data is empty");
+        this.error("Row " + (1 + i) + " of data is empty");
         return null;
       }
       if (parsedData[i][0] == null
@@ -2031,7 +2084,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
  */
@@ -2039,15 +2092,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_;
@@ -2065,8 +2109,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' ||
@@ -2082,8 +2159,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++) {
@@ -2100,7 +2192,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.
@@ -2166,7 +2262,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.
@@ -2307,6 +2403,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 + " }", 0);
+  } 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.
  */