Merge branch 'master' of https://github.com/danvk/dygraphs
authorJeremy Brewer <jeremy.d.brewer@gmail.com>
Fri, 4 Feb 2011 21:50:21 +0000 (16:50 -0500)
committerJeremy Brewer <jeremy.d.brewer@gmail.com>
Fri, 4 Feb 2011 21:50:21 +0000 (16:50 -0500)
Conflicts:
dygraph.js

1  2 
dygraph.js

diff --cc dygraph.js
@@@ -1885,47 -1918,75 +1990,112 @@@ Dygraph.dateTicker = function(startDate
    }
  };
  
+ // This is a list of human-friendly values at which to show tick marks on a log
+ // scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
+ // ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
+ // NOTE: this assumes that Dygraph.LOG_SCALE = 10.
+ Dygraph.PREFERRED_LOG_TICK_VALUES = function() {
+   var vals = [];
+   for (var power = -39; power <= 39; power++) {
+     var range = Math.pow(10, power);
+     for (var mult = 1; mult <= 9; mult++) {
+       var val = range * mult;
+       vals.push(val);
+     }
+   }
+   return vals;
+ }();
+ // val is the value to search for
+ // arry is the value over which to search
+ // if abs > 0, find the lowest entry greater than val
+ // if abs < 0, find the highest entry less than val
+ // if abs == 0, find the entry that equals val.
+ // Currently does not work when val is outside the range of arry's values.
+ Dygraph.binarySearch = function(val, arry, abs, low, high) {
+   if (low == null || high == null) {
+     low = 0;
+     high = arry.length - 1;
+   }
+   if (low > high) {
+     return -1;
+   }
+   if (abs == null) {
+     abs = 0;
+   }
+   var validIndex = function(idx) {
+     return idx >= 0 && idx < arry.length;
+   }
+   var mid = parseInt((low + high) / 2);
+   var element = arry[mid];
+   if (element == val) {
+     return mid;
+   }
+   if (element > val) {
+     if (abs > 0) {
+       // Accept if element > val, but also if prior element < val.
+       var idx = mid - 1;
+       if (validIndex(idx) && arry[idx] < val) {
+         return mid;
+       }
+     }
+     return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
+   }
+   if (element < val) {
+     if (abs < 0) {
+       // Accept if element < val, but also if prior element > val.
+       var idx = mid + 1;
+       if (validIndex(idx) && arry[idx] > val) {
+         return mid;
+       }
+     }
+     return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
+   }
+ };
  /**
 + * Determine the number of significant figures in a Number up to the specified
 + * precision.  Note that there is no way to determine if a trailing '0' is
 + * significant or not, so by convention we return 1 for all of the following
 + * inputs: 1, 1.0, 1.00, 1.000 etc.
 + * @param {Number} x The input value.
 + * @param {Number} opt_maxPrecision Optional maximum precision to consider.
 + *                                  Default and maximum allowed value is 13.
 + * @return {Number} The number of significant figures which is >= 1.
 + */
 +Dygraph.significantFigures = function(x, opt_maxPrecision) {
 +  var precision = Math.max(opt_maxPrecision || 13, 13);
 +
 +  // Convert the number to its exponential notation form and work backwards,
 +  // ignoring the 'e+xx' bit.  This may seem like a hack, but doing a loop and
 +  // dividing by 10 leads to roundoff errors.  By using toExponential(), we let
 +  // the JavaScript interpreter handle the low level bits of the Number for us.
 +  var s = x.toExponential(precision);
 +  var ePos = s.lastIndexOf('e');  // -1 case handled by return below.
 +
 +  for (var i = ePos - 1; i >= 0; i--) {
 +    if (s[i] == '.') {
 +      // Got to the decimal place.  We'll call this 1 digit of precision because
 +      // we can't know for sure how many trailing 0s are significant.
 +      return 1;
 +    } else if (s[i] != '0') {
 +      // Found the first non-zero digit.  Return the number of characters
 +      // except for the '.'.
 +      return i;  // This is i - 1 + 1 (-1 is for '.', +1 is for 0 based index).
 +    }
 +  }
 +
 +  // Occurs if toExponential() doesn't return a string containing 'e', which
 +  // should never happen.
 +  return 1;
 +};
 +
 +/**
   * Add ticks when the x axis has numbers on it (instead of dates)
-  * @param {Number} startDate Start of the date window (millis since epoch)
-  * @param {Number} endDate End of the date window (millis since epoch)
+  * TODO(konigsberg): Update comment.
+  *
+  * @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.
@@@ -1940,46 -2001,92 +2110,92 @@@ Dygraph.numericTicks = function(minV, m
    var ticks = [];
    if (vals) {
      for (var i = 0; i < vals.length; i++) {
 -      ticks.push({v: vals[i]});
 +      ticks[i].push({v: vals[i]});
      }
    } else {
-     // Basic idea:
-     // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
-     // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
-     // The first spacing greater than pixelsPerYLabel is what we use.
-     // TODO(danvk): version that works on a log scale.
-     if (attr("labelsKMG2")) {
-       var mults = [1, 2, 4, 8];
-     } else {
-       var mults = [1, 2, 5];
+     if (axis_props && attr("logscale")) {
+       var pixelsPerTick = attr('pixelsPerYLabel');
+       // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
+       var nTicks  = Math.floor(self.height_ / pixelsPerTick);
+       var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
+       var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
+       if (minIdx == -1) {
+         minIdx = 0;
+       }
+       if (maxIdx == -1) {
+         maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
+       }
+       // Count the number of tick values would appear, if we can get at least
+       // nTicks / 4 accept them.
+       var lastDisplayed = null;
+       if (maxIdx - minIdx >= nTicks / 4) {
+         var axisId = axis_props.yAxisId;
+         for (var idx = maxIdx; idx >= minIdx; idx--) {
+           var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
+           var domCoord = axis_props.g.toDomYCoord(tickValue, axisId);
+           var tick = { v: tickValue };
+           if (lastDisplayed == null) {
+             lastDisplayed = {
+               tickValue : tickValue,
+               domCoord : domCoord
+             };
+           } else {
+             if (domCoord - lastDisplayed.domCoord >= pixelsPerTick) {
+               lastDisplayed = {
+                 tickValue : tickValue,
+                 domCoord : domCoord
+               };
+             } else {
+               tick.label = "";
+             }
+           }
+           ticks.push(tick);
+         }
+         // Since we went in backwards order.
+         ticks.reverse();
+       }
      }
-     var scale, low_val, high_val, nTicks;
-     // TODO(danvk): make it possible to set this for x- and y-axes independently.
-     var pixelsPerTick = attr('pixelsPerYLabel');
-     for (var i = -10; i < 50; i++) {
+     // ticks.length won't be 0 if the log scale function finds values to insert.
+     if (ticks.length == 0) {
+       // Basic idea:
+       // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
+       // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
+       // The first spacing greater than pixelsPerYLabel is what we use.
+       // TODO(danvk): version that works on a log scale.
        if (attr("labelsKMG2")) {
-         var base_scale = Math.pow(16, i);
+         var mults = [1, 2, 4, 8];
        } else {
-         var base_scale = Math.pow(10, i);
+         var mults = [1, 2, 5];
        }
-       for (var j = 0; j < mults.length; j++) {
-         scale = base_scale * mults[j];
-         low_val = Math.floor(minV / scale) * scale;
-         high_val = Math.ceil(maxV / scale) * scale;
-         nTicks = Math.abs(high_val - low_val) / scale;
-         var spacing = self.height_ / nTicks;
-         // wish I could break out of both loops at once...
+       var scale, low_val, high_val, nTicks;
+       // TODO(danvk): make it possible to set this for x- and y-axes independently.
+       var pixelsPerTick = attr('pixelsPerYLabel');
+       for (var i = -10; i < 50; i++) {
+         if (attr("labelsKMG2")) {
+           var base_scale = Math.pow(16, i);
+         } else {
+           var base_scale = Math.pow(10, i);
+         }
+         for (var j = 0; j < mults.length; j++) {
+           scale = base_scale * mults[j];
+           low_val = Math.floor(minV / scale) * scale;
+           high_val = Math.ceil(maxV / scale) * scale;
+           nTicks = Math.abs(high_val - low_val) / scale;
+           var spacing = self.height_ / nTicks;
+           // wish I could break out of both loops at once...
+           if (spacing > pixelsPerTick) break;
+         }
          if (spacing > pixelsPerTick) break;
        }
-       if (spacing > pixelsPerTick) break;
-     }
  
-     // Construct the set of ticks.
-     // Allow reverse y-axis if it's explicitly requested.
-     if (low_val > high_val) scale *= -1;
-     for (var i = 0; i < nTicks; i++) {
-       var tickV = low_val + i * scale;
-       ticks.push( {v: tickV} );
+       // Construct the set of ticks.
+       // Allow reverse y-axis if it's explicitly requested.
+       if (low_val > high_val) scale *= -1;
+       for (var i = 0; i < nTicks; i++) {
+         var tickV = low_val + i * scale;
+         ticks.push( {v: tickV} );
+       }
      }
    }
  
      k = 1024;
      k_labels = [ "k", "M", "G", "T" ];
    }
 -  var formatter = attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter'); 
 +  var formatter = attr('yAxisLabelFormatter') ?
 +      attr('yAxisLabelFormatter') : attr('yValueFormatter');
 +
 +  // Determine the number of decimal places needed for the labels below by
 +  // taking the maximum number of significant figures for any label.  We must
 +  // take the max because we can't tell if trailing 0s are significant.
 +  var numDigits = 0;
 +  for (var i = 0; i < ticks.length; i++) {
 +    numDigits = Math.max(Dygraph.significantFigures(ticks[i].v), numDigits);
 +  }
  
+   // Add labels to the ticks.
    for (var i = 0; i < ticks.length; i++) {
 -    if (ticks[i].label == null) {
 -      var tickV = ticks[i].v;
 -      var absTickV = Math.abs(tickV);
 -      var label;
 -      if (formatter != undefined) {
 -        label = formatter(tickV);
 -      } else {
 -        label = Dygraph.round_(tickV, 2);
 -      }
 -      if (k_labels.length) {
 -        // Round up to an appropriate unit.
 -        var n = k*k*k*k;
 -        for (var j = 3; j >= 0; j--, n /= k) {
 -          if (absTickV >= n) {
 -            label = Dygraph.round_(tickV / n, 1) + k_labels[j];
 -            break;
 -          }
 +    var tickV = ticks[i].v;
 +    var absTickV = Math.abs(tickV);
 +    var label = (formatter !== undefined) ?
 +        formatter(tickV, numDigits) : tickV.toPrecision(numDigits);
 +    if (k_labels.length > 0) {
 +      // Round up to an appropriate unit.
 +      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];
 +          break;
          }
        }
+       ticks[i].label = label;
      }
-     ticks[i].label = label;
    }
 -  return ticks;
 +  return {ticks: ticks, numDigits: numDigits};
  };
  
  // Computes the range of the data series (including confidence intervals).