X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;ds=sidebyside;f=dygraph.js;h=4ab665eb47a26f579e7c34184c5b46d63eec9d6d;hb=2cf95fff7f77658eccb3d00e7d52d081863c61ff;hp=848fa2627e8e4415af720f671a0a99c8d469d02e;hpb=032e4c1d547158c1d72912b4e74776b15e05088d;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 848fa26..4ab665e 100644 --- a/dygraph.js +++ b/dygraph.js @@ -100,7 +100,10 @@ Dygraph.DEFAULT_ATTRS = { labelsKMG2: false, showLabelsOnHighlight: true, - yValueFormatter: function(x) { return Dygraph.round_(x, 2); }, + yValueFormatter: function(a,b) { return Dygraph.numberFormatter(a,b); }, + digitsAfterDecimal: 2, + maxNumberWidth: 6, + sigFigs: null, strokeWidth: 1.0, @@ -158,6 +161,22 @@ Dygraph.VERTICAL = 2; // Used for initializing annotation CSS rules only once. Dygraph.addedAnnotationCSS = false; +/** + * Return the 2d context for a dygraph canvas. + * + * This method is only exposed for the sake of replacing the function in + * automated tests, e.g. + * + * var oldFunc = Dygraph.getContext(); + * Dygraph.getContext = function(canvas) { + * var realContext = oldFunc(canvas); + * return new Proxy(realContext); + * }; + */ +Dygraph.getContext = function(canvas) { + return canvas.getContext("2d"); +}; + Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) { // Labels is no longer a constructor parameter, since it's typically set // directly from the data source. It also conains a name for the x-axis, @@ -654,8 +673,11 @@ Dygraph.prototype.createInterface_ = function() { this.canvas_.style.width = this.width_ + "px"; // for IE this.canvas_.style.height = this.height_ + "px"; // for IE + this.canvas_ctx_ = Dygraph.getContext(this.canvas_); + // ... and for static parts of the chart. this.hidden_ = this.createPlotKitCanvas_(this.canvas_); + this.hidden_ctx_ = Dygraph.getContext(this.hidden_); // The interactive parts of the graph are drawn on top of the chart. this.graphDiv.appendChild(this.hidden_); @@ -1215,9 +1237,7 @@ Dygraph.endZoom = function(event, g, context) { g.doZoomY_(Math.min(context.dragStartY, context.dragEndY), Math.max(context.dragStartY, context.dragEndY)); } else { - g.canvas_.getContext("2d").clearRect(0, 0, - g.canvas_.width, - g.canvas_.height); + g.canvas_ctx_.clearRect(0, 0, g.canvas_.width, g.canvas_.height); } context.dragStartX = null; context.dragStartY = null; @@ -1395,7 +1415,7 @@ Dygraph.prototype.createDragInterface_ = function() { Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY, prevDirection, prevEndX, prevEndY) { - var ctx = this.canvas_.getContext("2d"); + var ctx = this.canvas_ctx_; // Clean up from the previous rect if necessary if (prevDirection == Dygraph.HORIZONTAL) { @@ -1628,10 +1648,11 @@ 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; } @@ -1647,16 +1668,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); + 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. @@ -1664,7 +1698,7 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { */ Dygraph.prototype.updateSelection_ = function() { // Clear the previously drawn vertical, if there is one - var ctx = this.canvas_.getContext("2d"); + var ctx = this.canvas_ctx_; if (this.previousVerticalX_ >= 0) { // Determine the maximum highlight circle size. var maxCircleSize = 0; @@ -1681,8 +1715,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 @@ -1738,7 +1771,6 @@ Dygraph.prototype.setSelection = function(row) { this.lastx_ = this.selPoints_[0].xval; this.updateSelection_(); } else { - this.lastx_ = -1; this.clearSelection(); } @@ -1765,9 +1797,8 @@ Dygraph.prototype.mouseOut_ = function(event) { */ 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.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); + this.setLegendHTML_(); this.selPoints_ = []; this.lastx_ = -1; } @@ -1788,11 +1819,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. @@ -1810,7 +1910,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 @@ -1833,7 +1933,7 @@ Dygraph.dateAxisFormatter = function(date, granularity) { return Dygraph.hmsString_(date.getTime()); } } -} +}; /** * Convert a JS date (millis since epoch) to YYYY/MM/DD @@ -2274,14 +2374,13 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { 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) : Dygraph.round_(tickV, 2); + 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 = Dygraph.round_(tickV / n, 1) + k_labels[j]; + label = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j]; break; } } @@ -2346,7 +2445,9 @@ Dygraph.prototype.predraw_ = function() { // Create a new plotter. if (this.plotter_) this.plotter_.clear(); this.plotter_ = new DygraphCanvasRenderer(this, - this.hidden_, this.layout_, + this.hidden_, + this.hidden_ctx_, + this.layout_, this.renderOptions_); // The roller sits in the bottom left corner of the chart. We don't know where @@ -2510,7 +2611,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) { @@ -2943,6 +3051,7 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { this.attrs_.xTicker = Dygraph.dateTicker; this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else { + // 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; @@ -3067,10 +3176,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 @@ -4114,6 +4234,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." } } ; //