}
};
+ // 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.
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).