X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=529552d0c5fdee5f476ade89d596893a739c447b;hb=6d0aaa096fa90889f5370cc02933cf3cfc1a5777;hp=661ebfbc74df5efaf8591dd9102d757536f3cf4d;hpb=c1bc242a0326dc48ee2de27321195090a3eb486a;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 661ebfb..529552d 100644 --- a/dygraph.js +++ b/dygraph.js @@ -72,6 +72,59 @@ 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; @@ -100,7 +153,11 @@ Dygraph.DEFAULT_ATTRS = { labelsKMG2: false, showLabelsOnHighlight: true, - yValueFormatter: function(x) { return Dygraph.round_(x, 2); }, + yValueFormatter: function(x, opt_precision) { + var s = Dygraph.floatFormat(x, opt_precision); + var s2 = Dygraph.intFormat(x); + return s.length < s2.length ? s : s2; + }, strokeWidth: 1.0, @@ -198,6 +255,20 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.is_initial_draw_ = true; this.annotations_ = []; + // 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 = ""; @@ -260,6 +331,12 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.start_(); }; +Dygraph.prototype.toString = function() { + var maindiv = this.maindiv_; + var id = (maindiv && maindiv.id) ? maindiv.id : maindiv + return "[Dygraph " + id + "]"; +} + Dygraph.prototype.attr_ = function(name, seriesName) { if (seriesName && typeof(this.user_attrs_[seriesName]) != 'undefined' && @@ -306,7 +383,7 @@ Dygraph.prototype.error = function(message) { /** * Returns the current rolling period, as set by the user or an option. - * @return {Number} The number of days in the rolling window + * @return {Number} The number of points in the rolling window */ Dygraph.prototype.rollPeriod = function() { return this.rollPeriod_; @@ -561,6 +638,7 @@ Dygraph.cancelEvent = function(e) { return false; } + /** * Generates interface elements for the Dygraph: a containing div, a div to * display the current point, and a textbox to adjust the rolling average @@ -966,7 +1044,8 @@ Dygraph.movePan = function(event, g, context) { var maxValue = axis.initialTopValue + unitsDragged; var minValue = maxValue - axis.dragValueRange; if (axis.logscale) { - axis.valueWindow = [ Math.pow(10, minValue), Math.pow(10, maxValue) ]; + axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue), + Math.pow(Dygraph.LOG_SCALE, maxValue) ]; } else { axis.valueWindow = [ minValue, maxValue ]; } @@ -1236,6 +1315,7 @@ Dygraph.prototype.createDragInterface_ = function() { }); }; + /** * Draw a gray zoom rectangle over the desired area of the canvas. Also clears * up any previous zoom rectangles that were drawn. This could be optimized to @@ -1258,8 +1338,9 @@ Dygraph.prototype.createDragInterface_ = function() { * function. Used to avoid excess redrawing * @private */ -Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY, - prevDirection, prevEndX, prevEndY) { +Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, + endY, prevDirection, prevEndX, + prevEndY) { var ctx = this.canvas_.getContext("2d"); // Clean up from the previous rect if necessary @@ -1394,6 +1475,9 @@ Dygraph.prototype.mouseMove_ = function(event) { var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); var points = this.layout_.points; + // This prevents JS errors when mousing over the canvas before data loads. + if (points === undefined) return; + var lastx = -1; var lasty = -1; @@ -1410,10 +1494,6 @@ Dygraph.prototype.mouseMove_ = function(event) { idx = i; } if (idx >= 0) lastx = points[idx].xval; - // Check that you can really highlight the last day's data - var last = points[points.length-1]; - if (last != null && canvasx > last.canvasx) - lastx = points[points.length-1].xval; // Extract the points we've selected this.selPoints_ = []; @@ -1500,7 +1580,8 @@ Dygraph.prototype.updateSelection_ = function() { var canvasx = this.selPoints_[0].canvasx; // Set the status message to indicate the selected point(s) - var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":"; + var replace = this.attr_('xValueFormatter')( + this.lastx_, this.numXDigits_ + this.numExtraDigits_) + ":"; var fmtFunc = this.attr_('yValueFormatter'); var clen = this.colors_.length; @@ -1514,7 +1595,7 @@ Dygraph.prototype.updateSelection_ = function() { } var point = this.selPoints_[i]; var c = new RGBColor(this.plotter_.colors[point.name]); - var yval = fmtFunc(point.yval); + var yval = fmtFunc(point.yval, this.numYDigits_ + this.numExtraDigits_); replace += " " + point.name + ":" + yval; @@ -1678,7 +1759,7 @@ Dygraph.dateAxisFormatter = function(date, granularity) { * @return {String} A date of the form "YYYY/MM/DD" * @private */ -Dygraph.dateString_ = function(date, self) { +Dygraph.dateString_ = function(date) { var zeropad = Dygraph.zeropad; var d = new Date(date); @@ -1697,18 +1778,6 @@ Dygraph.dateString_ = function(date, self) { }; /** - * 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 @@ -1728,16 +1797,27 @@ Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"]; */ Dygraph.prototype.addXTicks_ = function() { // Determine the correct ticks scale on the x-axis: quarterly, monthly, ... - var startDate, endDate; + var range; if (this.dateWindow_) { - startDate = this.dateWindow_[0]; - endDate = this.dateWindow_[1]; + range = [this.dateWindow_[0], this.dateWindow_[1]]; } else { - startDate = this.rawData_[0][0]; - endDate = this.rawData_[this.rawData_.length - 1][0]; + 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')(startDate, endDate, this); this.layout_.updateOptions({xTicks: xTicks}); }; @@ -1924,6 +2004,7 @@ Dygraph.dateTicker = function(startDate, endDate, self) { // 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++) { @@ -1981,7 +2062,44 @@ Dygraph.binarySearch = function(val, arry, abs, low, high) { } 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) @@ -2104,33 +2222,38 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { 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; - } + 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); + 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]; + 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). @@ -2181,7 +2304,7 @@ Dygraph.prototype.extremeValues_ = function(series) { * number of axes, rolling averages, etc. */ Dygraph.prototype.predraw_ = function() { - // TODO(danvk): movabilitye more computations out of drawGraph_ and into here. + // TODO(danvk): move more computations out of drawGraph_ and into here. this.computeYAxes_(); // Create a new plotter. @@ -2245,7 +2368,7 @@ Dygraph.prototype.drawGraph_ = function() { // 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) { + if (point <= 0) { point = null; } series.push([date, point]); @@ -2332,12 +2455,9 @@ Dygraph.prototype.drawGraph_ = function() { this.layout_.addDataset(this.attr_("labels")[i], datasets[i]); } - // TODO(danvk): this method doesn't need to return anything. - var out = this.computeYAxisRanges_(extremes); - var axes = out[0]; - var seriesToAxisMap = out[1]; - this.layout_.updateOptions( { yAxes: axes, - seriesToAxisMap: seriesToAxisMap + this.computeYAxisRanges_(extremes); + this.layout_.updateOptions( { yAxes: this.axes_, + seriesToAxisMap: this.seriesToAxisMap_ } ); this.addXTicks_(); @@ -2528,11 +2648,13 @@ 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) { - axis.ticks = + var ret = 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; @@ -2545,14 +2667,14 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { tick_values.push(y_val); } - axis.ticks = + var ret = Dygraph.numericTicks(axis.computedValueRange[0], axis.computedValueRange[1], this, axis, tick_values); + axis.ticks = ret.ticks; + this.numYDigits_ = ret.numDigits; } } - - return [this.axes_, this.seriesToAxisMap_]; }; /** @@ -2564,7 +2686,8 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { * Note that this is where fractional input (i.e. '5/10') is converted into * decimal values. * @param {Array} originalData The data in the appropriate format (see above) - * @param {Number} rollPeriod The number of days over which to average the data + * @param {Number} rollPeriod The number of points over which to average the + * data */ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { if (originalData.length < 2) @@ -2641,7 +2764,7 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { } } else { // Calculate the rolling average for the first rollPeriod - 1 points where - // there is not enough data to roll over the full number of days + // there is not enough data to roll over the full number of points var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2); if (!this.attr_("errorBars")){ if (rollPeriod == 1) { @@ -2732,7 +2855,7 @@ Dygraph.dateParser = function(dateStr, self) { */ Dygraph.prototype.detectTypeFromString_ = function(str) { var isDate = false; - if (str.indexOf('-') >= 0 || + if (str.indexOf('-') > 0 || str.indexOf('/') >= 0 || isNaN(parseFloat(str))) { isDate = true; @@ -2747,7 +2870,7 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else { - this.attrs_.xValueFormatter = function(x) { return x; }; + this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; @@ -2842,13 +2965,30 @@ Dygraph.prototype.parseCSV_ = function(data) { if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) { outOfOrder = true; } - ret.push(fields); if (fields.length != expectedCols) { this.error("Number of columns in line " + i + " (" + fields.length + ") does not agree with number of labels (" + expectedCols + ") " + line); } + + // If the user specified the 'labels' option and none of the cells of the + // first row parsed correctly, then they probably double-specified the + // labels. We go with the values set in the option, discard this row and + // log a warning to the JS console. + if (i == 0 && this.attr_('labels')) { + var all_null = true; + for (var j = 0; all_null && j < fields.length; j++) { + if (fields[j]) all_null = false; + } + if (all_null) { + this.warn("The dygraphs 'labels' option is set, but the first row of " + + "CSV data ('" + line + "') appears to also contain labels. " + + "Will drop the CSV labels and use the option labels."); + continue; + } + } + ret.push(fields); } if (outOfOrder) { @@ -2910,7 +3050,7 @@ Dygraph.prototype.parseArray_ = function(data) { return parsedData; } else { // Some intelligent defaults for a numeric x-axis. - this.attrs_.xValueFormatter = function(x) { return x; }; + this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; this.attrs_.xTicker = Dygraph.numericTicks; return data; } @@ -2936,7 +3076,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else if (indepType == 'number') { - this.attrs_.xValueFormatter = function(x) { return x; }; + this.attrs_.xValueFormatter = this.attrs_.yValueFormatter; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; @@ -3213,9 +3353,9 @@ Dygraph.prototype.resize = function(width, height) { }; /** - * Adjusts the number of days in the rolling average. Updates the graph to + * Adjusts the number of points in the rolling average. Updates the graph to * reflect the new averaging period. - * @param {Number} length Number of days over which to average the data. + * @param {Number} length Number of points over which to average the data. */ Dygraph.prototype.adjustRoll = function(length) { this.rollPeriod_ = length;