X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=ae1faba4639bb94cf7a4db0874d63759f8ddd3b6;hb=6512fe1e28b31618517c5f3a91786e75168255bd;hp=fe677f29688d874c74b06319cff267ae9d6d6108;hpb=5bc3e265be640a29bc57d7d58c059322ab361d63;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index fe677f2..ae1faba 100644 --- a/dygraph.js +++ b/dygraph.js @@ -72,59 +72,6 @@ Dygraph.toString = function() { return this.__repr__(); }; -/** - * Formatting to use for an integer number. - * - * @param {Number} x The number to format - * @param {Number} unused_precision The precision to use, ignored. - * @return {String} A string formatted like %g in printf. The max generated - * string length should be precision + 6 (e.g 1.123e+300). - */ -Dygraph.intFormat = function(x, unused_precision) { - return x.toString(); -} - -/** - * Number formatting function which mimicks the behavior of %g in printf, i.e. - * either exponential or fixed format (without trailing 0s) is used depending on - * the length of the generated string. The advantage of this format is that - * there is a predictable upper bound on the resulting string length, - * significant figures are not dropped, and normal numbers are not displayed in - * exponential notation. - * - * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g. - * It creates strings which are too long for absolute values between 10^-4 and - * 10^-6. See tests/number-format.html for output examples. - * - * @param {Number} x The number to format - * @param {Number} opt_precision The precision to use, default 2. - * @return {String} A string formatted like %g in printf. The max generated - * string length should be precision + 6 (e.g 1.123e+300). - */ -Dygraph.floatFormat = function(x, opt_precision) { - // Avoid invalid precision values; [1, 21] is the valid range. - var p = Math.min(Math.max(1, opt_precision || 2), 21); - - // This is deceptively simple. The actual algorithm comes from: - // - // Max allowed length = p + 4 - // where 4 comes from 'e+n' and '.'. - // - // Length of fixed format = 2 + y + p - // where 2 comes from '0.' and y = # of leading zeroes. - // - // Equating the two and solving for y yields y = 2, or 0.00xxxx which is - // 1.0e-3. - // - // Since the behavior of toPrecision() is identical for larger numbers, we - // don't have to worry about the other bound. - // - // Finally, the argument for toExponential() is the number of trailing digits, - // so we take off 1 for the value before the '.'. - return (Math.abs(x) < 1.0e-3 && x != 0.0) ? - x.toExponential(p - 1) : x.toPrecision(p); -}; - // Various default values Dygraph.DEFAULT_ROLL_PERIOD = 1; Dygraph.DEFAULT_WIDTH = 480; @@ -153,11 +100,10 @@ Dygraph.DEFAULT_ATTRS = { labelsKMG2: false, showLabelsOnHighlight: true, - yValueFormatter: function(x, opt_precision) { - var s = Dygraph.floatFormat(x, opt_precision); - var s2 = Dygraph.intFormat(x); - return s.length < s2.length ? s : s2; - }, + yValueFormatter: function(a,b) { return Dygraph.numberFormatter(a,b); }, + digitsAfterDecimal: 2, + maxNumberWidth: 6, + sigFigs: null, strokeWidth: 1.0, @@ -267,20 +213,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.zoomed_x_ = false; this.zoomed_y_ = false; - // Number of digits to use when labeling the x (if numeric) and y axis - // ticks. - this.numXDigits_ = 2; - this.numYDigits_ = 2; - - // When labeling x (if numeric) or y values in the legend, there are - // numDigits + numExtraDigits of precision used. For axes labels with N - // digits of precision, the data should be displayed with at least N+1 digits - // of precision. The reason for this is to divide each interval between - // successive ticks into tenths (for 1) or hundredths (for 2), etc. For - // example, if the labels are [0, 1, 2], we want data to be displayed as - // 0.1, 1.3, etc. - this.numExtraDigits_ = 1; - // Clear the div. This ensure that, if multiple dygraphs are passed the same // div, then only one will be drawn. div.innerHTML = ""; @@ -1699,16 +1631,16 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { var labels = this.attr_('labels'); var html = ''; for (var i = 1; i < labels.length; i++) { - var c = new RGBColor(this.plotter_.colors[labels[i]]); - if (i > 1) html += (sepLines ? '
' : ' '); - html += "—" + labels[i] + - ""; + if (!this.visibility()[i - 1]) continue; + var c = this.plotter_.colors[labels[i]]; + if (html != '') html += (sepLines ? '
' : ' '); + html += "—" + labels[i] + + ""; } return html; } - var displayDigits = this.numXDigits_ + this.numExtraDigits_; - var html = this.attr_('xValueFormatter')(x, displayDigits) + ":"; + var html = this.attr_('xValueFormatter')(x) + ":"; var fmtFunc = this.attr_('yValueFormatter'); var showZeros = this.attr_("labelsShowZeroValues"); @@ -1719,16 +1651,29 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { if (!Dygraph.isOK(pt.canvasy)) continue; if (sepLines) html += "
"; - var c = new RGBColor(this.plotter_.colors[pt.name]); - var yval = fmtFunc(pt.yval, displayDigits); + var c = this.plotter_.colors[pt.name]; + var yval = fmtFunc(pt.yval, this); // TODO(danvk): use a template string here and make it an attribute. - html += " " - + pt.name + ":" + html += " " + + pt.name + ":" + yval; } return html; }; +Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { + var html = this.generateLegendHTML_(x, sel_points); + var labelsDiv = this.attr_("labelsDiv"); + if (labelsDiv !== null) { + labelsDiv.innerHTML = html; + } else { + if (typeof(this.shown_legend_error_) == 'undefined') { + this.error('labelsDiv is set to something nonexistent; legend will not be shown.'); + this.shown_legend_error_ = true; + } + } +}; + /** * Draw dots over the selectied points in the data series. This function * takes care of cleanup of previously-drawn dots. @@ -1753,8 +1698,7 @@ Dygraph.prototype.updateSelection_ = function() { if (this.selPoints_.length > 0) { // Set the status message to indicate the selected point(s) if (this.attr_('showLabelsOnHighlight')) { - var html = this.generateLegendHTML_(this.lastx_, this.selPoints_); - this.attr_("labelsDiv").innerHTML = html; + this.setLegendHTML_(this.lastx_, this.selPoints_); } // Draw colored circles over the center of each selected point @@ -1810,7 +1754,6 @@ Dygraph.prototype.setSelection = function(row) { this.lastx_ = this.selPoints_[0].xval; this.updateSelection_(); } else { - this.lastx_ = -1; this.clearSelection(); } @@ -1839,7 +1782,7 @@ Dygraph.prototype.clearSelection = function() { // Get rid of the overlay data var ctx = this.canvas_.getContext("2d"); ctx.clearRect(0, 0, this.width_, this.height_); - this.attr_('labelsDiv').innerHTML = this.generateLegendHTML_(); + this.setLegendHTML_(); this.selPoints_ = []; this.lastx_ = -1; } @@ -1860,11 +1803,80 @@ Dygraph.prototype.getSelection = function() { } } return -1; -} +}; + +/** + * Number formatting function which mimicks the behavior of %g in printf, i.e. + * either exponential or fixed format (without trailing 0s) is used depending on + * the length of the generated string. The advantage of this format is that + * there is a predictable upper bound on the resulting string length, + * significant figures are not dropped, and normal numbers are not displayed in + * exponential notation. + * + * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g. + * It creates strings which are too long for absolute values between 10^-4 and + * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for + * output examples. + * + * @param {Number} x The number to format + * @param {Number} opt_precision The precision to use, default 2. + * @return {String} A string formatted like %g in printf. The max generated + * string length should be precision + 6 (e.g 1.123e+300). + */ +Dygraph.floatFormat = function(x, opt_precision) { + // Avoid invalid precision values; [1, 21] is the valid range. + var p = Math.min(Math.max(1, opt_precision || 2), 21); + + // This is deceptively simple. The actual algorithm comes from: + // + // Max allowed length = p + 4 + // where 4 comes from 'e+n' and '.'. + // + // Length of fixed format = 2 + y + p + // where 2 comes from '0.' and y = # of leading zeroes. + // + // Equating the two and solving for y yields y = 2, or 0.00xxxx which is + // 1.0e-3. + // + // Since the behavior of toPrecision() is identical for larger numbers, we + // don't have to worry about the other bound. + // + // Finally, the argument for toExponential() is the number of trailing digits, + // so we take off 1 for the value before the '.'. + return (Math.abs(x) < 1.0e-3 && x != 0.0) ? + x.toExponential(p - 1) : x.toPrecision(p); +}; + +/** + * Return a string version of a number. This respects the digitsAfterDecimal + * and maxNumberWidth options. + * @param {Number} x The number to be formatted + * @param {Dygraph} g The dygraph object + */ +Dygraph.numberFormatter = function(x, g) { + var sigFigs = g.attr_('sigFigs'); + + if (sigFigs !== null) { + // User has opted for a fixed number of significant figures. + return Dygraph.floatFormat(x, sigFigs); + } + + var digits = g.attr_('digitsAfterDecimal'); + var maxNumberWidth = g.attr_('maxNumberWidth'); + + // switch to scientific notation if we underflow or overflow fixed display. + if (x !== 0.0 && + (Math.abs(x) >= Math.pow(10, maxNumberWidth) || + Math.abs(x) < Math.pow(10, -digits))) { + return x.toExponential(digits); + } else { + return '' + Dygraph.round_(x, digits); + } +}; Dygraph.zeropad = function(x) { if (x < 10) return "0" + x; else return "" + x; -} +}; /** * Return a string version of the hours, minutes and seconds portion of a date. @@ -1882,7 +1894,7 @@ Dygraph.hmsString_ = function(date) { } else { return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes()); } -} +}; /** * Convert a JS date to a string appropriate to display on an axis that @@ -1905,7 +1917,7 @@ Dygraph.dateAxisFormatter = function(date, granularity) { return Dygraph.hmsString_(date.getTime()); } } -} +}; /** * Convert a JS date (millis since epoch) to YYYY/MM/DD @@ -1932,6 +1944,18 @@ Dygraph.dateString_ = function(date) { }; /** + * Round a number to the specified number of digits past the decimal point. + * @param {Number} num The number to round + * @param {Number} places The number of decimals to which to round + * @return {Number} The rounded number + * @private + */ +Dygraph.round_ = function(num, places) { + var shift = Math.pow(10, places); + return Math.round(num * shift)/shift; +}; + +/** * Fires when there's data available to be graphed. * @param {String} data Raw CSV data to be plotted * @private @@ -1958,20 +1982,7 @@ Dygraph.prototype.addXTicks_ = function() { range = [this.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]]; } - var formatter = this.attr_('xTicker'); - var ret = formatter(range[0], range[1], this); - var xTicks = []; - - // Note: numericTicks() returns a {ticks: [...], numDigits: yy} dictionary, - // whereas dateTicker and user-defined tickers typically just return a ticks - // array. - if (ret.ticks !== undefined) { - xTicks = ret.ticks; - this.numXDigits_ = ret.numDigits; - } else { - xTicks = ret; - } - + var xTicks = this.attr_('xTicker')(range[0], range[1], this); this.layout_.updateOptions({xTicks: xTicks}); }; @@ -2219,43 +2230,6 @@ Dygraph.binarySearch = function(val, arry, abs, low, 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) * TODO(konigsberg): Update comment. * @@ -2379,27 +2353,18 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { 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 !== undefined) continue; // Use current label. var tickV = ticks[i].v; var absTickV = Math.abs(tickV); - var label = (formatter !== undefined) ? - formatter(tickV, numDigits) : tickV.toPrecision(numDigits); + var label = formatter(tickV, self); 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 = formatter(tickV / n, numDigits) + k_labels[j]; + label = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j]; break; } } @@ -2407,7 +2372,7 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { ticks[i].label = label; } - return {ticks: ticks, numDigits: numDigits}; + return ticks; }; // Computes the range of the data series (including confidence intervals). @@ -2628,7 +2593,14 @@ Dygraph.prototype.drawGraph_ = function() { if (is_initial_draw) { // Generate a static legend before any particular point is selected. - this.attr_('labelsDiv').innerHTML = this.generateLegendHTML_(); + this.setLegendHTML_(); + } else { + if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) { + this.lastx_ = this.selPoints_[0].xval; + this.updateSelection_(); + } else { + this.clearSelection(); + } } if (this.attr_("drawCallback") !== null) { @@ -2770,25 +2742,14 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { seriesForAxis[idx].push(series); } - // If no series are defined or visible then fill in some reasonable defaults. - if (seriesForAxis.length == 0) { - var axis = this.axes_[0]; - axis.computedValueRange = [0, 1]; - var ret = - Dygraph.numericTicks(axis.computedValueRange[0], - axis.computedValueRange[1], - this, - axis); - axis.ticks = ret.ticks; - this.numYDigits_ = ret.numDigits; - return; - } - // Compute extreme values, a span and tick marks for each axis. for (var i = 0; i < this.axes_.length; i++) { var axis = this.axes_[i]; - { + if (!seriesForAxis[i]) { + // If no series are defined or visible then use a reasonable default + axis.extremeRange = [0, 1]; + } else { // Calculate the extremes of extremes. var series = seriesForAxis[i]; var minY = Infinity; // extremes[series[0]][0]; @@ -2854,13 +2815,11 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // primary axis. However, if an axis is specifically marked as having // independent ticks, then that is permissible as well. if (i == 0 || axis.independentTicks) { - var ret = + axis.ticks = Dygraph.numericTicks(axis.computedValueRange[0], axis.computedValueRange[1], this, axis); - axis.ticks = ret.ticks; - this.numYDigits_ = ret.numDigits; } else { var p_axis = this.axes_[0]; var p_ticks = p_axis.ticks; @@ -2873,12 +2832,10 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { tick_values.push(y_val); } - var ret = + axis.ticks = Dygraph.numericTicks(axis.computedValueRange[0], axis.computedValueRange[1], this, axis, tick_values); - axis.ticks = ret.ticks; - this.numYDigits_ = ret.numDigits; } } }; @@ -3076,7 +3033,8 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else { - this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; + // TODO(danvk): use Dygraph.numberFormatter here? + this.attrs_.xValueFormatter = function(x) { return x; }; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; @@ -3200,10 +3158,21 @@ Dygraph.prototype.parseCSV_ = function(data) { } else if (this.attr_("customBars")) { // Bars are a low;center;high tuple for (var j = 1; j < inFields.length; j++) { - var vals = inFields[j].split(";"); - fields[j] = [ this.parseFloat_(vals[0], i, line), - this.parseFloat_(vals[1], i, line), - this.parseFloat_(vals[2], i, line) ]; + var val = inFields[j]; + if (/^ *$/.test(val)) { + fields[j] = [null, null, null]; + } else { + var vals = val.split(";"); + if (vals.length == 3) { + fields[j] = [ this.parseFloat_(vals[0], i, line), + this.parseFloat_(vals[1], i, line), + this.parseFloat_(vals[2], i, line) ]; + } else { + this.warning('When using customBars, values must be either blank ' + + 'or "low;center;high" tuples (got "' + val + + '" on line ' + (1+i)); + } + } } } else { // Values are just numbers @@ -3299,7 +3268,7 @@ Dygraph.prototype.parseArray_ = function(data) { return parsedData; } else { // Some intelligent defaults for a numeric x-axis. - this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; + this.attrs_.xValueFormatter = function(x) { return x; }; this.attrs_.xTicker = Dygraph.numericTicks; return data; } @@ -3325,7 +3294,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else if (indepType == 'number') { - this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; + this.attrs_.xValueFormatter = function(x) { return x; }; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; @@ -4247,6 +4216,24 @@ Dygraph.OPTIONS_REFERENCE = // "labels": ["Zooming"], "type": "boolean", "description" : "When this option is passed to updateOptions() along with either the dateWindow or valueRange options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the isZoomed method to determine this." + }, + "sigFigs" : { + "default": "null", + "labels": ["Value display/formatting"], + "type": "integer", + "description": "By default, dygraphs displays numbers with a fixed number of digits after the decimal point. If you'd prefer to have a fixed number of significant figures, set this option to that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0 and 1234 to be displayed as 1.23e+3." + }, + "digitsAfterDecimal" : { + "default": "2", + "labels": ["Value display/formatting"], + "type": "integer", + "description": "Unless it's run in scientific mode (see the sigFigs option), dygraphs displays numbers with digitsAfterDecimal digits after the decimal point. Trailing zeros are not displayed, so with a value of 2 you'll get '0', '0.1', '0.12', '123.45' but not '123.456' (it will be rounded to '123.46'). Numbers with absolute value less than 0.1^digitsAfterDecimal (i.e. those which would show up as '0.00') will be displayed in scientific notation." + }, + "maxNumberWidth" : { + "default": "6", + "labels": ["Value display/formatting"], + "type": "integer", + "description": "When displaying numbers in normal (not scientific) mode, large numbers will be displayed with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis labels. If there are more than maxNumberWidth digits to the left of the decimal in a number, dygraphs will switch to scientific notation, even when not operating in scientific mode. If you'd like to see all those digits, set this to something large, like 20 or 30." } } ; //