├── .gitignore ├── main.js ├── .travis.yml ├── test ├── reductio.test.js ├── exception-sum.test.js ├── count.test.js ├── value-count.test.js ├── alias.test.js ├── sum-of-squares.test.js ├── exception-count.test.js ├── max.test.js ├── min.test.js ├── median.test.js ├── value-list.test.js ├── sortBy.test.js ├── data-list.test.js ├── sum.test.js ├── aliasProp.test.js ├── histogram.test.js ├── custom.test.js ├── avg.test.js ├── std.test.js ├── values.test.js ├── groupAll.test.js ├── cap.test.js ├── filter.test.js └── nest.test.js ├── src ├── aliasProp.js ├── alias.js ├── postprocessors.js ├── custom.js ├── sum.js ├── data-list.js ├── sum-of-squares.js ├── count.js ├── parameters.js ├── min.js ├── avg.js ├── max.js ├── filter.js ├── sortBy.js ├── std.js ├── postprocess.js ├── value-list.js ├── exception-count.js ├── exception-sum.js ├── median.js ├── value-count.js ├── histogram.js ├── nest.js ├── cap.js ├── reductio.js ├── accessors.js └── build.js ├── CONTRIBUTORS ├── LICENSE ├── rollup.config.js ├── karma.conf.js ├── package.json ├── NOTES.md ├── reductio.min.js ├── README.md └── reductio.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | export { default } from './src/reductio.js'; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | services: 5 | - xvfb 6 | before_script: 7 | - export DISPLAY=:99.0 8 | -------------------------------------------------------------------------------- /test/reductio.test.js: -------------------------------------------------------------------------------- 1 | // Basic tests 2 | describe('Reductio', function() { 3 | it('is a function', function (done) { 4 | expect(typeof reductio).toEqual('function'); 5 | done(); 6 | }); 7 | }); -------------------------------------------------------------------------------- /src/aliasProp.js: -------------------------------------------------------------------------------- 1 | var alias_prop = { 2 | add: function (obj, prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | for(var prop in obj) { 6 | path(p)[prop] = obj[prop](path(p),v); 7 | } 8 | return p; 9 | }; 10 | } 11 | }; 12 | 13 | export default alias_prop; -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | The following people have contributed to Reductio 2 | 3 | L. Hauenstein - https://github.com/esjewett/reductio/commits?author=Hauenstein 4 | Tanner Linsley - https://github.com/esjewett/reductio/commits?author=tannerlinsley 5 | Einar Norðfjörð - https://github.com/esjewett/reductio/commits?author=nordfjord 6 | Chris Meier - https://github.com/esjewett/reductio/issues/2 -------------------------------------------------------------------------------- /src/alias.js: -------------------------------------------------------------------------------- 1 | var alias = { 2 | initial: function(prior, path, obj) { 3 | return function (p) { 4 | if(prior) p = prior(p); 5 | function buildAliasFunction(key){ 6 | return function(){ 7 | return obj[key](path(p)); 8 | }; 9 | } 10 | for(var prop in obj) { 11 | path(p)[prop] = buildAliasFunction(prop); 12 | } 13 | return p; 14 | }; 15 | } 16 | }; 17 | 18 | export default alias; -------------------------------------------------------------------------------- /src/postprocessors.js: -------------------------------------------------------------------------------- 1 | import cap from './cap'; 2 | import sortBy from './sortBy'; 3 | 4 | export default function(reductio){ 5 | reductio.postprocessors = {}; 6 | reductio.registerPostProcessor = function(name, func){ 7 | reductio.postprocessors[name] = func; 8 | }; 9 | 10 | reductio.registerPostProcessor('cap', cap); 11 | reductio.registerPostProcessor('sortBy', sortBy); 12 | } 13 | -------------------------------------------------------------------------------- /src/custom.js: -------------------------------------------------------------------------------- 1 | var custom = { 2 | add: function(prior, path, addFn) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | return addFn(p, v); 6 | }; 7 | }, 8 | remove: function(prior, path, removeFn) { 9 | return function (p, v, nf) { 10 | if(prior) prior(p, v, nf); 11 | return removeFn(p, v); 12 | }; 13 | }, 14 | initial: function(prior, path, initialFn) { 15 | return function (p) { 16 | if(prior) p = prior(p); 17 | return initialFn(p); 18 | }; 19 | } 20 | }; 21 | 22 | export default custom; -------------------------------------------------------------------------------- /src/sum.js: -------------------------------------------------------------------------------- 1 | var sum = { 2 | add: function (a, prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | path(p).sum = path(p).sum + a(v); 6 | return p; 7 | }; 8 | }, 9 | remove: function (a, prior, path) { 10 | return function (p, v, nf) { 11 | if(prior) prior(p, v, nf); 12 | path(p).sum = path(p).sum - a(v); 13 | return p; 14 | }; 15 | }, 16 | initial: function (prior, path) { 17 | return function (p) { 18 | p = prior(p); 19 | path(p).sum = 0; 20 | return p; 21 | }; 22 | } 23 | }; 24 | 25 | export default sum; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Ethan Jewett 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/data-list.js: -------------------------------------------------------------------------------- 1 | var data_list = { 2 | add: function(a, prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | path(p).dataList.push(v); 6 | return p; 7 | }; 8 | }, 9 | remove: function(a, prior, path) { 10 | return function (p, v, nf) { 11 | if(prior) prior(p, v, nf); 12 | path(p).dataList.splice(path(p).dataList.indexOf(v), 1); 13 | return p; 14 | }; 15 | }, 16 | initial: function(prior, path) { 17 | return function (p) { 18 | if(prior) p = prior(p); 19 | path(p).dataList = []; 20 | return p; 21 | }; 22 | } 23 | }; 24 | 25 | export default data_list; 26 | -------------------------------------------------------------------------------- /src/sum-of-squares.js: -------------------------------------------------------------------------------- 1 | var sum_of_sq = { 2 | add: function (a, prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | path(p).sumOfSq = path(p).sumOfSq + a(v)*a(v); 6 | return p; 7 | }; 8 | }, 9 | remove: function (a, prior, path) { 10 | return function (p, v, nf) { 11 | if(prior) prior(p, v, nf); 12 | path(p).sumOfSq = path(p).sumOfSq - a(v)*a(v); 13 | return p; 14 | }; 15 | }, 16 | initial: function (prior, path) { 17 | return function (p) { 18 | p = prior(p); 19 | path(p).sumOfSq = 0; 20 | return p; 21 | }; 22 | } 23 | }; 24 | 25 | export default sum_of_sq; -------------------------------------------------------------------------------- /src/count.js: -------------------------------------------------------------------------------- 1 | var count = { 2 | add: function(prior, path, propName) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | path(p)[propName]++; 6 | return p; 7 | }; 8 | }, 9 | remove: function(prior, path, propName) { 10 | return function (p, v, nf) { 11 | if(prior) prior(p, v, nf); 12 | path(p)[propName]--; 13 | return p; 14 | }; 15 | }, 16 | initial: function(prior, path, propName) { 17 | return function (p) { 18 | if(prior) p = prior(p); 19 | // if(p === undefined) p = {}; 20 | path(p)[propName] = 0; 21 | return p; 22 | }; 23 | } 24 | }; 25 | 26 | export default count; -------------------------------------------------------------------------------- /src/parameters.js: -------------------------------------------------------------------------------- 1 | var parameters = function() { 2 | return { 3 | order: false, 4 | avg: false, 5 | count: false, 6 | sum: false, 7 | exceptionAccessor: false, 8 | exceptionCount: false, 9 | exceptionSum: false, 10 | filter: false, 11 | valueList: false, 12 | median: false, 13 | histogramValue: false, 14 | min: false, 15 | max: false, 16 | histogramThresholds: false, 17 | std: false, 18 | sumOfSquares: false, 19 | values: false, 20 | nestKeys: false, 21 | aliasKeys: false, 22 | aliasPropKeys: false, 23 | groupAll: false, 24 | dataList: false, 25 | custom: false 26 | }; 27 | }; 28 | 29 | export default parameters; 30 | -------------------------------------------------------------------------------- /src/min.js: -------------------------------------------------------------------------------- 1 | var min = { 2 | add: function (prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | 6 | path(p).min = path(p).valueList[0]; 7 | 8 | return p; 9 | }; 10 | }, 11 | remove: function (prior, path) { 12 | return function (p, v, nf) { 13 | if(prior) prior(p, v, nf); 14 | 15 | // Check for undefined. 16 | if(path(p).valueList.length === 0) { 17 | path(p).min = undefined; 18 | return p; 19 | } 20 | 21 | path(p).min = path(p).valueList[0]; 22 | 23 | return p; 24 | }; 25 | }, 26 | initial: function (prior, path) { 27 | return function (p) { 28 | p = prior(p); 29 | path(p).min = undefined; 30 | return p; 31 | }; 32 | } 33 | }; 34 | 35 | export default min; -------------------------------------------------------------------------------- /src/avg.js: -------------------------------------------------------------------------------- 1 | var avg = { 2 | add: function (a, prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | if(path(p).count > 0) { 6 | path(p).avg = path(p).sum / path(p).count; 7 | } else { 8 | path(p).avg = 0; 9 | } 10 | return p; 11 | }; 12 | }, 13 | remove: function (a, prior, path) { 14 | return function (p, v, nf) { 15 | if(prior) prior(p, v, nf); 16 | if(path(p).count > 0) { 17 | path(p).avg = path(p).sum / path(p).count; 18 | } else { 19 | path(p).avg = 0; 20 | } 21 | return p; 22 | }; 23 | }, 24 | initial: function (prior, path) { 25 | return function (p) { 26 | p = prior(p); 27 | path(p).avg = 0; 28 | return p; 29 | }; 30 | } 31 | }; 32 | 33 | export default avg; -------------------------------------------------------------------------------- /src/max.js: -------------------------------------------------------------------------------- 1 | var max = { 2 | add: function (prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | 6 | path(p).max = path(p).valueList[path(p).valueList.length - 1]; 7 | 8 | return p; 9 | }; 10 | }, 11 | remove: function (prior, path) { 12 | return function (p, v, nf) { 13 | if(prior) prior(p, v, nf); 14 | 15 | // Check for undefined. 16 | if(path(p).valueList.length === 0) { 17 | path(p).max = undefined; 18 | return p; 19 | } 20 | 21 | path(p).max = path(p).valueList[path(p).valueList.length - 1]; 22 | 23 | return p; 24 | }; 25 | }, 26 | initial: function (prior, path) { 27 | return function (p) { 28 | p = prior(p); 29 | path(p).max = undefined; 30 | return p; 31 | }; 32 | } 33 | }; 34 | 35 | export default max; -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | var filter = { 2 | // The big idea here is that you give us a filter function to run on values, 3 | // a 'prior' reducer to run (just like the rest of the standard reducers), 4 | // and a reference to the last reducer (called 'skip' below) defined before 5 | // the most recent chain of reducers. This supports individual filters for 6 | // each .value('...') chain that you add to your reducer. 7 | add: function (filter, prior, skip) { 8 | return function (p, v, nf) { 9 | if (filter(v, nf)) { 10 | if (prior) prior(p, v, nf); 11 | } else { 12 | if (skip) skip(p, v, nf); 13 | } 14 | return p; 15 | }; 16 | }, 17 | remove: function (filter, prior, skip) { 18 | return function (p, v, nf) { 19 | if (filter(v, nf)) { 20 | if (prior) prior(p, v, nf); 21 | } else { 22 | if (skip) skip(p, v, nf); 23 | } 24 | return p; 25 | }; 26 | } 27 | }; 28 | 29 | export default filter; 30 | -------------------------------------------------------------------------------- /src/sortBy.js: -------------------------------------------------------------------------------- 1 | var pluck_n = function (n) { 2 | if (typeof n === 'function') { 3 | return n; 4 | } 5 | if (~n.indexOf('.')) { 6 | var split = n.split('.'); 7 | return function (d) { 8 | return split.reduce(function (p, v) { 9 | return p[v]; 10 | }, d); 11 | }; 12 | } 13 | return function (d) { 14 | return d[n]; 15 | }; 16 | }; 17 | 18 | function ascending(a, b) { 19 | return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; 20 | } 21 | 22 | var comparer = function (accessor, ordering) { 23 | return function (a, b) { 24 | return ordering(accessor(a), accessor(b)); 25 | }; 26 | }; 27 | 28 | export default function (prior) { 29 | return function (value, order) { 30 | if (arguments.length === 1) { 31 | order = ascending; 32 | } 33 | return prior().sort(comparer(pluck_n(value), order)); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/std.js: -------------------------------------------------------------------------------- 1 | var std = { 2 | add: function (prior, path) { 3 | return function (p, v, nf) { 4 | if(prior) prior(p, v, nf); 5 | if(path(p).count > 0) { 6 | path(p).std = 0.0; 7 | var n = path(p).sumOfSq - path(p).sum*path(p).sum/path(p).count; 8 | if (n>0.0) path(p).std = Math.sqrt(n/(path(p).count-1)); 9 | } else { 10 | path(p).std = 0.0; 11 | } 12 | return p; 13 | }; 14 | }, 15 | remove: function (prior, path) { 16 | return function (p, v, nf) { 17 | if(prior) prior(p, v, nf); 18 | if(path(p).count > 0) { 19 | path(p).std = 0.0; 20 | var n = path(p).sumOfSq - path(p).sum*path(p).sum/path(p).count; 21 | if (n>0.0) path(p).std = Math.sqrt(n/(path(p).count-1)); 22 | } else { 23 | path(p).std = 0; 24 | } 25 | return p; 26 | }; 27 | }, 28 | initial: function (prior, path) { 29 | return function (p) { 30 | p = prior(p); 31 | path(p).std = 0; 32 | return p; 33 | }; 34 | } 35 | }; 36 | 37 | export default std; -------------------------------------------------------------------------------- /src/postprocess.js: -------------------------------------------------------------------------------- 1 | function postProcess(reductio) { 2 | return function (group, p, f) { 3 | group.post = function(){ 4 | var postprocess = function () { 5 | return postprocess.all(); 6 | }; 7 | postprocess.all = function () { 8 | return group.all(); 9 | }; 10 | var postprocessors = reductio.postprocessors; 11 | Object.keys(postprocessors).forEach(function (name) { 12 | postprocess[name] = function () { 13 | var _all = postprocess.all; 14 | var args = [].slice.call(arguments); 15 | postprocess.all = function () { 16 | return postprocessors[name](_all, f, p).apply(null, args); 17 | }; 18 | return postprocess; 19 | }; 20 | }); 21 | return postprocess; 22 | }; 23 | }; 24 | } 25 | 26 | export default postProcess; 27 | -------------------------------------------------------------------------------- /src/value-list.js: -------------------------------------------------------------------------------- 1 | import crossfilter from 'crossfilter2'; 2 | 3 | var value_list = { 4 | add: function (a, prior, path) { 5 | var i; 6 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 7 | return function (p, v, nf) { 8 | if(prior) prior(p, v, nf); 9 | // Not sure if this is more efficient than sorting. 10 | i = bisect(path(p).valueList, a(v), 0, path(p).valueList.length); 11 | path(p).valueList.splice(i, 0, a(v)); 12 | return p; 13 | }; 14 | }, 15 | remove: function (a, prior, path) { 16 | var i; 17 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 18 | return function (p, v, nf) { 19 | if(prior) prior(p, v, nf); 20 | i = bisect(path(p).valueList, a(v), 0, path(p).valueList.length); 21 | // Value already exists or something has gone terribly wrong. 22 | path(p).valueList.splice(i, 1); 23 | return p; 24 | }; 25 | }, 26 | initial: function (prior, path) { 27 | return function (p) { 28 | p = prior(p); 29 | path(p).valueList = []; 30 | return p; 31 | }; 32 | } 33 | }; 34 | 35 | export default value_list; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import node from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import json from "rollup-plugin-json"; 4 | import { terser } from "rollup-plugin-terser"; 5 | import * as meta from "./package.json"; 6 | 7 | const name = 'reductio' 8 | 9 | const config = { 10 | input: `main.js`, 11 | external: ['crossfilter2'], 12 | output: { 13 | file: `${name}.js`, 14 | name: name, 15 | format: "umd", 16 | globals: { 17 | crossfilter2: 'crossfilter' 18 | }, 19 | indent: true, 20 | extend: true, 21 | banner: `// ${meta.homepage} v${meta.version} Copyright ${(new Date).getFullYear()} ${meta.author.name}` 22 | }, 23 | plugins: [ 24 | node(), 25 | json(), 26 | commonjs() 27 | ] 28 | }; 29 | 30 | export default [ 31 | config, 32 | { 33 | ...config, 34 | output: { 35 | ...config.output, 36 | file: `${name}.min.js` 37 | }, 38 | plugins: [ 39 | ...config.plugins, 40 | terser({ 41 | output: { 42 | preamble: config.output.banner 43 | } 44 | }) 45 | ] 46 | } 47 | ]; -------------------------------------------------------------------------------- /src/exception-count.js: -------------------------------------------------------------------------------- 1 | var exception_count = { 2 | add: function (a, prior, path) { 3 | var i, curr; 4 | return function (p, v, nf) { 5 | if(prior) prior(p, v, nf); 6 | // Only count++ if the p.values array doesn't contain a(v) or if it's 0. 7 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 8 | curr = path(p).values[i]; 9 | if((!curr || curr[0] !== a(v)) || curr[1] === 0) { 10 | path(p).exceptionCount++; 11 | } 12 | return p; 13 | }; 14 | }, 15 | remove: function (a, prior, path) { 16 | var i, curr; 17 | return function (p, v, nf) { 18 | if(prior) prior(p, v, nf); 19 | // Only count-- if the p.values array contains a(v) value of 1. 20 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 21 | curr = path(p).values[i]; 22 | if(curr && curr[0] === a(v) && curr[1] === 1) { 23 | path(p).exceptionCount--; 24 | } 25 | return p; 26 | }; 27 | }, 28 | initial: function (prior, path) { 29 | return function (p) { 30 | p = prior(p); 31 | path(p).exceptionCount = 0; 32 | return p; 33 | }; 34 | } 35 | }; 36 | 37 | export default exception_count; -------------------------------------------------------------------------------- /src/exception-sum.js: -------------------------------------------------------------------------------- 1 | var exception_sum = { 2 | add: function (a, sum, prior, path) { 3 | var i, curr; 4 | return function (p, v, nf) { 5 | if(prior) prior(p, v, nf); 6 | // Only sum if the p.values array doesn't contain a(v) or if it's 0. 7 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 8 | curr = path(p).values[i]; 9 | if((!curr || curr[0] !== a(v)) || curr[1] === 0) { 10 | path(p).exceptionSum = path(p).exceptionSum + sum(v); 11 | } 12 | return p; 13 | }; 14 | }, 15 | remove: function (a, sum, prior, path) { 16 | var i, curr; 17 | return function (p, v, nf) { 18 | if(prior) prior(p, v, nf); 19 | // Only sum if the p.values array contains a(v) value of 1. 20 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 21 | curr = path(p).values[i]; 22 | if(curr && curr[0] === a(v) && curr[1] === 1) { 23 | path(p).exceptionSum = path(p).exceptionSum - sum(v); 24 | } 25 | return p; 26 | }; 27 | }, 28 | initial: function (prior, path) { 29 | return function (p) { 30 | p = prior(p); 31 | path(p).exceptionSum = 0; 32 | return p; 33 | }; 34 | } 35 | }; 36 | 37 | export default exception_sum; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '.', 4 | 5 | frameworks: ['jasmine'], 6 | 7 | files: [ 8 | 'node_modules/crossfilter2/crossfilter.min.js', 9 | 'node_modules/lodash/lodash.js', 10 | // {pattern: 'main.js', type: 'module'}, 11 | 'reductio.min.js', 12 | { pattern: config.grep ? config.grep : 'test/**/*.test.js', type: 'module' }, 13 | // 'test/**/*.test.js' 14 | ], 15 | 16 | browsers: [ 17 | // 'Chrome' 18 | // 'ChromeCanary', 19 | // 'Firefox', 20 | 'ChromeHeadless' 21 | // 'PhantomJS' 22 | // 'PhantomJS' 23 | ], 24 | 25 | plugins: [ 26 | 'karma-jasmine', 27 | 'karma-chrome-launcher', 28 | // 'karma-firefox-launcher', 29 | // 'karma-phantomjs-launcher' 30 | ], 31 | 32 | // reporters: ['dots'], 33 | reporters: ['progress'], 34 | 35 | singleRun: true, 36 | 37 | concurrency: Infinity, 38 | autoWatch: false, 39 | colors: true, 40 | port: 9876, // karma web server port 41 | captureTimeout: 60000 42 | }); 43 | }; 44 | 45 | // logLevel: config.LOG_INFO, 46 | // browsers: ['ChromeHeadless'], 47 | // // singleRun: false, // Karma captures browsers, runs the tests and exits -------------------------------------------------------------------------------- /src/median.js: -------------------------------------------------------------------------------- 1 | var median = { 2 | add: function (prior, path) { 3 | var half; 4 | return function (p, v, nf) { 5 | if(prior) prior(p, v, nf); 6 | 7 | half = Math.floor(path(p).valueList.length/2); 8 | 9 | if(path(p).valueList.length % 2) { 10 | path(p).median = path(p).valueList[half]; 11 | } else { 12 | path(p).median = (path(p).valueList[half-1] + path(p).valueList[half]) / 2.0; 13 | } 14 | 15 | return p; 16 | }; 17 | }, 18 | remove: function (prior, path) { 19 | var half; 20 | return function (p, v, nf) { 21 | if(prior) prior(p, v, nf); 22 | 23 | half = Math.floor(path(p).valueList.length/2); 24 | 25 | // Check for undefined. 26 | if(path(p).valueList.length === 0) { 27 | path(p).median = undefined; 28 | return p; 29 | } 30 | 31 | if(path(p).valueList.length === 1 || path(p).valueList.length % 2) { 32 | path(p).median = path(p).valueList[half]; 33 | } else { 34 | path(p).median = (path(p).valueList[half-1] + path(p).valueList[half]) / 2.0; 35 | } 36 | 37 | return p; 38 | }; 39 | }, 40 | initial: function (prior, path) { 41 | return function (p) { 42 | p = prior(p); 43 | path(p).median = undefined; 44 | return p; 45 | }; 46 | } 47 | }; 48 | 49 | export default median; -------------------------------------------------------------------------------- /src/value-count.js: -------------------------------------------------------------------------------- 1 | import crossfilter from 'crossfilter2'; 2 | 3 | var value_count = { 4 | add: function (a, prior, path) { 5 | var i, curr; 6 | return function (p, v, nf) { 7 | if(prior) prior(p, v, nf); 8 | // Not sure if this is more efficient than sorting. 9 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 10 | curr = path(p).values[i]; 11 | if(curr && curr[0] === a(v)) { 12 | // Value already exists in the array - increment it 13 | curr[1]++; 14 | } else { 15 | // Value doesn't exist - add it in form [value, 1] 16 | path(p).values.splice(i, 0, [a(v), 1]); 17 | } 18 | return p; 19 | }; 20 | }, 21 | remove: function (a, prior, path) { 22 | var i; 23 | return function (p, v, nf) { 24 | if(prior) prior(p, v, nf); 25 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 26 | // Value already exists or something has gone terribly wrong. 27 | path(p).values[i][1]--; 28 | return p; 29 | }; 30 | }, 31 | initial: function (prior, path) { 32 | return function (p) { 33 | p = prior(p); 34 | // Array[Array[value, count]] 35 | path(p).values = []; 36 | path(p).bisect = crossfilter.bisect.by(function(d) { return d[0]; }).left; 37 | return p; 38 | }; 39 | } 40 | }; 41 | 42 | export default value_count; -------------------------------------------------------------------------------- /test/exception-sum.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio exception sum', function () { 3 | var group; 4 | 5 | beforeEach(function () { 6 | var data = crossfilter([ 7 | { foo: 'one', bar: 'A', num: 1 }, 8 | { foo: 'two', bar: 'B', num: 2 }, 9 | { foo: 'three', bar: 'A', num: 3 }, 10 | { foo: 'one', bar: 'B', num: 2 }, 11 | { foo: 'one', bar: 'A', num: 1 }, 12 | { foo: 'two', bar: 'B', num: 2 }, 13 | ]); 14 | 15 | var dim = data.dimension(function(d) { return d.foo; }); 16 | group = dim.group(); 17 | 18 | var reducer = reductio() 19 | .exception(function(d) { return d.bar; }) 20 | .exceptionSum(function(d) { return d.num; }); 21 | 22 | reducer(group); 23 | }); 24 | 25 | it('has three groups', function () { 26 | expect(group.top(Infinity).length).toEqual(3); 27 | }); 28 | 29 | it('grouping have the right counts', function () { 30 | var values = {}; 31 | group.top(Infinity).forEach(function (d) { 32 | values[d.key] = d.value; 33 | }); 34 | 35 | expect(values['one'].exceptionSum).toEqual(3); 36 | expect(values['two'].exceptionSum).toEqual(2); 37 | expect(values['three'].exceptionSum).toEqual(3); 38 | }); 39 | }); -------------------------------------------------------------------------------- /test/count.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio count', function () { 3 | var group; 4 | 5 | beforeEach(function () { 6 | var data = crossfilter([ 7 | { foo: 'one' }, 8 | { foo: 'two' }, 9 | { foo: 'three' }, 10 | { foo: 'one' }, 11 | { foo: 'one' }, 12 | { foo: 'two' }, 13 | ]); 14 | 15 | var dim = data.dimension(function(d) { return d.foo; }); 16 | group = dim.group(); 17 | 18 | var reducer = reductio() 19 | .count(true); 20 | 21 | reducer(group); 22 | }); 23 | 24 | it('has three groups', function () { 25 | expect(group.top(Infinity).length).toEqual(3); 26 | }); 27 | 28 | it('grouping have the right counts', function () { 29 | var values = {}; 30 | group.top(Infinity).forEach(function (d) { 31 | values[d.key] = d.value; 32 | }); 33 | 34 | expect(values['one'].count).toEqual(3); 35 | expect(values['two'].count).toEqual(2); 36 | expect(values['three'].count).toEqual(1); 37 | }); 38 | 39 | it('has a user-defined property name', function() { 40 | reductio().count(true, 'otherCount')(group); 41 | expect(group.top(1)[0].value.otherCount).toEqual(3); 42 | expect(group.top(1)[0].value.count).toEqual(undefined); 43 | }) 44 | }); -------------------------------------------------------------------------------- /src/histogram.js: -------------------------------------------------------------------------------- 1 | import crossfilter from 'crossfilter2'; 2 | 3 | var histogram = { 4 | add: function (a, prior, path) { 5 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 6 | var bisectHisto = crossfilter.bisect.by(function(d) { return d.x; }).right; 7 | var curr; 8 | return function (p, v, nf) { 9 | if(prior) prior(p, v, nf); 10 | curr = path(p).histogram[bisectHisto(path(p).histogram, a(v), 0, path(p).histogram.length) - 1]; 11 | curr.y++; 12 | curr.splice(bisect(curr, a(v), 0, curr.length), 0, a(v)); 13 | return p; 14 | }; 15 | }, 16 | remove: function (a, prior, path) { 17 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 18 | var bisectHisto = crossfilter.bisect.by(function(d) { return d.x; }).right; 19 | var curr; 20 | return function (p, v, nf) { 21 | if(prior) prior(p, v, nf); 22 | curr = path(p).histogram[bisectHisto(path(p).histogram, a(v), 0, path(p).histogram.length) - 1]; 23 | curr.y--; 24 | curr.splice(bisect(curr, a(v), 0, curr.length), 1); 25 | return p; 26 | }; 27 | }, 28 | initial: function (thresholds, prior, path) { 29 | return function (p) { 30 | p = prior(p); 31 | path(p).histogram = []; 32 | var arr = []; 33 | for(var i = 1; i < thresholds.length; i++) { 34 | arr = []; 35 | arr.x = thresholds[i - 1]; 36 | arr.dx = (thresholds[i] - thresholds[i - 1]); 37 | arr.y = 0; 38 | path(p).histogram.push(arr); 39 | } 40 | return p; 41 | }; 42 | } 43 | }; 44 | 45 | export default histogram; -------------------------------------------------------------------------------- /src/nest.js: -------------------------------------------------------------------------------- 1 | import crossfilter from 'crossfilter2'; 2 | 3 | var nest = { 4 | add: function (keyAccessors, prior, path) { 5 | var i; // Current key accessor 6 | var arrRef; 7 | var newRef; 8 | return function (p, v, nf) { 9 | if(prior) prior(p, v, nf); 10 | 11 | arrRef = path(p).nest; 12 | keyAccessors.forEach(function(a) { 13 | newRef = arrRef.filter(function(d) { return d.key === a(v); })[0]; 14 | if(newRef) { 15 | // There is another level. 16 | arrRef = newRef.values; 17 | } else { 18 | // Next level doesn't yet exist so we create it. 19 | newRef = []; 20 | arrRef.push({ key: a(v), values: newRef }); 21 | arrRef = newRef; 22 | } 23 | }); 24 | 25 | arrRef.push(v); 26 | 27 | return p; 28 | }; 29 | }, 30 | remove: function (keyAccessors, prior, path) { 31 | var arrRef; 32 | return function (p, v, nf) { 33 | if(prior) prior(p, v, nf); 34 | 35 | arrRef = path(p).nest; 36 | keyAccessors.forEach(function(a) { 37 | arrRef = arrRef.filter(function(d) { return d.key === a(v); })[0].values; 38 | }); 39 | 40 | // Array contains an actual reference to the row, so just splice it out. 41 | arrRef.splice(arrRef.indexOf(v), 1); 42 | 43 | // If the leaf now has length 0 and it's not the base array remove it. 44 | // TODO 45 | 46 | return p; 47 | }; 48 | }, 49 | initial: function (prior, path) { 50 | return function (p) { 51 | p = prior(p); 52 | path(p).nest = []; 53 | return p; 54 | }; 55 | } 56 | }; 57 | 58 | export default nest; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reductio", 3 | "version": "1.0.0", 4 | "description": "Reductio: Crossfilter groupings", 5 | "main": "src/reductio.js", 6 | "module": "main.js", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "dependencies": { 11 | "crossfilter2": "^1.5.2" 12 | }, 13 | "devDependencies": { 14 | "esm": "3.2.25", 15 | "lodash": "4", 16 | "rollup": "1", 17 | "rollup-plugin-commonjs": "^10.1.0", 18 | "rollup-plugin-json": "4", 19 | "rollup-plugin-node-resolve": "5", 20 | "rollup-plugin-terser": "5", 21 | "karma": "^4.3.0", 22 | "karma-chrome-launcher": "^2.2.0", 23 | "karma-coverage-istanbul-reporter": "^2.0.0", 24 | "karma-firefox-launcher": "^1.2.0", 25 | "karma-jasmine": "^2.0.1" 26 | }, 27 | "scripts": { 28 | "test": "node -r esm ./node_modules/karma/bin/karma start", 29 | "postpublish": "git push && git push --tags", 30 | "build": "rollup -c rollup.config.js" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/esjewett/reductio.git" 35 | }, 36 | "keywords": [ 37 | "crossfilter" 38 | ], 39 | "author": { 40 | "name": "Ethan Jewett" 41 | }, 42 | "files": [ 43 | "index.js", 44 | "main.js", 45 | "src/**", 46 | "reductio.js", 47 | "reductio.min.js" 48 | ], 49 | "license": "Apache-2.0", 50 | "bugs": { 51 | "url": "https://github.com/esjewett/reductio/issues" 52 | }, 53 | "homepage": "https://github.com/esjewett/reductio", 54 | "browser": { 55 | "crossfilter": "crossfilter2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/value-count.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio value count', function () { 3 | var group; 4 | 5 | beforeEach(function () { 6 | var data = crossfilter([ 7 | { foo: 'one', bar: 1 }, 8 | { foo: 'two', bar: 2 }, 9 | { foo: 'two', bar: 3 }, 10 | { foo: 'one', bar: 1 }, 11 | { foo: 'one', bar: 5 }, 12 | { foo: 'two', bar: 2 }, 13 | ]); 14 | 15 | var dim = data.dimension(function(d) { return d.foo; }); 16 | group = dim.group(); 17 | 18 | var reducer = reductio() 19 | .sum(function (d) { return d.bar; }) 20 | .count(true) 21 | .exception(function (d) { return d.bar; }); 22 | 23 | reducer(group); 24 | }); 25 | 26 | it('has two groups', function () { 27 | expect(group.top(Infinity).length).toEqual(2); 28 | }); 29 | 30 | it('grouping have the right sums', function () { 31 | var values = {}; 32 | group.top(Infinity).forEach(function (d) { 33 | values[d.key] = d.value; 34 | }); 35 | 36 | expect(values['one'].sum).toEqual(7); 37 | expect(values['two'].sum).toEqual(7); 38 | }); 39 | 40 | it('properly tracks values', function () { 41 | var values = {}; 42 | group.top(Infinity).forEach(function (d) { 43 | values[d.key] = d.value; 44 | }); 45 | 46 | expect(values['one'].values).toEqual([[1,2],[5,1]]); 47 | expect(values['two'].values).toEqual([[2,2],[3,1]]); 48 | }); 49 | }); -------------------------------------------------------------------------------- /test/alias.test.js: -------------------------------------------------------------------------------- 1 | // Alias tests 2 | 3 | describe('Alias function', function () { 4 | var group; 5 | var values = {}; 6 | 7 | beforeEach(function () { 8 | var data = crossfilter([ 9 | { foo: 'one' }, 10 | { foo: 'two' }, 11 | { foo: 'three' }, 12 | { foo: 'one' }, 13 | { foo: 'one' }, 14 | { foo: 'two' }, 15 | ]); 16 | 17 | var dim = data.dimension(function(d) { return d.foo; }); 18 | group = dim.group(); 19 | 20 | var reducer = reductio() 21 | .count(true) 22 | .alias({ newCount: function(g) { return g.count; }, 23 | twoCount: function(g) { return 2*g.count; } 24 | }); 25 | 26 | reducer(group); 27 | group.top(Infinity).forEach(function (d) { 28 | values[d.key] = d.value; 29 | }); 30 | }); 31 | 32 | it('has three groups', function () { 33 | expect(group.top(Infinity).length).toEqual(3); 34 | }); 35 | 36 | it('grouping for first alias have the right counts', function () { 37 | expect(values['one'].newCount()).toEqual(3); 38 | expect(values['two'].newCount()).toEqual(2); 39 | expect(values['three'].newCount()).toEqual(1); 40 | }); 41 | 42 | it('groupings for second alias have the right values', function(){ 43 | expect(values['one'].twoCount()).toEqual(6); 44 | expect(values['two'].twoCount()).toEqual(4); 45 | expect(values['three'].twoCount()).toEqual(2); 46 | }); 47 | 48 | }); -------------------------------------------------------------------------------- /test/sum-of-squares.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio sumOfSq', function () { 3 | var group; 4 | 5 | beforeEach(function () { 6 | var data = crossfilter([ 7 | { foo: 'one', bar: 1 }, 8 | { foo: 'two', bar: 2 }, 9 | { foo: 'three', bar: 3 }, 10 | { foo: 'one', bar: 4 }, 11 | { foo: 'one', bar: 5 }, 12 | { foo: 'two', bar: 6 }, 13 | ]); 14 | 15 | var dim = data.dimension(function(d) { return d.foo; }); 16 | group = dim.group(); 17 | 18 | var reducer = reductio() 19 | .count(true) 20 | .sumOfSq(function(d) { return d.bar; }); 21 | 22 | reducer(group); 23 | }); 24 | 25 | it('has three groups', function () { 26 | expect(group.top(Infinity).length).toEqual(3); 27 | }); 28 | 29 | it('grouping have the right sum of squares', function () { 30 | var values = {}; 31 | group.top(Infinity).forEach(function (d) { 32 | values[d.key] = d.value; 33 | }); 34 | 35 | expect(values['one'].sumOfSq).toEqual(42); 36 | expect(values['two'].sumOfSq).toEqual(40); 37 | expect(values['three'].sumOfSq).toEqual(9); 38 | }); 39 | 40 | it('grouping plays nicely with count', function () { 41 | var values = {}; 42 | group.top(Infinity).forEach(function (d) { 43 | values[d.key] = d.value; 44 | }); 45 | 46 | expect(values['one'].count).toEqual(3); 47 | expect(values['two'].count).toEqual(2); 48 | expect(values['three'].count).toEqual(1); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/cap.js: -------------------------------------------------------------------------------- 1 | var pluck = function(n){ 2 | return function(d){ 3 | return d[n]; 4 | }; 5 | }; 6 | 7 | // supported operators are sum, avg, and count 8 | const _grouper = function(path, prior){ 9 | if(!path) path = function(d){return d;}; 10 | return function(p, v){ 11 | if(prior) prior(p, v); 12 | var x = path(p), y = path(v); 13 | if(typeof y.count !== 'undefined') x.count += y.count; 14 | if(typeof y.sum !== 'undefined') x.sum += y.sum; 15 | if(typeof y.avg !== 'undefined') x.avg = x.sum/x.count; 16 | return p; 17 | }; 18 | }; 19 | 20 | const cap = function (prior, f, p) { 21 | var obj = f.reduceInitial(); 22 | // we want to support values so we'll need to know what those are 23 | var values = p.values ? Object.keys(p.values) : []; 24 | var _othersGrouper = _grouper(); 25 | if (values.length) { 26 | for (var i = 0; i < values.length; ++i) { 27 | _othersGrouper = _grouper(pluck(values[i]), _othersGrouper); 28 | } 29 | } 30 | return function (cap, othersName) { 31 | if (!arguments.length) return prior(); 32 | if( cap === Infinity || !cap ) return prior(); 33 | var all = prior(); 34 | var slice_idx = cap-1; 35 | if(all.length <= cap) return all; 36 | var data = all.slice(0, slice_idx); 37 | var others = {key: othersName || 'Others'}; 38 | others.value = f.reduceInitial(); 39 | for (var i = slice_idx; i < all.length; ++i) { 40 | _othersGrouper(others.value, all[i].value); 41 | } 42 | data.push(others); 43 | return data; 44 | }; 45 | }; 46 | 47 | export default cap; 48 | -------------------------------------------------------------------------------- /test/exception-count.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio exception count', function () { 3 | var group; 4 | var accGroup; 5 | 6 | beforeEach(function () { 7 | var data = crossfilter([ 8 | { foo: 'one', bar: 'A' }, 9 | { foo: 'two', bar: 'B' }, 10 | { foo: 'three', bar: 'A' }, 11 | { foo: 'one', bar: 'B' }, 12 | { foo: 'one', bar: 'A' }, 13 | { foo: 'two', bar: 'B' }, 14 | ]); 15 | 16 | var dim = data.dimension(function(d) { return d.foo; }); 17 | group = dim.group(); 18 | accGroup = dim.group(); 19 | 20 | var reducer = reductio() 21 | .exception(function(d) { return d.bar; }) 22 | .exceptionCount(true); 23 | 24 | reducer(group); 25 | 26 | reductio().exceptionCount(function(d) { return d.bar; })(accGroup); 27 | }); 28 | 29 | it('has three groups', function () { 30 | expect(group.top(Infinity).length).toEqual(3); 31 | }); 32 | 33 | it('grouping has the right counts', function () { 34 | var values = {}; 35 | group.top(Infinity).forEach(function (d) { 36 | values[d.key] = d.value; 37 | }); 38 | 39 | expect(values['one'].exceptionCount).toEqual(2); 40 | expect(values['two'].exceptionCount).toEqual(1); 41 | expect(values['three'].exceptionCount).toEqual(1); 42 | }); 43 | 44 | it('accessor-based grouping has the right counts', function () { 45 | var values = {}; 46 | accGroup.top(Infinity).forEach(function (d) { 47 | values[d.key] = d.value; 48 | }); 49 | 50 | expect(values['one'].exceptionCount).toEqual(2); 51 | expect(values['two'].exceptionCount).toEqual(1); 52 | expect(values['three'].exceptionCount).toEqual(1); 53 | }); 54 | }); -------------------------------------------------------------------------------- /test/max.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio maximum', function () { 3 | 4 | var max = {}, filterDim; 5 | 6 | beforeEach(function () { 7 | var data = crossfilter([ 8 | { foo: 'one', bar: 1 }, 9 | { foo: 'two', bar: 2 }, 10 | { foo: 'three', bar: 3 }, 11 | { foo: 'one', bar: 4 }, 12 | { foo: 'one', bar: 5 }, 13 | { foo: 'two', bar: 6 }, 14 | ]); 15 | 16 | var dim = data.dimension(function(d) { return d.foo; }); 17 | var group = dim.group(); 18 | 19 | filterDim = data.dimension(function(d) { return d.bar; }); 20 | 21 | reductio() 22 | .max(function(d) { return d.bar; })(group); 23 | 24 | max = group; 25 | }); 26 | 27 | it('has three groups', function (done) { 28 | expect(max.top(Infinity).length).toEqual(3); 29 | done(); 30 | }); 31 | 32 | it('grouping have the right maximums', function (done) { 33 | var values = {}; 34 | max.top(Infinity).forEach(function (d) { 35 | values[d.key] = d.value; 36 | }); 37 | 38 | expect(Math.round(values['one'].max)).toEqual(Math.round(5)); 39 | expect(Math.round(values['two'].max)).toEqual(Math.round(6)); 40 | expect(Math.round(values['three'].max)).toEqual(Math.round(3)); 41 | 42 | filterDim.filter(1); 43 | 44 | expect(Math.round(values['one'].max)).toEqual(Math.round(1)); 45 | expect(values['two'].max).toBeUndefined(); 46 | expect(values['three'].max).toBeUndefined(); 47 | 48 | filterDim.filterAll(); 49 | 50 | expect(Math.round(values['one'].max)).toEqual(Math.round(5)); 51 | expect(Math.round(values['two'].max)).toEqual(Math.round(6)); 52 | expect(Math.round(values['three'].max)).toEqual(Math.round(3)); 53 | 54 | done(); 55 | 56 | }); 57 | }); -------------------------------------------------------------------------------- /test/min.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio minimum', function () { 3 | 4 | var min = {}, filterDim; 5 | 6 | beforeEach(function () { 7 | var data = crossfilter([ 8 | { foo: 'one', bar: 1 }, 9 | { foo: 'two', bar: 2 }, 10 | { foo: 'three', bar: 3 }, 11 | { foo: 'one', bar: 4 }, 12 | { foo: 'one', bar: 5 }, 13 | { foo: 'two', bar: 6 }, 14 | ]); 15 | 16 | var dim = data.dimension(function(d) { return d.foo; }); 17 | var group = dim.group(); 18 | 19 | filterDim = data.dimension(function(d) { return d.bar; }); 20 | 21 | reductio() 22 | .min(function(d) { return d.bar; })(group); 23 | 24 | min = group; 25 | }); 26 | 27 | it('has three groups', function (done) { 28 | expect(min.top(Infinity).length).toEqual(3); 29 | done(); 30 | }); 31 | 32 | it('grouping have the right minimums', function (done) { 33 | var values = {}; 34 | min.top(Infinity).forEach(function (d) { 35 | values[d.key] = d.value; 36 | }); 37 | 38 | expect(Math.round(values['one'].min)).toEqual(Math.round(1)); 39 | expect(Math.round(values['two'].min)).toEqual(Math.round(2)); 40 | expect(Math.round(values['three'].min)).toEqual(Math.round(3)); 41 | 42 | filterDim.filter(4); 43 | 44 | expect(Math.round(values['one'].min)).toEqual(Math.round(4)); 45 | expect(values['two'].min).toBeUndefined(); 46 | expect(values['three'].min).toBeUndefined(); 47 | 48 | filterDim.filterAll(); 49 | 50 | expect(Math.round(values['one'].min)).toEqual(Math.round(1)); 51 | expect(Math.round(values['two'].min)).toEqual(Math.round(2)); 52 | expect(Math.round(values['three'].min)).toEqual(Math.round(3)); 53 | 54 | done(); 55 | 56 | }); 57 | }); -------------------------------------------------------------------------------- /test/median.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio median', function () { 3 | 4 | var median = {}, filterDim; 5 | 6 | beforeEach(function () { 7 | var data = crossfilter([ 8 | { foo: 'one', bar: 1 }, 9 | { foo: 'two', bar: 2 }, 10 | { foo: 'three', bar: 3 }, 11 | { foo: 'one', bar: 4 }, 12 | { foo: 'one', bar: 5 }, 13 | { foo: 'two', bar: 6 }, 14 | ]); 15 | 16 | var dim = data.dimension(function(d) { return d.foo; }); 17 | var group = dim.group(); 18 | 19 | filterDim = data.dimension(function(d) { return d.bar; }); 20 | 21 | reductio() 22 | .min(function(d) { return d.bar; }) 23 | .median(true)(group); 24 | 25 | median = group; 26 | }); 27 | 28 | it('has three groups', function (done) { 29 | expect(median.top(Infinity).length).toEqual(3); 30 | done(); 31 | }); 32 | 33 | it('grouping have the right medians', function (done) { 34 | var values = {}; 35 | median.top(Infinity).forEach(function (d) { 36 | values[d.key] = d.value; 37 | }); 38 | 39 | expect(Math.round(values['one'].median)).toEqual(Math.round(4)); 40 | expect(Math.round(values['two'].median)).toEqual(Math.round(4)); 41 | expect(Math.round(values['three'].median)).toEqual(Math.round(3)); 42 | 43 | filterDim.filter(1); 44 | 45 | expect(Math.round(values['one'].median)).toEqual(Math.round(1)); 46 | expect(values['two'].median).toBeUndefined(); 47 | expect(values['three'].median).toBeUndefined(); 48 | 49 | filterDim.filterAll(); 50 | 51 | expect(Math.round(values['one'].median)).toEqual(Math.round(4)); 52 | expect(Math.round(values['two'].median)).toEqual(Math.round(4)); 53 | expect(Math.round(values['three'].median)).toEqual(Math.round(3)); 54 | done(); 55 | 56 | }); 57 | }); -------------------------------------------------------------------------------- /test/value-list.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio value list', function () { 3 | var group; 4 | var filterDim; 5 | 6 | beforeEach(function () { 7 | var data = crossfilter([ 8 | { foo: 'one', bar: 1 }, 9 | { foo: 'two', bar: 2 }, 10 | { foo: 'two', bar: 3 }, 11 | { foo: 'one', bar: 1 }, 12 | { foo: 'one', bar: 5 }, 13 | { foo: 'two', bar: 2 }, 14 | ]); 15 | 16 | var dim = data.dimension(function(d) { return d.foo; }); 17 | group = dim.group(); 18 | 19 | filterDim = data.dimension(function(d) { return d.bar; }); 20 | 21 | var reducer = reductio() 22 | .sum(function (d) { return d.bar; }) 23 | .valueList(function (d) { return d.bar; }); 24 | 25 | reducer(group); 26 | }); 27 | 28 | it('has two groups', function () { 29 | expect(group.top(Infinity).length).toEqual(2); 30 | }); 31 | 32 | it('grouping have the right sums', function () { 33 | var values = {}; 34 | group.top(Infinity).forEach(function (d) { 35 | values[d.key] = d.value; 36 | }); 37 | 38 | expect(values['one'].sum).toEqual(7); 39 | expect(values['two'].sum).toEqual(7); 40 | }); 41 | 42 | it('properly tracks values', function () { 43 | var values = {}; 44 | group.top(Infinity).forEach(function (d) { 45 | values[d.key] = d.value; 46 | }); 47 | 48 | expect(values['one'].valueList).toEqual([1, 1, 5]); 49 | expect(values['two'].valueList).toEqual([2, 2, 3]); 50 | 51 | filterDim.filter(1); 52 | 53 | expect(values['one'].valueList).toEqual([1, 1]); 54 | expect(values['two'].valueList).toEqual([]); 55 | 56 | filterDim.filter(3); 57 | 58 | expect(values['one'].valueList).toEqual([]); 59 | expect(values['two'].valueList).toEqual([3]); 60 | 61 | filterDim.filterAll(); 62 | 63 | expect(values['one'].valueList).toEqual([1, 1, 5]); 64 | expect(values['two'].valueList).toEqual([2, 2, 3]); 65 | }); 66 | }); -------------------------------------------------------------------------------- /test/sortBy.test.js: -------------------------------------------------------------------------------- 1 | function descending(a, b) { 2 | return a < b ? 1 : a > b ? -1 : a >= b ? 0 : NaN; 3 | } 4 | 5 | describe('Reductio sortBy', function () { 6 | var group, reducer, groupString; 7 | 8 | beforeEach(function () { 9 | var data = crossfilter([ 10 | {foo: 1, bar: 6}, 11 | {foo: 2, bar: 5}, 12 | {foo: 3, bar: 4}, 13 | {foo: 4, bar: 3}, 14 | {foo: 5, bar: 2}, 15 | {foo: 6, bar: 1} 16 | ]); 17 | 18 | var dim = data.dimension(function(d) { return d.foo; }); 19 | group = dim.group(); 20 | groupString = dim.group(); 21 | 22 | reducer = reductio() 23 | .sum('bar') 24 | .count(true) 25 | .avg(true); 26 | 27 | reducer(group); 28 | }); 29 | 30 | it('has six groups', function () { 31 | expect(group.post().sortBy('value.sum')().length).toEqual(6); 32 | }); 33 | 34 | it('orders correctly', function () { 35 | var all = group.post().sortBy('value.sum', descending)(); 36 | for(var i = 0; i < all.length; ++i){ 37 | expect(all[i].value.sum).toBe(6-i); 38 | } 39 | all = group.post().sortBy('value.sum')(); 40 | for(i = 0; i < all.length; ++i){ 41 | expect(all[i].value.sum).toBe(i+1); 42 | } 43 | }); 44 | 45 | it('works with functions', function(){ 46 | var all = group.post().sortBy(function(d){ 47 | return -d.value.sum; 48 | })(); 49 | for(var i = 0; i < all.length; ++i){ 50 | expect(all[i].value.sum).toBe(6-i); 51 | } 52 | all = group.post().sortBy(function(d){ 53 | return d.value.sum; 54 | })(); 55 | for(i = 0; i < all.length; ++i){ 56 | expect(all[i].value.sum).toBe(i+1); 57 | } 58 | }); 59 | 60 | it('works with cap', function(){ 61 | var all = group.post().sortBy('value.sum', descending).cap(3)(); 62 | expect(all[0].value.sum).toBe(6); 63 | expect(all[1].value.sum).toBe(5); 64 | expect(all[2].value.sum).toBe(10); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/data-list.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio data list', function() { 3 | 4 | var max = {}, 5 | filterDim; 6 | 7 | beforeEach(function() { 8 | var data = crossfilter([{ 9 | foo: 'one', 10 | bar: 1 11 | }, { 12 | foo: 'two', 13 | bar: 2 14 | }, { 15 | foo: 'three', 16 | bar: 3 17 | }, { 18 | foo: 'one', 19 | bar: 4 20 | }, { 21 | foo: 'one', 22 | bar: 5 23 | }, { 24 | foo: 'two', 25 | bar: 6 26 | }, ]); 27 | 28 | var dim = data.dimension(function(d) { 29 | return d.foo; 30 | }); 31 | var group = dim.group(); 32 | 33 | filterDim = data.dimension(function(d) { 34 | return d.bar; 35 | }); 36 | 37 | reductio() 38 | .dataList(true)(group); 39 | 40 | max = group; 41 | 42 | }); 43 | 44 | it('has three groups', function(done) { 45 | expect(max.top(Infinity).length).toEqual(3); 46 | done(); 47 | }); 48 | 49 | it('grouping have the right maximums', function(done) { 50 | 51 | var rows = {}; 52 | max.top(Infinity).forEach(function(d) { 53 | rows[d.key] = d.value; 54 | }); 55 | 56 | expect(Math.round(rows['one'].dataList.length)).toEqual(Math.round(3)); 57 | expect(Math.round(rows['two'].dataList.length)).toEqual(Math.round(2)); 58 | expect(Math.round(rows['three'].dataList.length)).toEqual(Math.round(1)); 59 | 60 | filterDim.filterExact(1); 61 | 62 | expect(Math.round(rows['one'].dataList.length)).toEqual(Math.round(1)); 63 | expect(rows['two'].dataList.length).toEqual(Math.round(0)); 64 | expect(rows['three'].dataList.length).toEqual(Math.round(0)); 65 | 66 | filterDim.filterAll(); 67 | 68 | expect(Math.round(rows['one'].dataList.length)).toEqual(Math.round(3)); 69 | expect(Math.round(rows['two'].dataList.length)).toEqual(Math.round(2)); 70 | expect(Math.round(rows['three'].dataList.length)).toEqual(Math.round(1)); 71 | done(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/sum.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio sum', function () { 3 | var group, groupString; 4 | 5 | beforeEach(function () { 6 | var data = crossfilter([ 7 | { foo: 'one', bar: 1 }, 8 | { foo: 'two', bar: 2 }, 9 | { foo: 'three', bar: 3 }, 10 | { foo: 'one', bar: 4 }, 11 | { foo: 'one', bar: 5 }, 12 | { foo: 'two', bar: 6 }, 13 | ]); 14 | 15 | var dim = data.dimension(function(d) { return d.foo; }); 16 | group = dim.group(); 17 | groupString = dim.group(); 18 | 19 | var reducer = reductio() 20 | .sum(function(d) { return d.bar; }) 21 | .count(true); 22 | 23 | var reducerString = reductio() 24 | .sum('bar') 25 | .count(true); 26 | 27 | reducer(group); 28 | reducerString(groupString); 29 | }); 30 | 31 | it('has three groups', function () { 32 | expect(group.top(Infinity).length).toEqual(3); 33 | expect(groupString.top(Infinity).length).toEqual(3); 34 | }); 35 | 36 | it('grouping have the right sums', function () { 37 | var values = {}; 38 | groupString.top(Infinity).forEach(function (d) { 39 | values[d.key] = d.value; 40 | }); 41 | 42 | expect(values['one'].sum).toEqual(10); 43 | expect(values['two'].sum).toEqual(8); 44 | expect(values['three'].sum).toEqual(3); 45 | }); 46 | 47 | it('string grouping has the right sums', function () { 48 | var values = {}; 49 | group.top(Infinity).forEach(function (d) { 50 | values[d.key] = d.value; 51 | }); 52 | 53 | expect(values['one'].sum).toEqual(10); 54 | expect(values['two'].sum).toEqual(8); 55 | expect(values['three'].sum).toEqual(3); 56 | }); 57 | 58 | it('grouping plays nicely with count', function () { 59 | var values = {}; 60 | group.top(Infinity).forEach(function (d) { 61 | values[d.key] = d.value; 62 | }); 63 | 64 | expect(values['one'].count).toEqual(3); 65 | expect(values['two'].count).toEqual(2); 66 | expect(values['three'].count).toEqual(1); 67 | }); 68 | }); -------------------------------------------------------------------------------- /test/aliasProp.test.js: -------------------------------------------------------------------------------- 1 | // Alias tests 2 | describe('Alias property', function () { 3 | var group; 4 | 5 | beforeEach(function () { 6 | var data = crossfilter([ 7 | { foo: 'one', num: 1 }, 8 | { foo: 'two', num: 2 }, 9 | { foo: 'three', num: 3 }, 10 | { foo: 'one', num: 3 }, 11 | { foo: 'one', num: 2 }, 12 | { foo: 'two', num: 2 }, 13 | ]); 14 | 15 | var dim = data.dimension(function(d) { return d.foo; }); 16 | group = dim.group(); 17 | 18 | var reducer = reductio() 19 | .count(true) 20 | .sum(function(d) { return +d.num; }) 21 | .aliasProp({ 22 | newCount: function(g) { return g.count; }, 23 | average: function(g) { return g.sum / g.count; }, 24 | description: function(g, v) { return v.foo; } 25 | }); 26 | 27 | reducer(group); 28 | }); 29 | 30 | it('has three groups', function () { 31 | expect(group.top(Infinity).length).toEqual(3); 32 | }); 33 | 34 | it('grouping have the right counts', function () { 35 | var values = {}; 36 | group.top(Infinity).forEach(function (d) { 37 | values[d.key] = d.value; 38 | }); 39 | 40 | expect(values['one'].newCount).toEqual(3); 41 | expect(values['two'].newCount).toEqual(2); 42 | expect(values['three'].newCount).toEqual(1); 43 | }); 44 | 45 | it('grouping have the right averages', function () { 46 | var values = {}; 47 | group.top(Infinity).forEach(function (d) { 48 | values[d.key] = d.value; 49 | }); 50 | 51 | expect(values['one'].average).toEqual(2); 52 | expect(values['two'].average).toEqual(2); 53 | expect(values['three'].average).toEqual(3); 54 | }); 55 | 56 | it('grouping have the right descriptions', function () { 57 | var values = {}; 58 | group.top(Infinity).forEach(function (d) { 59 | values[d.key] = d.value; 60 | }); 61 | 62 | expect(values['one'].description).toEqual('one'); 63 | expect(values['two'].description).toEqual('two'); 64 | expect(values['three'].description).toEqual('three'); 65 | }); 66 | }); -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | Notes on development, possible changes, possible features 2 | 3 | API definition: 4 | ``` 5 | reductio <-- crossfilter (optional?) 6 | dimension(accessor | property, type) 7 | group(accessor | property, type) 8 | accessor (returns the accessor) 9 | count 10 | sum 11 | avg <-- change to 'average'? 12 | min <-- change to 'minimum'? 13 | max <-- change to 'maximum'? 14 | median 15 | sumOfSq <-- name change? 16 | std <-- name change? 17 | histogram(bins, value) ?? 18 | x histogramBins 19 | x histogramValue 20 | value 21 | ... 22 | filter 23 | nest 24 | alias 25 | aliasProp <-- name change? Or just get rid of it - efficiency issues 26 | uniqueBy (formerly exception) / distinct? 27 | count 28 | sum 29 | groupAll 30 | ... 31 | accessor (returns the accessor) 32 | groups - array of current groups on the dimension 33 | dimensions - array of current dimensions 34 | ``` 35 | 36 | Checks on dimension accessors for basic natural-ordered-ness. More less just checking if it always returns values of the same type for different types of edge-case inputs (text, number, 0, Infinity, "", [], {}, undefined, null, NaN) 37 | 38 | Dimension accessor helpers? We have some pretty complex time-dimension accessors in Palladio. 39 | 40 | 41 | 42 | Pre-aggregation is intended to serve as a framework for thinking through a 2-stage aggregation process. 43 | 44 | Stage 1 - Usually server-side, aggregating over all dimensions that are not relevant in the Reductio/Crossfilter and then sending to the client-side 45 | 46 | Stage 2 - Crossfilter/Reductio client-side aggregation with pure client-side filtering and re-aggregation 47 | 48 | The intent is to allow interactive filtering and aggregation on data sets that are too large to fit client-side. The problem is that 2-stage aggregations can be challenging. 49 | 50 | Pre-aggregation strategies: 51 | ``` 52 | count - sum a fake '1' dimension - can use the same dimension pre/post aggregation 53 | sum - commutative across aggregation 54 | avg - pure post-aggregation calculation 55 | min - commutative 56 | max - commutative 57 | median - ??? 58 | sumOfSq - ??? 59 | std - ??? 60 | histogram - ??? 61 | value - ??? 62 | filter - pure push-down (need a non-closure definition syntax) 63 | nest - ??? 64 | alias - pure post-aggregation calculation 65 | aliasProp - pure post-aggregation calculation 66 | exception - valueList provided, concat in aggregation ??? 67 | count - see above? 68 | sum - see above? 69 | ``` 70 | -------------------------------------------------------------------------------- /test/histogram.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio histogram', function () { 3 | var group; 4 | var filterDim; 5 | 6 | beforeEach(function () { 7 | var data = crossfilter([ 8 | { foo: 'one', bar: 1 }, 9 | { foo: 'two', bar: 2 }, 10 | { foo: 'three', bar: 3 }, 11 | { foo: 'one', bar: 4 }, 12 | { foo: 'one', bar: 5 }, 13 | { foo: 'two', bar: 6 }, 14 | ]); 15 | 16 | var dim = data.dimension(function(d) { return d.foo; }); 17 | group = dim.group(); 18 | 19 | filterDim = data.dimension(function (d) { return d.bar; }); 20 | 21 | var reducer = reductio() 22 | .histogramBins([0,2,6,10]) 23 | .histogramValue(function(d) { return d.bar; }) 24 | .count(true); 25 | 26 | reducer(group); 27 | }); 28 | 29 | it('has three groups', function () { 30 | expect(group.top(Infinity).length).toEqual(3); 31 | }); 32 | 33 | it('grouping have the right histograms', function () { 34 | var values = {}; 35 | group.top(Infinity).forEach(function (d) { 36 | values[d.key] = d.value; 37 | }); 38 | 39 | expect(values['one'].histogram.map(function(d) { return d.y; })).toEqual([1,2,0]); 40 | expect(values['two'].histogram.map(function(d) { return d.y; })).toEqual([0,1,1]); 41 | expect(values['three'].histogram.map(function(d) { return d.y; })).toEqual([0,1,0]); 42 | 43 | filterDim.filter([2,6]); 44 | 45 | expect(values['one'].histogram.map(function(d) { return d.y; })).toEqual([0,2,0]); 46 | expect(values['two'].histogram.map(function(d) { return d.y; })).toEqual([0,1,0]); 47 | expect(values['three'].histogram.map(function(d) { return d.y; })).toEqual([0,1,0]); 48 | 49 | filterDim.filterAll(); 50 | 51 | expect(values['one'].histogram.map(function(d) { return d.y; })).toEqual([1,2,0]); 52 | expect(values['two'].histogram.map(function(d) { return d.y; })).toEqual([0,1,1]); 53 | expect(values['three'].histogram.map(function(d) { return d.y; })).toEqual([0,1,0]); 54 | }); 55 | 56 | it('grouping plays nicely with count', function () { 57 | var values = {}; 58 | group.top(Infinity).forEach(function (d) { 59 | values[d.key] = d.value; 60 | }); 61 | 62 | expect(values['one'].count).toEqual(3); 63 | expect(values['two'].count).toEqual(2); 64 | expect(values['three'].count).toEqual(1); 65 | }); 66 | }); -------------------------------------------------------------------------------- /src/reductio.js: -------------------------------------------------------------------------------- 1 | import build from './build.js'; 2 | import accessors from './accessors.js'; 3 | import params from './parameters.js'; 4 | import postprocess from './postprocess'; 5 | import postprocessors from './postprocessors'; 6 | import crossfilter from 'crossfilter2'; 7 | 8 | function reductio() { 9 | var parameters = params(); 10 | 11 | var funcs = {}; 12 | 13 | function my(group) { 14 | // Start fresh each time. 15 | funcs = { 16 | reduceAdd: function(p) { return p; }, 17 | reduceRemove: function(p) { return p; }, 18 | reduceInitial: function () { return {}; }, 19 | }; 20 | 21 | build.build(parameters, funcs); 22 | 23 | // If we're doing groupAll 24 | if(parameters.groupAll) { 25 | if(group.top) { 26 | console.warn("'groupAll' is defined but attempting to run on a standard dimension.group(). Must run on dimension.groupAll()."); 27 | } else { 28 | var bisect = crossfilter.bisect.by(function(d) { return d.key; }).left; 29 | var i, j; 30 | var keys; 31 | var keysLength; 32 | var k; // Key 33 | group.reduce( 34 | function(p, v, nf) { 35 | keys = parameters.groupAll(v); 36 | keysLength = keys.length; 37 | for(j=0;j 3; }); 22 | 23 | var map = _.keyBy(reducer(dim.group()).top(Infinity), 'key'); 24 | expect(map.one.value.count).toEqual(2); 25 | expect(map.two.value.count).toEqual(1); 26 | expect(map.three.value.count).toEqual(0); 27 | }); 28 | 29 | it('groups an averaging reducer', function() { 30 | var reducer = reductio() 31 | .filter(function(d) { return d.bar > 3; }) 32 | .avg(function (d) { return d.bar; }); 33 | 34 | var map = _.keyBy(reducer(dim.group()).top(Infinity), 'key'); 35 | expect(map.one.value.avg).toBeCloseTo(4.5); 36 | expect(map.two.value.avg).toEqual(6); 37 | expect(map.three.value.avg).toEqual(0); 38 | }); 39 | 40 | it('handles value lists', function() { 41 | var reducer = reductio(); 42 | reducer.value("gt3").filter(function(d) { return d.bar > 3; }).count(true); 43 | reducer.value("even").filter(function(d) { return d.bar % 2 === 0; }).count(true); 44 | 45 | var map = _.keyBy(reducer(dim.group()).top(Infinity), 'key'); 46 | expect(map.one.value.gt3.count).toEqual(2); 47 | expect(map.one.value.even.count).toEqual(1); 48 | expect(map.two.value.gt3.count).toEqual(1); 49 | expect(map.two.value.even.count).toEqual(2); 50 | expect(map.three.value.gt3.count).toEqual(0); 51 | expect(map.three.value.even.count).toEqual(0); 52 | }); 53 | 54 | it('handles sparsely-populated data', function() { 55 | var reducer = reductio() 56 | .filter(function(d) { return typeof d.sparseVal !== "undefined"; }) 57 | .avg(function(d) { return d.sparseVal; }); 58 | 59 | var map = _.keyBy(reducer(dim.group()).top(Infinity), 'key'); 60 | expect(map.one.value.count).toEqual(2); 61 | expect(map.one.value.avg).toBeCloseTo(10); 62 | expect(map.two.value.count).toEqual(0); 63 | expect(map.three.value.count).toEqual(0); 64 | }); 65 | it('exposes data lifecycle information from Crossfilter2', function() { 66 | var reducer = reductio(); 67 | reducer.value('addOnly') 68 | .count(true) 69 | .filter(function(d, nf) { return d.bar > 3 && nf; }); 70 | reducer.value('normal') 71 | .count(true) 72 | .filter(function(d, nf) { return d.bar > 3; }); 73 | 74 | var group = reducer(dim.group()); 75 | 76 | var map = _.keyBy(group.top(Infinity), 'key'); 77 | expect(map.one.value.addOnly.count).toEqual(2); 78 | expect(map.two.value.addOnly.count).toEqual(1); 79 | expect(map.three.value.addOnly.count).toEqual(0); 80 | expect(map.one.value.normal.count).toEqual(2); 81 | expect(map.two.value.normal.count).toEqual(1); 82 | expect(map.three.value.normal.count).toEqual(0); 83 | 84 | dim2.filter('one'); 85 | 86 | map = _.keyBy(group.top(Infinity), 'key'); 87 | expect(map.one.value.addOnly.count).toEqual(2); 88 | expect(map.two.value.addOnly.count).toEqual(1); 89 | expect(map.three.value.addOnly.count).toEqual(0); 90 | expect(map.one.value.normal.count).toEqual(2); 91 | expect(map.two.value.normal.count).toEqual(0); 92 | expect(map.three.value.normal.count).toEqual(0); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/nest.test.js: -------------------------------------------------------------------------------- 1 | // Counting tests 2 | describe('Reductio nest', function () { 3 | var group; 4 | var filterDim; 5 | 6 | beforeEach(function () { 7 | var data = crossfilter([ 8 | { foo: 'one', bar: 1, city: 'NYC', type: 'car' }, 9 | { foo: 'two', bar: 2, city: 'NYC', type: 'bike' }, 10 | { foo: 'two', bar: 3, city: 'NYC', type: 'bike' }, 11 | { foo: 'one', bar: 1, city: 'LA', type: 'bike' }, 12 | { foo: 'one', bar: 5, city: 'LA', type: 'car' }, 13 | { foo: 'two', bar: 2, city: 'LA', type: 'car' }, 14 | ]); 15 | 16 | var dim = data.dimension(function(d) { return d.foo; }); 17 | group = dim.group(); 18 | 19 | filterDim = data.dimension(function(d) { return d.bar; }); 20 | 21 | var reducer = reductio() 22 | .nest([function(d) { return d.city; }, function(d) { return d.type; }]); 23 | 24 | reducer(group); 25 | }); 26 | 27 | it('has two groups', function () { 28 | expect(group.top(Infinity).length).toEqual(2); 29 | }); 30 | 31 | it('grouping have the right nests', function () { 32 | var values = {}; 33 | group.top(Infinity).forEach(function (d) { 34 | values[d.key] = d.value; 35 | }); 36 | 37 | // 'one.nest.values' should look like: 38 | // [ { key: 'NYC', values: [ 39 | // { key: 'car', values: [ 40 | // { foo: 'one', bar: 1, city: 'NYC', type: 'car' } 41 | // ]}, 42 | // { key: 'bike', values: [ 43 | // { foo: 'two', bar: 2, city: 'NYC', type: 'bike' }, 44 | // { foo: 'two', bar: 3, city: 'NYC', type: 'bike' } 45 | // ]} 46 | // ]}, 47 | // { key: 'LA', values: [ 48 | // { key: 'car', values: [ 49 | // { foo: 'one', bar: 5, city: 'LA', type: 'car' }, 50 | // { foo: 'two', bar: 2, city: 'LA', type: 'car' } 51 | // ]}, 52 | // { key: 'bike', values: [ 53 | // { foo: 'one', bar: 1, city: 'LA', type: 'bike' } 54 | // ]} 55 | // ]} 56 | // ] 57 | 58 | var OneNYCcars = values['one'].nest 59 | .filter(function(d) { return d.key === 'NYC'; })[0] 60 | .values.filter(function(d) { return d.key === 'car'; })[0] 61 | .values; 62 | 63 | var TwoNYCbike = values['two'].nest 64 | .filter(function(d) { return d.key === 'NYC'; })[0] 65 | .values.filter(function(d) { return d.key === 'bike'; })[0] 66 | .values; 67 | 68 | var TwoLAbike = values['two'].nest 69 | .filter(function(d) { return d.key === 'LA'; })[0] 70 | .values.filter(function(d) { return d.key === 'bike'; })[0]; 71 | 72 | var TwoLAcars = values['two'].nest 73 | .filter(function(d) { return d.key === 'LA'; })[0] 74 | .values.filter(function(d) { return d.key === 'car'; })[0] 75 | .values; 76 | 77 | expect(values['one'].nest.length).toEqual(2); 78 | expect(values['two'].nest.length).toEqual(2); 79 | expect(OneNYCcars.length).toEqual(1); 80 | expect(TwoNYCbike.length).toEqual(2); 81 | expect(TwoLAcars.length).toEqual(1); 82 | expect(TwoLAbike).toBeUndefined(); 83 | 84 | filterDim.filter(2); 85 | 86 | expect(values['one'].nest.length).toEqual(2); // Should be 0. 87 | expect(values['two'].nest.length).toEqual(2); 88 | expect(OneNYCcars.length).toEqual(0); 89 | expect(TwoNYCbike.length).toEqual(1); 90 | expect(TwoLAcars.length).toEqual(1); 91 | expect(TwoLAbike).toBeUndefined(); 92 | 93 | filterDim.filterAll(); 94 | 95 | expect(values['one'].nest.length).toEqual(2); 96 | expect(values['two'].nest.length).toEqual(2); 97 | expect(OneNYCcars.length).toEqual(1); 98 | expect(TwoNYCbike.length).toEqual(2); 99 | expect(TwoLAcars.length).toEqual(1); 100 | expect(TwoLAbike).toBeUndefined(); 101 | }); 102 | }); -------------------------------------------------------------------------------- /src/accessors.js: -------------------------------------------------------------------------------- 1 | import parameters from './parameters.js'; 2 | 3 | function assign(target) { 4 | if (target == null) { 5 | throw new TypeError('Cannot convert undefined or null to object'); 6 | } 7 | 8 | var output = Object(target); 9 | for (var index = 1; index < arguments.length; ++index) { 10 | var source = arguments[index]; 11 | if (source != null) { 12 | for (var nextKey in source) { 13 | if(source.hasOwnProperty(nextKey)) { 14 | output[nextKey] = source[nextKey]; 15 | } 16 | } 17 | } 18 | } 19 | return output; 20 | } 21 | 22 | function accessor_build(obj, p) { 23 | // obj.order = function(value) { 24 | // if (!arguments.length) return p.order; 25 | // p.order = value; 26 | // return obj; 27 | // }; 28 | 29 | // Converts a string to an accessor function 30 | function accessorify(v) { 31 | if( typeof v === 'string' ) { 32 | // Rewrite to a function 33 | var tempValue = v; 34 | var func = function (d) { return d[tempValue]; } 35 | return func; 36 | } else { 37 | return v; 38 | } 39 | } 40 | 41 | // Converts a string to an accessor function 42 | function accessorifyNumeric(v) { 43 | if( typeof v === 'string' ) { 44 | // Rewrite to a function 45 | var tempValue = v; 46 | var func = function (d) { return +d[tempValue]; } 47 | return func; 48 | } else { 49 | return v; 50 | } 51 | } 52 | 53 | obj.fromObject = function(value) { 54 | if(!arguments.length) return p; 55 | assign(p, value); 56 | return obj; 57 | }; 58 | 59 | obj.toObject = function() { 60 | return p; 61 | }; 62 | 63 | obj.count = function(value, propName) { 64 | if (!arguments.length) return p.count; 65 | if (!propName) { 66 | propName = 'count'; 67 | } 68 | p.count = propName; 69 | return obj; 70 | }; 71 | 72 | obj.sum = function(value) { 73 | if (!arguments.length) return p.sum; 74 | 75 | value = accessorifyNumeric(value); 76 | 77 | p.sum = value; 78 | return obj; 79 | }; 80 | 81 | obj.avg = function(value) { 82 | if (!arguments.length) return p.avg; 83 | 84 | value = accessorifyNumeric(value); 85 | 86 | // We can take an accessor function, a boolean, or a string 87 | if( typeof value === 'function' ) { 88 | if(p.sum && p.sum !== value) console.warn('SUM aggregation is being overwritten by AVG aggregation'); 89 | p.sum = value; 90 | p.avg = true; 91 | p.count = 'count'; 92 | } else { 93 | p.avg = value; 94 | } 95 | return obj; 96 | }; 97 | 98 | obj.exception = function(value) { 99 | if (!arguments.length) return p.exceptionAccessor; 100 | 101 | value = accessorify(value); 102 | 103 | p.exceptionAccessor = value; 104 | return obj; 105 | }; 106 | 107 | obj.filter = function(value) { 108 | if (!arguments.length) return p.filter; 109 | p.filter = value; 110 | return obj; 111 | }; 112 | 113 | obj.valueList = function(value) { 114 | if (!arguments.length) return p.valueList; 115 | 116 | value = accessorify(value); 117 | 118 | p.valueList = value; 119 | return obj; 120 | }; 121 | 122 | obj.median = function(value) { 123 | if (!arguments.length) return p.median; 124 | 125 | value = accessorifyNumeric(value); 126 | 127 | if(typeof value === 'function') { 128 | if(p.valueList && p.valueList !== value) console.warn('VALUELIST accessor is being overwritten by median aggregation'); 129 | p.valueList = value; 130 | } 131 | p.median = value; 132 | return obj; 133 | }; 134 | 135 | obj.min = function(value) { 136 | if (!arguments.length) return p.min; 137 | 138 | value = accessorifyNumeric(value); 139 | 140 | if(typeof value === 'function') { 141 | if(p.valueList && p.valueList !== value) console.warn('VALUELIST accessor is being overwritten by min aggregation'); 142 | p.valueList = value; 143 | } 144 | p.min = value; 145 | return obj; 146 | }; 147 | 148 | obj.max = function(value) { 149 | if (!arguments.length) return p.max; 150 | 151 | value = accessorifyNumeric(value); 152 | 153 | if(typeof value === 'function') { 154 | if(p.valueList && p.valueList !== value) console.warn('VALUELIST accessor is being overwritten by max aggregation'); 155 | p.valueList = value; 156 | } 157 | p.max = value; 158 | return obj; 159 | }; 160 | 161 | obj.exceptionCount = function(value) { 162 | if (!arguments.length) return p.exceptionCount; 163 | 164 | value = accessorify(value); 165 | 166 | if( typeof value === 'function' ) { 167 | if(p.exceptionAccessor && p.exceptionAccessor !== value) console.warn('EXCEPTION accessor is being overwritten by exception count aggregation'); 168 | p.exceptionAccessor = value; 169 | p.exceptionCount = true; 170 | } else { 171 | p.exceptionCount = value; 172 | } 173 | return obj; 174 | }; 175 | 176 | obj.exceptionSum = function(value) { 177 | if (!arguments.length) return p.exceptionSum; 178 | 179 | value = accessorifyNumeric(value); 180 | 181 | p.exceptionSum = value; 182 | return obj; 183 | }; 184 | 185 | obj.histogramValue = function(value) { 186 | if (!arguments.length) return p.histogramValue; 187 | 188 | value = accessorifyNumeric(value); 189 | 190 | p.histogramValue = value; 191 | return obj; 192 | }; 193 | 194 | obj.histogramBins = function(value) { 195 | if (!arguments.length) return p.histogramThresholds; 196 | p.histogramThresholds = value; 197 | return obj; 198 | }; 199 | 200 | obj.std = function(value) { 201 | if (!arguments.length) return p.std; 202 | 203 | value = accessorifyNumeric(value); 204 | 205 | if(typeof(value) === 'function') { 206 | p.sumOfSquares = value; 207 | p.sum = value; 208 | p.count = 'count'; 209 | p.std = true; 210 | } else { 211 | p.std = value; 212 | } 213 | return obj; 214 | }; 215 | 216 | obj.sumOfSq = function(value) { 217 | if (!arguments.length) return p.sumOfSquares; 218 | 219 | value = accessorifyNumeric(value); 220 | 221 | p.sumOfSquares = value; 222 | return obj; 223 | }; 224 | 225 | obj.value = function(value, accessor) { 226 | if (!arguments.length || typeof value !== 'string' ) { 227 | console.error("'value' requires a string argument."); 228 | } else { 229 | if(!p.values) p.values = {}; 230 | p.values[value] = {}; 231 | p.values[value].parameters = parameters(); 232 | accessor_build(p.values[value], p.values[value].parameters); 233 | if(accessor) p.values[value].accessor = accessor; 234 | return p.values[value]; 235 | } 236 | }; 237 | 238 | obj.nest = function(keyAccessorArray) { 239 | if(!arguments.length) return p.nestKeys; 240 | 241 | keyAccessorArray.map(accessorify); 242 | 243 | p.nestKeys = keyAccessorArray; 244 | return obj; 245 | }; 246 | 247 | obj.alias = function(propAccessorObj) { 248 | if(!arguments.length) return p.aliasKeys; 249 | p.aliasKeys = propAccessorObj; 250 | return obj; 251 | }; 252 | 253 | obj.aliasProp = function(propAccessorObj) { 254 | if(!arguments.length) return p.aliasPropKeys; 255 | p.aliasPropKeys = propAccessorObj; 256 | return obj; 257 | }; 258 | 259 | obj.groupAll = function(groupTest) { 260 | if(!arguments.length) return p.groupAll; 261 | p.groupAll = groupTest; 262 | return obj; 263 | }; 264 | 265 | obj.dataList = function(value) { 266 | if (!arguments.length) return p.dataList; 267 | p.dataList = value; 268 | return obj; 269 | }; 270 | 271 | obj.custom = function(addRemoveInitialObj) { 272 | if (!arguments.length) return p.custom; 273 | p.custom = addRemoveInitialObj; 274 | return obj; 275 | }; 276 | 277 | } 278 | 279 | var accessors = { 280 | build: accessor_build 281 | }; 282 | 283 | export default accessors; 284 | -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | import filter from './filter.js'; 2 | import count from './count.js'; 3 | import sum from './sum.js'; 4 | import avg from './avg.js'; 5 | import median from './median.js'; 6 | import min from './min.js'; 7 | import max from './max.js'; 8 | import value_count from './value-count.js'; 9 | import value_list from './value-list.js'; 10 | import exception_count from './exception-count.js'; 11 | import exception_sum from './exception-sum.js'; 12 | import histogram from './histogram.js'; 13 | import sum_of_sq from './sum-of-squares.js'; 14 | import std from './std.js'; 15 | import nest from './nest.js'; 16 | import alias from './alias.js'; 17 | import alias_prop from './aliasProp.js'; 18 | import data_list from './data-list.js'; 19 | import custom from './custom.js'; 20 | 21 | function build_function(p, f, path) { 22 | // We have to build these functions in order. Eventually we can include dependency 23 | // information and create a dependency graph if the process becomes complex enough. 24 | 25 | if(!path) path = function (d) { return d; }; 26 | 27 | // Keep track of the original reducers so that filtering can skip back to 28 | // them if this particular value is filtered out. 29 | var origF = { 30 | reduceAdd: f.reduceAdd, 31 | reduceRemove: f.reduceRemove, 32 | reduceInitial: f.reduceInitial 33 | }; 34 | 35 | if(p.count || p.std) { 36 | f.reduceAdd = count.add(f.reduceAdd, path, p.count); 37 | f.reduceRemove = count.remove(f.reduceRemove, path, p.count); 38 | f.reduceInitial = count.initial(f.reduceInitial, path, p.count); 39 | } 40 | 41 | if(p.sum) { 42 | f.reduceAdd = sum.add(p.sum, f.reduceAdd, path); 43 | f.reduceRemove = sum.remove(p.sum, f.reduceRemove, path); 44 | f.reduceInitial = sum.initial(f.reduceInitial, path); 45 | } 46 | 47 | if(p.avg) { 48 | if(!p.count || !p.sum) { 49 | console.error("You must set .count(true) and define a .sum(accessor) to use .avg(true)."); 50 | } else { 51 | f.reduceAdd = avg.add(p.sum, f.reduceAdd, path); 52 | f.reduceRemove = avg.remove(p.sum, f.reduceRemove, path); 53 | f.reduceInitial = avg.initial(f.reduceInitial, path); 54 | } 55 | } 56 | 57 | // The unique-only reducers come before the value_count reducers. They need to check if 58 | // the value is already in the values array on the group. They should only increment/decrement 59 | // counts if the value not in the array or the count on the value is 0. 60 | if(p.exceptionCount) { 61 | if(!p.exceptionAccessor) { 62 | console.error("You must define an .exception(accessor) to use .exceptionCount(true)."); 63 | } else { 64 | f.reduceAdd = exception_count.add(p.exceptionAccessor, f.reduceAdd, path); 65 | f.reduceRemove = exception_count.remove(p.exceptionAccessor, f.reduceRemove, path); 66 | f.reduceInitial = exception_count.initial(f.reduceInitial, path); 67 | } 68 | } 69 | 70 | if(p.exceptionSum) { 71 | if(!p.exceptionAccessor) { 72 | console.error("You must define an .exception(accessor) to use .exceptionSum(accessor)."); 73 | } else { 74 | f.reduceAdd = exception_sum.add(p.exceptionAccessor, p.exceptionSum, f.reduceAdd, path); 75 | f.reduceRemove = exception_sum.remove(p.exceptionAccessor, p.exceptionSum, f.reduceRemove, path); 76 | f.reduceInitial = exception_sum.initial(f.reduceInitial, path); 77 | } 78 | } 79 | 80 | // Maintain the values array. 81 | if(p.valueList || p.median || p.min || p.max) { 82 | f.reduceAdd = value_list.add(p.valueList, f.reduceAdd, path); 83 | f.reduceRemove = value_list.remove(p.valueList, f.reduceRemove, path); 84 | f.reduceInitial = value_list.initial(f.reduceInitial, path); 85 | } 86 | 87 | // Maintain the data array. 88 | if(p.dataList) { 89 | f.reduceAdd = data_list.add(p.dataList, f.reduceAdd, path); 90 | f.reduceRemove = data_list.remove(p.dataList, f.reduceRemove, path); 91 | f.reduceInitial = data_list.initial(f.reduceInitial, path); 92 | } 93 | 94 | if(p.median) { 95 | f.reduceAdd = median.add(f.reduceAdd, path); 96 | f.reduceRemove = median.remove(f.reduceRemove, path); 97 | f.reduceInitial = median.initial(f.reduceInitial, path); 98 | } 99 | 100 | if(p.min) { 101 | f.reduceAdd = min.add(f.reduceAdd, path); 102 | f.reduceRemove = min.remove(f.reduceRemove, path); 103 | f.reduceInitial = min.initial(f.reduceInitial, path); 104 | } 105 | 106 | if(p.max) { 107 | f.reduceAdd = max.add(f.reduceAdd, path); 108 | f.reduceRemove = max.remove(f.reduceRemove, path); 109 | f.reduceInitial = max.initial(f.reduceInitial, path); 110 | } 111 | 112 | // Maintain the values count array. 113 | if(p.exceptionAccessor) { 114 | f.reduceAdd = value_count.add(p.exceptionAccessor, f.reduceAdd, path); 115 | f.reduceRemove = value_count.remove(p.exceptionAccessor, f.reduceRemove, path); 116 | f.reduceInitial = value_count.initial(f.reduceInitial, path); 117 | } 118 | 119 | // Histogram 120 | if(p.histogramValue && p.histogramThresholds) { 121 | f.reduceAdd = histogram.add(p.histogramValue, f.reduceAdd, path); 122 | f.reduceRemove = histogram.remove(p.histogramValue, f.reduceRemove, path); 123 | f.reduceInitial = histogram.initial(p.histogramThresholds ,f.reduceInitial, path); 124 | } 125 | 126 | // Sum of Squares 127 | if(p.sumOfSquares) { 128 | f.reduceAdd = sum_of_sq.add(p.sumOfSquares, f.reduceAdd, path); 129 | f.reduceRemove = sum_of_sq.remove(p.sumOfSquares, f.reduceRemove, path); 130 | f.reduceInitial = sum_of_sq.initial(f.reduceInitial, path); 131 | } 132 | 133 | // Standard deviation 134 | if(p.std) { 135 | if(!p.sumOfSquares || !p.sum) { 136 | console.error("You must set .sumOfSq(accessor) and define a .sum(accessor) to use .std(true). Or use .std(accessor)."); 137 | } else { 138 | f.reduceAdd = std.add(f.reduceAdd, path); 139 | f.reduceRemove = std.remove(f.reduceRemove, path); 140 | f.reduceInitial = std.initial(f.reduceInitial, path); 141 | } 142 | } 143 | 144 | // Custom reducer defined by 3 functions : add, remove, initial 145 | if (p.custom) { 146 | f.reduceAdd = custom.add(f.reduceAdd, path, p.custom.add); 147 | f.reduceRemove = custom.remove(f.reduceRemove, path, p.custom.remove); 148 | f.reduceInitial = custom.initial(f.reduceInitial, path, p.custom.initial); 149 | } 150 | 151 | // Nesting 152 | if(p.nestKeys) { 153 | f.reduceAdd = nest.add(p.nestKeys, f.reduceAdd, path); 154 | f.reduceRemove = nest.remove(p.nestKeys, f.reduceRemove, path); 155 | f.reduceInitial = nest.initial(f.reduceInitial, path); 156 | } 157 | 158 | // Alias functions 159 | if(p.aliasKeys) { 160 | f.reduceInitial = alias.initial(f.reduceInitial, path, p.aliasKeys); 161 | } 162 | 163 | // Alias properties - this is less efficient than alias functions 164 | if(p.aliasPropKeys) { 165 | f.reduceAdd = alias_prop.add(p.aliasPropKeys, f.reduceAdd, path); 166 | // This isn't a typo. The function is the same for add/remove. 167 | f.reduceRemove = alias_prop.add(p.aliasPropKeys, f.reduceRemove, path); 168 | } 169 | 170 | // Filters determine if our built-up priors should run, or if it should skip 171 | // back to the filters given at the beginning of this build function. 172 | if (p.filter) { 173 | f.reduceAdd = filter.add(p.filter, f.reduceAdd, origF.reduceAdd, path); 174 | f.reduceRemove = filter.remove(p.filter, f.reduceRemove, origF.reduceRemove, path); 175 | } 176 | 177 | // Values go last. 178 | if(p.values) { 179 | Object.getOwnPropertyNames(p.values).forEach(function(n) { 180 | // Set up the path on each group. 181 | var setupPath = function(prior) { 182 | return function (p) { 183 | p = prior(p); 184 | path(p)[n] = {}; 185 | return p; 186 | }; 187 | }; 188 | f.reduceInitial = setupPath(f.reduceInitial); 189 | build_function(p.values[n].parameters, f, function (p) { return p[n]; }); 190 | }); 191 | } 192 | } 193 | 194 | var build = { 195 | build: build_function 196 | }; 197 | 198 | export default build; 199 | -------------------------------------------------------------------------------- /reductio.min.js: -------------------------------------------------------------------------------- 1 | // https://github.com/esjewett/reductio v1.0.0 Copyright 2020 Ethan Jewett 2 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n(require("crossfilter2")):"function"==typeof define&&define.amd?define(["crossfilter2"],n):(e=e||self).reductio=n(e.crossfilter)}(this,(function(e){"use strict";e=e&&e.hasOwnProperty("default")?e.default:e;var n=function(e,n,t){return function(u,r,i){return e(r,i)?n&&n(u,r,i):t&&t(u,r,i),u}},t=function(e,n,t){return function(u,r,i){return e(r,i)?n&&n(u,r,i):t&&t(u,r,i),u}},u=function(e,n,t){return function(u,r,i){return e&&e(u,r,i),n(u)[t]++,u}},r=function(e,n,t){return function(u,r,i){return e&&e(u,r,i),n(u)[t]--,u}},i=function(e,n,t){return function(u){return e&&(u=e(u)),n(u)[t]=0,u}},o=function(e,n,t){return function(u,r,i){return n&&n(u,r,i),t(u).sum=t(u).sum+e(r),u}},c=function(e,n,t){return function(u,r,i){return n&&n(u,r,i),t(u).sum=t(u).sum-e(r),u}},s=function(e,n){return function(t){return t=e(t),n(t).sum=0,t}},a=function(e,n,t){return function(e,u,r){return n&&n(e,u,r),t(e).count>0?t(e).avg=t(e).sum/t(e).count:t(e).avg=0,e}},l=function(e,n,t){return function(e,u,r){return n&&n(e,u,r),t(e).count>0?t(e).avg=t(e).sum/t(e).count:t(e).avg=0,e}},d=function(e,n){return function(t){return t=e(t),n(t).avg=0,t}},f=function(e,n){var t;return function(u,r,i){return e&&e(u,r,i),t=Math.floor(n(u).valueList.length/2),n(u).valueList.length%2?n(u).median=n(u).valueList[t]:n(u).median=(n(u).valueList[t-1]+n(u).valueList[t])/2,u}},v=function(e,n){var t;return function(u,r,i){return e&&e(u,r,i),t=Math.floor(n(u).valueList.length/2),0===n(u).valueList.length?(n(u).median=void 0,u):(1===n(u).valueList.length||n(u).valueList.length%2?n(u).median=n(u).valueList[t]:n(u).median=(n(u).valueList[t-1]+n(u).valueList[t])/2,u)}},m=function(e,n){return function(t){return t=e(t),n(t).median=void 0,t}},g=function(e,n){return function(t,u,r){return e&&e(t,u,r),n(t).min=n(t).valueList[0],t}},h=function(e,n){return function(t,u,r){return e&&e(t,u,r),0===n(t).valueList.length?(n(t).min=void 0,t):(n(t).min=n(t).valueList[0],t)}},p=function(e,n){return function(t){return t=e(t),n(t).min=void 0,t}},A=function(e,n){return function(t,u,r){return e&&e(t,u,r),n(t).max=n(t).valueList[n(t).valueList.length-1],t}},x=function(e,n){return function(t,u,r){return e&&e(t,u,r),0===n(t).valueList.length?(n(t).max=void 0,t):(n(t).max=n(t).valueList[n(t).valueList.length-1],t)}},y=function(e,n){return function(t){return t=e(t),n(t).max=void 0,t}},L=function(e,n,t){var u,r;return function(i,o,c){return n&&n(i,o,c),u=t(i).bisect(t(i).values,e(o),0,t(i).values.length),(r=t(i).values[u])&&r[0]===e(o)?r[1]++:t(i).values.splice(u,0,[e(o),1]),i}},I=function(e,n,t){var u;return function(r,i,o){return n&&n(r,i,o),u=t(r).bisect(t(r).values,e(i),0,t(r).values.length),t(r).values[u][1]--,r}},b=function(n,t){return function(u){return u=n(u),t(u).values=[],t(u).bisect=e.bisect.by((function(e){return e[0]})).left,u}},R=function(n,t,u){var r,i=e.bisect.by((function(e){return e})).left;return function(e,o,c){return t&&t(e,o,c),r=i(u(e).valueList,n(o),0,u(e).valueList.length),u(e).valueList.splice(r,0,n(o)),e}},S=function(n,t,u){var r,i=e.bisect.by((function(e){return e})).left;return function(e,o,c){return t&&t(e,o,c),r=i(u(e).valueList,n(o),0,u(e).valueList.length),u(e).valueList.splice(r,1),e}},O=function(e,n){return function(t){return t=e(t),n(t).valueList=[],t}},q=function(e,n,t){var u,r;return function(i,o,c){return n&&n(i,o,c),u=t(i).bisect(t(i).values,e(o),0,t(i).values.length),(r=t(i).values[u])&&r[0]===e(o)&&0!==r[1]||t(i).exceptionCount++,i}},K=function(e,n,t){var u,r;return function(i,o,c){return n&&n(i,o,c),u=t(i).bisect(t(i).values,e(o),0,t(i).values.length),(r=t(i).values[u])&&r[0]===e(o)&&1===r[1]&&t(i).exceptionCount--,i}},P=function(e,n){return function(t){return t=e(t),n(t).exceptionCount=0,t}},w=function(e,n,t,u){var r,i;return function(o,c,s){return t&&t(o,c,s),r=u(o).bisect(u(o).values,e(c),0,u(o).values.length),(i=u(o).values[r])&&i[0]===e(c)&&0!==i[1]||(u(o).exceptionSum=u(o).exceptionSum+n(c)),o}},C=function(e,n,t,u){var r,i;return function(o,c,s){return t&&t(o,c,s),r=u(o).bisect(u(o).values,e(c),0,u(o).values.length),(i=u(o).values[r])&&i[0]===e(c)&&1===i[1]&&(u(o).exceptionSum=u(o).exceptionSum-n(c)),o}},V=function(e,n){return function(t){return t=e(t),n(t).exceptionSum=0,t}},T=function(n,t,u){var r,i=e.bisect.by((function(e){return e})).left,o=e.bisect.by((function(e){return e.x})).right;return function(e,c,s){return t&&t(e,c,s),(r=u(e).histogram[o(u(e).histogram,n(c),0,u(e).histogram.length)-1]).y++,r.splice(i(r,n(c),0,r.length),0,n(c)),e}},E=function(n,t,u){var r,i=e.bisect.by((function(e){return e})).left,o=e.bisect.by((function(e){return e.x})).right;return function(e,c,s){return t&&t(e,c,s),(r=u(e).histogram[o(u(e).histogram,n(c),0,u(e).histogram.length)-1]).y--,r.splice(i(r,n(c),0,r.length),1),e}},k=function(e,n,t){return function(u){u=n(u),t(u).histogram=[];for(var r=[],i=1;i0){n(t).std=0;var i=n(t).sumOfSq-n(t).sum*n(t).sum/n(t).count;i>0&&(n(t).std=Math.sqrt(i/(n(t).count-1)))}else n(t).std=0;return t}},Y=function(e,n){return function(t,u,r){if(e&&e(t,u,r),n(t).count>0){n(t).std=0;var i=n(t).sumOfSq-n(t).sum*n(t).sum/n(t).count;i>0&&(n(t).std=Math.sqrt(i/(n(t).count-1)))}else n(t).std=0;return t}},B=function(e,n){return function(t){return t=e(t),n(t).std=0,t}},G=function(e,n,t){var u,r;return function(i,o,c){return n&&n(i,o,c),u=t(i).nest,e.forEach((function(e){(r=u.filter((function(n){return n.key===e(o)}))[0])?u=r.values:(r=[],u.push({key:e(o),values:r}),u=r)})),u.push(o),i}},X=function(e,n,t){var u;return function(r,i,o){return n&&n(r,i,o),u=t(r).nest,e.forEach((function(e){u=u.filter((function(n){return n.key===e(i)}))[0].values})),u.splice(u.indexOf(i),1),r}},z=function(e,n){return function(t){return t=e(t),n(t).nest=[],t}},D=function(e,n,t){return function(u){function r(e){return function(){return t[e](n(u))}}for(var i in e&&(u=e(u)),t)n(u)[i]=r(i);return u}},F=function(e,n,t){return function(u,r,i){for(var o in n&&n(u,r,i),e)t(u)[o]=e[o](t(u),r);return u}},H=function(e,n,t){return function(e,u,r){return n&&n(e,u,r),t(e).dataList.push(u),e}},J=function(e,n,t){return function(e,u,r){return n&&n(e,u,r),t(e).dataList.splice(t(e).dataList.indexOf(u),1),e}},Q=function(e,n){return function(t){return e&&(t=e(t)),n(t).dataList=[],t}},W=function(e,n,t){return function(n,u,r){return e&&e(n,u,r),t(n,u)}},Z=function(e,n,t){return function(n,u,r){return e&&e(n,u,r),t(n,u)}},$=function(e,n,t){return function(n){return e&&(n=e(n)),t(n)}};var _={build:function e(_,ee,ne){ne||(ne=function(e){return e});var te={reduceAdd:ee.reduceAdd,reduceRemove:ee.reduceRemove,reduceInitial:ee.reduceInitial};(_.count||_.std)&&(ee.reduceAdd=u(ee.reduceAdd,ne,_.count),ee.reduceRemove=r(ee.reduceRemove,ne,_.count),ee.reduceInitial=i(ee.reduceInitial,ne,_.count)),_.sum&&(ee.reduceAdd=o(_.sum,ee.reduceAdd,ne),ee.reduceRemove=c(_.sum,ee.reduceRemove,ne),ee.reduceInitial=s(ee.reduceInitial,ne)),_.avg&&(_.count&&_.sum?(ee.reduceAdd=a(_.sum,ee.reduceAdd,ne),ee.reduceRemove=l(_.sum,ee.reduceRemove,ne),ee.reduceInitial=d(ee.reduceInitial,ne)):console.error("You must set .count(true) and define a .sum(accessor) to use .avg(true).")),_.exceptionCount&&(_.exceptionAccessor?(ee.reduceAdd=q(_.exceptionAccessor,ee.reduceAdd,ne),ee.reduceRemove=K(_.exceptionAccessor,ee.reduceRemove,ne),ee.reduceInitial=P(ee.reduceInitial,ne)):console.error("You must define an .exception(accessor) to use .exceptionCount(true).")),_.exceptionSum&&(_.exceptionAccessor?(ee.reduceAdd=w(_.exceptionAccessor,_.exceptionSum,ee.reduceAdd,ne),ee.reduceRemove=C(_.exceptionAccessor,_.exceptionSum,ee.reduceRemove,ne),ee.reduceInitial=V(ee.reduceInitial,ne)):console.error("You must define an .exception(accessor) to use .exceptionSum(accessor).")),(_.valueList||_.median||_.min||_.max)&&(ee.reduceAdd=R(_.valueList,ee.reduceAdd,ne),ee.reduceRemove=S(_.valueList,ee.reduceRemove,ne),ee.reduceInitial=O(ee.reduceInitial,ne)),_.dataList&&(ee.reduceAdd=H(_.dataList,ee.reduceAdd,ne),ee.reduceRemove=J(_.dataList,ee.reduceRemove,ne),ee.reduceInitial=Q(ee.reduceInitial,ne)),_.median&&(ee.reduceAdd=f(ee.reduceAdd,ne),ee.reduceRemove=v(ee.reduceRemove,ne),ee.reduceInitial=m(ee.reduceInitial,ne)),_.min&&(ee.reduceAdd=g(ee.reduceAdd,ne),ee.reduceRemove=h(ee.reduceRemove,ne),ee.reduceInitial=p(ee.reduceInitial,ne)),_.max&&(ee.reduceAdd=A(ee.reduceAdd,ne),ee.reduceRemove=x(ee.reduceRemove,ne),ee.reduceInitial=y(ee.reduceInitial,ne)),_.exceptionAccessor&&(ee.reduceAdd=L(_.exceptionAccessor,ee.reduceAdd,ne),ee.reduceRemove=I(_.exceptionAccessor,ee.reduceRemove,ne),ee.reduceInitial=b(ee.reduceInitial,ne)),_.histogramValue&&_.histogramThresholds&&(ee.reduceAdd=T(_.histogramValue,ee.reduceAdd,ne),ee.reduceRemove=E(_.histogramValue,ee.reduceRemove,ne),ee.reduceInitial=k(_.histogramThresholds,ee.reduceInitial,ne)),_.sumOfSquares&&(ee.reduceAdd=j(_.sumOfSquares,ee.reduceAdd,ne),ee.reduceRemove=M(_.sumOfSquares,ee.reduceRemove,ne),ee.reduceInitial=N(ee.reduceInitial,ne)),_.std&&(_.sumOfSquares&&_.sum?(ee.reduceAdd=U(ee.reduceAdd,ne),ee.reduceRemove=Y(ee.reduceRemove,ne),ee.reduceInitial=B(ee.reduceInitial,ne)):console.error("You must set .sumOfSq(accessor) and define a .sum(accessor) to use .std(true). Or use .std(accessor).")),_.custom&&(ee.reduceAdd=W(ee.reduceAdd,ne,_.custom.add),ee.reduceRemove=Z(ee.reduceRemove,ne,_.custom.remove),ee.reduceInitial=$(ee.reduceInitial,ne,_.custom.initial)),_.nestKeys&&(ee.reduceAdd=G(_.nestKeys,ee.reduceAdd,ne),ee.reduceRemove=X(_.nestKeys,ee.reduceRemove,ne),ee.reduceInitial=z(ee.reduceInitial,ne)),_.aliasKeys&&(ee.reduceInitial=D(ee.reduceInitial,ne,_.aliasKeys)),_.aliasPropKeys&&(ee.reduceAdd=F(_.aliasPropKeys,ee.reduceAdd,ne),ee.reduceRemove=F(_.aliasPropKeys,ee.reduceRemove,ne)),_.filter&&(ee.reduceAdd=n(_.filter,ee.reduceAdd,te.reduceAdd,ne),ee.reduceRemove=t(_.filter,ee.reduceRemove,te.reduceRemove,ne)),_.values&&Object.getOwnPropertyNames(_.values).forEach((function(n){var t;ee.reduceInitial=(t=ee.reduceInitial,function(e){return e=t(e),ne(e)[n]={},e}),e(_.values[n].parameters,ee,(function(e){return e[n]}))}))}};function ee(e){if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(e),t=1;tn?1:e>=n?0:NaN}var ce=function(e,n){return function(t,u){return n(e(t),e(u))}};function se(e){return function(n,t){return 1===arguments.length&&(t=oe),e().sort(ce(ie(n),t))}}function ae(){var n={order:!1,avg:!1,count:!1,sum:!1,exceptionAccessor:!1,exceptionCount:!1,exceptionSum:!1,filter:!1,valueList:!1,median:!1,histogramValue:!1,min:!1,max:!1,histogramThresholds:!1,std:!1,sumOfSquares:!1,values:!1,nestKeys:!1,aliasKeys:!1,aliasPropKeys:!1,groupAll:!1,dataList:!1,custom:!1},t={};function u(u){if(t={reduceAdd:function(e){return e},reduceRemove:function(e){return e},reduceInitial:function(){return{}}},_.build(n,t),n.groupAll)if(u.top)console.warn("'groupAll' is defined but attempting to run on a standard dimension.group(). Must run on dimension.groupAll().");else{var r,i,o,c,s,a=e.bisect.by((function(e){return e.key})).left;u.reduce((function(e,u,l){for(o=n.groupAll(u),c=o.length,i=0;iExample 14 | 15 | Basic use: 16 | 17 | ```javascript 18 | var data = crossfilter([ 19 | { foo: 'one', bar: 1 }, 20 | { foo: 'two', bar: 2 }, 21 | { foo: 'three', bar: 3 }, 22 | { foo: 'one', bar: 4 }, 23 | { foo: 'one', bar: 5 }, 24 | { foo: 'two', bar: 6 }, 25 | ]); 26 | 27 | var dim = data.dimension(function(d) { return d.foo; }); 28 | var group = dim.group(); 29 | 30 | // Equivalent to reductio().avg(function(d) { return d.bar; }), which sets the .sum() and .count() values. 31 | var reducer = reductio() 32 | .count(true) 33 | .sum(function(d) { return d.bar; }) 34 | .avg(true); 35 | 36 | // Now it should track count, sum, and avg. 37 | reducer(group); 38 | 39 | group.top(Infinity); 40 | // [ { key: 'one', value: { count: 3, sum: 10, avg: 3.3333333 }, 41 | // { key: 'two', value: { count: 2, sum: 8, avg: 4 }, 42 | // { key: 'three', value: { count: 1, sum: 3, avg: 3 } ] 43 | ``` 44 | 45 |

Table of Contents

46 | 47 | * [Example](#example) 48 | * [Table of Contents](#toc) 49 | * [Installation](#installation) 50 | * [NPM](#installation-npm) 51 | * [Bower](#installation-bower) 52 | * [Download](#installation-download) 53 | * [CDN](#installation-cdn) 54 | * [Getting Help](#getting-help) 55 | * [Accessor Functions](#accessor-functions) 56 | * [Aggregations](#aggregations) 57 | * [Standard aggregations](#aggregations-standard-aggregations) 58 | * [reductio.count()](#aggregations-standard-aggregations-reductio-b-count-b-) 59 | * [reductio.sum(value)](#aggregations-standard-aggregations-reductio-b-sum-b-i-value-i-) 60 | * [reductio.avg(boolean|value)](#aggregations-standard-aggregations-reductio-b-avg-b-i-boolean-i-i-value-i-) 61 | * [reductio.min(boolean|value)](#aggregations-standard-aggregations-reductio-b-min-b-i-boolean-i-i-value-i-) 62 | * [reductio.max(boolean|value)](#aggregations-standard-aggregations-reductio-b-max-b-i-boolean-i-i-value-i-) 63 | * [reductio.median(boolean|value)](#aggregations-standard-aggregations-reductio-b-median-b-i-boolean-i-i-value-i-) 64 | * [reductio.sumOfSq(value)](#aggregations-standard-aggregations-reductio-b-sumofsq-b-i-value-i-) 65 | * [reductio.std(boolean|value)](#aggregations-standard-aggregations-reductio-b-std-b-i-boolean-i-i-value-i-) 66 | * [Histogram](#aggregations-standard-aggregations-histogram) 67 | * [reductio.histogramBins(thresholdArray)](#aggregations-standard-aggregations-histogram-reductio-b-histogrambins-b-i-thresholdarray-i-) 68 | * [reductio.histogramValue(value)](#aggregations-standard-aggregations-histogram-reductio-b-histogramvalue-b-i-value-i-) 69 | * [reductio.value(propertyName)](#aggregations-standard-aggregations-reductio-b-value-b-i-propertyname-i-) 70 | * [reductio.filter(filterFn)](#aggregations-standard-aggregations-reductio-b-filter-b-i-filterfn-i-) 71 | * [reductio.nest(keyAccessorArray)](#aggregations-standard-aggregations-reductio-b-nest-b-i-keyaccessorarray-i-) 72 | * [reductio.alias(mapping)](#aggregations-standard-aggregations-reductio-b-alias-b-i-mapping-i-) 73 | * [reductio.aliasProp(mapping)](#aggregations-standard-aggregations-reductio-b-aliasprop-b-i-mapping-i-) 74 | * [reductio.valueList(accessor)](#aggregations-standard-aggregations-reductio-value-list) 75 | * [reductio.dataList(boolean)](#aggregations-standard-aggregations-reductio-data-list) 76 | * [reductio.custom(initial,add,remove)](#aggregations-standard-aggregations-reductio-custom) 77 | * [Exception aggregation](#aggregations-standard-aggregations-exception-aggregation) 78 | * [reductio.exception(accessor)](#aggregations-standard-aggregations-exception-aggregation-reductio-b-exception-b-i-accessor-i-) 79 | * [reductio.exceptionCount(boolean)](#aggregations-standard-aggregations-exception-aggregation-reductio-b-exceptioncount-b-i-boolean-i-) 80 | * [reductio.exceptionSum(value)](#aggregations-standard-aggregations-exception-aggregation-reductio-b-exceptionsum-b-i-value-i-) 81 | * [groupAll aggregations](#aggregations-groupall-aggregations) 82 | * [reductio.groupAll(groupingFunction)](#aggregations-groupall-aggregations-reductio-b-groupall-b-i-groupingfunction-i-) 83 | * [Chaining aggregations](#aggregations-chaining-aggregations) 84 | * [Post-processing](#postprocess) 85 | * [group.post().cap(length)](#cap) 86 | * [Utilities](#utilities) 87 | * [reductio().fromObject(parameters)](#utilities-fromObject) 88 | * [reductio().toObject()](#utilities-toObject) 89 | 90 | 91 |

Installation

92 | 93 |

NPM

94 | 95 | ```shell 96 | npm install --save-dev reductio 97 | ``` 98 | 99 |

Bower

100 | 101 | ```shell 102 | bower install --save-dev reductio 103 | ``` 104 | 105 |

Download

106 | Download from the [releases](https://github.com/esjewett/reductio/releases) page. Serve the reductio.js or reductio.min.js file in the top-level directory as part of your application. 107 | 108 |

CDN

109 | Reductio is available via cdnjs. You can generate links to different versions of Reductio at: https://cdnjs.com/libraries/reductio 110 | 111 |

Getting help

112 | 113 | If something doesn't appear to be working or you're having trouble with implementing something, it will usually be best to ask a question on Stackoverflow and tag the question with the [reductio tag](http://stackoverflow.com/questions/tagged/reductio). When you ask your question, it is best to put together a working example showing the problem you are having. A [JSFiddle template](https://jsfiddle.net/esjewett/jusjkm8j/) is available that already includes the Reductio and Crossfilter libraries, as well as dc.js. Use that template to make an example showing what you are seeing. In your question, reference the example, explain what you are seeing, and explain what you expect or want to see instead. 114 | 115 | There is also a [Reductio Gitter](https://gitter.im/crossfilter/reductio), which is a good place to check in for help if you have a quick question that doesn't fit into the Stackoverflow format, or if you are looking for a more conceptual discussion. 116 | 117 |

Accessor functions

118 | 119 | In most cases when an accessor function is required, Reductio supports the use of the property name to be accessed in the form or a string instead. When appropriate, Reductio will even cast the value of a property to a number for you, though be aware that this will convert nulls and undefined values into 0s. 120 | 121 | For example, the following: 122 | 123 | ```javascript 124 | reducer = reductio().sum(function(d) { return +d.number; }); 125 | reducer(group); 126 | ``` 127 | 128 | Is equivalent to: 129 | 130 | ```javascript 131 | reducer = reductio().sum('number'); 132 | reducer(group); 133 | ``` 134 | 135 | Aggregations that support this syntax with casting to a numeric value: sum, avg, exception sum, histogram value, min, max, median 136 | 137 | Aggregations that support this syntax without casting: nest, exception, value list, standard deviation, sum of squares 138 | 139 |

Aggregations

140 | 141 | Aggregations are composable (so you can track more than one aggregation on a given group) and may depend on each other (the 'avg' aggregation requires that 'count' and 'sum' be specified). 142 | 143 |

Standard aggregations

144 | Current aggregations supported are shown given the following setup. 145 | 146 | ```javascript 147 | var data = crossfilter([...]); 148 | var dim = data.dimension(...); 149 | var group = dim.group(); 150 | var reducer; 151 | ``` 152 | 153 |

reductio.count()

154 | Works the same way as Crossfilter's standard ```group.reduceCount()```. 155 | 156 | ```javascript 157 | reducer = reductio().count(true); 158 | reducer(group); 159 | ``` 160 | 161 | Stored under the 'count' property of groups. The value will be a count of every record that matches the group accessor. 162 | 163 |

reductio.sum(value)

164 | Works the same was as Crossfilter's standard ```group.reduceSum()```. 165 | 166 | ```javascript 167 | reducer = reductio().sum(function(d) { return +d.number; }); 168 | reducer(group); 169 | ``` 170 | 171 | Stored under the 'sum' property of groups. The value is a sum of ```accessor(d)``` for every record ```d``` that matches the group accessor. The accessor function must return a number. 172 | 173 |

reductio.avg(boolean|value)

174 | 175 | ```javascript 176 | reductio().avg(function(d) { return +d.number; })(group); 177 | ``` 178 | 179 | Stored under the 'avg' property of groups. Boolean variation depends on *count* and *sum* aggregations being specified. If an accessor function is provided, that function will be used to create a sum aggregation on the group, and a count aggregation will be created as well. The value on the 'avg' property is equal to sum/count for the group. 180 | 181 |

reductio.min(boolean|value)

182 |

reductio.max(boolean|value)

183 |

reductio.median(boolean|value)

184 | 185 | ```javascript 186 | reductio().min(function(d) { return +d.number; }) 187 | .max(true) 188 | .median(true)(group); 189 | ``` 190 | 191 | Stored under the 'median', 'min', and 'max' property of groups. 192 | 193 | Once you've defined one accessor function for min, max, or median (or if you have explicitly defined a ```redectio.valueList(value)```) it will be used by the others. This avoids warning messages about overwriting the valueList. 194 | 195 |

reductio.sumOfSq(value)

196 | 197 | ```javascript 198 | reductio().sumOfSq(function(d) { return d.number; })(group); 199 | ``` 200 | 201 | Stored under the 'sumOfSq' property of the group. Defined as the square of the value returned by the accessor function summed over all records in the group. This is used in the standard deviation aggregation, but can be used on its own as well. 202 | 203 |

reductio.std(boolean|value)

204 | 205 | ```javascript 206 | reductio().sumOfSq(function(d) { return d.number; }) 207 | .sum(function(d) { return d.number; }) 208 | .count(true) 209 | .std(true)(group); 210 | reductio() 211 | .std(function(d) { return d.number; })(group); 212 | ``` 213 | 214 | Stored under the 'std' property of the group. Defined as the sum-of-squares minus the average of the square of sums for all records in the group. In other words, for group 'g', ```g.sumOfSq - g.sum*g.sum/g.count```. 215 | 216 | If ```sumOfSq```, ```sum```, and ```count``` are already defined, takes a boolean. Otherwise pass in an accessor function directly. 217 | 218 |

Histogram

219 | 220 | ```javascript 221 | reductio().histogramBins([0,2,6,10]) 222 | .histogramValue(function(d) { return +d.number; })(group) 223 | ``` 224 | 225 | Histogram of values within grouping, stored on the 'histogram' property of the group. Acts like [d3.layout.histogram](https://github.com/mbostock/d3/wiki/Histogram-Layout) defined using bins(thresholds). 226 | 227 | This grouping should be usable anywhere d3.layout.histogram can be used. May be useful for small-multiples charts, or for use with the dc.js stack mixin. 228 | 229 | The property ```group.histogram``` is an array. Each element of the array is a sorted array of values returned by ```histogramValue``` that fall into that bin. Each element of the array also has properties, x, dx, and y, as defined in the d3.layout.histogram documentation. 230 | 231 |

reductio.histogramBins(thresholdArray)

232 | Defines the bin thresholds for the histogram. Will result in ```thresholdArray.length - 1``` bins. 233 | 234 |

reductio.histogramValue(value)

235 | Accessor for the value to be binned. 236 | 237 |

reductio.value(propertyName)

238 | 239 | ```javascript 240 | var reducer = reductio(); 241 | reducer.value("x").sum(xSumAccessor); 242 | reducer.value("y").count(true).sum(ySumAccessor).avg(true); 243 | reducer(group); 244 | ``` 245 | 246 | Allows group structures such as 247 | 248 | ```javascript 249 | { 250 | x: { sum: 5 } 251 | y: { count: 3, sum: 12, avg: 4 } 252 | } 253 | ``` 254 | 255 | Used for tracking multiple aggregations on a single group. For example, sum of x and sum of y. Useful for visualizations like scatter-plots where individual marks represent multiple dimensions in the data. ```propertyName``` must be a valid Javascript object property name and must not conflict with any of the property names already used by Reductio (i.e. ```count```, ```sum```, ```avg```, etc.). 256 | 257 | As many values as desired can be defined and any aggregation in this list can be defined on a value and they are calculated independently from aggregations defined on other values or on the base level of the group. Therefore, values are the way to deal with scenarios in which you want to calculated the same aggregation twice with difference logic (sum credits *and* sum debits) or where you are struggling with aggregations that are interdependent (average and sum are linked, so if you want to average a different value than you are summing, use a value). 258 | 259 | Note that exception aggregations are supported on values, but groupAll aggregations are not. 260 | 261 | A more comprehensive example: 262 | 263 | ```javascript 264 | var reducer = reductio(); 265 | reducer.value("w").exception(function(d) { return d.bar; }).exceptionSum(function(d) { return d.num1; }); 266 | reducer.value("x").exception(function(d) { return d.bar; }).exceptionSum(function(d) { return d.num2; }); 267 | reducer.value("y").sum(function(d) { return d.num3; }); 268 | reducer.value("z").sum(function(d) { return d.num4; }); 269 | reducer(group); 270 | ``` 271 | 272 | Will result in groups that look like 273 | 274 | ```javascript 275 | { key: groupKey, value: { 276 | w: { exceptionSum: 2 }, 277 | x: { exceptionSum: 3 }, 278 | y: { sum: 4 }, 279 | z: { sum: 2 } 280 | }} 281 | ``` 282 | 283 |

reductio.filter(filterFn)

284 | 285 | ```javascript 286 | reductio().filter(filterFn)(group) 287 | ``` 288 | 289 | Limit the values that can be added to / removed from groups. Works well with ```value``` 290 | chains, and is also very useful if you need to aggregate sparsely-populated fields. 291 | 292 | ```javascript 293 | var reducer = reductio(); 294 | reducer.value("evens") 295 | .count(true) 296 | .filter(function(d) { return d.num % 2 === 0; }); 297 | reducer.value("rare") 298 | .filter(function(d) { return typeof d.rareVal !== 'undefined'; }) 299 | .sum(function(d) { return d.rareVal; }); 300 | reducer(group); 301 | ``` 302 | 303 | For example: 304 | 305 | ```javascript 306 | // Given: 307 | [ 308 | { foo: 'one', num: 1 }, 309 | { foo: 'two', num: 2 }, 310 | { foo: 'three', num: 3, rareVal: 98 }, 311 | { foo: 'one', num: 3, rareVal: 99 }, 312 | { foo: 'one', num: 4, rareVal: 100 }, 313 | { foo: 'two', num: 6 } 314 | ] 315 | 316 | // The groups will look like: 317 | [ 318 | {key: "one", value: {evens: {count: 1}, rare: {sum: 199}}}, 319 | {key: "two", value: {evens: {count: 2}, rare: {sum: 0 }}}, 320 | {key: "three", value: {evens: {count: 0}, rare: {sum: 98 }}} 321 | ] 322 | ``` 323 | 324 | 325 |

reductio.nest(keyAccessorArray)

326 | 327 | ```javascript 328 | reductio().nest([keyAccessor1, keyAccessor2])(group) 329 | ``` 330 | 331 | Stored under the 'nest' property of the group. 332 | 333 | Provides a result similar to ```d3.nest().key(keyAccessor1).key(keyAccessor2)``` when applied to the records in the group. 334 | 335 | Usually you'll want to use the group key as the first level of nesting, then use this to accomplish sub-group nesting. 336 | 337 | Note that leaves will not be created when there is no record with that value in the branch. However, once a leaf is created it is not removed, so there is the possibility of leaves with empty 'values' arrays. 338 | 339 |

reductio.alias(mapping)

340 | 341 | ```javascript 342 | reductio().count(true).alias({ newCount: function(g) { return g.count; } }); 343 | ``` 344 | 345 | Allows definition of an accessor function of any name on the group that returns a value from the group. ```mapping``` is an object where keys are the new properties that will be added to the group and values are the accessor functions that get the required values from the group. 346 | 347 | On the group, we can then call the following function to retrieve the new count value. 348 | 349 | ```javascript 350 | group.top(1)[0].newCount(); 351 | ``` 352 | 353 | This approach to aliases is more efficient than the aliasProp approach below because it executes no logic at the time off aggregation. 354 | 355 |

reductio.aliasProp(mapping)

356 | 357 | ```javascript 358 | reductio().count(true) 359 | .sum(function(d) { return +d.num; }) 360 | .aliasProp({ 361 | newCount: function(g) { return g.count; }, 362 | average: function(g) { return g.sum / g.count; }, 363 | description: function (g, v) { return v.desc; } 364 | }); 365 | ``` 366 | 367 | Allows definition of an accessor function of any name on the group that returns a value from the group. ```mapping``` is an object where keys are the new properties that will be added to the group and values are the values returned by the accessor function. 368 | 369 | Accessors also have access to the record, so you can use this function to do things like assigning additional descriptive information to the group property based on the records seen. 370 | 371 | On the group, we can then call the following to retrieve the count value. 372 | 373 | ```javascript 374 | group.top(1)[0].newCount; 375 | group.top(1)[0].average; 376 | group.top(1)[0].description; 377 | ``` 378 | 379 | It is *very* important that the functions in the _mapping_ don't modify the group directly. The functions are run after all aggregations are calculated and the same function is run for adding and removing records. Because the accessor functions are run on the group every time a record is added or removed, this is less efficient than the function-based approach in reductio.alias above. 380 | 381 |

reductio.valueList(accessor)

382 | 383 | ```javascript 384 | var reducer = reductio() 385 | .sum(function (d) { return d.bar; }) 386 | .valueList(function (d) { return d.bar; }); 387 | ``` 388 | 389 | Maintains a `valueList` property on the group containing an array of values returned by `accessor` for every record added to the group. This property is used internally by other aggregations like `min`, `max`, and `median`, so watch for warning messages on the console. 390 | 391 |

reductio.dataList(boolean)

392 | 393 | ```javascript 394 | var reducer = reductio() 395 | .sum(function (d) { return d.bar; }) 396 | .dataList(true); 397 | ``` 398 | 399 | Maintains a `dataList` property on the group containing an array of records included in the group. This is similar to `valueList` used with the identity function as the accessor, but is slightly more efficient. 400 | 401 |

reductio.custom(initial,add,remove)

402 | 403 | ```javascript 404 | var reducer = reductio() 405 | .custom({ 406 | add: function(p, v) { 407 | p.myProp += v.foo.length; 408 | return p; 409 | }, 410 | remove: function(p, v) { 411 | p.myProp -= v.foo.length; 412 | return p; 413 | }, 414 | initial: function(p) { 415 | p.myProp = 0; 416 | return p; 417 | } 418 | }); 419 | ``` 420 | 421 | Applies specified custom reducer to every record of every group. 422 | This property allows to plug any custom extension in Reductio's API. 423 | 424 |

Exception aggregation

425 | We also support exception aggregation. For our purposes, this means only aggregating once for each unique value that the exception accessor returns. So: 426 | 427 | ```javascript 428 | var data = crossfilter([ 429 | { foo: 'one', bar: 'A', num: 1 }, 430 | { foo: 'two', bar: 'B', num: 2 }, 431 | { foo: 'three', bar: 'A', num: 3 }, 432 | { foo: 'one', bar: 'B', num: 2 }, 433 | { foo: 'one', bar: 'A', num: 1 }, 434 | { foo: 'two', bar: 'B', num: 2 }, 435 | ]); 436 | 437 | var dim = data.dimension(function(d) { return d.foo; }); 438 | var group = dim.group(); 439 | 440 | var reducer = reductio() 441 | .exception(function(d) { return d.bar; }) 442 | .exceptionCount(true) 443 | .exceptionSum(function(d) { return d.num; }); 444 | 445 | reducer(group); 446 | 447 | group.top(Infinity); 448 | // [ { key: 'one', value: { exceptionCount: 2, exceptionSum: 3 }, // 'bar' dimension has 2 values: 'A' and 'B'. 449 | // { key: 'two', value: { exceptionCount: 1, exceptionSum: 2 }, // 'bar' dimension has 1 value: 'B'. 450 | // { key: 'three', value: { exceptionCount: 1 , exceptionSum: 3} ] // 'bar' dimension has 1 value: 'A'. 451 | ``` 452 | 453 | Right now we support exceptionCount and exceptionSum, but it might also make sense to support other types of dependent aggregation. These types of aggregations are meant to help with a situation where you want to use Crossfilter on a flattened one-to-many or many-to-many relational model, which can result in redundant values. 454 | 455 |

reductio.exception(accessor)

456 | 457 | The exception accessor defines the value by which to restrict the calculation of the exception aggregation. In each group, only the first record with each unique value returned by this accessor will be considered for aggregation. 458 | 459 |

reductio.exceptionCount(boolean)

460 | 461 | A count subject to exception calculation. 462 | 463 |

reductio.exceptionSum(value)

464 | 465 | A sum subject to exception calculation. Make sure that for each value within a group that the exception accessor returns, the exceptionSum accessor returns an identical value, or results will be unpredictable because the record added for each exception value will not necessarily be the same record that is removed. 466 | 467 |

groupAll aggregations

468 | 469 | Sometimes it is necessary to include one record in multiple groups. This is common in OLAP scenarios, classification, and tracking moving averages, to give a few examples. Say we have a data set like 470 | 471 | ```javascript 472 | [ 473 | { foo: 'one', num: 1 }, 474 | { foo: 'two', num: 2 }, 475 | { foo: 'three', num: 2 }, 476 | { foo: 'one', num: 3 }, 477 | { foo: 'one', num: 4 }, 478 | { foo: 'two', num: 5 }, 479 | ] 480 | ``` 481 | 482 | We want to track a moving count of the last 2 values on the ```num``` property. So our group with a key ```2``` should count up all records with a ```num``` of ```2``` *or* ```1```. Normally this must be done using the Crossfilter dimension.groupAll method. With reductio we can use all the standard reductio reducers in this type of scenario by specifying some additional groupAll information and called the reducer on the output of ```dimension.groupAll``` *instead* of the output of ```dimension.group```. 483 | 484 | The object returned by ```dimension.groupAll``` in Crossfilter does not have the standard ```all```, ```top```, or ```order``` methods. As a convenience, the reducer function produced by Reductio adds an ```all``` method to this object if it does not already exist. 485 | 486 |

reductio.groupAll(groupingFunction)

487 | 488 | Takes a single argument: a function that takes a record from the data set (e.g. ```{ foo: 'three', num: 2 }```) and returns an array of keys of the groups that the record should be included in (e.g. ```[2,3]```). This is a very simple example, but the same thing could be done for dates, with a function for a 5-day moving average returning an array of 5 dates. 489 | 490 | ```javascript 491 | data.dimension(function(d) { return d.num; }); 492 | filterDim = data.dimension(function(d) { return d.foo; }); 493 | groupAll = dim.groupAll(); 494 | 495 | reducer = reductio() 496 | .groupAll(function(record) { 497 | if(record.num === 5) { 498 | return [5]; 499 | } else { 500 | return [record.num, record.num+1]; 501 | } 502 | }) 503 | .count(true); 504 | 505 | reducer(groupAll); 506 | ``` 507 | 508 |

Chaining aggregations

509 | As seen above, aggregations can be chained on a given instance of reductio. For example: 510 | 511 | ```javascript 512 | reductio().count(true) 513 | .sum(function(d) { return d.bar; }) 514 | .avg(true)(group); 515 | ``` 516 | 517 |

Post-processing

518 | Reductio adds a `post` function to your group. Calling this function returns an object on which you can make settings to allow you to post-process your data in useful ways. 519 | 520 |

group.post().cap(length)

521 | Cap the output of your group so that it never exceeds `length` elements. 522 | 523 | The last element will be an aggregation of the rest of the elements. It might be wise to set your groups `order` method on the Crossfilter group before using this. 524 | 525 | ```javascript 526 | reductio() 527 | .count(true) 528 | .sum('foo') 529 | .avg(true)(group); 530 | group.post().cap(4)().length // 4 or less 531 | ``` 532 | 533 |

Utilities

534 | 535 |

reductio().fromObject(parameters)

536 | 537 | A utility that will allow you to assign directly to the inner object from which reductio creates its groupings. 538 | 539 | Basic use: 540 | 541 | ```js 542 | reductio() 543 | .fromObject({ 544 | sum: function(d){ 545 | return d.foo; 546 | } 547 | })(group); 548 | ``` 549 | 550 |

reductio().toObject()

551 | Returns the current state of the reductio instance. 552 | -------------------------------------------------------------------------------- /reductio.js: -------------------------------------------------------------------------------- 1 | // https://github.com/esjewett/reductio v1.0.0 Copyright 2020 Ethan Jewett 2 | (function (global, factory) { 3 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('crossfilter2')) : 4 | typeof define === 'function' && define.amd ? define(['crossfilter2'], factory) : 5 | (global = global || self, global.reductio = factory(global.crossfilter)); 6 | }(this, (function (crossfilter) { 'use strict'; 7 | 8 | crossfilter = crossfilter && crossfilter.hasOwnProperty('default') ? crossfilter['default'] : crossfilter; 9 | 10 | var filter = { 11 | // The big idea here is that you give us a filter function to run on values, 12 | // a 'prior' reducer to run (just like the rest of the standard reducers), 13 | // and a reference to the last reducer (called 'skip' below) defined before 14 | // the most recent chain of reducers. This supports individual filters for 15 | // each .value('...') chain that you add to your reducer. 16 | add: function (filter, prior, skip) { 17 | return function (p, v, nf) { 18 | if (filter(v, nf)) { 19 | if (prior) prior(p, v, nf); 20 | } else { 21 | if (skip) skip(p, v, nf); 22 | } 23 | return p; 24 | }; 25 | }, 26 | remove: function (filter, prior, skip) { 27 | return function (p, v, nf) { 28 | if (filter(v, nf)) { 29 | if (prior) prior(p, v, nf); 30 | } else { 31 | if (skip) skip(p, v, nf); 32 | } 33 | return p; 34 | }; 35 | } 36 | }; 37 | 38 | var count = { 39 | add: function(prior, path, propName) { 40 | return function (p, v, nf) { 41 | if(prior) prior(p, v, nf); 42 | path(p)[propName]++; 43 | return p; 44 | }; 45 | }, 46 | remove: function(prior, path, propName) { 47 | return function (p, v, nf) { 48 | if(prior) prior(p, v, nf); 49 | path(p)[propName]--; 50 | return p; 51 | }; 52 | }, 53 | initial: function(prior, path, propName) { 54 | return function (p) { 55 | if(prior) p = prior(p); 56 | // if(p === undefined) p = {}; 57 | path(p)[propName] = 0; 58 | return p; 59 | }; 60 | } 61 | }; 62 | 63 | var sum = { 64 | add: function (a, prior, path) { 65 | return function (p, v, nf) { 66 | if(prior) prior(p, v, nf); 67 | path(p).sum = path(p).sum + a(v); 68 | return p; 69 | }; 70 | }, 71 | remove: function (a, prior, path) { 72 | return function (p, v, nf) { 73 | if(prior) prior(p, v, nf); 74 | path(p).sum = path(p).sum - a(v); 75 | return p; 76 | }; 77 | }, 78 | initial: function (prior, path) { 79 | return function (p) { 80 | p = prior(p); 81 | path(p).sum = 0; 82 | return p; 83 | }; 84 | } 85 | }; 86 | 87 | var avg = { 88 | add: function (a, prior, path) { 89 | return function (p, v, nf) { 90 | if(prior) prior(p, v, nf); 91 | if(path(p).count > 0) { 92 | path(p).avg = path(p).sum / path(p).count; 93 | } else { 94 | path(p).avg = 0; 95 | } 96 | return p; 97 | }; 98 | }, 99 | remove: function (a, prior, path) { 100 | return function (p, v, nf) { 101 | if(prior) prior(p, v, nf); 102 | if(path(p).count > 0) { 103 | path(p).avg = path(p).sum / path(p).count; 104 | } else { 105 | path(p).avg = 0; 106 | } 107 | return p; 108 | }; 109 | }, 110 | initial: function (prior, path) { 111 | return function (p) { 112 | p = prior(p); 113 | path(p).avg = 0; 114 | return p; 115 | }; 116 | } 117 | }; 118 | 119 | var median = { 120 | add: function (prior, path) { 121 | var half; 122 | return function (p, v, nf) { 123 | if(prior) prior(p, v, nf); 124 | 125 | half = Math.floor(path(p).valueList.length/2); 126 | 127 | if(path(p).valueList.length % 2) { 128 | path(p).median = path(p).valueList[half]; 129 | } else { 130 | path(p).median = (path(p).valueList[half-1] + path(p).valueList[half]) / 2.0; 131 | } 132 | 133 | return p; 134 | }; 135 | }, 136 | remove: function (prior, path) { 137 | var half; 138 | return function (p, v, nf) { 139 | if(prior) prior(p, v, nf); 140 | 141 | half = Math.floor(path(p).valueList.length/2); 142 | 143 | // Check for undefined. 144 | if(path(p).valueList.length === 0) { 145 | path(p).median = undefined; 146 | return p; 147 | } 148 | 149 | if(path(p).valueList.length === 1 || path(p).valueList.length % 2) { 150 | path(p).median = path(p).valueList[half]; 151 | } else { 152 | path(p).median = (path(p).valueList[half-1] + path(p).valueList[half]) / 2.0; 153 | } 154 | 155 | return p; 156 | }; 157 | }, 158 | initial: function (prior, path) { 159 | return function (p) { 160 | p = prior(p); 161 | path(p).median = undefined; 162 | return p; 163 | }; 164 | } 165 | }; 166 | 167 | var min = { 168 | add: function (prior, path) { 169 | return function (p, v, nf) { 170 | if(prior) prior(p, v, nf); 171 | 172 | path(p).min = path(p).valueList[0]; 173 | 174 | return p; 175 | }; 176 | }, 177 | remove: function (prior, path) { 178 | return function (p, v, nf) { 179 | if(prior) prior(p, v, nf); 180 | 181 | // Check for undefined. 182 | if(path(p).valueList.length === 0) { 183 | path(p).min = undefined; 184 | return p; 185 | } 186 | 187 | path(p).min = path(p).valueList[0]; 188 | 189 | return p; 190 | }; 191 | }, 192 | initial: function (prior, path) { 193 | return function (p) { 194 | p = prior(p); 195 | path(p).min = undefined; 196 | return p; 197 | }; 198 | } 199 | }; 200 | 201 | var max = { 202 | add: function (prior, path) { 203 | return function (p, v, nf) { 204 | if(prior) prior(p, v, nf); 205 | 206 | path(p).max = path(p).valueList[path(p).valueList.length - 1]; 207 | 208 | return p; 209 | }; 210 | }, 211 | remove: function (prior, path) { 212 | return function (p, v, nf) { 213 | if(prior) prior(p, v, nf); 214 | 215 | // Check for undefined. 216 | if(path(p).valueList.length === 0) { 217 | path(p).max = undefined; 218 | return p; 219 | } 220 | 221 | path(p).max = path(p).valueList[path(p).valueList.length - 1]; 222 | 223 | return p; 224 | }; 225 | }, 226 | initial: function (prior, path) { 227 | return function (p) { 228 | p = prior(p); 229 | path(p).max = undefined; 230 | return p; 231 | }; 232 | } 233 | }; 234 | 235 | var value_count = { 236 | add: function (a, prior, path) { 237 | var i, curr; 238 | return function (p, v, nf) { 239 | if(prior) prior(p, v, nf); 240 | // Not sure if this is more efficient than sorting. 241 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 242 | curr = path(p).values[i]; 243 | if(curr && curr[0] === a(v)) { 244 | // Value already exists in the array - increment it 245 | curr[1]++; 246 | } else { 247 | // Value doesn't exist - add it in form [value, 1] 248 | path(p).values.splice(i, 0, [a(v), 1]); 249 | } 250 | return p; 251 | }; 252 | }, 253 | remove: function (a, prior, path) { 254 | var i; 255 | return function (p, v, nf) { 256 | if(prior) prior(p, v, nf); 257 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 258 | // Value already exists or something has gone terribly wrong. 259 | path(p).values[i][1]--; 260 | return p; 261 | }; 262 | }, 263 | initial: function (prior, path) { 264 | return function (p) { 265 | p = prior(p); 266 | // Array[Array[value, count]] 267 | path(p).values = []; 268 | path(p).bisect = crossfilter.bisect.by(function(d) { return d[0]; }).left; 269 | return p; 270 | }; 271 | } 272 | }; 273 | 274 | var value_list = { 275 | add: function (a, prior, path) { 276 | var i; 277 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 278 | return function (p, v, nf) { 279 | if(prior) prior(p, v, nf); 280 | // Not sure if this is more efficient than sorting. 281 | i = bisect(path(p).valueList, a(v), 0, path(p).valueList.length); 282 | path(p).valueList.splice(i, 0, a(v)); 283 | return p; 284 | }; 285 | }, 286 | remove: function (a, prior, path) { 287 | var i; 288 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 289 | return function (p, v, nf) { 290 | if(prior) prior(p, v, nf); 291 | i = bisect(path(p).valueList, a(v), 0, path(p).valueList.length); 292 | // Value already exists or something has gone terribly wrong. 293 | path(p).valueList.splice(i, 1); 294 | return p; 295 | }; 296 | }, 297 | initial: function (prior, path) { 298 | return function (p) { 299 | p = prior(p); 300 | path(p).valueList = []; 301 | return p; 302 | }; 303 | } 304 | }; 305 | 306 | var exception_count = { 307 | add: function (a, prior, path) { 308 | var i, curr; 309 | return function (p, v, nf) { 310 | if(prior) prior(p, v, nf); 311 | // Only count++ if the p.values array doesn't contain a(v) or if it's 0. 312 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 313 | curr = path(p).values[i]; 314 | if((!curr || curr[0] !== a(v)) || curr[1] === 0) { 315 | path(p).exceptionCount++; 316 | } 317 | return p; 318 | }; 319 | }, 320 | remove: function (a, prior, path) { 321 | var i, curr; 322 | return function (p, v, nf) { 323 | if(prior) prior(p, v, nf); 324 | // Only count-- if the p.values array contains a(v) value of 1. 325 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 326 | curr = path(p).values[i]; 327 | if(curr && curr[0] === a(v) && curr[1] === 1) { 328 | path(p).exceptionCount--; 329 | } 330 | return p; 331 | }; 332 | }, 333 | initial: function (prior, path) { 334 | return function (p) { 335 | p = prior(p); 336 | path(p).exceptionCount = 0; 337 | return p; 338 | }; 339 | } 340 | }; 341 | 342 | var exception_sum = { 343 | add: function (a, sum, prior, path) { 344 | var i, curr; 345 | return function (p, v, nf) { 346 | if(prior) prior(p, v, nf); 347 | // Only sum if the p.values array doesn't contain a(v) or if it's 0. 348 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 349 | curr = path(p).values[i]; 350 | if((!curr || curr[0] !== a(v)) || curr[1] === 0) { 351 | path(p).exceptionSum = path(p).exceptionSum + sum(v); 352 | } 353 | return p; 354 | }; 355 | }, 356 | remove: function (a, sum, prior, path) { 357 | var i, curr; 358 | return function (p, v, nf) { 359 | if(prior) prior(p, v, nf); 360 | // Only sum if the p.values array contains a(v) value of 1. 361 | i = path(p).bisect(path(p).values, a(v), 0, path(p).values.length); 362 | curr = path(p).values[i]; 363 | if(curr && curr[0] === a(v) && curr[1] === 1) { 364 | path(p).exceptionSum = path(p).exceptionSum - sum(v); 365 | } 366 | return p; 367 | }; 368 | }, 369 | initial: function (prior, path) { 370 | return function (p) { 371 | p = prior(p); 372 | path(p).exceptionSum = 0; 373 | return p; 374 | }; 375 | } 376 | }; 377 | 378 | var histogram = { 379 | add: function (a, prior, path) { 380 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 381 | var bisectHisto = crossfilter.bisect.by(function(d) { return d.x; }).right; 382 | var curr; 383 | return function (p, v, nf) { 384 | if(prior) prior(p, v, nf); 385 | curr = path(p).histogram[bisectHisto(path(p).histogram, a(v), 0, path(p).histogram.length) - 1]; 386 | curr.y++; 387 | curr.splice(bisect(curr, a(v), 0, curr.length), 0, a(v)); 388 | return p; 389 | }; 390 | }, 391 | remove: function (a, prior, path) { 392 | var bisect = crossfilter.bisect.by(function(d) { return d; }).left; 393 | var bisectHisto = crossfilter.bisect.by(function(d) { return d.x; }).right; 394 | var curr; 395 | return function (p, v, nf) { 396 | if(prior) prior(p, v, nf); 397 | curr = path(p).histogram[bisectHisto(path(p).histogram, a(v), 0, path(p).histogram.length) - 1]; 398 | curr.y--; 399 | curr.splice(bisect(curr, a(v), 0, curr.length), 1); 400 | return p; 401 | }; 402 | }, 403 | initial: function (thresholds, prior, path) { 404 | return function (p) { 405 | p = prior(p); 406 | path(p).histogram = []; 407 | var arr = []; 408 | for(var i = 1; i < thresholds.length; i++) { 409 | arr = []; 410 | arr.x = thresholds[i - 1]; 411 | arr.dx = (thresholds[i] - thresholds[i - 1]); 412 | arr.y = 0; 413 | path(p).histogram.push(arr); 414 | } 415 | return p; 416 | }; 417 | } 418 | }; 419 | 420 | var sum_of_sq = { 421 | add: function (a, prior, path) { 422 | return function (p, v, nf) { 423 | if(prior) prior(p, v, nf); 424 | path(p).sumOfSq = path(p).sumOfSq + a(v)*a(v); 425 | return p; 426 | }; 427 | }, 428 | remove: function (a, prior, path) { 429 | return function (p, v, nf) { 430 | if(prior) prior(p, v, nf); 431 | path(p).sumOfSq = path(p).sumOfSq - a(v)*a(v); 432 | return p; 433 | }; 434 | }, 435 | initial: function (prior, path) { 436 | return function (p) { 437 | p = prior(p); 438 | path(p).sumOfSq = 0; 439 | return p; 440 | }; 441 | } 442 | }; 443 | 444 | var std = { 445 | add: function (prior, path) { 446 | return function (p, v, nf) { 447 | if(prior) prior(p, v, nf); 448 | if(path(p).count > 0) { 449 | path(p).std = 0.0; 450 | var n = path(p).sumOfSq - path(p).sum*path(p).sum/path(p).count; 451 | if (n>0.0) path(p).std = Math.sqrt(n/(path(p).count-1)); 452 | } else { 453 | path(p).std = 0.0; 454 | } 455 | return p; 456 | }; 457 | }, 458 | remove: function (prior, path) { 459 | return function (p, v, nf) { 460 | if(prior) prior(p, v, nf); 461 | if(path(p).count > 0) { 462 | path(p).std = 0.0; 463 | var n = path(p).sumOfSq - path(p).sum*path(p).sum/path(p).count; 464 | if (n>0.0) path(p).std = Math.sqrt(n/(path(p).count-1)); 465 | } else { 466 | path(p).std = 0; 467 | } 468 | return p; 469 | }; 470 | }, 471 | initial: function (prior, path) { 472 | return function (p) { 473 | p = prior(p); 474 | path(p).std = 0; 475 | return p; 476 | }; 477 | } 478 | }; 479 | 480 | var nest = { 481 | add: function (keyAccessors, prior, path) { 482 | var arrRef; 483 | var newRef; 484 | return function (p, v, nf) { 485 | if(prior) prior(p, v, nf); 486 | 487 | arrRef = path(p).nest; 488 | keyAccessors.forEach(function(a) { 489 | newRef = arrRef.filter(function(d) { return d.key === a(v); })[0]; 490 | if(newRef) { 491 | // There is another level. 492 | arrRef = newRef.values; 493 | } else { 494 | // Next level doesn't yet exist so we create it. 495 | newRef = []; 496 | arrRef.push({ key: a(v), values: newRef }); 497 | arrRef = newRef; 498 | } 499 | }); 500 | 501 | arrRef.push(v); 502 | 503 | return p; 504 | }; 505 | }, 506 | remove: function (keyAccessors, prior, path) { 507 | var arrRef; 508 | return function (p, v, nf) { 509 | if(prior) prior(p, v, nf); 510 | 511 | arrRef = path(p).nest; 512 | keyAccessors.forEach(function(a) { 513 | arrRef = arrRef.filter(function(d) { return d.key === a(v); })[0].values; 514 | }); 515 | 516 | // Array contains an actual reference to the row, so just splice it out. 517 | arrRef.splice(arrRef.indexOf(v), 1); 518 | 519 | // If the leaf now has length 0 and it's not the base array remove it. 520 | // TODO 521 | 522 | return p; 523 | }; 524 | }, 525 | initial: function (prior, path) { 526 | return function (p) { 527 | p = prior(p); 528 | path(p).nest = []; 529 | return p; 530 | }; 531 | } 532 | }; 533 | 534 | var alias = { 535 | initial: function(prior, path, obj) { 536 | return function (p) { 537 | if(prior) p = prior(p); 538 | function buildAliasFunction(key){ 539 | return function(){ 540 | return obj[key](path(p)); 541 | }; 542 | } 543 | for(var prop in obj) { 544 | path(p)[prop] = buildAliasFunction(prop); 545 | } 546 | return p; 547 | }; 548 | } 549 | }; 550 | 551 | var alias_prop = { 552 | add: function (obj, prior, path) { 553 | return function (p, v, nf) { 554 | if(prior) prior(p, v, nf); 555 | for(var prop in obj) { 556 | path(p)[prop] = obj[prop](path(p),v); 557 | } 558 | return p; 559 | }; 560 | } 561 | }; 562 | 563 | var data_list = { 564 | add: function(a, prior, path) { 565 | return function (p, v, nf) { 566 | if(prior) prior(p, v, nf); 567 | path(p).dataList.push(v); 568 | return p; 569 | }; 570 | }, 571 | remove: function(a, prior, path) { 572 | return function (p, v, nf) { 573 | if(prior) prior(p, v, nf); 574 | path(p).dataList.splice(path(p).dataList.indexOf(v), 1); 575 | return p; 576 | }; 577 | }, 578 | initial: function(prior, path) { 579 | return function (p) { 580 | if(prior) p = prior(p); 581 | path(p).dataList = []; 582 | return p; 583 | }; 584 | } 585 | }; 586 | 587 | var custom = { 588 | add: function(prior, path, addFn) { 589 | return function (p, v, nf) { 590 | if(prior) prior(p, v, nf); 591 | return addFn(p, v); 592 | }; 593 | }, 594 | remove: function(prior, path, removeFn) { 595 | return function (p, v, nf) { 596 | if(prior) prior(p, v, nf); 597 | return removeFn(p, v); 598 | }; 599 | }, 600 | initial: function(prior, path, initialFn) { 601 | return function (p) { 602 | if(prior) p = prior(p); 603 | return initialFn(p); 604 | }; 605 | } 606 | }; 607 | 608 | function build_function(p, f, path) { 609 | // We have to build these functions in order. Eventually we can include dependency 610 | // information and create a dependency graph if the process becomes complex enough. 611 | 612 | if(!path) path = function (d) { return d; }; 613 | 614 | // Keep track of the original reducers so that filtering can skip back to 615 | // them if this particular value is filtered out. 616 | var origF = { 617 | reduceAdd: f.reduceAdd, 618 | reduceRemove: f.reduceRemove, 619 | reduceInitial: f.reduceInitial 620 | }; 621 | 622 | if(p.count || p.std) { 623 | f.reduceAdd = count.add(f.reduceAdd, path, p.count); 624 | f.reduceRemove = count.remove(f.reduceRemove, path, p.count); 625 | f.reduceInitial = count.initial(f.reduceInitial, path, p.count); 626 | } 627 | 628 | if(p.sum) { 629 | f.reduceAdd = sum.add(p.sum, f.reduceAdd, path); 630 | f.reduceRemove = sum.remove(p.sum, f.reduceRemove, path); 631 | f.reduceInitial = sum.initial(f.reduceInitial, path); 632 | } 633 | 634 | if(p.avg) { 635 | if(!p.count || !p.sum) { 636 | console.error("You must set .count(true) and define a .sum(accessor) to use .avg(true)."); 637 | } else { 638 | f.reduceAdd = avg.add(p.sum, f.reduceAdd, path); 639 | f.reduceRemove = avg.remove(p.sum, f.reduceRemove, path); 640 | f.reduceInitial = avg.initial(f.reduceInitial, path); 641 | } 642 | } 643 | 644 | // The unique-only reducers come before the value_count reducers. They need to check if 645 | // the value is already in the values array on the group. They should only increment/decrement 646 | // counts if the value not in the array or the count on the value is 0. 647 | if(p.exceptionCount) { 648 | if(!p.exceptionAccessor) { 649 | console.error("You must define an .exception(accessor) to use .exceptionCount(true)."); 650 | } else { 651 | f.reduceAdd = exception_count.add(p.exceptionAccessor, f.reduceAdd, path); 652 | f.reduceRemove = exception_count.remove(p.exceptionAccessor, f.reduceRemove, path); 653 | f.reduceInitial = exception_count.initial(f.reduceInitial, path); 654 | } 655 | } 656 | 657 | if(p.exceptionSum) { 658 | if(!p.exceptionAccessor) { 659 | console.error("You must define an .exception(accessor) to use .exceptionSum(accessor)."); 660 | } else { 661 | f.reduceAdd = exception_sum.add(p.exceptionAccessor, p.exceptionSum, f.reduceAdd, path); 662 | f.reduceRemove = exception_sum.remove(p.exceptionAccessor, p.exceptionSum, f.reduceRemove, path); 663 | f.reduceInitial = exception_sum.initial(f.reduceInitial, path); 664 | } 665 | } 666 | 667 | // Maintain the values array. 668 | if(p.valueList || p.median || p.min || p.max) { 669 | f.reduceAdd = value_list.add(p.valueList, f.reduceAdd, path); 670 | f.reduceRemove = value_list.remove(p.valueList, f.reduceRemove, path); 671 | f.reduceInitial = value_list.initial(f.reduceInitial, path); 672 | } 673 | 674 | // Maintain the data array. 675 | if(p.dataList) { 676 | f.reduceAdd = data_list.add(p.dataList, f.reduceAdd, path); 677 | f.reduceRemove = data_list.remove(p.dataList, f.reduceRemove, path); 678 | f.reduceInitial = data_list.initial(f.reduceInitial, path); 679 | } 680 | 681 | if(p.median) { 682 | f.reduceAdd = median.add(f.reduceAdd, path); 683 | f.reduceRemove = median.remove(f.reduceRemove, path); 684 | f.reduceInitial = median.initial(f.reduceInitial, path); 685 | } 686 | 687 | if(p.min) { 688 | f.reduceAdd = min.add(f.reduceAdd, path); 689 | f.reduceRemove = min.remove(f.reduceRemove, path); 690 | f.reduceInitial = min.initial(f.reduceInitial, path); 691 | } 692 | 693 | if(p.max) { 694 | f.reduceAdd = max.add(f.reduceAdd, path); 695 | f.reduceRemove = max.remove(f.reduceRemove, path); 696 | f.reduceInitial = max.initial(f.reduceInitial, path); 697 | } 698 | 699 | // Maintain the values count array. 700 | if(p.exceptionAccessor) { 701 | f.reduceAdd = value_count.add(p.exceptionAccessor, f.reduceAdd, path); 702 | f.reduceRemove = value_count.remove(p.exceptionAccessor, f.reduceRemove, path); 703 | f.reduceInitial = value_count.initial(f.reduceInitial, path); 704 | } 705 | 706 | // Histogram 707 | if(p.histogramValue && p.histogramThresholds) { 708 | f.reduceAdd = histogram.add(p.histogramValue, f.reduceAdd, path); 709 | f.reduceRemove = histogram.remove(p.histogramValue, f.reduceRemove, path); 710 | f.reduceInitial = histogram.initial(p.histogramThresholds ,f.reduceInitial, path); 711 | } 712 | 713 | // Sum of Squares 714 | if(p.sumOfSquares) { 715 | f.reduceAdd = sum_of_sq.add(p.sumOfSquares, f.reduceAdd, path); 716 | f.reduceRemove = sum_of_sq.remove(p.sumOfSquares, f.reduceRemove, path); 717 | f.reduceInitial = sum_of_sq.initial(f.reduceInitial, path); 718 | } 719 | 720 | // Standard deviation 721 | if(p.std) { 722 | if(!p.sumOfSquares || !p.sum) { 723 | console.error("You must set .sumOfSq(accessor) and define a .sum(accessor) to use .std(true). Or use .std(accessor)."); 724 | } else { 725 | f.reduceAdd = std.add(f.reduceAdd, path); 726 | f.reduceRemove = std.remove(f.reduceRemove, path); 727 | f.reduceInitial = std.initial(f.reduceInitial, path); 728 | } 729 | } 730 | 731 | // Custom reducer defined by 3 functions : add, remove, initial 732 | if (p.custom) { 733 | f.reduceAdd = custom.add(f.reduceAdd, path, p.custom.add); 734 | f.reduceRemove = custom.remove(f.reduceRemove, path, p.custom.remove); 735 | f.reduceInitial = custom.initial(f.reduceInitial, path, p.custom.initial); 736 | } 737 | 738 | // Nesting 739 | if(p.nestKeys) { 740 | f.reduceAdd = nest.add(p.nestKeys, f.reduceAdd, path); 741 | f.reduceRemove = nest.remove(p.nestKeys, f.reduceRemove, path); 742 | f.reduceInitial = nest.initial(f.reduceInitial, path); 743 | } 744 | 745 | // Alias functions 746 | if(p.aliasKeys) { 747 | f.reduceInitial = alias.initial(f.reduceInitial, path, p.aliasKeys); 748 | } 749 | 750 | // Alias properties - this is less efficient than alias functions 751 | if(p.aliasPropKeys) { 752 | f.reduceAdd = alias_prop.add(p.aliasPropKeys, f.reduceAdd, path); 753 | // This isn't a typo. The function is the same for add/remove. 754 | f.reduceRemove = alias_prop.add(p.aliasPropKeys, f.reduceRemove, path); 755 | } 756 | 757 | // Filters determine if our built-up priors should run, or if it should skip 758 | // back to the filters given at the beginning of this build function. 759 | if (p.filter) { 760 | f.reduceAdd = filter.add(p.filter, f.reduceAdd, origF.reduceAdd, path); 761 | f.reduceRemove = filter.remove(p.filter, f.reduceRemove, origF.reduceRemove, path); 762 | } 763 | 764 | // Values go last. 765 | if(p.values) { 766 | Object.getOwnPropertyNames(p.values).forEach(function(n) { 767 | // Set up the path on each group. 768 | var setupPath = function(prior) { 769 | return function (p) { 770 | p = prior(p); 771 | path(p)[n] = {}; 772 | return p; 773 | }; 774 | }; 775 | f.reduceInitial = setupPath(f.reduceInitial); 776 | build_function(p.values[n].parameters, f, function (p) { return p[n]; }); 777 | }); 778 | } 779 | } 780 | 781 | var build = { 782 | build: build_function 783 | }; 784 | 785 | var parameters = function() { 786 | return { 787 | order: false, 788 | avg: false, 789 | count: false, 790 | sum: false, 791 | exceptionAccessor: false, 792 | exceptionCount: false, 793 | exceptionSum: false, 794 | filter: false, 795 | valueList: false, 796 | median: false, 797 | histogramValue: false, 798 | min: false, 799 | max: false, 800 | histogramThresholds: false, 801 | std: false, 802 | sumOfSquares: false, 803 | values: false, 804 | nestKeys: false, 805 | aliasKeys: false, 806 | aliasPropKeys: false, 807 | groupAll: false, 808 | dataList: false, 809 | custom: false 810 | }; 811 | }; 812 | 813 | function assign(target) { 814 | if (target == null) { 815 | throw new TypeError('Cannot convert undefined or null to object'); 816 | } 817 | 818 | var output = Object(target); 819 | for (var index = 1; index < arguments.length; ++index) { 820 | var source = arguments[index]; 821 | if (source != null) { 822 | for (var nextKey in source) { 823 | if(source.hasOwnProperty(nextKey)) { 824 | output[nextKey] = source[nextKey]; 825 | } 826 | } 827 | } 828 | } 829 | return output; 830 | } 831 | 832 | function accessor_build(obj, p) { 833 | // obj.order = function(value) { 834 | // if (!arguments.length) return p.order; 835 | // p.order = value; 836 | // return obj; 837 | // }; 838 | 839 | // Converts a string to an accessor function 840 | function accessorify(v) { 841 | if( typeof v === 'string' ) { 842 | // Rewrite to a function 843 | var tempValue = v; 844 | var func = function (d) { return d[tempValue]; }; 845 | return func; 846 | } else { 847 | return v; 848 | } 849 | } 850 | 851 | // Converts a string to an accessor function 852 | function accessorifyNumeric(v) { 853 | if( typeof v === 'string' ) { 854 | // Rewrite to a function 855 | var tempValue = v; 856 | var func = function (d) { return +d[tempValue]; }; 857 | return func; 858 | } else { 859 | return v; 860 | } 861 | } 862 | 863 | obj.fromObject = function(value) { 864 | if(!arguments.length) return p; 865 | assign(p, value); 866 | return obj; 867 | }; 868 | 869 | obj.toObject = function() { 870 | return p; 871 | }; 872 | 873 | obj.count = function(value, propName) { 874 | if (!arguments.length) return p.count; 875 | if (!propName) { 876 | propName = 'count'; 877 | } 878 | p.count = propName; 879 | return obj; 880 | }; 881 | 882 | obj.sum = function(value) { 883 | if (!arguments.length) return p.sum; 884 | 885 | value = accessorifyNumeric(value); 886 | 887 | p.sum = value; 888 | return obj; 889 | }; 890 | 891 | obj.avg = function(value) { 892 | if (!arguments.length) return p.avg; 893 | 894 | value = accessorifyNumeric(value); 895 | 896 | // We can take an accessor function, a boolean, or a string 897 | if( typeof value === 'function' ) { 898 | if(p.sum && p.sum !== value) console.warn('SUM aggregation is being overwritten by AVG aggregation'); 899 | p.sum = value; 900 | p.avg = true; 901 | p.count = 'count'; 902 | } else { 903 | p.avg = value; 904 | } 905 | return obj; 906 | }; 907 | 908 | obj.exception = function(value) { 909 | if (!arguments.length) return p.exceptionAccessor; 910 | 911 | value = accessorify(value); 912 | 913 | p.exceptionAccessor = value; 914 | return obj; 915 | }; 916 | 917 | obj.filter = function(value) { 918 | if (!arguments.length) return p.filter; 919 | p.filter = value; 920 | return obj; 921 | }; 922 | 923 | obj.valueList = function(value) { 924 | if (!arguments.length) return p.valueList; 925 | 926 | value = accessorify(value); 927 | 928 | p.valueList = value; 929 | return obj; 930 | }; 931 | 932 | obj.median = function(value) { 933 | if (!arguments.length) return p.median; 934 | 935 | value = accessorifyNumeric(value); 936 | 937 | if(typeof value === 'function') { 938 | if(p.valueList && p.valueList !== value) console.warn('VALUELIST accessor is being overwritten by median aggregation'); 939 | p.valueList = value; 940 | } 941 | p.median = value; 942 | return obj; 943 | }; 944 | 945 | obj.min = function(value) { 946 | if (!arguments.length) return p.min; 947 | 948 | value = accessorifyNumeric(value); 949 | 950 | if(typeof value === 'function') { 951 | if(p.valueList && p.valueList !== value) console.warn('VALUELIST accessor is being overwritten by min aggregation'); 952 | p.valueList = value; 953 | } 954 | p.min = value; 955 | return obj; 956 | }; 957 | 958 | obj.max = function(value) { 959 | if (!arguments.length) return p.max; 960 | 961 | value = accessorifyNumeric(value); 962 | 963 | if(typeof value === 'function') { 964 | if(p.valueList && p.valueList !== value) console.warn('VALUELIST accessor is being overwritten by max aggregation'); 965 | p.valueList = value; 966 | } 967 | p.max = value; 968 | return obj; 969 | }; 970 | 971 | obj.exceptionCount = function(value) { 972 | if (!arguments.length) return p.exceptionCount; 973 | 974 | value = accessorify(value); 975 | 976 | if( typeof value === 'function' ) { 977 | if(p.exceptionAccessor && p.exceptionAccessor !== value) console.warn('EXCEPTION accessor is being overwritten by exception count aggregation'); 978 | p.exceptionAccessor = value; 979 | p.exceptionCount = true; 980 | } else { 981 | p.exceptionCount = value; 982 | } 983 | return obj; 984 | }; 985 | 986 | obj.exceptionSum = function(value) { 987 | if (!arguments.length) return p.exceptionSum; 988 | 989 | value = accessorifyNumeric(value); 990 | 991 | p.exceptionSum = value; 992 | return obj; 993 | }; 994 | 995 | obj.histogramValue = function(value) { 996 | if (!arguments.length) return p.histogramValue; 997 | 998 | value = accessorifyNumeric(value); 999 | 1000 | p.histogramValue = value; 1001 | return obj; 1002 | }; 1003 | 1004 | obj.histogramBins = function(value) { 1005 | if (!arguments.length) return p.histogramThresholds; 1006 | p.histogramThresholds = value; 1007 | return obj; 1008 | }; 1009 | 1010 | obj.std = function(value) { 1011 | if (!arguments.length) return p.std; 1012 | 1013 | value = accessorifyNumeric(value); 1014 | 1015 | if(typeof(value) === 'function') { 1016 | p.sumOfSquares = value; 1017 | p.sum = value; 1018 | p.count = 'count'; 1019 | p.std = true; 1020 | } else { 1021 | p.std = value; 1022 | } 1023 | return obj; 1024 | }; 1025 | 1026 | obj.sumOfSq = function(value) { 1027 | if (!arguments.length) return p.sumOfSquares; 1028 | 1029 | value = accessorifyNumeric(value); 1030 | 1031 | p.sumOfSquares = value; 1032 | return obj; 1033 | }; 1034 | 1035 | obj.value = function(value, accessor) { 1036 | if (!arguments.length || typeof value !== 'string' ) { 1037 | console.error("'value' requires a string argument."); 1038 | } else { 1039 | if(!p.values) p.values = {}; 1040 | p.values[value] = {}; 1041 | p.values[value].parameters = parameters(); 1042 | accessor_build(p.values[value], p.values[value].parameters); 1043 | if(accessor) p.values[value].accessor = accessor; 1044 | return p.values[value]; 1045 | } 1046 | }; 1047 | 1048 | obj.nest = function(keyAccessorArray) { 1049 | if(!arguments.length) return p.nestKeys; 1050 | 1051 | keyAccessorArray.map(accessorify); 1052 | 1053 | p.nestKeys = keyAccessorArray; 1054 | return obj; 1055 | }; 1056 | 1057 | obj.alias = function(propAccessorObj) { 1058 | if(!arguments.length) return p.aliasKeys; 1059 | p.aliasKeys = propAccessorObj; 1060 | return obj; 1061 | }; 1062 | 1063 | obj.aliasProp = function(propAccessorObj) { 1064 | if(!arguments.length) return p.aliasPropKeys; 1065 | p.aliasPropKeys = propAccessorObj; 1066 | return obj; 1067 | }; 1068 | 1069 | obj.groupAll = function(groupTest) { 1070 | if(!arguments.length) return p.groupAll; 1071 | p.groupAll = groupTest; 1072 | return obj; 1073 | }; 1074 | 1075 | obj.dataList = function(value) { 1076 | if (!arguments.length) return p.dataList; 1077 | p.dataList = value; 1078 | return obj; 1079 | }; 1080 | 1081 | obj.custom = function(addRemoveInitialObj) { 1082 | if (!arguments.length) return p.custom; 1083 | p.custom = addRemoveInitialObj; 1084 | return obj; 1085 | }; 1086 | 1087 | } 1088 | 1089 | var accessors = { 1090 | build: accessor_build 1091 | }; 1092 | 1093 | function postProcess(reductio) { 1094 | return function (group, p, f) { 1095 | group.post = function(){ 1096 | var postprocess = function () { 1097 | return postprocess.all(); 1098 | }; 1099 | postprocess.all = function () { 1100 | return group.all(); 1101 | }; 1102 | var postprocessors = reductio.postprocessors; 1103 | Object.keys(postprocessors).forEach(function (name) { 1104 | postprocess[name] = function () { 1105 | var _all = postprocess.all; 1106 | var args = [].slice.call(arguments); 1107 | postprocess.all = function () { 1108 | return postprocessors[name](_all, f, p).apply(null, args); 1109 | }; 1110 | return postprocess; 1111 | }; 1112 | }); 1113 | return postprocess; 1114 | }; 1115 | }; 1116 | } 1117 | 1118 | var pluck = function(n){ 1119 | return function(d){ 1120 | return d[n]; 1121 | }; 1122 | }; 1123 | 1124 | // supported operators are sum, avg, and count 1125 | const _grouper = function(path, prior){ 1126 | if(!path) path = function(d){return d;}; 1127 | return function(p, v){ 1128 | if(prior) prior(p, v); 1129 | var x = path(p), y = path(v); 1130 | if(typeof y.count !== 'undefined') x.count += y.count; 1131 | if(typeof y.sum !== 'undefined') x.sum += y.sum; 1132 | if(typeof y.avg !== 'undefined') x.avg = x.sum/x.count; 1133 | return p; 1134 | }; 1135 | }; 1136 | 1137 | const cap = function (prior, f, p) { 1138 | var obj = f.reduceInitial(); 1139 | // we want to support values so we'll need to know what those are 1140 | var values = p.values ? Object.keys(p.values) : []; 1141 | var _othersGrouper = _grouper(); 1142 | if (values.length) { 1143 | for (var i = 0; i < values.length; ++i) { 1144 | _othersGrouper = _grouper(pluck(values[i]), _othersGrouper); 1145 | } 1146 | } 1147 | return function (cap, othersName) { 1148 | if (!arguments.length) return prior(); 1149 | if( cap === Infinity || !cap ) return prior(); 1150 | var all = prior(); 1151 | var slice_idx = cap-1; 1152 | if(all.length <= cap) return all; 1153 | var data = all.slice(0, slice_idx); 1154 | var others = {key: othersName || 'Others'}; 1155 | others.value = f.reduceInitial(); 1156 | for (var i = slice_idx; i < all.length; ++i) { 1157 | _othersGrouper(others.value, all[i].value); 1158 | } 1159 | data.push(others); 1160 | return data; 1161 | }; 1162 | }; 1163 | 1164 | var pluck_n = function (n) { 1165 | if (typeof n === 'function') { 1166 | return n; 1167 | } 1168 | if (~n.indexOf('.')) { 1169 | var split = n.split('.'); 1170 | return function (d) { 1171 | return split.reduce(function (p, v) { 1172 | return p[v]; 1173 | }, d); 1174 | }; 1175 | } 1176 | return function (d) { 1177 | return d[n]; 1178 | }; 1179 | }; 1180 | 1181 | function ascending(a, b) { 1182 | return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; 1183 | } 1184 | 1185 | var comparer = function (accessor, ordering) { 1186 | return function (a, b) { 1187 | return ordering(accessor(a), accessor(b)); 1188 | }; 1189 | }; 1190 | 1191 | function sortBy (prior) { 1192 | return function (value, order) { 1193 | if (arguments.length === 1) { 1194 | order = ascending; 1195 | } 1196 | return prior().sort(comparer(pluck_n(value), order)); 1197 | }; 1198 | } 1199 | 1200 | function postprocessors(reductio){ 1201 | reductio.postprocessors = {}; 1202 | reductio.registerPostProcessor = function(name, func){ 1203 | reductio.postprocessors[name] = func; 1204 | }; 1205 | 1206 | reductio.registerPostProcessor('cap', cap); 1207 | reductio.registerPostProcessor('sortBy', sortBy); 1208 | } 1209 | 1210 | function reductio() { 1211 | var parameters$1 = parameters(); 1212 | 1213 | var funcs = {}; 1214 | 1215 | function my(group) { 1216 | // Start fresh each time. 1217 | funcs = { 1218 | reduceAdd: function(p) { return p; }, 1219 | reduceRemove: function(p) { return p; }, 1220 | reduceInitial: function () { return {}; }, 1221 | }; 1222 | 1223 | build.build(parameters$1, funcs); 1224 | 1225 | // If we're doing groupAll 1226 | if(parameters$1.groupAll) { 1227 | if(group.top) { 1228 | console.warn("'groupAll' is defined but attempting to run on a standard dimension.group(). Must run on dimension.groupAll()."); 1229 | } else { 1230 | var bisect = crossfilter.bisect.by(function(d) { return d.key; }).left; 1231 | var i, j; 1232 | var keys; 1233 | var keysLength; 1234 | var k; // Key 1235 | group.reduce( 1236 | function(p, v, nf) { 1237 | keys = parameters$1.groupAll(v); 1238 | keysLength = keys.length; 1239 | for(j=0;j