From a49c164ae4b251553a87517ef7d1dc57f3f2ad4c Mon Sep 17 00:00:00 2001 From: David Eberlein Date: Tue, 27 Aug 2013 22:55:47 -0400 Subject: [PATCH] Datahandler and Unified Data Format This commit unifies the various data formats used by dygraphs (error bars, custom bars, fractions, simple numbers) into a single, canonical format. It also allows users to create their own data formats. Original proposal: https://docs.google.com/document/d/1IWmX4oDbQbVtUoRNzSRG3yMpoBQ7LseVCQhGnuOZz_A/edit?pli=1# This commit corresponds to pull request 249. (https://github.com/danvk/dygraphs/pull/249) Squashed commit of the following: commit acf2bc1dcdede4a52a5a9530476fbaf031009244 Author: Dan Vanderkam Date: Tue Aug 27 22:50:46 2013 -0400 update closure TODO with new files commit 13116fcf57a47b878f5cae2348718d61eebdc2e4 Merge: 0668281 625324f Author: Dan Vanderkam Date: Tue Aug 27 22:43:50 2013 -0400 Merge branch 'sauter-custom-datahandler' of https://github.com/sauter-hq/dygraphs into 249-datahandler commit 625324fb1ce5d172537e484408497aab8bb1fcf0 Author: eberldav Date: Mon Aug 5 14:44:06 2013 +0200 BUGFIX: Readded callback.js to the tests which was mistakenly removed. commit d18669c853b3e906a614fd3e251b06ab60be5ae7 Author: eberldav Date: Mon Aug 5 14:28:41 2013 +0200 DOC: Refactored JSDoc to fit the closure complier standard. commit a5bb18294faca0c411507b74bd9ee25ac364ccda Author: eberldav Date: Mon Jul 22 18:04:37 2013 +0200 BUGFIX: Fixed lint errors for static usage of Dygraph and DygraphLayout commit a7b2e9493011710b2cb602200be6c2e839ffd6c0 Author: David Eberlein Date: Mon Jul 22 16:53:36 2013 +0200 REFACTORING: Changed some method parameters based on suggestions of Dan and Robert and added some documentation. commit 1d65e4effe51a00166a346cc06117d4613476231 Author: David Eberlein Date: Mon Jul 22 15:16:07 2013 +0200 REFACTORING: Renamed datahandler files commit edb9bdfac79a810f5e0ae1758062e63bb21e8b5d Author: David Eberlein Date: Mon Jul 22 15:12:55 2013 +0200 DOC: Added documentation for data handlers and fixed wrong method visibilities. commit 9ed5d88e2bfe2518293565cc97ee06bf56040c72 Merge: af5decb 22bc1f1 Author: David Eberlein Date: Fri Jul 19 10:09:28 2013 +0200 Merge branch 'official-master' into sauter-custom-datahandler Conflicts: dygraph.js commit af5decb0e678e57eb9a50bfc7b141b5d647be1a1 Author: David Eberlein Date: Wed Jul 10 10:21:25 2013 +0200 REFACTORING: Extracted getHandlerId_ method and refactored some variable names for better readability commit 84be5b402420a10013ebf2e49943e8faabf54b01 Author: David Eberlein Date: Tue Jul 9 18:02:45 2013 +0200 Fixed formatting issues. commit b2085b50a3f16d4067b02a1a2069f44835476d3d Author: David Eberlein Date: Tue Jun 25 08:49:54 2013 +0200 REFACTORING: Romoved old extract series method which is now moved into the data handlers. commit 7d851638386c9e94bbd593a2bee7bfdec8486753 Merge: c8f5edb 464538e Author: David Eberlein Date: Thu Jun 20 15:13:22 2013 +0200 Merge branch 'official-master' into sauter-custom-datahandler Conflicts: dygraph.js commit c8f5edbb690ffdc3bc88393f26f8d3fd8f717533 Author: David Eberlein Date: Fri Jun 14 12:20:41 2013 +0200 FEATURE: Integreated the new creation of points into the data handlers. This proposal is now fully functional again. commit 5c1ce9c76243c430f9f5d50864b9cb909f611f6f Author: David Eberlein Date: Fri Jun 14 12:19:33 2013 +0200 Reverted file EOL back to unix commit 2684ae56b8e2881680771f783449b3fd77903626 Author: David Eberlein Date: Fri Jun 14 12:18:02 2013 +0200 BUGFIX: Fixed incorrect handling of logscale case in the datahandlers extractSeries methods. commit bcb28c102b91e902a881abdec6cf29b327610235 Author: David Eberlein Date: Fri Jun 14 11:06:34 2013 +0200 CLEAN: Removed old imports of sauter js files. commit 565afa87132d521a18dc7286bd4f79c6a1b743f8 Merge: 40f9f50 95a9512 Author: David Eberlein Date: Thu Jun 13 18:07:16 2013 +0200 Merge branch 'official-master' into sauter-custom-datahandler: This is not yet functional, Dygraphs.seriesToPoints, and DygraphsLayout.evaluteLineCharts must still be refactored. Conflicts: dygraph-layout.js dygraph.js commit 40f9f50c2d2f1af65bb2ece54311d424d54a3afa Author: David Eberlein Date: Mon Jun 3 09:44:29 2013 +0200 REFACTORING: Removed benchmark tests since they are now integrated into the dygraphs-perf project. commit 12940792fa7999da6910947cb7d0f407dd8ad81f Merge: c24f3f3 8cf57f1 Author: David Eberlein Date: Mon Jun 3 09:41:16 2013 +0200 Merge remote-tracking branch 'official/master' into sauter-custom-datahandler commit c24f3f3832a906d9c0c5a61173a94e26150b188b Author: eberldav Date: Thu May 30 17:10:57 2013 +0200 REFACTORING: Fixed lint issues and broken test as well as some code style for the pull. commit ea03a641f42d42d54842111e5f337c39ea3156ef Author: David Eberlein Date: Thu May 30 16:51:42 2013 +0200 CLEANUP: Removed sauter specific data handlers and the not 100% finished Bugfix for the y extremes date window issue so that this can be pulled. commit 3782b55d0f66a600fe863ba3cb2a6c1588839525 Author: eberldav Date: Thu May 16 13:51:09 2013 +0200 BUGFIX: Deleted falsely added comma commit 803715042a4ba225097411be78f89f0c46df9236 Author: eberldav Date: Thu May 16 13:47:58 2013 +0200 OPTIMIZATION: Replaced DataHandler onPointCreated callback with onLineEvaluted passing all created points for one series instead of calling the other method for each created point. Also added some bug fixes to the yextemes method. commit 97efdf6a4d01c8e888c614025a55252c0a55fa8d Author: eberldav Date: Wed May 15 11:26:17 2013 +0200 FEATURE: Made the onPointCreated callback optional for better performance commit 0b7892c335ab4309325efecaff95f7d6d8368848 Merge: c15adff 1bffeb9 Author: David Eberlein Date: Wed May 15 11:02:46 2013 +0200 Merge branch 'sauter-custom-datahandler' of https://github.com/sauter-hq/dygraphs into sauter-custom-datahandler commit 1bffeb9e60da0d1123cca874ca00cd86d53dfb2d Author: eberldav Date: Wed May 15 11:01:00 2013 +0200 REFACTORING: Removed unneseccary fractions check in default data handler. commit c15adff4d2f857b84dfc231e0c19c5616c48d95f Author: David Eberlein Date: Wed May 15 10:58:05 2013 +0200 FEATURE: Removed copying the rolled series in the gather data method since the previous bug is fixed with the new data handler concept commit 7c9d9f243a6b3149c495e1194baeaee1c6e1f479 Author: David Eberlein Date: Mon May 13 14:56:22 2013 +0200 BUGFIX: Fixed wrong call of DataHandlers commit 88f7311197bc5896a693a37aa52997c4be700454 Author: David Eberlein Date: Mon May 13 13:48:24 2013 +0200 Added data handler to the generate combined and jsTestDriver imports. commit 4eb44304546ae705e41cc90f7f6f027846d33968 Author: David Eberlein Date: Mon May 13 11:18:36 2013 +0200 BUGFIX: fixed wrong value comparison for data pruning. commit c21c7f09bd545f6ad2a99e87d9f1584ceafd4c3e Author: David Eberlein Date: Mon May 13 11:06:04 2013 +0200 BUGFIX: Fixed not complete adaption of pruning. commit f5fcd68c8fad65ac7008972dfe8ed1b92868b5d2 Author: David Eberlein Date: Mon May 13 10:51:32 2013 +0200 REFACTORING: Adapted samples pruning to the unified data format. commit 34bf8984e75437f4e0f59f5f1f18cd041308d4b4 Merge: 925e74a b839102 Author: David Eberlein Date: Mon May 13 10:42:55 2013 +0200 Merge branch 'official-master' into sauter-custom-datahandler Conflicts: auto_tests/misc/local.html commit 925e74a6116c988060f83a78d34a4a60853a01e4 Merge: f9c4250 9f890c2 Author: David Eberlein Date: Fri May 3 16:07:07 2013 +0200 Merge commit '9f890c23ad80924d0a30f3a14f8680b7c2d6318e' into sauter-custom-datahandler commit f9c4250752b8a1d71aa8a37b69ca8a2bef42cd98 Author: David Eberlein Date: Thu Apr 25 16:42:34 2013 +0200 TEST: Added benchmark test allowing for different dygaphs benchmarks. They are currently deactivated because they take quite a while to run. commit e6de1648346eaec0f253ef73836bf5c0e24a347f Author: David Eberlein Date: Thu Apr 25 14:29:59 2013 +0200 TEST: Adapted tests to conform to the new format and wrote new tests for the rolling options. commit 8e8161efa9d79ef237a067f52f346dd1e36254ac Author: David Eberlein Date: Thu Apr 25 14:29:03 2013 +0200 FEATURE: Adapted DataHandler proposal to implement a unified data format. commit 5667ec0d745ec446f29a444246a9c0c4555cbc53 Merge: 1049bc4 de25451 Author: David Eberlein Date: Thu Apr 25 09:19:40 2013 +0200 Merge commit 'de2545148870a1bdb0957c4c42e80bdb8ce1656d' into sauter-custom-datahandler commit 1049bc479f0245f35453110f74eb03fd5919a642 Merge: 531fd88 d88dec8 Author: David Eberlein Date: Tue Apr 23 18:17:37 2013 +0200 Merge commit 'd88dec82afd6b902ffa56339d4afbf3277ad5ba3' into sauter-custom-datahandler commit 531fd88e2fcae671f59e62b771df54310db1085d Author: David Eberlein Date: Fri Apr 19 16:15:59 2013 +0200 FEATURE: removed dygraphs-layout.evaluteWithErrors and added its content to the datahandler-bars. commit 5235abea1f1f77668c15e6f1c44be185f00fe5e2 Author: David Eberlein Date: Fri Apr 19 16:06:36 2013 +0200 REFACTORING: Reordered the datahandler methods to fit the calling order commit e8c157be072cb3b1bc58e105311e3354555b5ea0 Author: David Eberlein Date: Fri Apr 19 15:47:19 2013 +0200 DOC: Added documentation and minor fixes to the datahandler commit 58c69c1e5defe57d92b19cfbbf8ab49b8acba163 Author: David Eberlein Date: Fri Apr 19 15:36:32 2013 +0200 REFACTORING: Minor enhancments in extremes computation commit 3ab152656a0f46239b13bac5aa854381965c907f Author: David Eberlein Date: Fri Apr 19 13:42:31 2013 +0200 BUGFIX: Fixed bug still using the customData option in dygraph-layout. commit 4bea6089553269046e80c18b6bd9a77beff3ed92 Author: David Eberlein Date: Fri Apr 19 12:24:29 2013 +0200 BUGFIX: Removed wrong code in bars datahandler callback commit 442151876269da341a3932750a7e11a52e594d0f Author: David Eberlein Date: Fri Apr 19 11:43:57 2013 +0200 TESTS: Adapted tests to fit the new dataHandler integration commit ce7550aaf00a2cafbdc1be3abc4f12dc7ea3a687 Author: David Eberlein Date: Fri Apr 19 11:42:52 2013 +0200 FEATURE: Integrated the dataHandler model into dygraphs. commit e13eb4e4ea0514e62cba89975ed6b8aadebfad0e Author: eichsjul Date: Thu Apr 18 15:38:36 2013 +0200 BUGFIX: Moved extremeValues method to the correct place and added the needed arguments to the call. commit 05afa764addc217e87cf1a29ff350517207f7f65 Author: eichsjul Date: Thu Apr 18 14:51:47 2013 +0200 FEATURE: Added initial implementation of custom data support. --- auto_tests/tests/error_bars.js | 47 +++-- auto_tests/tests/rolling_average.js | 146 ++++++++++++++- closure-todo.txt | 9 + datahandler/bars-custom.js | 93 ++++++++++ datahandler/bars-error.js | 92 +++++++++ datahandler/bars-fractions.js | 103 +++++++++++ datahandler/bars.js | 74 ++++++++ datahandler/datahandler.js | 305 ++++++++++++++++++++++++++++++ datahandler/default-fractions.js | 79 ++++++++ datahandler/default.js | 88 +++++++++ dygraph-dev.js | 9 +- dygraph-layout.js | 10 +- dygraph.js | 359 ++++++------------------------------ generate-combined.sh | 9 +- jsTestDriver.conf | 7 + lint.sh | 2 +- 16 files changed, 1082 insertions(+), 350 deletions(-) create mode 100644 datahandler/bars-custom.js create mode 100644 datahandler/bars-error.js create mode 100644 datahandler/bars-fractions.js create mode 100644 datahandler/bars.js create mode 100644 datahandler/datahandler.js create mode 100644 datahandler/default-fractions.js create mode 100644 datahandler/default.js 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..ff4ba92 --- /dev/null +++ b/datahandler/bars-error.js @@ -0,0 +1,92 @@ +/** + * @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"; + + var ErrorBarsHandler = Dygraph.DataHandler(); + ErrorBarsHandler.prototype = Dygraph.DataHandlers.createHandler("bars"); + Dygraph.DataHandlers.registerHandler("bars-error", ErrorBarsHandler); + // 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..88c5ca6 --- /dev/null +++ b/datahandler/bars-fractions.js @@ -0,0 +1,103 @@ +/** + * @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"; + + var FractionsBarsHandler = Dygraph.DataHandler(); + FractionsBarsHandler.prototype = Dygraph.DataHandlers.createHandler("bars"); + Dygraph.DataHandlers.registerHandler("bars-fractions", FractionsBarsHandler); + // 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..5b84b69 --- /dev/null +++ b/datahandler/bars.js @@ -0,0 +1,74 @@ +/** + * @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"; + + var BarsHandler = Dygraph.DataHandler(); + Dygraph.DataHandlers.registerHandler("bars", 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..d08f7a7 --- /dev/null +++ b/datahandler/datahandler.js @@ -0,0 +1,305 @@ +/** + * @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] ] + * + */ +/*jshint globalstrict: true */ +/*global Dygraph:false */ +/*global DygraphLayout:false */ +"use strict"; + +/** + * A collection of functions to create and retrieve data handlers. + */ +Dygraph.DataHandlers = {}; + +/** + * All registered data handlers are stored here. + * + * @private + */ +Dygraph.DataHandlers.handlers_ = {}; + +/** + * @param name {!string} The name the data handler should be registered to. + * Registers a data handler by the given name and makes it publicly + * accessible. + * @param handler {!Dygraph.DataHandler} DataHandler implementation which must be an + * instance of Dygraph.DataHandler. + * @public + */ +Dygraph.DataHandlers.registerHandler = function(name, handler) { + if (!handler instanceof Dygraph.DataHandler) { + throw ("the handler must be a prototype of Dygraph.DataHandler"); + } + Dygraph.DataHandlers.handlers_[name] = handler; +}; + +/** + * Returns the data handler registered to the given name. + * Note this is the data handler constructor method. + * + * @param name {!string} The name, the handler was registered to. + * @returns {Dygraph.DataHandler} The data handler constructor. + * @public + */ +Dygraph.DataHandlers.getHandler = function(name) { + return Dygraph.DataHandlers.handlers_[name]; +}; + +/** + * Returns the cunstructed data handler registered to the given name. + * + * @param name {!string} The name, the handler was registered to. + * @returns {Dygraph.DataHandler} A constructed instance of the data handler. + * @public + */ +Dygraph.DataHandlers.createHandler = function(name) { + return new Dygraph.DataHandlers.handlers_[name](); +}; + +/** + * + * 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 + * + * @class + */ +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..7e234b0 --- /dev/null +++ b/datahandler/default-fractions.js @@ -0,0 +1,79 @@ +/** + * @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"; + + var DefaultFractionHandler = Dygraph.DataHandler(); + DefaultFractionHandler.prototype = Dygraph.DataHandlers.createHandler("default"); + Dygraph.DataHandlers.registerHandler("default-fractions", DefaultFractionHandler); + + 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..79adcb5 --- /dev/null +++ b/datahandler/default.js @@ -0,0 +1,88 @@ +/** + * @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"; + + var DefaultHandler = Dygraph.DataHandler(); + Dygraph.DataHandlers.registerHandler("default", 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.js b/dygraph.js index f05ac47..48475ef 100644 --- a/dygraph.js +++ b/dygraph.js @@ -2154,45 +2154,28 @@ Dygraph.prototype.addXTicks_ = function() { /** * @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; - } + * Returns the correct handler ID for the currently set options. + * The actual handler may then be retrieved using the + * Dygraph.DataHandlers.getHandler() method. + */ +Dygraph.prototype.getHandlerId_ = function() { + var handlerId; + if (this.attr_("dataHandlerId")) { + handlerId = this.attr_("dataHandlerId"); + } else if (this.fractions_){ + if (this.attr_("errorBars")) { + handlerId = "bars-fractions"; + } else { + handlerId = "default-fractions"; } + } else if (this.attr_("customBars")) { + handlerId = "bars-custom"; + } else if (this.attr_("errorBars")) { + handlerId = "bars-error"; } 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; - } - } + handlerId = "default"; } - - return [minY, maxY]; + return handlerId; }; /** @@ -2205,6 +2188,9 @@ Dygraph.prototype.extremeValues_ = function(series) { */ Dygraph.prototype.predraw_ = function() { var start = new Date(); + + // Create the correct dataHandler + this.dataHandler_ = new (Dygraph.DataHandlers.getHandler(this.getHandlerId_()))(); this.layout_.computePlotArea(); @@ -2241,9 +2227,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 +2268,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 +2383,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 +2419,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 +2428,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 +2438,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 +2460,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 +2837,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. 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 -- 2.7.4