universe
Version:
The fastest way to query and explore multivariate datasets
1,557 lines (1,354 loc) • 657 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.universe = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
module.exports = require("./src/crossfilter").crossfilter;
},{"./src/crossfilter":5}],2:[function(require,module,exports){
module.exports={
"name": "crossfilter2",
"version": "1.4.0-alpha.06",
"description": "Fast multidimensional filtering for coordinated views.",
"keywords": [
"analytics",
"visualization",
"crossfilter"
],
"author": {
"name": "Mike Bostock",
"url": "http://bost.ocks.org/mike"
},
"contributors": [
{
"name": "Jason Davies",
"url": "http://www.jasondavies.com/"
}
],
"maintainers": [
{
"name": "Gordon Woodhull",
"url": "https://github.com/gordonwoodhull"
},
{
"name": "Tanner Linsley",
"url": "https://github.com/tannerlinsley"
},
{
"name": "Ethan Jewett",
"url": "https://github.com/esjewett"
}
],
"homepage": "http://crossfilter.github.com/crossfilter/",
"main": "./index.js",
"repository": {
"type": "git",
"url": "http://github.com/crossfilter/crossfilter.git"
},
"dependencies": {
"lodash.result": "^4.4.0"
},
"devDependencies": {
"browserify": "^13.0.0",
"d3": "3.5",
"package-json-versionify": "1.0.2",
"uglify-js": "2.4.0",
"vows": "0.7.0"
},
"scripts": {
"benchmark": "node test/benchmark.js",
"build": "browserify index.js -t package-json-versionify --standalone crossfilter -o crossfilter.js && uglifyjs --compress --mangle --screw-ie8 crossfilter.js -o crossfilter.min.js",
"clean": "rm -f crossfilter.js crossfilter.min.js",
"test": "./node_modules/.bin/vows --verbose"
},
"files": [
"src",
"index.js",
"crossfilter.js",
"crossfilter.min.js"
]
}
},{}],3:[function(require,module,exports){
if (typeof Uint8Array !== "undefined") {
crossfilter_array8 = function(n) { return new Uint8Array(n); };
crossfilter_array16 = function(n) { return new Uint16Array(n); };
crossfilter_array32 = function(n) { return new Uint32Array(n); };
crossfilter_arrayLengthen = function(array, length) {
if (array.length >= length) return array;
var copy = new array.constructor(length);
copy.set(array);
return copy;
};
crossfilter_arrayWiden = function(array, width) {
var copy;
switch (width) {
case 16: copy = crossfilter_array16(array.length); break;
case 32: copy = crossfilter_array32(array.length); break;
default: throw new Error("invalid array width!");
}
copy.set(array);
return copy;
};
}
function crossfilter_arrayUntyped(n) {
var array = new Array(n), i = -1;
while (++i < n) array[i] = 0;
return array;
}
function crossfilter_arrayLengthenUntyped(array, length) {
var n = array.length;
while (n < length) array[n++] = 0;
return array;
}
function crossfilter_arrayWidenUntyped(array, width) {
if (width > 32) throw new Error("invalid array width!");
return array;
}
// An arbitrarily-wide array of bitmasks
function crossfilter_bitarray(n) {
this.length = n;
this.subarrays = 1;
this.width = 8;
this.masks = {
0: 0
}
this[0] = crossfilter_array8(n);
}
crossfilter_bitarray.prototype.lengthen = function(n) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
this[i] = crossfilter_arrayLengthen(this[i], n);
}
this.length = n;
};
// Reserve a new bit index in the array, returns {offset, one}
crossfilter_bitarray.prototype.add = function() {
var m, w, one, i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
m = this.masks[i];
w = this.width - (32 * i);
one = ~m & -~m;
if (w >= 32 && !one) {
continue;
}
if (w < 32 && (one & (1 << w))) {
// widen this subarray
this[i] = crossfilter_arrayWiden(this[i], w <<= 1);
this.width = 32 * i + w;
}
this.masks[i] |= one;
return {
offset: i,
one: one
};
}
// add a new subarray
this[this.subarrays] = crossfilter_array8(this.length);
this.masks[this.subarrays] = 1;
this.width += 8;
return {
offset: this.subarrays++,
one: 1
};
};
// Copy record from index src to index dest
crossfilter_bitarray.prototype.copy = function(dest, src) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
this[i][dest] = this[i][src];
}
};
// Truncate the array to the given length
crossfilter_bitarray.prototype.truncate = function(n) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
for (var j = this.length - 1; j >= n; j--) {
this[i][j] = 0;
}
this[i].length = n;
}
this.length = n;
};
// Checks that all bits for the given index are 0
crossfilter_bitarray.prototype.zero = function(n) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
if (this[i][n]) {
return false;
}
}
return true;
};
// Checks that all bits for the given index are 0 except for possibly one
crossfilter_bitarray.prototype.zeroExcept = function(n, offset, zero) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
if (i === offset ? this[i][n] & zero : this[i][n]) {
return false;
}
}
return true;
};
// Checks that all bits for the given indez are 0 except for the specified mask.
// The mask should be an array of the same size as the filter subarrays width.
crossfilter_bitarray.prototype.zeroExceptMask = function(n, mask) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
if (this[i][n] & mask[i]) {
return false;
}
}
return true;
}
// Checks that only the specified bit is set for the given index
crossfilter_bitarray.prototype.only = function(n, offset, one) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
if (this[i][n] != (i === offset ? one : 0)) {
return false;
}
}
return true;
};
// Checks that only the specified bit is set for the given index except for possibly one other
crossfilter_bitarray.prototype.onlyExcept = function(n, offset, zero, onlyOffset, onlyOne) {
var mask;
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
mask = this[i][n];
if (i === offset)
mask &= zero;
if (mask != (i === onlyOffset ? onlyOne : 0)) {
return false;
}
}
return true;
};
module.exports = {
array8: crossfilter_arrayUntyped,
array16: crossfilter_arrayUntyped,
array32: crossfilter_arrayUntyped,
arrayLengthen: crossfilter_arrayLengthenUntyped,
arrayWiden: crossfilter_arrayWidenUntyped,
bitarray: crossfilter_bitarray
};
},{}],4:[function(require,module,exports){
var crossfilter_identity = require('./identity');
function bisect_by(f) {
// Locate the insertion point for x in a to maintain sorted order. The
// arguments lo and hi may be used to specify a subset of the array which
// should be considered; by default the entire array is used. If x is already
// present in a, the insertion point will be before (to the left of) any
// existing entries. The return value is suitable for use as the first
// argument to `array.splice` assuming that a is already sorted.
//
// The returned insertion point i partitions the array a into two halves so
// that all v < x for v in a[lo:i] for the left side and all v >= x for v in
// a[i:hi] for the right side.
function bisectLeft(a, x, lo, hi) {
while (lo < hi) {
var mid = lo + hi >>> 1;
if (f(a[mid]) < x) lo = mid + 1;
else hi = mid;
}
return lo;
}
// Similar to bisectLeft, but returns an insertion point which comes after (to
// the right of) any existing entries of x in a.
//
// The returned insertion point i partitions the array into two halves so that
// all v <= x for v in a[lo:i] for the left side and all v > x for v in
// a[i:hi] for the right side.
function bisectRight(a, x, lo, hi) {
while (lo < hi) {
var mid = lo + hi >>> 1;
if (x < f(a[mid])) hi = mid;
else lo = mid + 1;
}
return lo;
}
bisectRight.right = bisectRight;
bisectRight.left = bisectLeft;
return bisectRight;
}
module.exports = bisect_by(crossfilter_identity);
module.exports.by = bisect_by; // assign the raw function to the export as well
},{"./identity":9}],5:[function(require,module,exports){
var xfilterArray = require('./array');
var xfilterFilter = require('./filter');
var crossfilter_identity = require('./identity');
var crossfilter_null = require('./null');
var crossfilter_zero = require('./zero');
var xfilterHeapselect = require('./heapselect');
var xfilterHeap = require('./heap');
var bisect = require('./bisect');
var insertionsort = require('./insertionsort');
var permute = require('./permute');
var quicksort = require('./quicksort');
var xfilterReduce = require('./reduce');
var packageJson = require('./../package.json'); // require own package.json for the version field
var result = require('lodash.result');
// expose API exports
exports.crossfilter = crossfilter;
exports.crossfilter.heap = xfilterHeap;
exports.crossfilter.heapselect = xfilterHeapselect;
exports.crossfilter.bisect = bisect;
exports.crossfilter.insertionsort = insertionsort;
exports.crossfilter.permute = permute;
exports.crossfilter.quicksort = quicksort;
exports.crossfilter.version = packageJson.version; // please note use of "package-json-versionify" transform
function crossfilter() {
var crossfilter = {
add: add,
remove: removeData,
dimension: dimension,
groupAll: groupAll,
size: size,
all: all,
onChange: onChange,
isElementFiltered: isElementFiltered,
};
var data = [], // the records
n = 0, // the number of records; data.length
filters, // 1 is filtered out
filterListeners = [], // when the filters change
dataListeners = [], // when data is added
removeDataListeners = [], // when data is removed
callbacks = [];
filters = new xfilterArray.bitarray(0);
// Adds the specified new records to this crossfilter.
function add(newData) {
var n0 = n,
n1 = newData.length;
// If there's actually new data to add…
// Merge the new data into the existing data.
// Lengthen the filter bitset to handle the new records.
// Notify listeners (dimensions and groups) that new data is available.
if (n1) {
data = data.concat(newData);
filters.lengthen(n += n1);
dataListeners.forEach(function(l) { l(newData, n0, n1); });
triggerOnChange('dataAdded');
}
return crossfilter;
}
// Removes all records that match the current filters.
function removeData() {
var newIndex = crossfilter_index(n, n),
removed = [];
for (var i = 0, j = 0; i < n; ++i) {
if (!filters.zero(i)) newIndex[i] = j++;
else removed.push(i);
}
// Remove all matching records from groups.
filterListeners.forEach(function(l) { l(-1, -1, [], removed, true); });
// Update indexes.
removeDataListeners.forEach(function(l) { l(newIndex); });
// Remove old filters and data by overwriting.
for (var i = 0, j = 0; i < n; ++i) {
if (!filters.zero(i)) {
if (i !== j) filters.copy(j, i), data[j] = data[i];
++j;
}
}
data.length = n = j;
filters.truncate(j);
triggerOnChange('dataRemoved');
}
// Return true if the data element at index i is filtered IN.
// Optionally, ignore the filters of any dimensions in the ignore_dimensions list.
function isElementFiltered(i, ignore_dimensions) {
var n,
d,
id,
len,
mask = Array(filters.subarrays);
for (n = 0; n < filters.subarrays; n++) { mask[n] = ~0; }
if (ignore_dimensions) {
for (d = 0, len = ignore_dimensions.length; d < len; d++) {
// The top bits of the ID are the subarray offset and the lower bits are the bit
// offset of the "one" mask.
id = ignore_dimensions[d].id();
mask[id >> 7] &= ~(0x1 << (id & 0x3f));
}
}
return filters.zeroExceptMask(i,mask);
}
// Adds a new dimension with the specified value accessor function.
function dimension(value, iterable) {
if (typeof value === 'string') {
var accessorPath = value;
value = function(d) { return result(d, accessorPath); };
}
var dimension = {
filter: filter,
filterExact: filterExact,
filterRange: filterRange,
filterFunction: filterFunction,
filterAll: filterAll,
top: top,
bottom: bottom,
group: group,
groupAll: groupAll,
dispose: dispose,
remove: dispose, // for backwards-compatibility
accessor: value,
id: function() { return id; }
};
var one, // lowest unset bit as mask, e.g., 00001000
zero, // inverted one, e.g., 11110111
offset, // offset into the filters arrays
id, // unique ID for this dimension (reused when dimensions are disposed)
values, // sorted, cached array
index, // value rank ↦ object id
oldValues, // temporary array storing previously-added values
oldIndex, // temporary array storing previously-added index
newValues, // temporary array storing newly-added values
newIndex, // temporary array storing newly-added index
iterablesIndexCount,
newIterablesIndexCount,
iterablesIndexFilterStatus,
newIterablesIndexFilterStatus,
oldIterablesIndexFilterStatus,
iterablesEmptyRows,
sort = quicksort.by(function(i) { return newValues[i]; }),
refilter = xfilterFilter.filterAll, // for recomputing filter
refilterFunction, // the custom filter function in use
indexListeners = [], // when data is added
dimensionGroups = [],
lo0 = 0,
hi0 = 0,
t = 0;
// Updating a dimension is a two-stage process. First, we must update the
// associated filters for the newly-added records. Once all dimensions have
// updated their filters, the groups are notified to update.
dataListeners.unshift(preAdd);
dataListeners.push(postAdd);
removeDataListeners.push(removeData);
// Add a new dimension in the filter bitmap and store the offset and bitmask.
var tmp = filters.add();
offset = tmp.offset;
one = tmp.one;
zero = ~one;
// Create a unique ID for the dimension
// IDs will be re-used if dimensions are disposed.
// For internal use the ID is the subarray offset shifted left 7 bits or'd with the
// bit offset of the set bit in the dimension's "one" mask.
id = (offset << 7) | (Math.log(one) / Math.log(2));
preAdd(data, 0, n);
postAdd(data, 0, n);
// Incorporates the specified new records into this dimension.
// This function is responsible for updating filters, values, and index.
function preAdd(newData, n0, n1) {
if (iterable){
// Count all the values
t = 0;
j = 0;
k = [];
for (i = 0; i < newData.length; i++) {
for(j = 0, k = value(newData[i]); j < k.length; j++) {
t++;
}
}
newValues = [];
newIterablesIndexCount = crossfilter_range(newData.length);
newIterablesIndexFilterStatus = crossfilter_index(t,1);
iterablesEmptyRows = [];
var unsortedIndex = crossfilter_range(t);
for (l = 0, i = 0; i < newData.length; i++) {
k = value(newData[i])
//
if(!k.length){
newIterablesIndexCount[i] = 0;
iterablesEmptyRows.push(i);
continue;
}
newIterablesIndexCount[i] = k.length
for (j = 0; j < k.length; j++) {
newValues.push(k[j]);
unsortedIndex[l] = i;
l++;
}
}
// Create the Sort map used to sort both the values and the valueToData indices
var sortMap = sort(crossfilter_range(t), 0, t);
// Use the sortMap to sort the newValues
newValues = permute(newValues, sortMap);
// Use the sortMap to sort the unsortedIndex map
// newIndex should be a map of sortedValue -> crossfilterData
newIndex = permute(unsortedIndex, sortMap)
} else{
// Permute new values into natural order using a standard sorted index.
newValues = newData.map(value);
newIndex = sort(crossfilter_range(n1), 0, n1);
newValues = permute(newValues, newIndex);
}
if(iterable) {
n1 = t;
}
// Bisect newValues to determine which new records are selected.
var bounds = refilter(newValues), lo1 = bounds[0], hi1 = bounds[1];
if (refilterFunction) {
for (i = 0; i < n1; ++i) {
if (!refilterFunction(newValues[i], i)) {
filters[offset][newIndex[i] + n0] |= one;
if(iterable) newIterablesIndexFilterStatus[i] = 1;
}
}
} else {
for (i = 0; i < lo1; ++i) {
filters[offset][newIndex[i] + n0] |= one;
if(iterable) newIterablesIndexFilterStatus[i] = 1;
}
for (i = hi1; i < n1; ++i) {
filters[offset][newIndex[i] + n0] |= one;
if(iterable) newIterablesIndexFilterStatus[i] = 1;
}
}
// If this dimension previously had no data, then we don't need to do the
// more expensive merge operation; use the new values and index as-is.
if (!n0) {
values = newValues;
index = newIndex;
iterablesIndexCount = newIterablesIndexCount;
iterablesIndexFilterStatus = newIterablesIndexFilterStatus;
lo0 = lo1;
hi0 = hi1;
return;
}
var oldValues = values,
oldIndex = index,
oldIterablesIndexFilterStatus = iterablesIndexFilterStatus
i0 = 0,
i1 = 0;
if(iterable){
old_n0 = n0
n0 = oldValues.length;
n1 = t
}
// Otherwise, create new arrays into which to merge new and old.
values = iterable ? new Array(n0 + n1) : new Array(n);
index = iterable ? new Array(n0 + n1) : crossfilter_index(n, n);
if(iterable) iterablesIndexFilterStatus = crossfilter_index(n0 + n1, 1);
// Concatenate the newIterablesIndexCount onto the old one.
if(iterable) {
var oldiiclength = iterablesIndexCount.length;
iterablesIndexCount = xfilterArray.arrayLengthen(iterablesIndexCount, n);
for(var j=0; j+oldiiclength < n; j++) {
iterablesIndexCount[j+oldiiclength] = newIterablesIndexCount[j];
}
}
// Merge the old and new sorted values, and old and new index.
for (i = 0; i0 < n0 && i1 < n1; ++i) {
if (oldValues[i0] < newValues[i1]) {
values[i] = oldValues[i0];
if(iterable) iterablesIndexFilterStatus[i] = oldIterablesIndexFilterStatus[i0];
index[i] = oldIndex[i0++];
} else {
values[i] = newValues[i1];
if(iterable) iterablesIndexFilterStatus[i] = oldIterablesIndexFilterStatus[i1];
index[i] = newIndex[i1++] + (iterable ? old_n0 : n0);
}
}
// Add any remaining old values.
for (; i0 < n0; ++i0, ++i) {
values[i] = oldValues[i0];
if(iterable) iterablesIndexFilterStatus[i] = oldIterablesIndexFilterStatus[i0];
index[i] = oldIndex[i0];
}
// Add any remaining new values.
for (; i1 < n1; ++i1, ++i) {
values[i] = newValues[i1];
if(iterable) iterablesIndexFilterStatus[i] = oldIterablesIndexFilterStatus[i1];
index[i] = newIndex[i1] + (iterable ? old_n0 : n0);
}
// Bisect again to recompute lo0 and hi0.
bounds = refilter(values), lo0 = bounds[0], hi0 = bounds[1];
}
// When all filters have updated, notify index listeners of the new values.
function postAdd(newData, n0, n1) {
indexListeners.forEach(function(l) { l(newValues, newIndex, n0, n1); });
newValues = newIndex = null;
}
function removeData(reIndex) {
for (var i = 0, j = 0, k; i < n; ++i) {
if (!filters.zero(k = index[i])) {
if (i !== j) values[j] = values[i];
index[j] = reIndex[k];
++j;
}
}
values.length = j;
while (j < n) index[j++] = 0;
// Bisect again to recompute lo0 and hi0.
var bounds = refilter(values);
lo0 = bounds[0], hi0 = bounds[1];
}
// Updates the selected values based on the specified bounds [lo, hi].
// This implementation is used by all the public filter methods.
function filterIndexBounds(bounds) {
var lo1 = bounds[0],
hi1 = bounds[1];
if (refilterFunction) {
refilterFunction = null;
filterIndexFunction(function(d, i) { return lo1 <= i && i < hi1; }, bounds[0] === 0 && bounds[1] === index.length);
lo0 = lo1;
hi0 = hi1;
return dimension;
}
var i,
j,
k,
added = [],
removed = [],
valueIndexAdded = [],
valueIndexRemoved = [];
// Fast incremental update based on previous lo index.
if (lo1 < lo0) {
for (i = lo1, j = Math.min(lo0, hi1); i < j; ++i) {
added.push(index[i]);
valueIndexAdded.push(i);
}
} else if (lo1 > lo0) {
for (i = lo0, j = Math.min(lo1, hi0); i < j; ++i) {
removed.push(index[i]);
valueIndexRemoved.push(i);
}
}
// Fast incremental update based on previous hi index.
if (hi1 > hi0) {
for (i = Math.max(lo1, hi0), j = hi1; i < j; ++i) {
added.push(index[i]);
valueIndexAdded.push(i);
}
} else if (hi1 < hi0) {
for (i = Math.max(lo0, hi1), j = hi0; i < j; ++i) {
removed.push(index[i]);
valueIndexRemoved.push(i);
}
}
if(!iterable) {
// Flip filters normally.
for(i=0; i<added.length; i++) {
filters[offset][added[i]] ^= one;
}
for(i=0; i<removed.length; i++) {
filters[offset][removed[i]] ^= one;
}
} else {
// For iterables, we need to figure out if the row has been completely removed vs partially included
// Only count a row as added if it is not already being aggregated. Only count a row
// as removed if the last element being aggregated is removed.
var newAdded = [];
var newRemoved = [];
for (i = 0; i < added.length; i++) {
iterablesIndexCount[added[i]]++
iterablesIndexFilterStatus[valueIndexAdded[i]] = 0;
if(iterablesIndexCount[added[i]] === 1) {
filters[offset][added[i]] ^= one;
newAdded.push(added[i]);
}
}
for (i = 0; i < removed.length; i++) {
iterablesIndexCount[removed[i]]--
iterablesIndexFilterStatus[valueIndexRemoved[i]] = 1;
if(iterablesIndexCount[removed[i]] === 0) {
filters[offset][removed[i]] ^= one;
newRemoved.push(removed[i]);
}
}
added = newAdded;
removed = newRemoved;
// Now handle empty rows.
if(bounds[0] === 0 && bounds[1] === index.length) {
for(i = 0; i < iterablesEmptyRows.length; i++) {
if((filters[offset][k = iterablesEmptyRows[i]] & one)) {
// Was not in the filter, so set the filter and add
filters[offset][k] ^= one;
added.push(k);
}
}
} else {
// filter in place - remove empty rows if necessary
for(i = 0; i < iterablesEmptyRows.length; i++) {
if(!(filters[offset][k = iterablesEmptyRows[i]] & one)) {
// Was in the filter, so set the filter and remove
filters[offset][k] ^= one;
removed.push(k);
}
}
}
}
lo0 = lo1;
hi0 = hi1;
filterListeners.forEach(function(l) { l(one, offset, added, removed); });
triggerOnChange('filtered');
return dimension;
}
// Filters this dimension using the specified range, value, or null.
// If the range is null, this is equivalent to filterAll.
// If the range is an array, this is equivalent to filterRange.
// Otherwise, this is equivalent to filterExact.
function filter(range) {
return range == null
? filterAll() : Array.isArray(range)
? filterRange(range) : typeof range === "function"
? filterFunction(range)
: filterExact(range);
}
// Filters this dimension to select the exact value.
function filterExact(value) {
return filterIndexBounds((refilter = xfilterFilter.filterExact(bisect, value))(values));
}
// Filters this dimension to select the specified range [lo, hi].
// The lower bound is inclusive, and the upper bound is exclusive.
function filterRange(range) {
return filterIndexBounds((refilter = xfilterFilter.filterRange(bisect, range))(values));
}
// Clears any filters on this dimension.
function filterAll() {
return filterIndexBounds((refilter = xfilterFilter.filterAll)(values));
}
// Filters this dimension using an arbitrary function.
function filterFunction(f) {
refilterFunction = f;
refilter = xfilterFilter.filterAll;
filterIndexFunction(f, false);
lo0 = 0;
hi0 = n;
return dimension;
}
function filterIndexFunction(f, filterAll) {
var i,
k,
x,
added = [],
removed = [],
valueIndexAdded = [],
valueIndexRemoved = [],
indexLength = index.length;
if(!iterable) {
for (i = 0; i < indexLength; ++i) {
if (!(filters[offset][k = index[i]] & one) ^ !!(x = f(values[i], i))) {
if (x) added.push(k);
else removed.push(k);
}
}
}
if(iterable) {
for(i=0; i < indexLength; ++i) {
if(f(values[i], i)) {
added.push(index[i]);
valueIndexAdded.push(i);
} else {
removed.push(index[i]);
valueIndexRemoved.push(i);
}
}
}
if(!iterable) {
for(i=0; i<added.length; i++) {
if(filters[offset][added[i]] & one) filters[offset][added[i]] &= zero;
}
for(i=0; i<removed.length; i++) {
if(!(filters[offset][removed[i]] & one)) filters[offset][removed[i]] |= one;
}
} else {
var newAdded = [];
var newRemoved = [];
for (i = 0; i < added.length; i++) {
// First check this particular value needs to be added
if(iterablesIndexFilterStatus[valueIndexAdded[i]] === 1) {
iterablesIndexCount[added[i]]++
iterablesIndexFilterStatus[valueIndexAdded[i]] = 0;
if(iterablesIndexCount[added[i]] === 1) {
filters[offset][added[i]] ^= one;
newAdded.push(added[i]);
}
}
}
for (i = 0; i < removed.length; i++) {
// First check this particular value needs to be removed
if(iterablesIndexFilterStatus[valueIndexRemoved[i]] === 0) {
iterablesIndexCount[removed[i]]--
iterablesIndexFilterStatus[valueIndexRemoved[i]] = 1;
if(iterablesIndexCount[removed[i]] === 0) {
filters[offset][removed[i]] ^= one;
newRemoved.push(removed[i]);
}
}
}
added = newAdded;
removed = newRemoved;
// Now handle empty rows.
if(filterAll) {
for(i = 0; i < iterablesEmptyRows.length; i++) {
if((filters[offset][k = iterablesEmptyRows[i]] & one)) {
// Was not in the filter, so set the filter and add
filters[offset][k] ^= one;
added.push(k);
}
}
} else {
// filter in place - remove empty rows if necessary
for(i = 0; i < iterablesEmptyRows.length; i++) {
if(!(filters[offset][k = iterablesEmptyRows[i]] & one)) {
// Was in the filter, so set the filter and remove
filters[offset][k] ^= one;
removed.push(k);
}
}
}
}
filterListeners.forEach(function(l) { l(one, offset, added, removed); });
triggerOnChange('filtered');
}
// Returns the top K selected records based on this dimension's order.
// Note: observes this dimension's filter, unlike group and groupAll.
function top(k, top_offset) {
var array = [],
i = hi0,
j,
toSkip = 0;
if(top_offset && top_offset > 0) toSkip = top_offset;
while (--i >= lo0 && k > 0) {
if (filters.zero(j = index[i])) {
if(toSkip > 0) {
//skip matching row
--toSkip;
} else {
array.push(data[j]);
--k;
}
}
}
if(iterable){
for(i = 0; i < iterablesEmptyRows.length && k > 0; i++) {
// Add row with empty iterable column at the end
if(filters.zero(j = iterablesEmptyRows[i])) {
if(toSkip > 0) {
//skip matching row
--toSkip;
} else {
array.push(data[j]);
--k;
}
}
}
}
return array;
}
// Returns the bottom K selected records based on this dimension's order.
// Note: observes this dimension's filter, unlike group and groupAll.
function bottom(k, bottom_offset) {
var array = [],
i,
j,
toSkip = 0;
if(bottom_offset && bottom_offset > 0) toSkip = bottom_offset;
if(iterable) {
// Add row with empty iterable column at the top
for(i = 0; i < iterablesEmptyRows.length && k > 0; i++) {
if(filters.zero(j = iterablesEmptyRows[i])) {
if(toSkip > 0) {
//skip matching row
--toSkip;
} else {
array.push(data[j]);
--k;
}
}
}
}
i = lo0;
while (i < hi0 && k > 0) {
if (filters.zero(j = index[i])) {
if(toSkip > 0) {
//skip matching row
--toSkip;
} else {
array.push(data[j]);
--k;
}
}
i++;
}
return array;
}
// Adds a new group to this dimension, using the specified key function.
function group(key) {
var group = {
top: top,
all: all,
reduce: reduce,
reduceCount: reduceCount,
reduceSum: reduceSum,
order: order,
orderNatural: orderNatural,
size: size,
dispose: dispose,
remove: dispose // for backwards-compatibility
};
// Ensure that this group will be removed when the dimension is removed.
dimensionGroups.push(group);
var groups, // array of {key, value}
groupIndex, // object id ↦ group id
groupWidth = 8,
groupCapacity = crossfilter_capacity(groupWidth),
k = 0, // cardinality
select,
heap,
reduceAdd,
reduceRemove,
reduceInitial,
update = crossfilter_null,
reset = crossfilter_null,
resetNeeded = true,
groupAll = key === crossfilter_null;
if (arguments.length < 1) key = crossfilter_identity;
// The group listens to the crossfilter for when any dimension changes, so
// that it can update the associated reduce values. It must also listen to
// the parent dimension for when data is added, and compute new keys.
filterListeners.push(update);
indexListeners.push(add);
removeDataListeners.push(removeData);
// Incorporate any existing data into the grouping.
add(values, index, 0, n);
// Incorporates the specified new values into this group.
// This function is responsible for updating groups and groupIndex.
function add(newValues, newIndex, n0, n1) {
if(iterable) {
n0old = n0
n0 = values.length - newValues.length
n1 = newValues.length;
}
var oldGroups = groups,
reIndex = iterable ? [] : crossfilter_index(k, groupCapacity),
add = reduceAdd,
remove = reduceRemove,
initial = reduceInitial,
k0 = k, // old cardinality
i0 = 0, // index of old group
i1 = 0, // index of new record
j, // object id
g0, // old group
x0, // old key
x1, // new key
g, // group to add
x; // key of group to add
// If a reset is needed, we don't need to update the reduce values.
if (resetNeeded) add = initial = crossfilter_null;
if (resetNeeded) remove = initial = crossfilter_null;
// Reset the new groups (k is a lower bound).
// Also, make sure that groupIndex exists and is long enough.
groups = new Array(k), k = 0;
if(iterable){
groupIndex = k0 > 1 ? groupIndex : [];
}
else{
groupIndex = k0 > 1 ? xfilterArray.arrayLengthen(groupIndex, n) : crossfilter_index(n, groupCapacity);
}
// Get the first old key (x0 of g0), if it exists.
if (k0) x0 = (g0 = oldGroups[0]).key;
// Find the first new key (x1), skipping NaN keys.
while (i1 < n1 && !((x1 = key(newValues[i1])) >= x1)) ++i1;
// While new keys remain…
while (i1 < n1) {
// Determine the lesser of the two current keys; new and old.
// If there are no old keys remaining, then always add the new key.
if (g0 && x0 <= x1) {
g = g0, x = x0;
// Record the new index of the old group.
reIndex[i0] = k;
// Retrieve the next old key.
if (g0 = oldGroups[++i0]) x0 = g0.key;
} else {
g = {key: x1, value: initial()}, x = x1;
}
// Add the lesser group.
groups[k] = g;
// Add any selected records belonging to the added group, while
// advancing the new key and populating the associated group index.
while (x1 <= x) {
j = newIndex[i1] + (iterable ? n0old : n0)
if(iterable){
if(groupIndex[j]){
groupIndex[j].push(k)
}
else{
groupIndex[j] = [k]
}
}
else{
groupIndex[j] = k;
}
// Always add new values to groups. Only remove when not in filter.
// This gives groups full information on data life-cycle.
g.value = add(g.value, data[j], true);
if (!filters.zeroExcept(j, offset, zero)) g.value = remove(g.value, data[j], false);
if (++i1 >= n1) break;
x1 = key(newValues[i1]);
}
groupIncrement();
}
// Add any remaining old groups that were greater th1an all new keys.
// No incremental reduce is needed; these groups have no new records.
// Also record the new index of the old group.
while (i0 < k0) {
groups[reIndex[i0] = k] = oldGroups[i0++];
groupIncrement();
}
// Fill in gaps with empty arrays where there may have been rows with empty iterables
if(iterable){
for (i = 0; i < n; i++) {
if(!groupIndex[i]){
groupIndex[i] = []
}
}
}
// If we added any new groups before any old groups,
// update the group index of all the old records.
if(k > i0){
if(iterable){
groupIndex = permute(groupIndex, reIndex, true)
}
else{
for (i0 = 0; i0 < n0; ++i0) {
groupIndex[i0] = reIndex[groupIndex[i0]];
}
}
}
// Modify the update and reset behavior based on the cardinality.
// If the cardinality is less than or equal to one, then the groupIndex
// is not needed. If the cardinality is zero, then there are no records
// and therefore no groups to update or reset. Note that we also must
// change the registered listener to point to the new method.
j = filterListeners.indexOf(update);
if (k > 1) {
update = updateMany;
reset = resetMany;
} else {
if (!k && groupAll) {
k = 1;
groups = [{key: null, value: initial()}];
}
if (k === 1) {
update = updateOne;
reset = resetOne;
} else {
update = crossfilter_null;
reset = crossfilter_null;
}
groupIndex = null;
}
filterListeners[j] = update;
// Count the number of added groups,
// and widen the group index as needed.
function groupIncrement() {
if(iterable){
k++
return
}
if (++k === groupCapacity) {
reIndex = xfilterArray.arrayWiden(reIndex, groupWidth <<= 1);
groupIndex = xfilterArray.arrayWiden(groupIndex, groupWidth);
groupCapacity = crossfilter_capacity(groupWidth);
}
}
}
function removeData() {
if (k > 1) {
var oldK = k,
oldGroups = groups,
seenGroups = crossfilter_index(oldK, oldK);
// Filter out non-matches by copying matching group index entries to
// the beginning of the array.
for (var i = 0, j = 0; i < n; ++i) {
if (!filters.zero(i)) {
seenGroups[groupIndex[j] = groupIndex[i]] = 1;
++j;
}
}
// Reassemble groups including only those groups that were referred
// to by matching group index entries. Note the new group index in
// seenGroups.
groups = [], k = 0;
for (i = 0; i < oldK; ++i) {
if (seenGroups[i]) {
seenGroups[i] = k++;
groups.push(oldGroups[i]);
}
}
if (k > 1) {
// Reindex the group index using seenGroups to find the new index.
for (var i = 0; i < j; ++i) groupIndex[i] = seenGroups[groupIndex[i]];
} else {
groupIndex = null;
}
filterListeners[filterListeners.indexOf(update)] = k > 1
? (reset = resetMany, update = updateMany)
: k === 1 ? (reset = resetOne, update = updateOne)
: reset = update = crossfilter_null;
} else if (k === 1) {
if (groupAll) return;
for (var i = 0; i < n; ++i) if (!filters.zero(i)) return;
groups = [], k = 0;
filterListeners[filterListeners.indexOf(update)] =
update = reset = crossfilter_null;
}
}
// Reduces the specified selected or deselected records.
// This function is only used when the cardinality is greater than 1.
// notFilter indicates a crossfilter.add/remove operation.
function updateMany(filterOne, filterOffset, added, removed, notFilter) {
if ((filterOne === one && filterOffset === offset) || resetNeeded) return;
var i,
j,
k,
n,
g;
if(iterable){
// Add the added values.
for (i = 0, n = added.length; i < n; ++i) {
if (filters.zeroExcept(k = added[i], offset, zero)) {
for (j = 0; j < groupIndex[k].length; j++) {
g = groups[groupIndex[k][j]];
g.value = reduceAdd(g.value, data[k], false, j);
}
}
}
// Remove the removed values.
for (i = 0, n = removed.length; i < n; ++i) {
if (filters.onlyExcept(k = removed[i], offset, zero, filterOffset, filterOne)) {
for (j = 0; j < groupIndex[k].length; j++) {
g = groups[groupIndex[k][j]];
g.value = reduceRemove(g.value, data[k], notFilter, j);
}
}
}
return;
}
// Add the added values.
for (i = 0, n = added.length; i < n; ++i) {
if (filters.zeroExcept(k = added[i], offset, zero)) {
g = groups[groupIndex[k]];
g.value = reduceAdd(g.value, data[k], false);
}
}
// Remove the removed values.
for (i = 0, n = removed.length; i < n; ++i) {
if (filters.onlyExcept(k = removed[i], offset, zero, filterOffset, filterOne)) {
g = groups[groupIndex[k]];
g.value = reduceRemove(g.value, data[k], notFilter);
}
}
}
// Reduces the specified selected or deselected records.
// This function is only used when the cardinality is 1.
// notFilter indicates a crossfilter.add/remove operation.
function updateOne(filterOne, filterOffset, added, removed, notFilter) {
if ((filterOne === one && filterOffset === offset) || resetNeeded) return;
var i,
k,
n,
g = groups[0];
// Add the added values.
for (i = 0, n = added.length; i < n; ++i) {
if (filters.zeroExcept(k = added[i], offset, zero)) {
g.value = reduceAdd(g.value, data[k], false);
}
}
// Remove the removed values.
for (i = 0, n = removed.length; i < n; ++i) {
if (filters.onlyExcept(k = removed[i], offset, zero, filterOffset, filterOne)) {
g.value = reduceRemove(g.value, data[k], notFilter);
}
}
}
// Recomputes the group reduce values from scratch.
// This function is only used when the cardinality is greater than 1.
function resetMany() {
var i,
j,
g;
// Reset all group values.
for (i = 0; i < k; ++i) {
groups[i].value = reduceInitial();
}
// We add all records and then remove filtered records so that reducers
// can build an 'unfiltered' view even if there are already filters in
// place on other dimensions.
if(iterable){
for (i = 0; i < n; ++i) {
for (j = 0; j < groupIndex[i].length; j++) {
g = groups[groupIndex[i][j]];
g.value = reduceAdd(g.value, data[i], true, j);
}
}
for (i = 0; i < n; ++i) {
if (!filters.zeroExcept(i, offset, zero)) {
for (j = 0; j < groupIndex[i].length; j++) {
g = groups[groupIndex[i][j]];
g.value = reduceRemove(g.value, data[i], false, j);
}
}
}
return;
}
for (i = 0; i < n; ++i) {
g = groups[groupIndex[i]];
g.value = reduceAdd(g.value, data[i], true);
}
for (i = 0; i < n; ++i) {
if (!filters.zeroExcept(i, offset, zero)) {
g = groups[groupIndex[i]];
g.value = reduceRemove(g.value, data[i], false);
}
}
}
// Recomputes the group reduce values from scratch.
// This function is only used when the cardinality is 1.
function resetOne() {
var i,
g = groups[0];
// Reset the singleton group values.
g.value = reduceInitial();
// We add all records and then remove filtered records so that reducers
// can build an 'unfiltered' view even if there are already filters in
// place on other dimensions.
for (i = 0; i < n; ++i) {
g.value = reduceAdd(g.value, data[i], true);
}
for (i = 0; i < n; ++i) {
if (!filters.zeroExcept(i, offset, zero)) {
g.value = reduceRemove(g.value, data[i], false);
}
}
}
// Returns the array of group values, in the dimension's natural order.
function all() {
if (resetNeeded) reset(), resetNeeded = false;
return groups;
}
// Returns a new array containing the top K group values, in reduce order.
function top(k) {
var top = select(all(), 0, groups.length, k);
return heap.sort(top, 0, top.length);
}
// Sets the reduce behavior for this group to use the specified functions.
// This method lazily recomputes the reduce values, waiting until needed.
function reduce(add, remove, initial) {
reduceAdd = add;
reduceRemove = remove;
reduceInitial = initial;
resetNeeded = true;
return group;
}
// A convenience method for reducing by count.
function reduceCount() {
return reduce(xfilterReduce.reduceIncrement, xfilterReduce.reduceDecrement, crossfilter_zero);
}
// A convenience method for reducing by sum(value).
function reduceSum(value) {
return reduce(xfilterReduce.reduceAdd(value), xfilterReduce.reduceSubtract(value), crossfilter_zero);
}
// Sets the reduce order, using the specified accessor.
function order(value) {
select = xfilterHeapselect.by(valueOf);
heap = xfilterHeap.by(valueOf);
function valueOf(d) { return value(d.value); }
return group;
}
// A convenience method for natural ordering by reduce value.
function orderNatural() {
return order(crossfilter_identity);
}
// Returns the cardinality of this group, irrespective of any filters.
function size() {
return k;
}
// Removes this group and associated event listeners.
function dispose() {
var i = filterListeners.indexOf(update);
if (i >= 0) filterListeners.splice(i, 1);
i = indexListeners.indexOf(add);
if (i >= 0) indexListeners.splice(i, 1);
i = removeDataListeners.indexOf(removeData);
if (i >= 0) removeDataListeners.splice(i, 1);
return group;
}
return reduceCount().orderNatural();
}
// A convenience function for generating a singleton group.
function groupAll() {
var g = group(crossfilter_null), all = g.all;
delete g.all;
delete g.top;
delete g.order;
delete g.orderNatural;
delete g.size;
g.value = function() { return all()[0].value; };
return g;
}
// Removes this dimension and associated groups and event listeners.
function dispose() {
dimensionGroups.forEach(function(group) { group.dispose(); });
var i = dataListeners.indexOf(preAdd);
if (i >= 0) dataListeners.splice(i, 1);
i = dataListeners.indexOf(postAdd);
if (i >= 0) dataListeners.splice(i, 1);
i = removeDataListeners.indexOf(removeData);
if (i >= 0) removeDataListeners.splice(i, 1);
filters.masks[offset] &= zero;
return filterAll();
}
return dimension;
}
// A convenience method for groupAll on a dummy dimension.
// This implementation can be optimized since it always has cardinality 1.
function groupAll() {
var group = {
reduce: reduce,
reduceCount: reduceCount,
reduceSum: reduceSum,
value: value,
dispose: dispose,
remove: dispose // for backwards-compatibility
};
var reduceValue,
reduceAdd,
reduceRemove,
reduceInitial,
resetNeeded = true;
// The group listens to the crossfilter for when any dimension changes, so
// that it can update the reduce value. It must also listen to the parent
// dimension for when data is added.
filterListeners.push(update);
dataListeners.push(add);
// For consistency; actually a no-op since resetNeeded is true.
add(data, 0, n);
// Incorporates the specified new values into this group.
function add(newData, n0) {
var i;
if (resetNeeded) return;
// Cycle through all the values.
for (i = n0; i < n; ++i) {
// Add all values all the time.
reduceValue = reduceAdd(reduceValue, data[i], true);
// Remove the value if filtered.
if (!filters.zero(i)) {
reduceValue = reduceRemove(reduceValue, data[i], false);
}
}
}
// Reduces the specified selected or deselected records.
function update(filterOne, filterOffset, added, removed, notFilter) {
var i,
k,
n;
if (resetNeeded) return;
// Add the added values.
for (i = 0, n = added.length; i < n; ++i) {
if (filters.zero(k = added[i])) {
reduceValue = reduceAdd(reduceValue, data[k], notFilter);
}
}
// Remove the removed values.
for (i = 0, n = removed.length; i < n; ++i) {
if (filters.only(k = removed[i], filterOffset, filterOne)) {
reduceValue = reduceRemove(reduceValue, data[k], notFilter);