Log scale graphs no longer show points with values less than zero.
[dygraphs.git] / dygraph.js
index 8a5d870..0390a6f 100644 (file)
@@ -79,6 +79,11 @@ Dygraph.DEFAULT_WIDTH = 480;
 Dygraph.DEFAULT_HEIGHT = 320;
 Dygraph.AXIS_LINE_WIDTH = 0.3;
 
+Dygraph.LOG_SCALE = 10;
+Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
+Dygraph.log10 = function(x) {
+  return Math.log(x) / Dygraph.LN_TEN;
+}
 
 // Default attribute values.
 Dygraph.DEFAULT_ATTRS = {
@@ -114,7 +119,6 @@ Dygraph.DEFAULT_ATTRS = {
 
   delimiter: ',',
 
-  logScale: false,
   sigma: 2.0,
   errorBars: false,
   fractions: false,
@@ -357,7 +361,7 @@ Dygraph.prototype.yAxisRanges = function() {
  * axis. Uses the first axis by default.
  * Returns a two-element array: [X, Y]
  *
- * Note: use toDomXCoord instead of toDomCoords(x. null) and use toDomYCoord
+ * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
  * instead of toDomCoords(null, y, axis).
  */
 Dygraph.prototype.toDomCoords = function(x, y, axis) {
@@ -367,8 +371,8 @@ Dygraph.prototype.toDomCoords = function(x, y, axis) {
 /**
  * Convert from data x coordinates to canvas/div X coordinate.
  * If specified, do this conversion for the coordinate system of a particular
- * axis. Uses the first axis by default.
- * returns a single value or null if x is null.
+ * axis.
+ * Returns a single value or null if x is null.
  */
 Dygraph.prototype.toDomXCoord = function(x) {
   if (x == null) {
@@ -387,11 +391,12 @@ Dygraph.prototype.toDomXCoord = function(x) {
  * returns a single value or null if y is null.
  */
 Dygraph.prototype.toDomYCoord = function(y, axis) {
-  var pct = toPercentYCoord(y, axis);
+  var pct = this.toPercentYCoord(y, axis);
 
   if (pct == null) {
     return null;
   }
+  var area = this.plotter_.area;
   return area.y + pct * area.h;
 }
 
@@ -401,7 +406,7 @@ Dygraph.prototype.toDomYCoord = function(y, axis) {
  * axis. Uses the first axis by default.
  * Returns a two-element array: [X, Y].
  *
- * Note: use toDataXCoord instead of toDataCoords(x. null) and use toDataYCoord
+ * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
  * instead of toDataCoords(null, y, axis).
  */
 Dygraph.prototype.toDataCoords = function(x, y, axis) {
@@ -437,7 +442,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) {
   var area = this.plotter_.area;
   var yRange = this.yAxisRange(axis);
 
-  if (!this.attr_("logscale")) {
+  if (!axis.logscale) {
     return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
   } else {
     // Computing the inverse of toDomCoord.
@@ -447,22 +452,22 @@ Dygraph.prototype.toDataYCoord = function(y, axis) {
     // the following steps:
     //
     // Original calcuation:
-    // pct = (logr1 - Math.log(y)) / (logr1 - Math.log(yRange[0]));
+    // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
     //
     // Move denominator to both sides:
-    // pct * (logr1 - Math.log(yRange[0])) = logr1 - Math.log(y);
+    // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
     //
     // subtract logr1, and take the negative value.
-    // logr1 - (pct * (logr1 - Math.log(yRange[0]))) = Math.log(y);
+    // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
     //
     // Swap both sides of the equation, and we can compute the log of the
     // return value. Which means we just need to use that as the exponent in
     // e^exponent.
-    // Math.log(y) = logr1 - (pct * (logr1 - Math.log(yRange[0])));
+    // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
 
-    var logr1 = Math.log(yRange[1]);
-    var exponent = logr1 - (pct * (logr1 - Math.log(yRange[0])));
-    var value = Math.pow(Math.E, exponent);
+    var logr1 = Dygraph.log10(yRange[1]);
+    var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
+    var value = Math.pow(Dygraph.LOG_SCALE, exponent);
     return value;
   }
 };
@@ -483,19 +488,20 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) {
   if (y == null) {
     return null;
   }
+  if (typeof(axis) == "undefined") axis = 0;
 
   var area = this.plotter_.area;
   var yRange = this.yAxisRange(axis);
 
   var pct;
-  if (!this.attr_("logscale")) {
+  if (!this.axes_[axis].logscale) {
     // yrange[1] - y is unit distance from the bottom.
     // yrange[1] - yrange[0] is the scale of the range.
     // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
     pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
   } else {
-    var logr1 = Math.log(yRange[1]);
-    pct = (logr1 - Math.log(y)) / (logr1 - Math.log(yRange[0]));
+    var logr1 = Dygraph.log10(yRange[1]);
+    pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
   }
   return pct;
 }
@@ -948,7 +954,13 @@ Dygraph.movePan = function(event, g, context) {
   // y-axis scaling is automatic unless this is a full 2D pan.
   if (context.is2DPan) {
     // Adjust each axis appropriately.
+    // NOTE(konigsberg): I don't think this computation for y_frac is correct.
+    // I think it doesn't take into account the display of the x axis.
+    // See, when I tested this with console.log(y_frac), and move the mouse
+    // cursor to the botom, the largest y_frac was 0.94, and not 1.0. That
+    // could also explain why panning tends to start with a small jumpy shift.
     var y_frac = context.dragEndY / g.height_;
+
     for (var i = 0; i < g.axes_.length; i++) {
       var axis = g.axes_[i];
       var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
@@ -1906,8 +1918,8 @@ Dygraph.dateTicker = function(startDate, endDate, self) {
  * Add ticks when the x axis has numbers on it (instead of dates)
  * TODO(konigsberg): Update comment.
  *
- * @param {Number} startDate Start of the date window (millis since epoch)
- * @param {Number} endDate End of the date window (millis since epoch)
+ * @param {Number} minV minimum value
+ * @param {Number} maxV maximum value
  * @param self
  * @param {function} attribute accessor function.
  * @return {Array.<Object>} Array of {label, value} tuples.
@@ -1925,7 +1937,7 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
       ticks.push({v: vals[i]});
     }
   } else {
-    if (self.attr_("logscale")) {
+    if (axis_props && attr("logscale")) {
       // As opposed to the other ways for computing ticks, we're just going
       // for nearby values. There's no reasonable way to scale the values
       // (unless we want to show strings like "log(" + x + ")") in which case
@@ -1933,13 +1945,14 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
 
       // so compute height / pixelsPerTick and move on.
       var pixelsPerTick = attr('pixelsPerYLabel');
+      // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
       var nTicks  = Math.floor(self.height_ / pixelsPerTick);
       var vv = minV;
 
       // Construct the set of ticks.
       for (var i = 0; i < nTicks; i++) {
         ticks.push( {v: vv} );
-        vv = vv * Math.E;
+        vv = vv * Dygraph.LOG_SCALE;
       }
     } else {
       // Basic idea:
@@ -2069,7 +2082,7 @@ Dygraph.prototype.extremeValues_ = function(series) {
  * number of axes, rolling averages, etc.
  */
 Dygraph.prototype.predraw_ = function() {
-  // TODO(danvk): move more computations out of drawGraph_ and into here.
+  // TODO(danvk): movabilitye more computations out of drawGraph_ and into here.
   this.computeYAxes_();
 
   // Create a new plotter.
@@ -2123,12 +2136,24 @@ Dygraph.prototype.drawGraph_ = function() {
 
     var seriesName = this.attr_("labels")[i];
     var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
+    var logScale = this.attr_('logscale', i);
 
     var series = [];
     for (var j = 0; j < data.length; j++) {
-      if (data[j][i] != null || !connectSeparatedPoints) {
-        var date = data[j][0];
-        series.push([date, data[j][i]]);
+      var date = data[j][0];
+      var point = data[j][i];
+      if (logScale) {
+        // 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) {
+          point = null;
+        }
+        series.push([date, point]);
+      } else {
+        if (point != null || !connectSeparatedPoints) {
+          series.push([date, point]);
+        }
       }
     }
 
@@ -2259,7 +2284,8 @@ Dygraph.prototype.computeYAxes_ = function() {
     'pixelsPerYLabel',
     'yAxisLabelWidth',
     'axisLabelFontSize',
-    'axisTickSize'
+    'axisTickSize',
+    'logscale'
   ];
 
   // Copy global axis options over to the first axis.
@@ -2347,7 +2373,6 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
 
   // Compute extreme values, a span and tick marks for each axis.
   for (var i = 0; i < this.axes_.length; i++) {
-    var isLogScale = this.attr_("logscale");
     var axis = this.axes_[i];
     if (axis.valueWindow) {
       // This is only set if the user has zoomed on the y-axis. It is never set
@@ -2375,7 +2400,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
 
       var maxAxisY;
       var minAxisY;
-      if (isLogScale) {
+      if (axis.logscale) {
         var maxAxisY = maxY + 0.1 * span;
         var minAxisY = minY;
       } else {