Merge pull request #135 from klausw-g/highlight-NaN-3
[dygraphs.git] / dygraph.js
index 735be8c..40f1d11 100644 (file)
@@ -187,8 +187,7 @@ Dygraph.dateAxisFormatter = function(date, granularity) {
 Dygraph.DEFAULT_ATTRS = {
   highlightCircleSize: 3,
   highlightSeriesOpts: null,
-  highlightSeriesBackgroundFade: 0,
-  highlightSeriesAnimated: false,
+  highlightSeriesBackgroundAlpha: 0.5,
 
   labelsDivWidth: 250,
   labelsDivStyles: {
@@ -461,14 +460,16 @@ Dygraph.prototype.attr_ = function(name, seriesName) {
 
   var sources = [];
   sources.push(this.attrs_);
-  if (this.user_attrs_) sources.push(this.user_attrs_);
-  if (this.user_attrs_ && seriesName) {
-    if (this.user_attrs_.hasOwnProperty(seriesName)) {
-      sources.push(this.user_attrs_[seriesName]);
-    }
-    if (seriesName === this.highlightSet_ &&
-        this.user_attrs_.hasOwnProperty('highlightSeriesOpts')) {
-      sources.push(this.user_attrs_['highlightSeriesOpts']);
+  if (this.user_attrs_) {
+    sources.push(this.user_attrs_);
+    if (seriesName) {
+      if (this.user_attrs_.hasOwnProperty(seriesName)) {
+        sources.push(this.user_attrs_[seriesName]);
+      }
+      if (seriesName === this.highlightSet_ &&
+          this.user_attrs_.hasOwnProperty('highlightSeriesOpts')) {
+        sources.push(this.user_attrs_['highlightSeriesOpts']);
+      }
     }
   }
 
@@ -1020,7 +1021,11 @@ Dygraph.prototype.createStatusMessage_ = function() {
     div.className = "dygraph-legend";
     for (var name in messagestyle) {
       if (messagestyle.hasOwnProperty(name)) {
-        div.style[name] = messagestyle[name];
+        try {
+          div.style[name] = messagestyle[name];
+        } catch (e) {
+          this.warn("You are using unsupported css properties for your browser in labelsDivStyles");
+        }
       }
     }
     this.graphDiv.appendChild(div);
@@ -1510,17 +1515,23 @@ Dygraph.prototype.findClosestRow = function(domX) {
   var l = points.length;
   for (var i = 0; i < l; i++) {
     var point = points[i];
-    if (point === null) continue;
+    if (!Dygraph.isValidPoint(point)) continue;
     var dist = Math.abs(point.canvasx - domX);
-    if (minDistX !== null && dist >= minDistX) continue;
-    minDistX = dist;
-    idx = i;
+    if (minDistX === null || dist < minDistX) {
+      minDistX = dist;
+      idx = i;
+    }
   }
   return this.idxToRow_(idx);
 };
 
 /**
- * Given canvas X,Y coordinates, find the closest point
+ * Given canvas X,Y coordinates, find the closest point.
+ *
+ * This finds the individual data point across all visible series
+ * that's closest to the supplied DOM coordinates using the standard
+ * Euclidean X,Y distance.
+ *
  * @param {Number} domX graph-relative DOM X coordinate
  * @param {Number} domY graph-relative DOM Y coordinate
  * Returns: {row, seriesName, point}
@@ -1536,15 +1547,16 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) {
     var len = this.layout_.setPointsLengths[setIdx];
     for (var i = 0; i < len; ++i) {
       var point = points[first + i];
-      if (point === null) continue;
+      if (!Dygraph.isValidPoint(point)) continue;
       dx = point.canvasx - domX;
       dy = point.canvasy - domY;
       dist = dx * dx + dy * dy;
-      if (minDist !== null && dist >= minDist) continue;
-      minDist = dist;
-      closestPoint = point;
-      closestSeries = setIdx;
-      idx = i;
+      if (minDist === null || dist < minDist) {
+        minDist = dist;
+        closestPoint = point;
+        closestSeries = setIdx;
+        idx = i;
+      }
     }
   }
   var name = this.layout_.setNames[closestSeries];
@@ -1557,6 +1569,11 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) {
 
 /**
  * Given canvas X,Y coordinates, find the touched area in a stacked graph.
+ *
+ * This first finds the X data point closest to the supplied DOM X coordinate,
+ * then finds the series which puts the Y coordinate on top of its filled area,
+ * using linear interpolation between adjacent point pairs.
+ *
  * @param {Number} domX graph-relative DOM X coordinate
  * @param {Number} domY graph-relative DOM Y coordinate
  * Returns: {row, seriesName, point}
@@ -1571,28 +1588,34 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) {
     var len = this.layout_.setPointsLengths[setIdx];
     if (row >= len) continue;
     var p1 = points[first + row];
+    if (!Dygraph.isValidPoint(p1)) continue;
     var py = p1.canvasy;
     if (domX > p1.canvasx && row + 1 < len) {
       // interpolate series Y value using next point
       var p2 = points[first + row + 1];
-      var dx = p2.canvasx - p1.canvasx;
-      if (dx > 0) {
-        var r = (domX - p1.canvasx) / dx;
-        py += r * (p2.canvasy - p1.canvasy);
+      if (Dygraph.isValidPoint(p2)) {
+        var dx = p2.canvasx - p1.canvasx;
+        if (dx > 0) {
+          var r = (domX - p1.canvasx) / dx;
+          py += r * (p2.canvasy - p1.canvasy);
+        }
       }
     } else if (domX < p1.canvasx && row > 0) {
       // interpolate series Y value using previous point
       var p0 = points[first + row - 1];
-      var dx = p1.canvasx - p0.canvasx;
-      if (dx > 0) {
-        var r = (p1.canvasx - domX) / dx;
-        py += r * (p0.canvasy - p1.canvasy);
+      if (Dygraph.isValidPoint(p0)) {
+        var dx = p1.canvasx - p0.canvasx;
+        if (dx > 0) {
+          var r = (p1.canvasx - domX) / dx;
+          py += r * (p0.canvasy - p1.canvasy);
+        }
       }
     }
     // Stop if the point (domX, py) is above this series' upper edge
-    if (setIdx > 0 && py >= domY) break;
-    closestPoint = p1;
-    closestSeries = setIdx;
+    if (setIdx == 0 || py < domY) {
+      closestPoint = p1;
+      closestSeries = setIdx;
+    }
   }
   var name = this.layout_.setNames[closestSeries];
   return {
@@ -1618,13 +1641,6 @@ Dygraph.prototype.mouseMove_ = function(event) {
   var canvasx = canvasCoords[0];
   var canvasy = canvasCoords[1];
 
-  var mouseoverCallback = this.attr_("mouseoverCallback");
-  if (mouseoverCallback) {
-    var highlightRow = this.idxToRow_(idx);
-    var ret = mouseoverCallback(this, event);
-    if (ret) return;
-  }
-
   var highlightSeriesOpts = this.attr_("highlightSeriesOpts");
   var selectionChanged = false;
   if (highlightSeriesOpts) {
@@ -1840,10 +1856,8 @@ Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
 Dygraph.prototype.animateSelection_ = function(direction) {
   var totalSteps = 10;
   var millis = 30;
-  if (this.fadeLevel === undefined) {
-    this.fadeLevel = 0;
-    this.animateId = 0;
-  }
+  if (this.fadeLevel === undefined) this.fadeLevel = 0;
+  if (this.animateId === undefined) this.animateId = 0;
   var start = this.fadeLevel;
   var steps = direction < 0 ? start : totalSteps - start;
   if (steps <= 0) {
@@ -1855,18 +1869,19 @@ Dygraph.prototype.animateSelection_ = function(direction) {
 
   var thisId = ++this.animateId;
   var that = this;
-  Dygraph.repeatAndCleanup(function(n) {
-        // ignore simultaneous animations
-        if (that.animateId != thisId) return;
-
-        that.fadeLevel += direction;
-        if (that.fadeLevel === 0) {
-          that.clearSelection();
-        } else {
-          that.updateSelection_(that.fadeLevel / totalSteps);
-        }
-      },
-      steps, millis, function() {});
+  Dygraph.repeatAndCleanup(
+    function(n) {
+      // ignore simultaneous animations
+      if (that.animateId != thisId) return;
+
+      that.fadeLevel += direction;
+      if (that.fadeLevel === 0) {
+        that.clearSelection();
+      } else {
+        that.updateSelection_(that.fadeLevel / totalSteps);
+      }
+    },
+    steps, millis, function() {});
 };
 
 /**
@@ -1880,9 +1895,13 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
   var ctx = this.canvas_ctx_;
   if (this.attr_('highlightSeriesOpts')) {
     ctx.clearRect(0, 0, this.width_, this.height_);
-    var alpha = this.attr_('highlightSeriesBackgroundFade');
+    var alpha = 1.0 - this.attr_('highlightSeriesBackgroundAlpha');
     if (alpha) {
-      if (this.attr_('highlightSeriesAnimate')) {
+      // Activating background fade includes an animation effect for a gradual
+      // fade. TODO(klausw): make this independently configurable if it causes
+      // issues? Use a shared preference to control animations?
+      var animateBackgroundFade = true;
+      if (animateBackgroundFade) {
         if (opt_animFraction === undefined) {
           // start a new animation
           this.animateSelection_(1);
@@ -1894,8 +1913,6 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
       ctx.fillRect(0, 0, this.width_, this.height_);
     }
     var setIdx = this.datasetIndexFromSetName_(this.highlightSet_);
-    var underlay = this.attr_('highlightUnderlay');
-    if (underlay) underlay(this, ctx, setIdx);
     this.plotter_._drawLine(ctx, setIdx);
   } else if (this.previousVerticalX_ >= 0) {
     // Determine the maximum highlight circle size.
@@ -1928,10 +1945,16 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
       if (!Dygraph.isOK(pt.canvasy)) continue;
 
       var circleSize = this.attr_('highlightCircleSize', pt.name);
-      ctx.beginPath();
-      ctx.fillStyle = this.plotter_.colors[pt.name];
-      ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
-      ctx.fill();
+      var callback = this.attr_("drawHighlightPointCallback", pt.name);
+      var color = this.plotter_.colors[pt.name];
+      if (!callback) {
+        callback = Dygraph.Circles.DEFAULT;
+      }
+      ctx.lineWidth = this.attr_('strokeWidth', pt.name);
+      ctx.strokeStyle = color;
+      ctx.fillStyle = color;
+      callback(this.g, pt.name, ctx, canvasx, pt.canvasy,
+          color, circleSize);
     }
     ctx.restore();