X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=1257b23dfa5655bb73700be2ab1824da1f418e9f;hb=4cb5e2d2664a6798953e305628cb75448280f31b;hp=dd4e7b569f9859405b444116844e97cbfd439fcf;hpb=158e2643585a524d29894101daaf11735eec8cdb;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index dd4e7b5..1257b23 100644 --- a/dygraph.js +++ b/dygraph.js @@ -73,6 +73,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; @@ -96,7 +149,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, @@ -129,8 +186,8 @@ Dygraph.DEFAULT_ATTRS = { stepPlot: false, avoidMinZero: false, - - interactionModel: null // will be set to Dygraph.defaultInteractionModel. + + interactionModel: null // will be set to Dygraph.defaultInteractionModel. }; // Various logging levels. @@ -161,7 +218,7 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) { /** * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit - * and context <canvas> inside of it. See the constructor for details. + * and context <canvas> inside of it. See the constructor for details * on the parameters. * @param {Element} div the Element to render the graph into. * @param {String | Function} file Source data @@ -194,6 +251,20 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.wilsonInterval_ = attrs.wilsonInterval || true; 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. @@ -303,7 +374,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_; @@ -450,6 +521,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 @@ -987,50 +1059,50 @@ Dygraph.endZoom = function(event, g, context) { Dygraph.defaultInteractionModel = { // Track the beginning of drag events - mousedown : function(event, g, context) { - context.initializeMouseDown(event, g, context); + mousedown: function(event, g, context) { + context.initializeMouseDown(event, g, context); - if (event.altKey || event.shiftKey) { - Dygraph.startPan(event, g, context); - } else { - Dygraph.startZoom(event, g, context); - } - }, + if (event.altKey || event.shiftKey) { + Dygraph.startPan(event, g, context); + } else { + Dygraph.startZoom(event, g, context); + } + }, // Draw zoom rectangles when the mouse is down and the user moves around - mousemove : function(event, g, context) { - if (context.isZooming) { - Dygraph.moveZoom(event, g, context); - } else if (context.isPanning) { - Dygraph.movePan(event, g, context); - } - }, + mousemove: function(event, g, context) { + if (context.isZooming) { + Dygraph.moveZoom(event, g, context); + } else if (context.isPanning) { + Dygraph.movePan(event, g, context); + } + }, - mouseup : function(event, g, context) { - if (context.isZooming) { - Dygraph.endZoom(event, g, context); - } else if (context.isPanning) { - Dygraph.endPan(event, g, context); - } - }, + mouseup: function(event, g, context) { + if (context.isZooming) { + Dygraph.endZoom(event, g, context); + } else if (context.isPanning) { + Dygraph.endPan(event, g, context); + } + }, // Temporarily cancel the dragging event when the mouse leaves the graph - mouseout : function(event, g, context) { - if (context.isZooming) { - context.dragEndX = null; - context.dragEndY = null; - } - }, + mouseout: function(event, g, context) { + if (context.isZooming) { + context.dragEndX = null; + context.dragEndY = null; + } + }, // Disable zooming out if panning. - dblclick : function(event, g, context) { - if (event.altKey || event.shiftKey) { - return; - } - // TODO(konigsberg): replace g.doUnzoom()_ with something that is - // friendlier to public use. - g.doUnzoom_(); + dblclick: function(event, g, context) { + if (event.altKey || event.shiftKey) { + return; } + // TODO(konigsberg): replace g.doUnzoom()_ with something that is + // friendlier to public use. + g.doUnzoom_(); + } }; Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel; @@ -1043,43 +1115,43 @@ Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel; Dygraph.prototype.createDragInterface_ = function() { var context = { // Tracks whether the mouse is down right now - isZooming : false, - isPanning : false, // is this drag part of a pan? - is2DPan : false, // if so, is that pan 1- or 2-dimensional? - dragStartX : null, - dragStartY : null, - dragEndX : null, - dragEndY : null, - dragDirection : null, - prevEndX : null, - prevEndY : null, - prevDragDirection : null, + isZooming: false, + isPanning: false, // is this drag part of a pan? + is2DPan: false, // if so, is that pan 1- or 2-dimensional? + dragStartX: null, + dragStartY: null, + dragEndX: null, + dragEndY: null, + dragDirection: null, + prevEndX: null, + prevEndY: null, + prevDragDirection: null, // TODO(danvk): update this comment // draggingDate and draggingValue represent the [date,value] point on the // graph at which the mouse was pressed. As the mouse moves while panning, // the viewport must pan so that the mouse position points to // [draggingDate, draggingValue] - draggingDate : null, + draggingDate: null, // TODO(danvk): update this comment // The range in second/value units that the viewport encompasses during a // panning operation. - dateRange : null, + dateRange: null, // Utility function to convert page-wide coordinates to canvas coords - px : 0, - py : 0, + px: 0, + py: 0, - initializeMouseDown : function(event, g, context) { + initializeMouseDown: function(event, g, context) { // prevents mouse drags from selecting page text. if (event.preventDefault) { event.preventDefault(); // Firefox, Chrome, etc. } else { event.returnValue = false; // IE - event.cancelBubble = true; + event.cancelBubble = true; } - + context.px = Dygraph.findPosX(g.canvas_); context.py = Dygraph.findPosY(g.canvas_); context.dragStartX = g.dragGetX_(event, context); @@ -1089,7 +1161,6 @@ Dygraph.prototype.createDragInterface_ = function() { var interactionModel = this.attr_("interactionModel"); - // Self is the graph. var self = this; @@ -1127,6 +1198,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 @@ -1149,8 +1221,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 @@ -1393,7 +1466,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; @@ -1407,7 +1481,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; @@ -1551,7 +1625,9 @@ Dygraph.hmsString_ = function(date) { * @private */ Dygraph.dateAxisFormatter = function(date, granularity) { - if (granularity >= Dygraph.MONTHLY) { + if (granularity >= Dygraph.DECADAL) { + return date.strftime('%Y'); + } else if (granularity >= Dygraph.MONTHLY) { return date.strftime('%b %y'); } else { var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds(); @@ -1569,7 +1645,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); @@ -1588,18 +1664,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 @@ -1619,16 +1683,25 @@ 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 = []; + + if (ret.ticks !== undefined) { + // numericTicks() returns multiple values. + xTicks = ret.ticks; + this.numXDigits_ = ret.numDigits; + } else { + xTicks = ret; } - var xTicks = this.attr_('xTicker')(startDate, endDate, this); this.layout_.updateOptions({xTicks: xTicks}); }; @@ -1653,7 +1726,8 @@ Dygraph.QUARTERLY = 16; Dygraph.BIANNUAL = 17; Dygraph.ANNUAL = 18; Dygraph.DECADAL = 19; -Dygraph.NUM_GRANULARITIES = 20; +Dygraph.CENTENNIAL = 20; +Dygraph.NUM_GRANULARITIES = 21; Dygraph.SHORT_SPACINGS = []; Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1; @@ -1689,6 +1763,7 @@ Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) { if (granularity == Dygraph.BIANNUAL) num_months = 2; if (granularity == Dygraph.ANNUAL) num_months = 1; if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; } + if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; } var msInYear = 365.2524 * 24 * 3600 * 1000; var num_years = 1.0 * (end_time - start_time) / msInYear; @@ -1761,6 +1836,11 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { } else if (granularity == Dygraph.DECADAL) { months = [ 0 ]; year_mod = 10; + } else if (granularity == Dygraph.CENTENNIAL) { + months = [ 0 ]; + year_mod = 100; + } else { + this.warn("Span of dates is too long"); } var start_year = new Date(start_time).getFullYear(); @@ -1806,6 +1886,43 @@ Dygraph.dateTicker = function(startDate, endDate, self) { }; /** + * 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) @@ -1823,7 +1940,7 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { 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: @@ -1878,30 +1995,35 @@ 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); + } for (var i = 0; i < ticks.length; i++) { 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) { + 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 = Dygraph.round_(tickV / n, 1) + k_labels[j]; + label = (tickV / n).toPrecision(numDigits) + k_labels[j]; break; } } } ticks[i].label = label; } - return ticks; + return {ticks: ticks, numDigits: numDigits}; }; // Computes the range of the data series (including confidence intervals). @@ -2092,12 +2214,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_(); @@ -2276,11 +2395,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; @@ -2293,14 +2414,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_]; }; /** @@ -2312,7 +2433,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) @@ -2389,7 +2511,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) { @@ -2495,7 +2617,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_.xValueFormatter; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; @@ -2658,7 +2780,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; } @@ -2684,7 +2806,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; @@ -2961,9 +3083,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;