From: Dan Vanderkam Date: Thu, 29 Aug 2013 20:34:26 +0000 (-0400) Subject: Merge branch 'release-1.0.1' X-Git-Tag: v1.1.0~94 X-Git-Url: https://adrianiainlam.tk/git/?a=commitdiff_plain;h=3b3b39e7140cf45dfdc95f636978c5353e3a6c38;hp=c8e018631f692f84d90e827465a0046d33c7836e;p=dygraphs.git Merge branch 'release-1.0.1' --- diff --git a/auto_tests/tests/error_bars.js b/auto_tests/tests/error_bars.js index 1e9d0f4..1c3894c 100644 --- a/auto_tests/tests/error_bars.js +++ b/auto_tests/tests/error_bars.js @@ -135,8 +135,8 @@ errorBarsTestCase.prototype.testErrorBarsCorrectColors = function() { // Regression test for http://code.google.com/p/dygraphs/issues/detail?id=392 errorBarsTestCase.prototype.testRollingAveragePreservesNaNs = function() { var graph = document.getElementById("graph"); - var g = new Dygraph(graph, - [ + var data = + [ [1, [null, null], [3,1]], [2, [2, 1], [null, null]], [3, [null, null], [5,1]], @@ -145,8 +145,9 @@ errorBarsTestCase.prototype.testRollingAveragePreservesNaNs = function() { [6, [NaN, NaN], [null, null]], [8, [8, 1], [null, null]], [10, [10, 1], [null, null]] - ] - , { + ]; + var g = new Dygraph(graph, data, + { labels: ['x', 'A', 'B' ], connectSeparatedPoints: true, drawPoints: true, @@ -154,26 +155,20 @@ errorBarsTestCase.prototype.testRollingAveragePreservesNaNs = function() { } ); - var in_series = [ - [1, [null, null]], - [2, [2, 1]], - [3, [null, null]], - [4, [4, 0.5]], - [5, [null, null]], - [6, [NaN, NaN]], - [8, [8, 1]], - [10, [10, 1]] - ]; - assertEquals(null, in_series[4][1][0]); - assertEquals(null, in_series[4][1][1]); - assertNaN(in_series[5][1][0]); - assertNaN(in_series[5][1][1]); - - var out_series = g.rollingAverage(in_series, 1); - assertNaN(out_series[5][1][0]); - assertNaN(out_series[5][1][1]); - assertNaN(out_series[5][1][2]); - assertEquals(null, out_series[4][1][0]); - assertEquals(null, out_series[4][1][1]); - assertEquals(null, out_series[4][1][1]); + var in_series = g.dataHandler_.extractSeries(data, 1, g.attributes_); + + assertEquals(null, in_series[4][1]); + assertEquals(null, in_series[4][2][0]); + assertEquals(null, in_series[4][2][1]); + assertNaN(in_series[5][1]); + assertNaN(in_series[5][2][0]); + assertNaN(in_series[5][2][1]); + + var out_series = g.dataHandler_.rollingAverage(in_series, 1, g.attributes_); + assertNaN(out_series[5][1]); + assertNaN(out_series[5][2][0]); + assertNaN(out_series[5][2][1]); + assertEquals(null, out_series[4][1]); + assertEquals(null, out_series[4][2][0]); + assertEquals(null, out_series[4][2][1]); }; diff --git a/auto_tests/tests/rolling_average.js b/auto_tests/tests/rolling_average.js index c0af8f9..86b3e3c 100644 --- a/auto_tests/tests/rolling_average.js +++ b/auto_tests/tests/rolling_average.js @@ -87,16 +87,150 @@ rollingAverageTestCase.prototype.testRollShortFractions = function() { customBars: true, labels: ['x', 'A'] }; - var data1 = [ [1, [1, 10, 20]] ]; - var data2 = [ [1, [1, 10, 20]], - [2, [1, 20, 30]], + var data1 = [ [1, 10, [1, 20]] ]; + var data2 = [ [1, 10, [1, 20]], + [2, 20, [1, 30]], ]; var graph = document.getElementById("graph"); - var g = new Dygraph(graph, data1, opts); + var g = new Dygraph(graph, data2, opts); - var rolled1 = g.rollingAverage(data1, 1); - var rolled2 = g.rollingAverage(data2, 1); + var rolled1 = g.dataHandler_.rollingAverage(data1, 1, g); + var rolled2 = g.dataHandler_.rollingAverage(data2, 1, g); assertEquals(rolled1[0], rolled2[0]); }; + +rollingAverageTestCase.prototype.testRollCustomBars = function() { + var opts = { + customBars: true, + rollPeriod: 2, + labels: ['x', 'A'] + }; + var data = [ [1, [1, 10, 20]], + [2, [1, 20, 30]], + [3, [1, 30, 40]], + [4, [1, 40, 50]] + ]; + + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + var rolled = this.getRolledData(g, data, 1, 2); + assertEquals([1, 10, [1, 20]], rolled[0]); + assertEquals([2, 15, [1, 25]], rolled[1]); + assertEquals([3, 25, [1, 35]], rolled[2]); + assertEquals([4, 35, [1, 45]], rolled[3]); +}; + +rollingAverageTestCase.prototype.testRollErrorBars = function() { + var opts = { + errorBars: true, + rollPeriod: 2, + labels: ['x', 'A'] + }; + var data = [ [1, [10, 1]], + [2, [20, 1]], + [3, [30, 1]], + [4, [40, 1]] + ]; + + var graph = document.getElementById("graph"); + var g = new Dygraph(graph, data, opts); + var rolled = this.getRolledData(g, data, 1, 2); + assertEquals([1, 10, [8, 12]], rolled[0]); + + // variance = sqrt( pow(error) * rollPeriod) + var variance = Math.sqrt(2); + for (var i=1;i= 0) { + var prev = originalData[i - rollPeriod]; + if (prev[1] !== null && !isNaN(prev[1])) { + low -= prev[2][0]; + mid -= prev[1]; + high -= prev[2][1]; + count -= 1; + } + } + if (count) { + rollingData[i] = [ + originalData[i][0], + 1.0 * mid / count, + [ 1.0 * low / count, + 1.0 * high / count ] ]; + } else { + rollingData[i] = [ originalData[i][0], null, [ null, null ] ]; + } + } + + return rollingData; +}; + +})(); diff --git a/datahandler/bars-error.js b/datahandler/bars-error.js new file mode 100644 index 0000000..6da2fc5 --- /dev/null +++ b/datahandler/bars-error.js @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview DataHandler implementation for the error bars option. + * @author David Eberlein (david.eberlein@ch.sauter-bc.com) + */ + +(function() { + +/*global Dygraph:false */ +"use strict"; + +Dygraph.DataHandlers.ErrorBarsHandler = Dygraph.DataHandler(); +var ErrorBarsHandler = Dygraph.DataHandlers.ErrorBarsHandler; +ErrorBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler(); + +// errorBars +ErrorBarsHandler.prototype.extractSeries = function(rawData, i, options) { + // TODO(danvk): pre-allocate series here. + var series = []; + var x, y, variance, point; + var sigma = options.get("sigma"); + var logScale = options.get('logscale'); + for ( var j = 0; j < rawData.length; j++) { + x = rawData[j][0]; + point = rawData[j][i]; + if (logScale && point !== null) { + // On the log scale, points less than zero do not exist. + // This will create a gap in the chart. + if (point[0] <= 0 || point[0] - sigma * point[1] <= 0) { + point = null; + } + } + // Extract to the unified data format. + if (point !== null) { + y = point[0]; + if (y !== null && !isNaN(y)) { + variance = sigma * point[1]; + // preserve original error value in extras for further + // filtering + series.push([ x, y, [ y - variance, y + variance, point[1] ] ]); + } else { + series.push([ x, y, [ y, y, y ] ]); + } + } else { + series.push([ x, null, [ null, null, null ] ]); + } + } + return series; +}; + +ErrorBarsHandler.prototype.rollingAverage = function(originalData, rollPeriod, + options) { + rollPeriod = Math.min(rollPeriod, originalData.length); + var rollingData = []; + var sigma = options.get("sigma"); + + var i, j, y, v, sum, num_ok, stddev, variance, value; + + // Calculate the rolling average for the first rollPeriod - 1 points + // where there is not enough data to roll over the full number of points + for (i = 0; i < originalData.length; i++) { + sum = 0; + variance = 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 += y; + variance += Math.pow(originalData[j][2][2], 2); + } + if (num_ok) { + stddev = Math.sqrt(variance) / num_ok; + value = sum / num_ok; + rollingData[i] = [ originalData[i][0], value, + [value - sigma * stddev, value + sigma * stddev] ]; + } else { + // This explicitly preserves NaNs to aid with "independent + // series". + // See testRollingAveragePreservesNaNs. + v = (rollPeriod == 1) ? originalData[i][1] : null; + rollingData[i] = [ originalData[i][0], v, [ v, v ] ]; + } + } + + return rollingData; +}; + +})(); diff --git a/datahandler/bars-fractions.js b/datahandler/bars-fractions.js new file mode 100644 index 0000000..28c0491 --- /dev/null +++ b/datahandler/bars-fractions.js @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview DataHandler implementation for the combination + * of error bars and fractions options. + * @author David Eberlein (david.eberlein@ch.sauter-bc.com) + */ + +(function() { + +/*global Dygraph:false */ +"use strict"; + +Dygraph.DataHandlers.FractionsBarsHandler = Dygraph.DataHandler(); +var FractionsBarsHandler = Dygraph.DataHandlers.FractionsBarsHandler; +FractionsBarsHandler.prototype = new Dygraph.DataHandlers.BarsHandler(); + +// errorBars +FractionsBarsHandler.prototype.extractSeries = function(rawData, i, options) { + // TODO(danvk): pre-allocate series here. + var series = []; + var x, y, point, num, den, value, stddev, variance; + var mult = 100.0; + var sigma = options.get("sigma"); + var logScale = options.get('logscale'); + for ( var j = 0; j < rawData.length; j++) { + x = rawData[j][0]; + point = rawData[j][i]; + if (logScale && point !== null) { + // On the log scale, points less than zero do not exist. + // This will create a gap in the chart. + if (point[0] <= 0 || point[1] <= 0) { + point = null; + } + } + // Extract to the unified data format. + if (point !== null) { + num = point[0]; + den = point[1]; + if (num !== null && !isNaN(num)) { + value = den ? num / den : 0.0; + stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0; + variance = mult * stddev; + y = mult * value; + // preserve original values in extras for further filtering + series.push([ x, y, [ y - variance, y + variance, num, den ] ]); + } else { + series.push([ x, num, [ num, num, num, den ] ]); + } + } else { + series.push([ x, null, [ null, null, null, null ] ]); + } + } + return series; +}; + +FractionsBarsHandler.prototype.rollingAverage = function(originalData, rollPeriod, + options) { + rollPeriod = Math.min(rollPeriod, originalData.length); + var rollingData = []; + var sigma = options.get("sigma"); + var wilsonInterval = options.get("wilsonInterval"); + + var low, high, i, stddev; + var num = 0; + var den = 0; // numerator/denominator + var mult = 100.0; + for (i = 0; i < originalData.length; i++) { + num += originalData[i][2][2]; + den += originalData[i][2][3]; + if (i - rollPeriod >= 0) { + num -= originalData[i - rollPeriod][2][2]; + den -= originalData[i - rollPeriod][2][3]; + } + + var date = originalData[i][0]; + var value = den ? num / den : 0.0; + if (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, + [ low * mult, high * 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 * (value - stddev), mult * (value + stddev) ] ]; + } + } + + return rollingData; +}; + +})(); diff --git a/datahandler/bars.js b/datahandler/bars.js new file mode 100644 index 0000000..914ee27 --- /dev/null +++ b/datahandler/bars.js @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview DataHandler base implementation for the "bar" + * data formats. This implementation must be extended and the + * extractSeries and rollingAverage must be implemented. + * @author David Eberlein (david.eberlein@ch.sauter-bc.com) + */ + +(function() { + +/*global Dygraph:false */ +/*global DygraphLayout:false */ +"use strict"; + +Dygraph.DataHandlers.BarsHandler = Dygraph.DataHandler(); +var BarsHandler = Dygraph.DataHandlers.BarsHandler; + +// errorBars +BarsHandler.prototype.extractSeries = function(rawData, i, options) { + // Not implemented here must be extended +}; + +BarsHandler.prototype.rollingAverage = + function(originalData, rollPeriod, options) { + // Not implemented here, must be extended. +}; + +BarsHandler.prototype.onPointsCreated_ = function(series, points) { + for (var i = 0; i < series.length; ++i) { + var item = series[i]; + var point = points[i]; + point.y_top = NaN; + point.y_bottom = NaN; + point.yval_minus = DygraphLayout.parseFloat_(item[2][0]); + point.yval_plus = DygraphLayout.parseFloat_(item[2][1]); + } +}; + +BarsHandler.prototype.getExtremeYValues = function(series, dateWindow, options) { + var minY = null, maxY = null, y; + + var firstIdx = 0; + var lastIdx = series.length - 1; + + for ( var j = firstIdx; j <= lastIdx; j++) { + y = series[j][1]; + if (y === null || isNaN(y)) continue; + + var low = series[j][2][0]; + var high = series[j][2][1]; + + 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; + } + + return [ minY, maxY ]; +}; + +BarsHandler.prototype.onLineEvaluated = function(points, axis, logscale) { + var point; + for (var j = 0; j < points.length; j++) { + // Copy over the error terms + point = points[j]; + point.y_top = DygraphLayout._calcYNormal(axis, point.yval_minus, logscale); + point.y_bottom = DygraphLayout._calcYNormal(axis, point.yval_plus, logscale); + } +}; + +})(); diff --git a/datahandler/datahandler.js b/datahandler/datahandler.js new file mode 100644 index 0000000..5ed0112 --- /dev/null +++ b/datahandler/datahandler.js @@ -0,0 +1,263 @@ +/** + * @license + * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview This file contains the managment of data handlers + * @author David Eberlein (david.eberlein@ch.sauter-bc.com) + * + * The idea is to define a common, generic data format that works for all data + * structures supported by dygraphs. To make this possible, the DataHandler + * interface is introduced. This makes it possible, that dygraph itself can work + * with the same logic for every data type independent of the actual format and + * the DataHandler takes care of the data format specific jobs. + * DataHandlers are implemented for all data types supported by Dygraphs and + * return Dygraphs compliant formats. + * By default the correct DataHandler is chosen based on the options set. + * Optionally the user may use his own DataHandler (similar to the plugin + * system). + * + * + * The unified data format returend by each handler is defined as so: + * series[n][point] = [x,y,(extras)] + * + * This format contains the common basis that is needed to draw a simple line + * series extended by optional extras for more complex graphing types. It + * contains a primitive x value as first array entry, a primitive y value as + * second array entry and an optional extras object for additional data needed. + * + * x must always be a number. + * y must always be a number, NaN of type number or null. + * extras is optional and must be interpreted by the DataHandler. It may be of + * any type. + * + * In practice this might look something like this: + * default: [x, yVal] + * errorBar / customBar: [x, yVal, [yTopVariance, yBottomVariance] ] + * + */ +/*global Dygraph:false */ +/*global DygraphLayout:false */ + +(function() { + +"use strict"; + +/** + * A collection of functions to create and retrieve data handlers. + * @type {Object.} + */ +Dygraph.DataHandlers = {}; + +/** + * + * The data handler is responsible for all data specific operations. All of the + * series data it receives and returns is always in the unified data format. + * Initially the unified data is created by the extractSeries method + */ +Dygraph.DataHandler = function () { + /** + * Constructor for all data handlers. + * @constructor + */ + var handler = function() { + return this; + }; + + /** + * X-value array index constant for unified data samples. + * @const + * @type {number} + */ + handler.X = 0; + + /** + * Y-value array index constant for unified data samples. + * @const + * @type {number} + */ + handler.Y = 1; + + /** + * Extras-value array index constant for unified data samples. + * @const + * @type {number} + */ + handler.EXTRAS = 2; + + /** + * Extracts one series from the raw data (a 2D array) into an array of the + * unified data format. + * 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. + * + * @param rawData {!Array.} The raw data passed into dygraphs where + * rawData[i] = [x,ySeries1,...,ySeriesN]. + * @param seriesIndex {!number} Index of the series to extract. All other series should + * be ignored. + * @param options {!DygraphOptions} Dygraph options. + * @returns {Array.<[!number,?number,?]>} The series in the unified data format + * where series[i] = [x,y,{extras}]. + * @public + */ + handler.prototype.extractSeries = function(rawData, seriesIndex, options) { + }; + + /** + * Converts a series to a Point array. + * + * @param {!Array.<[!number,?number,?]>} series The series in the unified + * data format where series[i] = [x,y,{extras}]. + * @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. + * @public + */ + handler.prototype.seriesToPoints = function(series, setName, boundaryIdStart) { + // 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. + var points = []; + for ( var i = 0; i < series.length; ++i) { + var item = series[i]; + var yraw = 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 + }; + points.push(point); + } + handler.prototype.onPointsCreated_(series, points); + return points; + }; + + /** + * Callback called for each series after the series points have been generated + * which will later be used by the plotters to draw the graph. + * Here data may be added to the seriesPoints which is needed by the plotters. + * The indexes of series and points are in sync meaning the original data + * sample for series[i] is points[i]. + * + * @param {!Array.<[!number,?number,?]>} series The series in the unified + * data format where series[i] = [x,y,{extras}]. + * @param {!Array.} points The corresponding points passed + * to the plotter. + * @private + */ + handler.prototype.onPointsCreated_ = function(series, points) { + }; + + /** + * Calculates the rolling average of a data set. + * + * @param {!Array.<[!number,?number,?]>} series The series in the unified + * data format where series[i] = [x,y,{extras}]. + * @param {!number} rollPeriod The number of points over which to average the data + * @param {!DygraphOptions} options The dygraph options. + * @return the rolled series. + * @public + */ + handler.prototype.rollingAverage = function(series, rollPeriod, options) { + }; + + /** + * Computes the range of the data series (including confidence intervals). + * + * @param {!Array.<[!number,?number,?]>} series The series in the unified + * data format where series[i] = [x,y,{extras}]. + * @param {!Array.} dateWindow The x-value range to display with + * the format: [min,max]. + * @param {!DygraphOptions} options The dygraph options. + * @return {Array.} The low and high extremes of the series in the given window with + * the format: [low, high]. + * @public + */ + handler.prototype.getExtremeYValues = function(series, dateWindow, options) { + }; + + /** + * Callback called for each series after the layouting data has been + * calculated before the series is drawn. Here normalized positioning data + * should be calculated for the extras of each point. + * + * @param {!Array.} points The points passed to + * the plotter. + * @param {!Object} axis The axis on which the series will be plotted. + * @param {!boolean} logscale Weather or not to use a logscale. + * @public + */ + handler.prototype.onLineEvaluated = function(points, axis, logscale) { + }; + + /** + * Helper method that computes the y value of a line defined by the points p1 + * and p2 and a given x value. + * + * @param {!Array.} p1 left point ([x,y]). + * @param {!Array.} p2 right point ([x,y]). + * @param {!number} xValue The x value to compute the y-intersection for. + * @return {number} corresponding y value to x on the line defined by p1 and p2. + * @private + */ + handler.prototype.computeYInterpolation_ = function(p1, p2, xValue) { + var deltaY = p2[1] - p1[1]; + var deltaX = p2[0] - p1[0]; + var gradient = deltaY / deltaX; + var growth = (xValue - p1[0]) * gradient; + return p1[1] + growth; + }; + + /** + * Helper method that returns the first and the last index of the given series + * that lie inside the given dateWindow. + * + * @param {!Array.<[!number,?number,?]>} series The series in the unified + * data format where series[i] = [x,y,{extras}]. + * @param {!Array.} dateWindow The x-value range to display with + * the format: [min,max]. + * @return {!Array.<[!number,?number,?]>} The samples of the series that + * are in the given date window. + * @private + */ + handler.prototype.getIndexesInWindow_ = function(series, dateWindow) { + var firstIdx = 0, lastIdx = series.length - 1; + if (dateWindow) { + var idx = 0; + var low = dateWindow[0]; + var high = dateWindow[1]; + + // Start from each side of the array to minimize the performance + // needed. + while (idx < series.length - 1 && series[idx][0] < low) { + firstIdx++; + idx++; + } + idx = series.length - 1; + while (idx > 0 && series[idx][0] > high) { + lastIdx--; + idx--; + } + } + if (firstIdx <= lastIdx) { + return [ firstIdx, lastIdx ]; + } else { + return [ 0, series.length - 1 ]; + } + }; + + return handler; +}; + +})(); diff --git a/datahandler/default-fractions.js b/datahandler/default-fractions.js new file mode 100644 index 0000000..8da8e42 --- /dev/null +++ b/datahandler/default-fractions.js @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview DataHandler implementation for the fractions option. + * @author David Eberlein (david.eberlein@ch.sauter-bc.com) + */ + +(function() { + +/*global Dygraph:false */ +"use strict"; + +Dygraph.DataHandlers.DefaultFractionHandler = Dygraph.DataHandler(); +var DefaultFractionHandler = Dygraph.DataHandlers.DefaultFractionHandler; +DefaultFractionHandler.prototype = new Dygraph.DataHandlers.DefaultHandler(); + +DefaultFractionHandler.prototype.extractSeries = function(rawData, i, options) { + // TODO(danvk): pre-allocate series here. + var series = []; + var x, y, point, num, den, value; + var mult = 100.0; + var logScale = options.get('logscale'); + for ( var j = 0; j < rawData.length; j++) { + x = rawData[j][0]; + point = rawData[j][i]; + if (logScale && point !== null) { + // On the log scale, points less than zero do not exist. + // This will create a gap in the chart. + if (point[0] <= 0 || point[1] <= 0) { + point = null; + } + } + // Extract to the unified data format. + if (point !== null) { + num = point[0]; + den = point[1]; + if (num !== null && !isNaN(num)) { + value = den ? num / den : 0.0; + y = mult * value; + // preserve original values in extras for further filtering + series.push([ x, y, [ num, den ] ]); + } else { + series.push([ x, num, [ num, den ] ]); + } + } else { + series.push([ x, null, [ null, null ] ]); + } + } + return series; +}; + +DefaultFractionHandler.prototype.rollingAverage = function(originalData, rollPeriod, + options) { + rollPeriod = Math.min(rollPeriod, originalData.length); + var rollingData = []; + + var i; + var num = 0; + var den = 0; // numerator/denominator + var mult = 100.0; + for (i = 0; i < originalData.length; i++) { + num += originalData[i][2][0]; + den += originalData[i][2][1]; + if (i - rollPeriod >= 0) { + num -= originalData[i - rollPeriod][2][0]; + den -= originalData[i - rollPeriod][2][1]; + } + + var date = originalData[i][0]; + var value = den ? num / den : 0.0; + rollingData[i] = [ date, mult * value ]; + } + + return rollingData; +}; + +})(); diff --git a/datahandler/default.js b/datahandler/default.js new file mode 100644 index 0000000..6e6697f --- /dev/null +++ b/datahandler/default.js @@ -0,0 +1,90 @@ +/** + * @license + * Copyright 2013 David Eberlein (david.eberlein@ch.sauter-bc.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview DataHandler default implementation used for simple line charts. + * @author David Eberlein (david.eberlein@ch.sauter-bc.com) + */ + +(function() { + +/*global Dygraph:false */ +"use strict"; + +Dygraph.DataHandlers.DefaultHandler = Dygraph.DataHandler(); +var DefaultHandler = Dygraph.DataHandlers.DefaultHandler; + +DefaultHandler.prototype.extractSeries = function(rawData, i, options) { + // TODO(danvk): pre-allocate series here. + var series = []; + var logScale = options.get('logscale'); + 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) { + point = null; + } + } + series.push([ x, point ]); + } + return series; +}; + +DefaultHandler.prototype.rollingAverage = function(originalData, rollPeriod, + options) { + rollPeriod = Math.min(rollPeriod, originalData.length); + var rollingData = []; + + var i, j, y, sum, num_ok; + // 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 (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 ]; + } + } + + return rollingData; +}; + +DefaultHandler.prototype.getExtremeYValues = function(series, dateWindow, + options) { + var minY = null, maxY = null, y; + var firstIdx = 0, lastIdx = series.length - 1; + + for ( var j = firstIdx; j <= lastIdx; 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; + } + } + return [ minY, maxY ]; +}; + +})(); diff --git a/dygraph-dev.js b/dygraph-dev.js index c06d23d..160a01e 100644 --- a/dygraph-dev.js +++ b/dygraph-dev.js @@ -36,7 +36,14 @@ "plugins/legend.js", "plugins/range-selector.js", "dygraph-plugin-install.js", - "dygraph-options-reference.js" // Shouldn't be included in generate-combined.sh + "dygraph-options-reference.js", // Shouldn't be included in generate-combined.sh + "datahandler/datahandler.js", + "datahandler/default.js", + "datahandler/default-fractions.js", + "datahandler/bars.js", + "datahandler/bars-error.js", + "datahandler/bars-custom.js", + "datahandler/bars-fractions.js" ]; for (var i = 0; i < source_files.length; i++) { diff --git a/dygraph-layout.js b/dygraph-layout.js index e766e8f..93e0c3a 100644 --- a/dygraph-layout.js +++ b/dygraph-layout.js @@ -222,7 +222,6 @@ DygraphLayout._calcYNormal = function(axis, value, logscale) { DygraphLayout.prototype._evaluateLineCharts = function() { var connectSeparated = this.attr_('connectSeparatedPoints'); var isStacked = this.attr_("stackedGraph"); - var hasBars = this.attr_('errorBars') || this.attr_('customBars'); for (var setIdx = 0; setIdx < this.points.length; setIdx++) { var points = this.points[setIdx]; @@ -252,14 +251,9 @@ DygraphLayout.prototype._evaluateLineCharts = function() { } } point.y = DygraphLayout._calcYNormal(axis, yval, logscale); - - if (hasBars) { - point.y_top = DygraphLayout._calcYNormal( - axis, yval - point.yval_minus, logscale); - point.y_bottom = DygraphLayout._calcYNormal( - axis, yval + point.yval_plus, logscale); - } } + + this.dygraph_.dataHandler_.onLineEvaluated(points, axis, logscale); } }; diff --git a/dygraph-options-reference.js b/dygraph-options-reference.js index c952072..6220750 100644 --- a/dygraph-options-reference.js +++ b/dygraph-options-reference.js @@ -802,7 +802,13 @@ Dygraph.OPTIONS_REFERENCE = // "default": "[]", "labels": ["Configuration"], "type": "Array", - "description": "Defines per-graph plug-ins. Useful for per-graph customization" + "description": "Defines per-graph plugins. Useful for per-graph customization" + }, + "dataHandler": { + "default": "(depends on data)", + "labels": ["Data"], + "type": "Dygraph.DataHandler", + "description": "Custom DataHandler. This is an advanced customization. See http://bit.ly/151E7Aq." } } ; // diff --git a/dygraph.js b/dygraph.js index 1d33570..effe10d 100644 --- a/dygraph.js +++ b/dygraph.js @@ -2153,46 +2153,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; }; /** @@ -2205,6 +2186,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(); @@ -2241,9 +2225,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); } @@ -2280,49 +2266,6 @@ Dygraph.prototype.predraw_ = function() { */ 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. - * - * @private - * @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. * @@ -2438,43 +2381,34 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { var points = []; var cumulativeYval = []; // For stacked series. var extremes = {}; // series name -> [low, high] - 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; - } - }; - + 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; var series; - for (i = num_series; i >= 1; i--) { - if (!this.visibility()[i - 1]) continue; + 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[i]; + series = rolledSeries[seriesIdx]; var low = dateWindow[0]; var high = dateWindow[1]; // 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; } } @@ -2483,7 +2417,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; @@ -2491,10 +2426,9 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { isInvalidValue = true; while (isInvalidValue && correctedLastIdx < series.length - 1) { correctedLastIdx++; - isInvalidValue = isValueNull(series[correctedLastIdx]); + isInvalidValue = series[correctedLastIdx][1] === null; } - if (correctedFirstIdx!==firstIdx) { firstIdx = correctedFirstIdx; } @@ -2502,20 +2436,21 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { lastIdx = correctedLastIdx; } - boundaryIds[i-1] = [firstIdx, lastIdx]; + boundaryIds[seriesIdx-1] = [firstIdx, lastIdx]; // .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]; + series = rolledSeries[seriesIdx]; + boundaryIds[seriesIdx-1] = [0, series.length-1]; } - var seriesName = this.attr_("labels")[i]; - var seriesExtremes = this.extremeValues_(series); + var seriesName = this.attr_("labels")[seriesIdx]; + var seriesExtremes = this.dataHandler_.getExtremeYValues(series, + dateWindow, this.attr_("stepPlot",seriesName)); - var seriesPoints = Dygraph.seriesToPoints_( - series, bars, seriesName, boundaryIds[i-1][0]); + var seriesPoints = this.dataHandler_.seriesToPoints(series, + seriesName, boundaryIds[seriesIdx-1][0]); if (this.attr_("stackedGraph")) { Dygraph.stackPoints_(seriesPoints, cumulativeYval, seriesExtremes, @@ -2523,7 +2458,7 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { } extremes[seriesName] = seriesExtremes; - points[i] = seriesPoints; + points[seriesIdx] = seriesPoints; } return { points: points, extremes: extremes, boundaryIds: boundaryIds }; @@ -2900,198 +2835,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 - * @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 (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; - } - } - // 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. @@ -3886,6 +3629,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; diff --git a/generate-combined.sh b/generate-combined.sh index 9e184ae..09f316f 100755 --- a/generate-combined.sh +++ b/generate-combined.sh @@ -19,7 +19,14 @@ GetSources () { dygraph-tickers.js \ dygraph-plugin-base.js \ plugins/*.js \ - dygraph-plugin-install.js + dygraph-plugin-install.js \ + datahandler/datahandler.js \ + datahandler/default.js \ + datahandler/default-fractions.js \ + datahandler/bars.js \ + datahandler/bars-custom.js \ + datahandler/bars-error.js \ + datahandler/bars-fractions.js do echo "$F" done diff --git a/jsTestDriver.conf b/jsTestDriver.conf index ffbc369..04f49da 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -20,4 +20,11 @@ load: - dygraph-plugin-base.js - plugins/*.js - dygraph-plugin-install.js + - datahandler/datahandler.js + - datahandler/default.js + - datahandler/default-fractions.js + - datahandler/bars.js + - datahandler/bars-error.js + - datahandler/bars-custom.js + - datahandler/bars-fractions.js - auto_tests/tests/*.js diff --git a/lint.sh b/lint.sh index ef4dacd..eb61924 100755 --- a/lint.sh +++ b/lint.sh @@ -21,7 +21,7 @@ fi RETURN_VALUE=0 if [ $# -eq 0 ]; then - files=$(ls dygraph*.js plugins/*.js | grep -v combined | grep -v dev.js| grep -v externs) + files=$(ls dygraph*.js plugins/*.js datahandler/*.js | grep -v combined | grep -v dev.js| grep -v externs) else files=$1 fi