X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=db9a8bfa7c79e9b2d5e17f1b24f20dce80f08b2a;hb=c1c42651dd140ecb0460cfcff736aceb117f9cc6;hp=f198cffca4b2b8ceafb4a78013b25604ec5e1ea8;hpb=34ad56b341279a066dc8bad129501d8d254699c1;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index f198cff..db9a8bf 100644 --- a/dygraph.js +++ b/dygraph.js @@ -95,6 +95,12 @@ Dygraph.DEFAULT_HEIGHT = 320; Dygraph.ANIMATION_STEPS = 12; Dygraph.ANIMATION_DURATION = 200; +// Label constants for the labelsKMB and labelsKMG2 options. +// (i.e. '100000' -> '100K') +Dygraph.KMB_LABELS = [ 'K', 'M', 'B', 'T', 'Q' ]; +Dygraph.KMG2_BIG_LABELS = [ 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ]; +Dygraph.KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ]; + // These are defined before DEFAULT_ATTRS so that it can refer to them. /** * @private @@ -116,14 +122,60 @@ Dygraph.numberValueFormatter = function(x, opts, pt, g) { var digits = opts('digitsAfterDecimal'); var maxNumberWidth = opts('maxNumberWidth'); + var kmb = opts('labelsKMB'); + var kmg2 = opts('labelsKMG2'); + + var label; + // 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); + label = x.toExponential(digits); } else { - return '' + Dygraph.round_(x, digits); + label = '' + Dygraph.round_(x, digits); + } + + if (kmb || kmg2) { + var k; + var k_labels = []; + var m_labels = []; + if (kmb) { + k = 1000; + k_labels = Dygraph.KMB_LABELS; + } + if (kmg2) { + if (kmb) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!"); + k = 1024; + k_labels = Dygraph.KMG2_BIG_LABELS; + m_labels = Dygraph.KMG2_SMALL_LABELS; + } + + var absx = Math.abs(x); + var n = Dygraph.pow(k, k_labels.length); + for (var j = k_labels.length - 1; j >= 0; j--, n /= k) { + if (absx >= n) { + label = Dygraph.round_(x / n, digits) + k_labels[j]; + break; + } + } + if (kmg2) { + // TODO(danvk): clean up this logic. Why so different than kmb? + var x_parts = String(x.toExponential()).split('e-'); + if (x_parts.length === 2 && x_parts[1] >= 3 && x_parts[1] <= 24) { + if (x_parts[1] % 3 > 0) { + label = Dygraph.round_(x_parts[0] / + Dygraph.pow(10, (x_parts[1] % 3)), + digits); + } else { + label = Number(x_parts[0]).toFixed(2); + } + label += m_labels[Math.floor(x_parts[1] / 3) - 1]; + } + } } + + return label; }; /** @@ -239,6 +291,7 @@ Dygraph.DEFAULT_ATTRS = { connectSeparatedPoints: false, stackedGraph: false, + stackedGraphNaNFill: 'all', hideOverlayOnMouseOut: true, // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms. @@ -246,6 +299,8 @@ Dygraph.DEFAULT_ATTRS = { stepPlot: false, avoidMinZero: false, + xRangePad: 0, + yRangePad: null, drawAxesAtZero: false, // Sizes of the various chart labels. @@ -290,18 +345,24 @@ Dygraph.DEFAULT_ATTRS = { pixelsPerLabel: 60, axisLabelFormatter: Dygraph.dateAxisFormatter, valueFormatter: Dygraph.dateString_, + drawGrid: true, + independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, y: { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawGrid: true, + independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, y2: { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawGrid: false, + independentTicks: false, ticker: null // will be set in dygraph-tickers.js } } @@ -405,9 +466,11 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { div.style.width = Dygraph.DEFAULT_WIDTH + "px"; } } - // these will be zero if the dygraph's div is hidden. - this.width_ = div.clientWidth; - this.height_ = div.clientHeight; + // These will be zero if the dygraph's div is hidden. In that case, + // use the user-specified attributes if present. If not, use zero + // and assume the user will call resize to fix things later. + this.width_ = div.clientWidth || attrs.width || 0; + this.height_ = div.clientHeight || attrs.height || 0; // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. if (attrs.stackedGraph) { @@ -610,7 +673,7 @@ Dygraph.prototype.optionsViewForAxis_ = function(axis) { var self = this; return function(opt) { var axis_opts = self.user_attrs_.axes; - if (axis_opts && axis_opts[axis] && axis_opts[axis][opt]) { + if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { return axis_opts[axis][opt]; } // user-specified attributes always trump defaults, even if they're less @@ -620,7 +683,7 @@ Dygraph.prototype.optionsViewForAxis_ = function(axis) { } axis_opts = self.attrs_.axes; - if (axis_opts && axis_opts[axis] && axis_opts[axis][opt]) { + if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { return axis_opts[axis][opt]; } // check old-style axis options @@ -657,8 +720,18 @@ Dygraph.prototype.xAxisRange = function() { * data set. */ Dygraph.prototype.xAxisExtremes = function() { + var pad = this.attr_('xRangePad') / this.plotter_.area.w; + if (this.numRows() === 0) { + return [0 - pad, 1 + pad]; + } var left = this.rawData_[0][0]; var right = this.rawData_[this.rawData_.length - 1][0]; + if (pad) { + // Must keep this in sync with dygraph-layout _evaluateLimits() + var range = right - left; + left -= range * pad; + right += range * pad; + } return [left, right]; }; @@ -874,6 +947,7 @@ Dygraph.prototype.toPercentXCoord = function(x) { * @return { Integer } The number of columns. */ Dygraph.prototype.numColumns = function() { + if (!this.rawData_) return 0; return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length; }; @@ -882,25 +956,11 @@ Dygraph.prototype.numColumns = function() { * @return { Integer } The number of rows, less any header. */ Dygraph.prototype.numRows = function() { + if (!this.rawData_) return 0; return this.rawData_.length; }; /** - * Returns the full range of the x-axis, as determined by the most extreme - * values in the data set. Not affected by zooming, visibility, etc. - * TODO(danvk): merge w/ xAxisExtremes - * @return { Array } A [low, high] pair - * @private - */ -Dygraph.prototype.fullXRange_ = function() { - if (this.numRows() > 0) { - return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]]; - } else { - return [0, 1]; - } -}; - -/** * Returns the value in the given row and column. If the row and column exceed * the bounds on the data, returns null. Also returns null if the value is * missing. @@ -928,8 +988,7 @@ Dygraph.prototype.createInterface_ = function() { var enclosing = this.maindiv_; this.graphDiv = document.createElement("div"); - this.graphDiv.style.width = this.width_ + "px"; - this.graphDiv.style.height = this.height_ + "px"; + // TODO(danvk): any other styles that are useful to set here? this.graphDiv.style.textAlign = 'left'; // This is a CSS "reset" enclosing.appendChild(this.graphDiv); @@ -937,15 +996,13 @@ Dygraph.prototype.createInterface_ = function() { // Create the canvas for interactive parts of the chart. this.canvas_ = Dygraph.createCanvas(); this.canvas_.style.position = "absolute"; - this.canvas_.width = this.width_; - this.canvas_.height = this.height_; - 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.resizeElements_(); + + this.canvas_ctx_ = Dygraph.getContext(this.canvas_); this.hidden_ctx_ = Dygraph.getContext(this.hidden_); // The interactive parts of the graph are drawn on top of the chart. @@ -968,14 +1025,14 @@ Dygraph.prototype.createInterface_ = function() { // 2. e.relatedTarget is outside the chart var target = e.target || e.fromElement; var relatedTarget = e.relatedTarget || e.toElement; - if (Dygraph.isElementContainedBy(target, dygraph.graphDiv) && - !Dygraph.isElementContainedBy(relatedTarget, dygraph.graphDiv)) { + if (Dygraph.isNodeContainedBy(target, dygraph.graphDiv) && + !Dygraph.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) { dygraph.mouseOut_(e); } }; - this.addEvent(window, 'mouseout', this.mouseOutHandler_); - this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); + this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_); + this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); // Don't recreate and register the resize handler on subsequent calls. // This happens when the graph is resized. @@ -986,16 +1043,32 @@ Dygraph.prototype.createInterface_ = function() { // Update when the window is resized. // TODO(danvk): drop frames depending on complexity of the chart. - this.addEvent(window, 'resize', this.resizeHandler_); + this.addAndTrackEvent(window, 'resize', this.resizeHandler_); } }; +Dygraph.prototype.resizeElements_ = function() { + this.graphDiv.style.width = this.width_ + "px"; + this.graphDiv.style.height = this.height_ + "px"; + this.canvas_.width = this.width_; + this.canvas_.height = this.height_; + this.canvas_.style.width = this.width_ + "px"; // for IE + this.canvas_.style.height = this.height_ + "px"; // for IE + this.hidden_.width = this.width_; + this.hidden_.height = this.height_; + this.hidden_.style.width = this.width_ + "px"; // for IE + this.hidden_.style.height = this.height_ + "px"; // for IE +}; + /** * Detach DOM elements in the dygraph and null out all data references. * Calling this when you're done with a dygraph can dramatically reduce memory * usage. See, e.g., the tests/perf.html example. */ Dygraph.prototype.destroy = function() { + this.canvas_ctx_.restore(); + this.hidden_ctx_.restore(); + var removeRecursive = function(node) { while (node.hasChildNodes()) { removeRecursive(node.firstChild); @@ -1003,19 +1076,11 @@ Dygraph.prototype.destroy = function() { } }; - if (this.registeredEvents_) { - for (var idx = 0; idx < this.registeredEvents_.length; idx++) { - var reg = this.registeredEvents_[idx]; - Dygraph.removeEvent(reg.elem, reg.type, reg.fn); - } - } - - this.registeredEvents_ = []; + this.removeTrackedEvents_(); // remove mouse event handlers (This may not be necessary anymore) Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_); Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); - Dygraph.removeEvent(this.mouseEventElement_, 'mouseup', this.mouseUpHandler_); // remove window handlers Dygraph.removeEvent(window,'resize',this.resizeHandler_); @@ -1287,19 +1352,13 @@ Dygraph.prototype.createDragInterface_ = function() { for (var eventName in interactionModel) { if (!interactionModel.hasOwnProperty(eventName)) continue; - this.addEvent(this.mouseEventElement_, eventName, + this.addAndTrackEvent(this.mouseEventElement_, eventName, bindHandler(interactionModel[eventName])); } - // unregister the handler on subsequent calls. - // This happens when the graph is resized. - if (this.mouseUpHandler_) { - Dygraph.removeEvent(document, 'mouseup', this.mouseUpHandler_); - } - // If the user releases the mouse button during a drag, but not over the // canvas, then it doesn't count as a zooming action. - this.mouseUpHandler_ = function(event) { + var mouseUpHandler = function(event) { if (context.isZooming || context.isPanning) { context.isZooming = false; context.dragStartX = null; @@ -1319,7 +1378,7 @@ Dygraph.prototype.createDragInterface_ = function() { context.tarp.uncover(); }; - this.addEvent(document, 'mouseup', this.mouseUpHandler_); + this.addAndTrackEvent(document, 'mouseup', mouseUpHandler); }; /** @@ -1526,7 +1585,7 @@ Dygraph.prototype.resetZoom = function() { oldValueRanges = this.yAxisRanges(); // TODO(danvk): this is pretty inefficient var packed = this.gatherDatasets_(this.rolledSeries_, null); - var extremes = packed[1]; + var extremes = packed.extremes; // this has the side-effect of modifying this.axes_. // this doesn't make much sense in this context, but it's convenient (we @@ -1675,7 +1734,7 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { var minDist = Infinity; var idx = -1; var dist, dx, dy, point, closestPoint, closestSeries; - for ( var setIdx = this.layout_.datasets.length - 1 ; setIdx >= 0 ; --setIdx ) { + for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) { var points = this.layout_.points[setIdx]; for (var i = 0; i < points.length; ++i) { var point = points[i]; @@ -1716,7 +1775,7 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { var boundary = this.getLeftBoundary_(); var rowIdx = row - boundary; var closestPoint, closestSeries; - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { + for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) { var points = this.layout_.points[setIdx]; if (rowIdx >= points.length) continue; var p1 = points[rowIdx]; @@ -1790,7 +1849,11 @@ Dygraph.prototype.mouseMove_ = function(event) { var callback = this.attr_("highlightCallback"); if (callback && selectionChanged) { - callback(event, this.lastx_, this.selPoints_, this.lastRow_, this.highlightSet_); + callback(event, + this.lastx_, + this.selPoints_, + this.lastRow_ + this.getLeftBoundary_(), + this.highlightSet_); } }; @@ -1818,14 +1881,6 @@ Dygraph.prototype.idxToRow_ = function(setIdx, rowIdx) { var boundary = this.getLeftBoundary_(); return boundary + rowIdx; - // for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - // var set = this.layout_.datasets[setIdx]; - // if (idx < set.length) { - // return boundary + idx; - // } - // idx -= set.length; - // } - // return -1; }; Dygraph.prototype.animateSelection_ = function(direction) { @@ -1933,7 +1988,7 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { ctx.strokeStyle = color; ctx.fillStyle = color; callback(this.g, pt.name, ctx, canvasx, pt.canvasy, - color, circleSize); + color, circleSize, pt.idx); } ctx.restore(); @@ -1965,15 +2020,10 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) { if (row !== false && row >= 0) { if (row != this.lastRow_) changed = true; this.lastRow_ = row; - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var set = this.layout_.datasets[setIdx]; - if (row < set.length) { - var point = this.layout_.points[setIdx][row]; - - if (this.attr_("stackedGraph")) { - point = this.layout_.unstackPointAtIndex(setIdx, row); - } - + for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) { + var points = this.layout_.points[setIdx]; + if (row < points.length) { + var point = points[row]; if (point.yval !== null) this.selPoints_.push(point); } } @@ -2096,7 +2146,7 @@ Dygraph.prototype.addXTicks_ = function() { if (this.dateWindow_) { range = [this.dateWindow_[0], this.dateWindow_[1]]; } else { - range = this.fullXRange_(); + range = this.xAxisExtremes(); } var xAxisOptionsView = this.optionsViewForAxis_('x'); @@ -2175,6 +2225,15 @@ Dygraph.prototype.predraw_ = function() { this.cascadeEvents_('clearChart'); this.plotter_.clear(); } + + if(!this.is_initial_draw_) { + this.canvas_ctx_.restore(); + this.hidden_ctx_.restore(); + } + + this.canvas_ctx_.save(); + this.hidden_ctx_.save(); + this.plotter_ = new DygraphCanvasRenderer(this, this.hidden_, this.hidden_ctx_, @@ -2200,14 +2259,169 @@ Dygraph.prototype.predraw_ = function() { // If the data or options have changed, then we'd better redraw. this.drawGraph_(); - this.plotter_.onDoneDrawing(); - // This is used to determine whether to do various animations. var end = new Date(); this.drawingTimeMs_ = (end - start); }; /** + * Point structure. + * + * xval_* and yval_* are the original unscaled data values, + * while x_* and y_* are scaled to the range (0.0-1.0) for plotting. + * yval_stacked is the cumulative Y value used for stacking graphs, + * and bottom/top/minus/plus are used for error bar graphs. + * + * @typedef {{ + * idx: number, + * name: string, + * x: ?number, + * xval: ?number, + * y_bottom: ?number, + * y: ?number, + * y_stacked: ?number, + * y_top: ?number, + * yval_minus: ?number, + * yval: ?number, + * yval_plus: ?number, + * yval_stacked + * }} + */ +Dygraph.PointType = undefined; + +// TODO(bhs): these loops are a hot-spot for high-point-count charts. In fact, +// on chrome+linux, they are 6 times more expensive than iterating through the +// points and drawing the lines. The brunt of the cost comes from allocating +// the |point| structures. +/** + * Converts a series to a Point array. + * + * @param {Array.)>} series Array where + * series[row] = [x,y] or [x, [y, err]] or [x, [y, yplus, yminus]]. + * @param {boolean} bars True if error bars or custom bars are being drawn. + * @param {string} setName Name of the series. + * @param {number} boundaryIdStart Index offset of the first point, equal to + * the number of skipped points left of the date window minimum (if any). + * @return {Array.} List of points for this series. + */ +Dygraph.seriesToPoints_ = function(series, bars, setName, boundaryIdStart) { + var points = []; + for (var i = 0; i < series.length; ++i) { + var item = series[i]; + var yraw = bars ? item[1][0] : item[1]; + var yval = yraw === null ? null : DygraphLayout.parseFloat_(yraw); + var point = { + x: NaN, + y: NaN, + xval: DygraphLayout.parseFloat_(item[0]), + yval: yval, + name: setName, // TODO(danvk): is this really necessary? + idx: i + boundaryIdStart + }; + + if (bars) { + point.y_top = NaN; + point.y_bottom = NaN; + point.yval_minus = DygraphLayout.parseFloat_(item[1][1]); + point.yval_plus = DygraphLayout.parseFloat_(item[1][2]); + } + points.push(point); + } + return points; +}; + + +/** + * Calculates point stacking for stackedGraph=true. + * + * For stacking purposes, interpolate or extend neighboring data across + * NaN values based on stackedGraphNaNFill settings. This is for display + * only, the underlying data value as shown in the legend remains NaN. + * + * @param {Array.} points Point array for a single series. + * Updates each Point's yval_stacked property. + * @param {Array.} cumulativeYval Accumulated top-of-graph stacked Y + * values for the series seen so far. Index is the row number. Updated + * based on the current series's values. + * @param {Array.} seriesExtremes Min and max values, updated + * to reflect the stacked values. + * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or + * 'none'. + */ +Dygraph.stackPoints_ = function( + points, cumulativeYval, seriesExtremes, fillMethod) { + var lastXval = null; + var prevPoint = null; + var nextPoint = null; + var nextPointIdx = -1; + + // Find the next stackable point starting from the given index. + var updateNextPoint = function(idx) { + // If we've previously found a non-NaN point and haven't gone past it yet, + // just use that. + if (nextPointIdx >= idx) return; + + // We haven't found a non-NaN point yet or have moved past it, + // look towards the right to find a non-NaN point. + for (var j = idx; j < points.length; ++j) { + // Clear out a previously-found point (if any) since it's no longer + // valid, we shouldn't use it for interpolation anymore. + nextPoint = null; + if (!isNaN(points[j].yval) && points[j].yval !== null) { + nextPointIdx = j; + nextPoint = points[j]; + break; + } + } + }; + + for (var i = 0; i < points.length; ++i) { + var point = points[i]; + var xval = point.xval; + if (cumulativeYval[xval] === undefined) { + cumulativeYval[xval] = 0; + } + + var actualYval = point.yval; + if (isNaN(actualYval) || actualYval === null) { + // Interpolate/extend for stacking purposes if possible. + updateNextPoint(i); + if (prevPoint && nextPoint && fillMethod != 'none') { + // Use linear interpolation between prevPoint and nextPoint. + actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) * + ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval)); + } else if (prevPoint && fillMethod == 'all') { + actualYval = prevPoint.yval; + } else if (nextPoint && fillMethod == 'all') { + actualYval = nextPoint.yval; + } else { + actualYval = 0; + } + } else { + prevPoint = point; + } + + var stackedYval = cumulativeYval[xval]; + if (lastXval != xval) { + // If an x-value is repeated, we ignore the duplicates. + stackedYval += actualYval; + cumulativeYval[xval] = stackedYval; + } + lastXval = xval; + + point.yval_stacked = stackedYval; + + if (stackedYval > seriesExtremes[1]) { + seriesExtremes[1] = stackedYval; + } + if (stackedYval < seriesExtremes[0]) { + seriesExtremes[0] = stackedYval; + } + } +}; + + +/** * Loop over all fields and create datasets, calculating extreme y-values for * each series and extreme x-indices as we go. * @@ -2215,39 +2429,50 @@ Dygraph.prototype.predraw_ = function() { * extreme values "speculatively", i.e. without actually setting state on the * dygraph. * - * TODO(danvk): make this more of a true function - * @return [ datasets, seriesExtremes, boundaryIds ] + * @param {Array.)>>} rolledSeries, where + * rolledSeries[seriesIndex][row] = raw point, where + * seriesIndex is the column number starting with 1, and + * rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]]. + * @param {?Array.} dateWindow [xmin, xmax] pair, or null. + * @return {{ + * points: Array.>, + * seriesExtremes: Array.>, + * boundaryIds: Array.}} * @private */ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { var boundaryIds = []; - var cumulative_y = []; // For stacked series. - var datasets = []; + var points = []; + var cumulativeYval = []; // For stacked series. var extremes = {}; // series name -> [low, high] - var i, j, k; + var i, k; + var errorBars = this.attr_("errorBars"); + var customBars = this.attr_("customBars"); + var bars = errorBars || customBars; + var isValueNull = function(sample) { + if (!bars) { + return sample[1] === null; + } else { + return customBars ? sample[1][1] === null : + errorBars ? sample[1][0] === null : false; + } + }; // Loop over the fields (series). Go from the last to the first, // because if they're stacked that's how we accumulate the values. var num_series = rolledSeries.length - 1; + var series; for (i = num_series; i >= 1; i--) { if (!this.visibility()[i - 1]) continue; - // Note: this copy _is_ necessary at the moment. - // If you remove it, it breaks zooming with error bars on. - // TODO(danvk): investigate further & write a test for this. - var series = []; - for (j = 0; j < rolledSeries[i].length; j++) { - series.push(rolledSeries[i][j]); - } - // Prune down to the desired range, if necessary (for zooming) // Because there can be lines going to points outside of the visible area, // we actually prune to visible points, plus one on either side. - var bars = this.attr_("errorBars") || this.attr_("customBars"); if (dateWindow) { + series = rolledSeries[i]; var low = dateWindow[0]; var high = dateWindow[1]; - var pruned = []; + // TODO(danvk): do binary search instead of linear search. // TODO(danvk): pass firstIdx and lastIdx directly to the renderer. var firstIdx = null, lastIdx = null; @@ -2259,84 +2484,55 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { lastIdx = k; } } + if (firstIdx === null) firstIdx = 0; - if (firstIdx > 0) firstIdx--; + var correctedFirstIdx = firstIdx; + var isInvalidValue = true; + while (isInvalidValue && correctedFirstIdx > 0) { + correctedFirstIdx--; + isInvalidValue = isValueNull(series[correctedFirstIdx]); + } + if (lastIdx === null) lastIdx = series.length - 1; - if (lastIdx < series.length - 1) lastIdx++; - boundaryIds[i-1] = [firstIdx, lastIdx]; - for (k = firstIdx; k <= lastIdx; k++) { - pruned.push(series[k]); + var correctedLastIdx = lastIdx; + isInvalidValue = true; + while (isInvalidValue && correctedLastIdx < series.length - 1) { + correctedLastIdx++; + isInvalidValue = isValueNull(series[correctedLastIdx]); + } + + boundaryIds[i-1] = [(firstIdx > 0) ? firstIdx - 1 : firstIdx, + (lastIdx < series.length - 1) ? lastIdx + 1 : lastIdx]; + + if (correctedFirstIdx!==firstIdx) { + firstIdx = correctedFirstIdx; + } + if (correctedLastIdx !== lastIdx) { + lastIdx = correctedLastIdx; } - series = pruned; + // .slice's end is exclusive, we want to include lastIdx. + series = series.slice(firstIdx, lastIdx + 1); } else { + series = rolledSeries[i]; boundaryIds[i-1] = [0, series.length-1]; } + var seriesName = this.attr_("labels")[i]; var seriesExtremes = this.extremeValues_(series); - if (bars) { - for (j=0; j seriesExtremes[1]) { - seriesExtremes[1] = cumulative_y[x]; - } - if (cumulative_y[x] < seriesExtremes[0]) { - seriesExtremes[0] = cumulative_y[x]; - } - } + if (this.attr_("stackedGraph")) { + Dygraph.stackPoints_(seriesPoints, cumulativeYval, seriesExtremes, + this.attr_("stackedGraphNaNFill")); } - var seriesName = this.attr_("labels")[i]; extremes[seriesName] = seriesExtremes; - datasets[i] = series; - } - - // For stacked graphs, a NaN value for any point in the sum should create a - // clean gap in the graph. Back-propagate NaNs to all points at this X value. - if (this.attr_("stackedGraph")) { - for (k = datasets.length - 1; k >= 0; --k) { - // Use the first nonempty dataset to get X values. - if (!datasets[k]) continue; - for (j = 0; j < datasets[k].length; j++) { - var x = datasets[k][j][0]; - if (isNaN(cumulative_y[x])) { - // Set all Y values to NaN at that X value. - for (i = datasets.length - 1; i >= 0; i--) { - if (!datasets[i]) continue; - datasets[i][j][1] = NaN; - } - } - } - break; - } + points[i] = seriesPoints; } - return [ datasets, extremes, boundaryIds ]; + return { points: points, extremes: extremes, boundaryIds: boundaryIds }; }; /** @@ -2358,9 +2554,9 @@ Dygraph.prototype.drawGraph_ = function() { this.attrs_.pointSize = 0.5 * this.attr_('highlightCircleSize'); var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_); - var datasets = packed[0]; - var extremes = packed[1]; - this.boundaryIds_ = packed[2]; + var points = packed.points; + var extremes = packed.extremes; + this.boundaryIds_ = packed.boundaryIds; this.setIndexByName_ = {}; var labels = this.attr_("labels"); @@ -2368,10 +2564,10 @@ Dygraph.prototype.drawGraph_ = function() { this.setIndexByName_[labels[0]] = 0; } var dataIdx = 0; - for (var i = 1; i < datasets.length; i++) { + for (var i = 1; i < points.length; i++) { this.setIndexByName_[labels[i]] = i; if (!this.visibility()[i - 1]) continue; - this.layout_.addDataset(labels[i], datasets[i]); + this.layout_.addDataset(labels[i], points[i]); this.datasetIndex_[i] = dataIdx++; } @@ -2383,9 +2579,8 @@ Dygraph.prototype.drawGraph_ = function() { // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously var tmp_zoomed_x = this.zoomed_x_; // Tell PlotKit to use this new data and render itself - this.layout_.setDateWindow(this.dateWindow_); this.zoomed_x_ = tmp_zoomed_x; - this.layout_.evaluateWithError(); + this.layout_.evaluate(); this.renderGraph_(is_initial_draw); if (this.attr_("timingName")) { @@ -2418,6 +2613,7 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { this.cascadeEvents_('willDrawChart', e); this.plotter_.render(); this.cascadeEvents_('didDrawChart', e); + this.lastRow_ = -1; // because plugins/legend.js clears the legend // TODO(danvk): is this a performance bottleneck when panning? // The interaction canvas should already be empty in that situation. @@ -2472,7 +2668,12 @@ Dygraph.prototype.computeYAxes_ = function() { if (valueWindows !== undefined) { // Restore valueWindow settings. - for (index = 0; index < valueWindows.length; index++) { + + // When going from two axes back to one, we only restore + // one axis. + var idxCount = Math.min(valueWindows.length, this.axes_.length); + + for (index = 0; index < idxCount; index++) { this.axes_[index].valueWindow = valueWindows[index]; } } @@ -2519,20 +2720,44 @@ Dygraph.prototype.axisPropertiesForSeries = function(series) { * This fills in the valueRange and ticks fields in each entry of this.axes_. */ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { - var isNullUndefinedOrNaN = function(num) { return isNaN(parseFloat(num)); }; - var series; var numAxes = this.attributes_.numAxes(); + var ypadCompat, span, series, ypad; + + var p_axis; // Compute extreme values, a span and tick marks for each axis. for (var i = 0; i < numAxes; i++) { var axis = this.axes_[i]; var logscale = this.attributes_.getForAxis("logscale", i); var includeZero = this.attributes_.getForAxis("includeZero", i); + var independentTicks = this.attributes_.getForAxis("independentTicks", i); series = this.attributes_.seriesForAxis(i); + // Add some padding. This supports two Y padding operation modes: + // + // - backwards compatible (yRangePad not set): + // 10% padding for automatic Y ranges, but not for user-supplied + // ranges, and move a close-to-zero edge to zero except if + // avoidMinZero is set, since drawing at the edge results in + // invisible lines. Unfortunately lines drawn at the edge of a + // user-supplied range will still be invisible. If logscale is + // set, add a variable amount of padding at the top but + // none at the bottom. + // + // - new-style (yRangePad set by the user): + // always add the specified Y padding. + // + ypadCompat = true; + ypad = 0.1; // add 10% + if (this.attr_('yRangePad') !== null) { + ypadCompat = false; + // Convert pixel padding to ratio + ypad = this.attr_('yRangePad') / this.plotter_.area.h; + } + if (series.length === 0) { // If no series are defined or visible then use a reasonable default axis.extremeRange = [0, 1]; @@ -2556,35 +2781,49 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { maxY = Math.max(extremeMaxY, maxY); } } - if (includeZero && minY > 0) minY = 0; + + // Include zero if requested by the user. + if (includeZero && !logscale) { + if (minY > 0) minY = 0; + if (maxY < 0) maxY = 0; + } // Ensure we have a valid scale, otherwise default to [0, 1] for safety. if (minY == Infinity) minY = 0; if (maxY == -Infinity) maxY = 1; - // Add some padding and round up to an integer to be human-friendly. - var span = maxY - minY; - // special case: if we have no sense of scale, use +/-10% of the sole value. - if (span === 0) { span = maxY; } + span = maxY - minY; + // special case: if we have no sense of scale, center on the sole value. + if (span === 0) { + if (maxY !== 0) { + span = Math.abs(maxY); + } else { + // ... and if the sole value is zero, use range 0-1. + maxY = 1; + span = 1; + } + } var maxAxisY, minAxisY; if (logscale) { - maxAxisY = maxY + 0.1 * span; - minAxisY = minY; + if (ypadCompat) { + maxAxisY = maxY + ypad * span; + minAxisY = minY; + } else { + var logpad = Math.exp(Math.log(span) * ypad); + maxAxisY = maxY * logpad; + minAxisY = minY / logpad; + } } else { - maxAxisY = maxY + 0.1 * span; - minAxisY = minY - 0.1 * span; + maxAxisY = maxY + ypad * span; + minAxisY = minY - ypad * span; - // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense. - if (!this.attr_("avoidMinZero")) { + // Backwards-compatible behavior: Move the span to start or end at zero if it's + // close to zero, but not if avoidMinZero is set. + if (ypadCompat && !this.attr_("avoidMinZero")) { if (minAxisY < 0 && minY >= 0) minAxisY = 0; if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; } - - if (this.attr_("includeZero")) { - if (maxY < 0) maxAxisY = 0; - if (minY > 0) minAxisY = 0; - } } axis.extremeRange = [minAxisY, maxAxisY]; } @@ -2595,27 +2834,50 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; } else if (axis.valueRange) { // This is a user-set value range for this axis. - axis.computedValueRange = [ - isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0], - isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1] - ]; + var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0]; + var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1]; + if (!ypadCompat) { + if (axis.logscale) { + var logpad = Math.exp(Math.log(span) * ypad); + y0 *= logpad; + y1 /= logpad; + } else { + span = y1 - y0; + y0 -= span * ypad; + y1 += span * ypad; + } + } + axis.computedValueRange = [y0, y1]; } else { axis.computedValueRange = axis.extremeRange; } - - // Add ticks. By default, all axes inherit the tick positions of the - // primary axis. However, if an axis is specifically marked as having - // independent ticks, then that is permissible as well. - var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); - var ticker = opts('ticker'); - if (i === 0 || axis.independentTicks) { + + + if(independentTicks) { + axis.independentTicks = independentTicks; + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); axis.ticks = ticker(axis.computedValueRange[0], - axis.computedValueRange[1], - this.height_, // TODO(danvk): should be area.height - opts, - this); - } else { - var p_axis = this.axes_[0]; + axis.computedValueRange[1], + this.height_, // TODO(danvk): should be area.height + opts, + this); + // Define the first independent axis as primary axis. + if (!p_axis) p_axis = axis; + } + } + if (p_axis === undefined) { + throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated."); + } + // Add ticks. By default, all axes inherit the tick positions of the + // primary axis. However, if an axis is specifically marked as having + // independent ticks, then that is permissible as well. + for (var i = 0; i < numAxes; i++) { + var axis = this.axes_[i]; + + if (!axis.independentTicks) { + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); var p_ticks = p_axis.ticks; var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0]; var scale = axis.computedValueRange[1] - axis.computedValueRange[0]; @@ -2645,21 +2907,43 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { * TODO(danvk): the "missing values" bit above doesn't seem right. * * @private + * @param {Array.)>>} rawData Input data. Rectangular + * grid of points, where rawData[row][0] is the X value for the row, + * and rawData[row][i] is the Y data for series #i. + * @param {number} i Series index, starting from 1. + * @param {boolean} logScale True if using logarithmic Y scale. + * @return {Array.)>} Series array, where + * series[row] = [x,y] or [x, [y, err]] or [x, [y, yplus, yminus]]. */ Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) { // TODO(danvk): pre-allocate series here. var series = []; + var errorBars = this.attr_("errorBars"); + var customBars = this.attr_("customBars"); for (var j = 0; j < rawData.length; j++) { var x = rawData[j][0]; var point = rawData[j][i]; if (logScale) { // On the log scale, points less than zero do not exist. // This will create a gap in the chart. - if (point <= 0) { + if (errorBars || customBars) { + // point.length is either 2 (errorBars) or 3 (customBars) + for (var k = 0; k < point.length; k++) { + if (point[k] <= 0) { + point = null; + break; + } + } + } else if (point <= 0) { point = null; } } - series.push([x, point]); + // Fix null points to fit the display type standard. + if (point !== null) { + series.push([x, point]); + } else { + series.push([x, errorBars ? [null, null] : customBars ? [null, null, null] : point]); + } } return series; }; @@ -2794,7 +3078,10 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { rollingData[i] = [originalData[i][0], [sum / num_ok, sigma * stddev, sigma * stddev]]; } else { - rollingData[i] = [originalData[i][0], [null, null, null]]; + // This explicitly preserves NaNs to aid with "independent series". + // See testRollingAveragePreservesNaNs. + var v = (rollPeriod == 1) ? originalData[i][1][0] : null; + rollingData[i] = [originalData[i][0], [v, v, v]]; } } } @@ -3421,16 +3708,9 @@ Dygraph.prototype.resize = function(width, height) { } if (old_width != this.width_ || old_height != this.height_) { - // TODO(danvk): there should be a clear() method. - this.maindiv_.innerHTML = ""; - this.roller_ = null; - this.attrs_.labelsDiv = null; - this.createInterface_(); - if (this.annotations_.length) { - // createInterface_ reset the layout, so we need to do this. - this.layout_.setAnnotations(this.annotations_); - } - this.createDragInterface_(); + // Resizing a canvas erases it, even when the size doesn't change, so + // any resize needs to be followed by a redraw. + this.resizeElements_(); this.predraw_(); } @@ -3496,6 +3776,13 @@ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { // Only add the annotation CSS rule once we know it will be used. Dygraph.addAnnotationRule(); this.annotations_ = ann; + if (!this.layout_) { + this.warn("Tried to setAnnotations before dygraph was ready. " + + "Try setting them in a drawCallback. See " + + "dygraphs.com/tests/annotation.html"); + return; + } + this.layout_.setAnnotations(this.annotations_); if (!suppressDraw) { this.predraw_();