From: Jeremy Brewer Date: Fri, 4 Feb 2011 21:50:21 +0000 (-0500) Subject: Merge branch 'master' of https://github.com/danvk/dygraphs X-Git-Tag: v1.0.0~587 X-Git-Url: https://adrianiainlam.tk/git/?a=commitdiff_plain;h=6fd1990aa8fc8dc1f5c4017db17db6c4ba36c629;p=dygraphs.git Merge branch 'master' of https://github.com/danvk/dygraphs Conflicts: dygraph.js --- 6fd1990aa8fc8dc1f5c4017db17db6c4ba36c629 diff --cc dygraph.js index 1257b23,b3433e2..3e32297 --- a/dygraph.js +++ b/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.} 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} ); + } } } @@@ -1995,35 -2102,33 +2211,36 @@@ 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).