crossfilter2
Version:
Fast multidimensional filtering for coordinated views.
1,499 lines (1,298 loc) • 48 kB
JavaScript
import xfilterArray from './array';
import xfilterFilter from './filter';
import cr_identity from './identity';
import cr_null from './null';
import cr_zero from './zero';
import xfilterHeapselect from './heapselect';
import xfilterHeap from './heap';
import bisect from './bisect';
import permute from './permute';
import xfilterReduce from './reduce';
import result from './result';
// constants
var REMOVED_INDEX = -1;
crossfilter.heap = xfilterHeap;
crossfilter.heapselect = xfilterHeapselect;
crossfilter.bisect = bisect;
crossfilter.permute = permute;
export default crossfilter;
function crossfilter() {
var crossfilter = {
add: add,
remove: removeData,
dimension: dimension,
groupAll: groupAll,
size: size,
all: all,
allFiltered: allFiltered,
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, or if a predicate function is passed,
// removes all records matching the predicate (ignoring filters).
function removeData(predicate) {
var // Mapping from old record indexes to new indexes (after records removed)
newIndex = new Array(n),
removed = [],
usePred = typeof predicate === 'function',
shouldRemove = function (i) {
return usePred ? predicate(data[i], i) : filters.zero(i)
};
for (var index1 = 0, index2 = 0; index1 < n; ++index1) {
if ( shouldRemove(index1) ) {
removed.push(index1);
newIndex[index1] = REMOVED_INDEX;
} else {
newIndex[index1] = index2++;
}
}
// 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 index3 = 0, index4 = 0; index3 < n; ++index3) {
if ( newIndex[index3] !== REMOVED_INDEX ) {
if (index3 !== index4) filters.copy(index4, index3), data[index4] = data[index3];
++index4;
}
}
data.length = n = index4;
filters.truncate(index4);
triggerOnChange('dataRemoved');
}
function maskForDimensions(dimensions) {
var n,
d,
len,
id,
mask = Array(filters.subarrays);
for (n = 0; n < filters.subarrays; n++) { mask[n] = ~0; }
for (d = 0, len = 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 = dimensions[d].id();
mask[id >> 7] &= ~(0x1 << (id & 0x3f));
}
return mask;
}
// 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 mask = maskForDimensions(ignore_dimensions || []);
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,
currentFilter: currentFilter,
hasCurrentFilter: hasCurrentFilter,
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, // maps sorted value index -> record index (in data)
newValues, // temporary array storing newly-added values
newIndex, // temporary array storing newly-added index
iterablesIndexCount,
iterablesIndexFilterStatus,
iterablesEmptyRows = [],
sortRange = function(n) {
return cr_range(n).sort(function(A, B) {
var a = newValues[A], b = newValues[B];
return a < b ? -1 : a > b ? 1 : A - B;
});
},
refilter = xfilterFilter.filterAll, // for recomputing filter
refilterFunction, // the custom filter function in use
filterValue, // the value used for filtering (value, array, function or undefined)
filterValuePresent, // true if filterValue contains something
indexListeners = [], // when data is added
dimensionGroups = [],
lo0 = 0,
hi0 = 0,
t = 0,
k;
// 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) {
var newIterablesIndexCount,
newIterablesIndexFilterStatus;
if (iterable){
// Count all the values
t = 0;
j = 0;
k = [];
for (var i0 = 0; i0 < newData.length; i0++) {
for(j = 0, k = value(newData[i0]); j < k.length; j++) {
t++;
}
}
newValues = [];
newIterablesIndexCount = cr_range(newData.length);
newIterablesIndexFilterStatus = cr_index(t,1);
var unsortedIndex = cr_range(t);
for (var l = 0, index1 = 0; index1 < newData.length; index1++) {
k = value(newData[index1])
//
if(!k.length){
newIterablesIndexCount[index1] = 0;
iterablesEmptyRows.push(index1 + n0);
continue;
}
newIterablesIndexCount[index1] = k.length
for (j = 0; j < k.length; j++) {
newValues.push(k[j]);
unsortedIndex[l] = index1;
l++;
}
}
// Create the Sort map used to sort both the values and the valueToData indices
var sortMap = sortRange(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 = sortRange(n1);
newValues = permute(newValues, newIndex);
}
// Bisect newValues to determine which new records are selected.
var bounds = refilter(newValues), lo1 = bounds[0], hi1 = bounds[1];
var index2, index3, index4;
if(iterable) {
n1 = t;
if (refilterFunction) {
for (index2 = 0; index2 < n1; ++index2) {
if (!refilterFunction(newValues[index2], index2)) {
if(--newIterablesIndexCount[newIndex[index2]] === 0) {
filters[offset][newIndex[index2] + n0] |= one;
}
newIterablesIndexFilterStatus[index2] = 1;
}
}
} else {
for (index3 = 0; index3 < lo1; ++index3) {
if(--newIterablesIndexCount[newIndex[index3]] === 0) {
filters[offset][newIndex[index3] + n0] |= one;
}
newIterablesIndexFilterStatus[index3] = 1;
}
for (index4 = hi1; index4 < n1; ++index4) {
if(--newIterablesIndexCount[newIndex[index4]] === 0) {
filters[offset][newIndex[index4] + n0] |= one;
}
newIterablesIndexFilterStatus[index4] = 1;
}
}
} else {
if (refilterFunction) {
for (index2 = 0; index2 < n1; ++index2) {
if (!refilterFunction(newValues[index2], index2)) {
filters[offset][newIndex[index2] + n0] |= one;
}
}
} else {
for (index3 = 0; index3 < lo1; ++index3) {
filters[offset][newIndex[index3] + n0] |= one;
}
for (index4 = hi1; index4 < n1; ++index4) {
filters[offset][newIndex[index4] + n0] |= one;
}
}
}
// 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,
old_n0,
i1 = 0;
i0 = 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) : cr_index(n, n);
if(iterable) iterablesIndexFilterStatus = cr_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.
var index5 = 0;
for (; i0 < n0 && i1 < n1; ++index5) {
if (oldValues[i0] < newValues[i1]) {
values[index5] = oldValues[i0];
if(iterable) iterablesIndexFilterStatus[index5] = oldIterablesIndexFilterStatus[i0];
index[index5] = oldIndex[i0++];
} else {
values[index5] = newValues[i1];
if(iterable) iterablesIndexFilterStatus[index5] = newIterablesIndexFilterStatus[i1];
index[index5] = newIndex[i1++] + (iterable ? old_n0 : n0);
}
}
// Add any remaining old values.
for (; i0 < n0; ++i0, ++index5) {
values[index5] = oldValues[i0];
if(iterable) iterablesIndexFilterStatus[index5] = oldIterablesIndexFilterStatus[i0];
index[index5] = oldIndex[i0];
}
// Add any remaining new values.
for (; i1 < n1; ++i1, ++index5) {
values[index5] = newValues[i1];
if(iterable) iterablesIndexFilterStatus[index5] = newIterablesIndexFilterStatus[i1];
index[index5] = 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) {
if (iterable) {
for (var i0 = 0, i1 = 0; i0 < iterablesEmptyRows.length; i0++) {
if (reIndex[iterablesEmptyRows[i0]] !== REMOVED_INDEX) {
iterablesEmptyRows[i1] = reIndex[iterablesEmptyRows[i0]];
i1++;
}
}
iterablesEmptyRows.length = i1;
for (i0 = 0, i1 = 0; i0 < n; i0++) {
if (reIndex[i0] !== REMOVED_INDEX) {
if (i1 !== i0) iterablesIndexCount[i1] = iterablesIndexCount[i0];
i1++;
}
}
iterablesIndexCount = iterablesIndexCount.slice(0, i1);
}
// Rewrite our index, overwriting removed values
var n0 = values.length;
for (var i = 0, j = 0, oldDataIndex; i < n0; ++i) {
oldDataIndex = index[i];
if (reIndex[oldDataIndex] !== REMOVED_INDEX) {
if (i !== j) values[j] = values[i];
index[j] = reIndex[oldDataIndex];
if (iterable) {
iterablesIndexFilterStatus[j] = iterablesIndexFilterStatus[i];
}
++j;
}
}
values.length = j;
if (iterable) iterablesIndexFilterStatus = iterablesIndexFilterStatus.slice(0, j);
while (j < n0) 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] === values.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(refilter === xfilterFilter.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);
}
}
}
}
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) {
filterValue = value;
filterValuePresent = true;
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) {
filterValue = range;
filterValuePresent = true;
return filterIndexBounds((refilter = xfilterFilter.filterRange(bisect, range))(values));
}
// Clears any filters on this dimension.
function filterAll() {
filterValue = undefined;
filterValuePresent = false;
return filterIndexBounds((refilter = xfilterFilter.filterAll)(values));
}
// Filters this dimension using an arbitrary function.
function filterFunction(f) {
filterValue = f;
filterValuePresent = true;
refilterFunction = f;
refilter = xfilterFilter.filterAll;
filterIndexFunction(f, false);
var bounds = refilter(values);
lo0 = bounds[0], hi0 = bounds[1];
return dimension;
}
function filterIndexFunction(f, filterAll) {
var i,
k,
x,
added = [],
removed = [],
valueIndexAdded = [],
valueIndexRemoved = [],
indexLength = values.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');
}
function currentFilter() {
return filterValue;
}
function hasCurrentFilter() {
return filterValuePresent;
}
// 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 = capacity(groupWidth),
k = 0, // cardinality
select,
heap,
reduceAdd,
reduceRemove,
reduceInitial,
update = cr_null,
reset = cr_null,
resetNeeded = true,
groupAll = key === cr_null,
n0old;
if (arguments.length < 1) key = cr_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 ? [] : cr_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 = cr_null;
if (resetNeeded) remove = initial = cr_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 ? groupIndex : [];
}
else{
groupIndex = k0 > 1 ? xfilterArray.arrayLengthen(groupIndex, n) : cr_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.
g0 = oldGroups[++i0];
if (g0) 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 (var index1 = 0; index1 < n; index1++) {
if(!groupIndex[index1]){
groupIndex[index1] = [];
}
}
}
// If we added any new groups before any old groups,
// update the group index of all the old records.
if(k > i0){
if(iterable){
for (i0 = 0; i0 < n0old; ++i0) {
for (index1 = 0; index1 < groupIndex[i0].length; index1++) {
groupIndex[i0][index1] = reIndex[groupIndex[i0][index1]];
}
}
}
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 || iterable) {
update = updateMany;
reset = resetMany;
} else {
if (!k && groupAll) {
k = 1;
groups = [{key: null, value: initial()}];
}
if (k === 1) {
update = updateOne;
reset = resetOne;
} else {
update = cr_null;
reset = cr_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 = capacity(groupWidth);
}
}
}
function removeData(reIndex) {
if (k > 1 || iterable) {
var oldK = k,
oldGroups = groups,
seenGroups = cr_index(oldK, oldK),
i,
i0,
j;
// Filter out non-matches by copying matching group index entries to
// the beginning of the array.
if (!iterable) {
for (i = 0, j = 0; i < n; ++i) {
if (reIndex[i] !== REMOVED_INDEX) {
seenGroups[groupIndex[j] = groupIndex[i]] = 1;
++j;
}
}
} else {
for (i = 0, j = 0; i < n; ++i) {
if (reIndex[i] !== REMOVED_INDEX) {
groupIndex[j] = groupIndex[i];
for (i0 = 0; i0 < groupIndex[j].length; i0++) {
seenGroups[groupIndex[j][i0]] = 1;
}
++j;
}
}
groupIndex = groupIndex.slice(0, 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 || iterable) {
// Reindex the group index using seenGroups to find the new index.
if (!iterable) {
for (i = 0; i < j; ++i) groupIndex[i] = seenGroups[groupIndex[i]];
} else {
for (i = 0; i < j; ++i) {
for (i0 = 0; i0 < groupIndex[i].length; ++i0) {
groupIndex[i][i0] = seenGroups[groupIndex[i][i0]];
}
}
}
} else {
groupIndex = null;
}
filterListeners[filterListeners.indexOf(update)] = k > 1 || iterable
? (reset = resetMany, update = updateMany)
: k === 1 ? (reset = resetOne, update = updateOne)
: reset = update = cr_null;
} else if (k === 1) {
if (groupAll) return;
for (var index3 = 0; index3 < n; ++index3) if (reIndex[index3] !== REMOVED_INDEX) return;
groups = [], k = 0;
filterListeners[filterListeners.indexOf(update)] =
update = reset = cr_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, cr_zero);
}
// A convenience method for reducing by sum(value).
function reduceSum(value) {
return reduce(xfilterReduce.reduceAdd(value), xfilterReduce.reduceSubtract(value), cr_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(cr_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);
i = dimensionGroups.indexOf(group);
if (i >= 0) dimensionGroups.splice(i, 1);
return group;
}
return reduceCount().orderNatural();
}
// A convenience function for generating a singleton group.
function groupAll() {
var g = group(cr_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);
}
}
}
// Recomputes the group reduce value from scratch.
function reset() {
var i;
reduceValue = reduceInitial();
// Cycle through all the values.
for (i = 0; i < n; ++i) {
// Add all values all the time.
reduceValue = reduceAdd(reduceValue, data[i], true);
// Remove the value if it is filtered.
if (!filters.zero(i)) {
reduceValue = reduceRemove(reduceValue, data[i], false);
}
}
}
// Sets the reduce behavior for this group to use the specified functions.
// This method lazily recomputes the reduce value, 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, cr_zero);
}
// A convenience method for reducing by sum(value).
function reduceSum(value) {
return reduce(xfilterReduce.reduceAdd(value), xfilterReduce.reduceSubtract(value), cr_zero);
}
// Returns the computed reduce value.
function value() {
if (resetNeeded) reset(), resetNeeded = false;
return reduceValue;
}
// Removes this group and associated event listeners.
function dispose() {
var i = filterListeners.indexOf(update);
if (i >= 0) filterListeners.splice(i, 1);
i = dataListeners.indexOf(add);
if (i >= 0) dataListeners.splice(i, 1);
return group;
}
return reduceCount();
}
// Returns the number of records in this crossfilter, irrespective of any filters.
function size() {
return n;
}
// Returns the raw row data contained in this crossfilter
function all(){
return data;
}
// Returns row data with all dimension filters applied, except for filters in ignore_dimensions
function allFiltered(ignore_dimensions) {
var array = [],
i = 0,
mask = maskForDimensions(ignore_dimensions || []);
for (i = 0; i < n; i++) {
if (filters.zeroExceptMask(i, mask)) {
array.push(data[i]);
}
}
return array;
}
function onChange(cb){
if(typeof cb !== 'function'){
/* eslint no-console: 0 */
console.warn('onChange callback parameter must be a function!');
return;
}
callbacks.push(cb);
return function(){
callbacks.splice(callbacks.indexOf(cb), 1);
};
}
function triggerOnChange(eventName){
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](eventName);
}
}
return arguments.length
? add(arguments[0])
: crossfilter;
}
// Returns an array of size n, big enough to store ids up to m.
function cr_index(n, m) {
return (m < 0x101
? xfilterArray.array8 : m < 0x10001
? xfilterArray.array16
: xfilterArray.array32)(n);
}
// Constructs a new array of size n, with sequential values from 0 to n - 1.
function cr_range(n) {
var range = cr_index(n, n);
for (var i = -1; ++i < n;) range[i] = i;
return range;
}
function capacity(w) {
return w === 8
? 0x100 : w === 16
? 0x10000
: 0x100000000;
}