X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=6d970bb20dffd5083d940b2e9627fc8ed9bbe807;hb=d569e3a08324168e501e6ba1402273acdae554f9;hp=65eecb8d33bca13d00534eafa0e9af895ebe9749;hpb=680598af95e5627feb5951a8dbf9daecb15c8685;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 65eecb8..6d970bb 100644 --- a/dygraph.js +++ b/dygraph.js @@ -44,7 +44,7 @@ */ /*jshint globalstrict: true */ -/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false */ +/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */ "use strict"; /** @@ -62,6 +62,12 @@ * options, see http://dygraphs.com/options.html. */ var Dygraph = function(div, data, opts, opt_fourth_param) { + // These have to go above the "Hack for IE" in __init__ since .ready() can be + // called as soon as the constructor returns. Once support for OldIE is + // dropped, this can go down with the rest of the initializers. + this.is_initial_draw_ = true; + this.readyFns_ = []; + if (opt_fourth_param !== undefined) { // Old versions of dygraphs took in the series labels as a constructor // parameter. This doesn't make sense anymore, but it's easy to continue @@ -74,7 +80,7 @@ var Dygraph = function(div, data, opts, opt_fourth_param) { }; Dygraph.NAME = "Dygraph"; -Dygraph.VERSION = "1.2"; +Dygraph.VERSION = "1.0.1"; Dygraph.__repr__ = function() { return "[" + this.NAME + " " + this.VERSION + "]"; }; @@ -291,6 +297,7 @@ Dygraph.DEFAULT_ATTRS = { connectSeparatedPoints: false, stackedGraph: false, + stackedGraphNaNFill: 'all', hideOverlayOnMouseOut: true, // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms. @@ -345,6 +352,7 @@ Dygraph.DEFAULT_ATTRS = { axisLabelFormatter: Dygraph.dateAxisFormatter, valueFormatter: Dygraph.dateString_, drawGrid: true, + drawAxis: true, independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, @@ -353,6 +361,7 @@ Dygraph.DEFAULT_ATTRS = { valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, drawGrid: true, + drawAxis: true, independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, @@ -360,6 +369,7 @@ Dygraph.DEFAULT_ATTRS = { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawAxis: false, drawGrid: false, independentTicks: false, ticker: null // will be set in dygraph-tickers.js @@ -438,7 +448,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; - this.is_initial_draw_ = true; this.annotations_ = []; // Zoomed indicators - These indicate when the graph has been zoomed and on what axis. @@ -465,9 +474,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) { @@ -849,7 +860,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { var yRange = this.yAxisRange(axis); if (typeof(axis) == "undefined") axis = 0; - if (!this.axes_[axis].logscale) { + if (!this.attributes_.getForAxis("logscale", axis)) { return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]); } else { // Computing the inverse of toDomCoord. @@ -985,8 +996,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); @@ -994,15 +1004,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. @@ -1031,8 +1039,8 @@ Dygraph.prototype.createInterface_ = function() { } }; - 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. @@ -1043,16 +1051,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); @@ -1060,19 +1084,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_); @@ -1148,28 +1164,32 @@ Dygraph.prototype.setColors_ = function() { var num = labels.length - 1; this.colors_ = []; this.colorsMap_ = {}; + + // These are used for when no custom colors are specified. + var sat = this.attr_('colorSaturation') || 1.0; + var val = this.attr_('colorValue') || 0.5; + var half = Math.ceil(num / 2); + var colors = this.attr_('colors'); - var i; - if (!colors) { - var sat = this.attr_('colorSaturation') || 1.0; - var val = this.attr_('colorValue') || 0.5; - var half = Math.ceil(num / 2); - for (i = 1; i <= num; i++) { - if (!this.visibility()[i-1]) continue; - // alternate colors for high contrast. - var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2); - var hue = (1.0 * idx/ (1 + num)); - var colorStr = Dygraph.hsvToRGB(hue, sat, val); - this.colors_.push(colorStr); - this.colorsMap_[labels[i]] = colorStr; + var visibility = this.visibility(); + for (var i = 0; i < num; i++) { + if (!visibility[i]) { + continue; } - } else { - for (i = 0; i < num; i++) { - if (!this.visibility()[i]) continue; - var colorStr = colors[i % colors.length]; - this.colors_.push(colorStr); - this.colorsMap_[labels[1 + i]] = colorStr; + var label = labels[i + 1]; + var colorStr = this.attributes_.getForSeries('color', label); + if (!colorStr) { + if (colors) { + colorStr = colors[i % colors.length]; + } else { + // alternate colors for high contrast. + var idx = i % 2 ? (half + (i + 1)/ 2) : Math.ceil((i + 1) / 2); + var hue = (1.0 * idx / (1 + num)); + colorStr = Dygraph.hsvToRGB(hue, sat, val); + } } + this.colors_.push(colorStr); + this.colorsMap_[label] = colorStr; } }; @@ -1344,19 +1364,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; @@ -1376,7 +1390,7 @@ Dygraph.prototype.createDragInterface_ = function() { context.tarp.uncover(); }; - this.addEvent(document, 'mouseup', this.mouseUpHandler_); + this.addAndTrackEvent(document, 'mouseup', mouseUpHandler); }; /** @@ -1410,7 +1424,7 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, if (prevDirection == Dygraph.HORIZONTAL) { ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y, Math.abs(startX - prevEndX), this.layout_.getPlotArea().h); - } else if (prevDirection == Dygraph.VERTICAL){ + } else if (prevDirection == Dygraph.VERTICAL) { ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY), this.layout_.getPlotArea().w, Math.abs(startY - prevEndY)); } @@ -1583,7 +1597,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 @@ -1695,7 +1709,7 @@ Dygraph.prototype.eventToDomCoords = function(event) { */ Dygraph.prototype.findClosestRow = function(domX) { var minDistX = Infinity; - var pointIdx = -1, setIdx = -1; + var closestRow = -1; var sets = this.layout_.points; for (var i = 0; i < sets.length; i++) { var points = sets[i]; @@ -1706,14 +1720,12 @@ Dygraph.prototype.findClosestRow = function(domX) { var dist = Math.abs(point.canvasx - domX); if (dist < minDistX) { minDistX = dist; - setIdx = i; - pointIdx = j; + closestRow = point.idx; } } } - // TODO(danvk): remove this function; it's trivial and has only one use. - return this.idxToRow_(setIdx, pointIdx); + return closestRow; }; /** @@ -1730,12 +1742,11 @@ Dygraph.prototype.findClosestRow = function(domX) { */ 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 ) { + var dist, dx, dy, point, closestPoint, closestSeries, closestRow; + 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]; + point = points[i]; if (!Dygraph.isValidPoint(point)) continue; dx = point.canvasx - domX; dy = point.canvasy - domY; @@ -1744,13 +1755,13 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { minDist = dist; closestPoint = point; closestSeries = setIdx; - idx = i; + closestRow = point.idx; } } } var name = this.layout_.setNames[closestSeries]; return { - row: idx + this.getLeftBoundary_(), + row: closestRow, seriesName: name, point: closestPoint }; @@ -1770,10 +1781,10 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { */ Dygraph.prototype.findStackedPoint = function(domX, domY) { var row = this.findClosestRow(domX); - 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 boundary = this.getLeftBoundary_(setIdx); + var rowIdx = row - boundary; var points = this.layout_.points[setIdx]; if (rowIdx >= points.length) continue; var p1 = points[rowIdx]; @@ -1850,43 +1861,27 @@ Dygraph.prototype.mouseMove_ = function(event) { callback(event, this.lastx_, this.selPoints_, - this.lastRow_ + this.getLeftBoundary_(), + this.lastRow_, this.highlightSet_); } }; /** - * Fetch left offset from first defined boundaryIds record (see bug #236). + * Fetch left offset from the specified set index or if not passed, the + * first defined boundaryIds record (see bug #236). * @private */ -Dygraph.prototype.getLeftBoundary_ = function() { - for (var i = 0; i < this.boundaryIds_.length; i++) { - if (this.boundaryIds_[i] !== undefined) { - return this.boundaryIds_[i][0]; +Dygraph.prototype.getLeftBoundary_ = function(setIdx) { + if (this.boundaryIds_[setIdx]) { + return this.boundaryIds_[setIdx][0]; + } else { + for (var i = 0; i < this.boundaryIds_.length; i++) { + if (this.boundaryIds_[i] !== undefined) { + return this.boundaryIds_[i][0]; + } } + return 0; } - return 0; -}; - -/** - * Transforms layout_.points index into data row number. - * @param int layout_.points index - * @return int row number, or -1 if none could be found. - * @private - */ -Dygraph.prototype.idxToRow_ = function(setIdx, rowIdx) { - if (rowIdx < 0) return -1; - - 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) { @@ -2018,23 +2013,15 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) { // Extract the points we've selected this.selPoints_ = []; - if (row !== false) { - row -= this.getLeftBoundary_(); - } - var changed = false; 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]; + var setRow = row - this.getLeftBoundary_(setIdx); + if (setRow < points.length) { + var point = points[setRow]; if (point.yval !== null) this.selPoints_.push(point); } } @@ -2114,7 +2101,7 @@ Dygraph.prototype.getSelection = function() { var points = this.layout_.points[setIdx]; for (var row = 0; row < points.length; row++) { if (points[row].x == this.selPoints_[0].x) { - return row + this.getLeftBoundary_(); + return points[row].idx; } } } @@ -2173,46 +2160,27 @@ Dygraph.prototype.addXTicks_ = function() { }; /** + * Returns the correct handler class for the currently set options. * @private - * Computes the range of the data series (including confidence intervals). - * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or - * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ... - * @return [low, high] - */ -Dygraph.prototype.extremeValues_ = function(series) { - var minY = null, maxY = null, j, y; - - var bars = this.attr_("errorBars") || this.attr_("customBars"); - if (bars) { - // With custom bars, maxY is the max of the high values. - for (j = 0; j < series.length; j++) { - y = series[j][1][0]; - if (y === null || isNaN(y)) continue; - var low = y - series[j][1][1]; - var high = y + series[j][1][2]; - if (low > y) low = y; // this can happen with custom bars, - if (high < y) high = y; // e.g. in tests/custom-bars.html - if (maxY === null || high > maxY) { - maxY = high; - } - if (minY === null || low < minY) { - minY = low; - } + */ +Dygraph.prototype.getHandlerClass_ = function() { + var handlerClass; + if (this.attr_('dataHandler')) { + handlerClass = this.attr_('dataHandler'); + } else if (this.fractions_) { + if (this.attr_('errorBars')) { + handlerClass = Dygraph.DataHandlers.FractionsBarsHandler; + } else { + handlerClass = Dygraph.DataHandlers.DefaultFractionHandler; } + } else if (this.attr_('customBars')) { + handlerClass = Dygraph.DataHandlers.CustomBarsHandler; + } else if (this.attr_('errorBars')) { + handlerClass = Dygraph.DataHandlers.ErrorBarsHandler; } else { - for (j = 0; j < series.length; j++) { - y = series[j][1]; - if (y === null || isNaN(y)) continue; - if (maxY === null || y > maxY) { - maxY = y; - } - if (minY === null || y < minY) { - minY = y; - } - } + handlerClass = Dygraph.DataHandlers.DefaultHandler; } - - return [minY, maxY]; + return handlerClass; }; /** @@ -2225,6 +2193,9 @@ Dygraph.prototype.extremeValues_ = function(series) { */ Dygraph.prototype.predraw_ = function() { var start = new Date(); + + // Create the correct dataHandler + this.dataHandler_ = new (this.getHandlerClass_())(); this.layout_.computePlotArea(); @@ -2236,6 +2207,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_, @@ -2252,9 +2232,11 @@ Dygraph.prototype.predraw_ = function() { this.rolledSeries_ = [null]; // x-axis is the first series and it's special for (var i = 1; i < this.numColumns(); i++) { // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too. - var logScale = this.attr_('logscale'); - var series = this.extractSeries_(this.rawData_, i, logScale); - series = this.rollingAverage(series, this.rollPeriod_); + var series = this.dataHandler_.extractSeries(this.rawData_, i, this.attributes_); + if (this.rollPeriod_ > 1) { + series = this.dataHandler_.rollingAverage(series, this.rollPeriod_, this.attributes_); + } + this.rolledSeries_.push(series); } @@ -2267,6 +2249,122 @@ Dygraph.prototype.predraw_ = function() { }; /** + * 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; + +/** + * 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'. + * @private + */ +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. * @@ -2274,59 +2372,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 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; - } - }; - + var seriesIdx, sampleIdx; + var firstIdx, lastIdx; + // 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; - 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]); - } + var series; + for (seriesIdx = num_series; seriesIdx >= 1; seriesIdx--) { + if (!this.visibility()[seriesIdx - 1]) continue; // 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. if (dateWindow) { + series = rolledSeries[seriesIdx]; 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; - for (k = 0; k < series.length; k++) { - if (series[k][0] >= low && firstIdx === null) { - firstIdx = k; + firstIdx = null; + lastIdx = null; + for (sampleIdx = 0; sampleIdx < series.length; sampleIdx++) { + if (series[sampleIdx][0] >= low && firstIdx === null) { + firstIdx = sampleIdx; } - if (series[k][0] <= high) { - lastIdx = k; + if (series[sampleIdx][0] <= high) { + lastIdx = sampleIdx; } } @@ -2335,7 +2424,8 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { var isInvalidValue = true; while (isInvalidValue && correctedFirstIdx > 0) { correctedFirstIdx--; - isInvalidValue = isValueNull(series[correctedFirstIdx]); + // check if the y value is null. + isInvalidValue = series[correctedFirstIdx][1] === null; } if (lastIdx === null) lastIdx = series.length - 1; @@ -2343,97 +2433,42 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { isInvalidValue = true; while (isInvalidValue && correctedLastIdx < series.length - 1) { correctedLastIdx++; - isInvalidValue = isValueNull(series[correctedLastIdx]); + isInvalidValue = series[correctedLastIdx][1] === null; } - boundaryIds[i-1] = [(firstIdx > 0) ? firstIdx - 1 : firstIdx, - (lastIdx < series.length - 1) ? lastIdx + 1 : lastIdx]; - if (correctedFirstIdx!==firstIdx) { - pruned.push(series[correctedFirstIdx]); - } - for (k = firstIdx; k <= lastIdx; k++) { - pruned.push(series[k]); + firstIdx = correctedFirstIdx; } if (correctedLastIdx !== lastIdx) { - pruned.push(series[correctedLastIdx]); + lastIdx = correctedLastIdx; } - - series = pruned; + + boundaryIds[seriesIdx-1] = [firstIdx, lastIdx]; + + // .slice's end is exclusive, we want to include lastIdx. + series = series.slice(firstIdx, lastIdx + 1); } else { - boundaryIds[i-1] = [0, series.length-1]; + series = rolledSeries[seriesIdx]; + boundaryIds[seriesIdx-1] = [0, series.length-1]; } - 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[seriesIdx] = seriesPoints; } - return [ datasets, extremes, boundaryIds ]; + return { points: points, extremes: extremes, boundaryIds: boundaryIds }; }; /** @@ -2455,9 +2490,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"); @@ -2465,10 +2500,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++; } @@ -2480,9 +2515,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")) { @@ -2525,6 +2559,13 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { if (this.attr_("drawCallback") !== null) { this.attr_("drawCallback")(this, is_initial_draw); } + if (is_initial_draw) { + this.readyFired_ = true; + while (this.readyFns_.length > 0) { + var fn = this.readyFns_.pop(); + fn(this); + } + } }; /** @@ -2755,7 +2796,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { } - if(independentTicks) { + if (independentTicks) { axis.independentTicks = independentTicks; var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); var ticker = opts('ticker'); @@ -2801,192 +2842,6 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { }; /** - * Extracts one series from the raw data (a 2D array) into an array of (date, - * value) tuples. - * - * This is where undesirable points (i.e. negative values on log scales and - * missing values through which we wish to connect lines) are dropped. - * TODO(danvk): the "missing values" bit above doesn't seem right. - * - * @private - */ -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 (errorBars || customBars) { - for (var k = 0; k < point.length; k++) { - // point.length is either 2 (errorBars) or 3 (customBars) - if (point[k] <= 0) { - point = null; - break; - } - } - } - else if (point <= 0) { - point = null; - } - } - // 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; -}; - -/** - * @private - * Calculates the rolling average of a data set. - * If originalData is [label, val], rolls the average of those. - * If originalData is [label, [, it's interpreted as [value, stddev] - * and the roll is returned in the same form, with appropriately reduced - * stddev for each value. - * 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 points over which to average the - * data - */ -Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { - rollPeriod = Math.min(rollPeriod, originalData.length); - var rollingData = []; - var sigma = this.attr_("sigma"); - - var low, high, i, j, y, sum, num_ok, stddev; - if (this.fractions_) { - var num = 0; - var den = 0; // numerator/denominator - var mult = 100.0; - for (i = 0; i < originalData.length; i++) { - num += originalData[i][1][0]; - den += originalData[i][1][1]; - if (i - rollPeriod >= 0) { - num -= originalData[i - rollPeriod][1][0]; - den -= originalData[i - rollPeriod][1][1]; - } - - var date = originalData[i][0]; - var value = den ? num / den : 0.0; - if (this.attr_("errorBars")) { - if (this.attr_("wilsonInterval")) { - // For more details on this confidence interval, see: - // http://en.wikipedia.org/wiki/Binomial_confidence_interval - if (den) { - var p = value < 0 ? 0 : value, n = den; - var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n)); - var denom = 1 + sigma * sigma / den; - low = (p + sigma * sigma / (2 * den) - pm) / denom; - high = (p + sigma * sigma / (2 * den) + pm) / denom; - rollingData[i] = [date, - [p * mult, (p - low) * mult, (high - p) * mult]]; - } else { - rollingData[i] = [date, [0, 0, 0]]; - } - } else { - stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0; - rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]]; - } - } else { - rollingData[i] = [date, mult * value]; - } - } - } else if (this.attr_("customBars")) { - low = 0; - var mid = 0; - high = 0; - var count = 0; - for (i = 0; i < originalData.length; i++) { - var data = originalData[i][1]; - y = data[1]; - rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]]; - - if (y !== null && !isNaN(y)) { - low += data[0]; - mid += y; - high += data[2]; - count += 1; - } - if (i - rollPeriod >= 0) { - var prev = originalData[i - rollPeriod]; - if (prev[1][1] !== null && !isNaN(prev[1][1])) { - low -= prev[1][0]; - mid -= prev[1][1]; - high -= prev[1][2]; - count -= 1; - } - } - if (count) { - rollingData[i] = [originalData[i][0], [ 1.0 * mid / count, - 1.0 * (mid - low) / count, - 1.0 * (high - mid) / count ]]; - } else { - rollingData[i] = [originalData[i][0], [null, null, null]]; - } - } - } else { - // Calculate the rolling average for the first rollPeriod - 1 points where - // there is not enough data to roll over the full number of points - if (!this.attr_("errorBars")){ - if (rollPeriod == 1) { - return originalData; - } - - for (i = 0; i < originalData.length; i++) { - sum = 0; - num_ok = 0; - for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { - y = originalData[j][1]; - if (y === null || isNaN(y)) continue; - num_ok++; - sum += originalData[j][1]; - } - if (num_ok) { - rollingData[i] = [originalData[i][0], sum / num_ok]; - } else { - rollingData[i] = [originalData[i][0], null]; - } - } - - } else { - for (i = 0; i < originalData.length; i++) { - sum = 0; - var variance = 0; - num_ok = 0; - for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { - y = originalData[j][1][0]; - if (y === null || isNaN(y)) continue; - num_ok++; - sum += originalData[j][1][0]; - variance += Math.pow(originalData[j][1][1], 2); - } - if (num_ok) { - stddev = Math.sqrt(variance) / num_ok; - rollingData[i] = [originalData[i][0], - [sum / num_ok, sigma * stddev, sigma * stddev]]; - } else { - // 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]]; - } - } - } - } - - return rollingData; -}; - -/** * Detects the type of the str (date or numeric) and sets the various * formatting attributes in this.attrs_ based on this type. * @param {String} str An x value. @@ -3441,7 +3296,16 @@ Dygraph.prototype.start_ = function() { if (line_delimiter) { this.loadedEvent_(data); } else { - var req = new XMLHttpRequest(); + // REMOVE_FOR_IE + var req; + if (window.XMLHttpRequest) { + // Firefox, Opera, IE7, and other browsers will use the native object + req = new XMLHttpRequest(); + } else { + // IE 5 and 6 will use the ActiveX control + req = new ActiveXObject("Microsoft.XMLHTTP"); + } + var caller = this; req.onreadystatechange = function () { if (req.readyState == 4) { @@ -3564,6 +3428,10 @@ Dygraph.mapLegacyOptions_ = function(attrs) { map('pixelsPerYLabel', 'y', 'pixelsPerLabel'); map('yAxisLabelFormatter', 'y', 'axisLabelFormatter'); map('yTicker', 'y', 'ticker'); + map('drawXGrid', 'x', 'drawGrid'); + map('drawXAxis', 'x', 'drawAxis'); + map('drawYGrid', 'y', 'drawGrid'); + map('drawYAxis', 'y', 'drawAxis'); return my_attrs; }; @@ -3604,16 +3472,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_(); } @@ -3648,6 +3509,9 @@ Dygraph.prototype.visibility = function() { /** * Changes the visiblity of a series. + * + * @param {Number} num the series index + * @param {Boolean} value true or false, identifying the visibility. */ Dygraph.prototype.setVisibility = function(num, value) { var x = this.visibility(); @@ -3681,7 +3545,7 @@ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { this.annotations_ = ann; if (!this.layout_) { this.warn("Tried to setAnnotations before dygraph was ready. " + - "Try setting them in a drawCallback. See " + + "Try setting them in a ready() block. See " + "dygraphs.com/tests/annotation.html"); return; } @@ -3719,12 +3583,23 @@ Dygraph.prototype.indexFromSetName = function(name) { }; /** - * Get the internal dataset index given its name. These are numbered starting from 0, - * and only count visible sets. - * @private + * Trigger a callback when the dygraph has drawn itself and is ready to be + * manipulated. This is primarily useful when dygraphs has to do an XHR for the + * data (i.e. a URL is passed as the data source) and the chart is drawn + * asynchronously. If the chart has already drawn, the callback will fire + * immediately. + * + * This is a good place to call setAnnotation(). + * + * @param {function(!Dygraph)} callback The callback to trigger when the chart + * is ready. */ -Dygraph.prototype.datasetIndexFromSetName_ = function(name) { - return this.datasetIndex_[this.indexFromSetName(name)]; +Dygraph.prototype.ready = function(callback) { + if (this.is_initial_draw_) { + this.readyFns_.push(callback); + } else { + callback(this); + } }; /** @@ -3768,6 +3643,3 @@ Dygraph.addAnnotationRule = function() { this.warn("Unable to add default annotation CSS rule; display may be off."); }; - -// Older pages may still use this name. -var DateGraph = Dygraph;