crossfilter2
Version:
Fast multidimensional filtering for coordinated views.
1,534 lines (1,331 loc) • 61.8 kB
JavaScript
// https://crossfilter.github.io/crossfilter/ v1.5.4 Copyright 2020 Mike Bostock
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.crossfilter = factory());
}(this, (function () { 'use strict';
let array8 = arrayUntyped,
array16 = arrayUntyped,
array32 = arrayUntyped,
arrayLengthen = arrayLengthenUntyped,
arrayWiden = arrayWidenUntyped;
if (typeof Uint8Array !== "undefined") {
array8 = function(n) { return new Uint8Array(n); };
array16 = function(n) { return new Uint16Array(n); };
array32 = function(n) { return new Uint32Array(n); };
arrayLengthen = function(array, length) {
if (array.length >= length) return array;
var copy = new array.constructor(length);
copy.set(array);
return copy;
};
arrayWiden = function(array, width) {
var copy;
switch (width) {
case 16: copy = array16(array.length); break;
case 32: copy = array32(array.length); break;
default: throw new Error("invalid array width!");
}
copy.set(array);
return copy;
};
}
function arrayUntyped(n) {
var array = new Array(n), i = -1;
while (++i < n) array[i] = 0;
return array;
}
function arrayLengthenUntyped(array, length) {
var n = array.length;
while (n < length) array[n++] = 0;
return array;
}
function arrayWidenUntyped(array, width) {
if (width > 32) throw new Error("invalid array width!");
return array;
}
// An arbitrarily-wide array of bitmasks
function bitarray(n) {
this.length = n;
this.subarrays = 1;
this.width = 8;
this.masks = {
0: 0
};
this[0] = array8(n);
}
bitarray.prototype.lengthen = function(n) {
var i, len;
for (i = 0, len = this.subarrays; i < len; ++i) {
this[i] = arrayLengthen(this[i], n);
}
this.length = n;
};
// Reserve a new bit index in the array, returns {offset, one}
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);
// isolate the rightmost zero bit and return it as an unsigned int of 32 bits, if NaN or -1, return a 0
one = (~m & (m + 1)) >>> 0;
if (w >= 32 && !one) {
continue;
}
if (w < 32 && (one & (1 << w))) {
// widen this subarray
this[i] = 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] = 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
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
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.length = n;
};
// Checks that all bits for the given index are 0
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
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 index are 0 except for the specified mask.
// The mask should be an array of the same size as the filter subarrays width.
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
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
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 = (mask & zero) >>> 0;
if (mask != (i === onlyOffset ? onlyOne : 0)) {
return false;
}
}
return true;
};
var xfilterArray = {
array8: arrayUntyped,
array16: arrayUntyped,
array32: arrayUntyped,
arrayLengthen: arrayLengthenUntyped,
arrayWiden: arrayWidenUntyped,
bitarray: bitarray
};
const filterExact = (bisect, value) => {
return function(values) {
var n = values.length;
return [bisect.left(values, value, 0, n), bisect.right(values, value, 0, n)];
};
};
const filterRange = (bisect, range) => {
var min = range[0],
max = range[1];
return function(values) {
var n = values.length;
return [bisect.left(values, min, 0, n), bisect.left(values, max, 0, n)];
};
};
const filterAll = values => {
return [0, values.length];
};
var xfilterFilter = {
filterExact,
filterRange,
filterAll
};
var cr_identity = d => {
return d;
};
var cr_null = () => {
return null;
};
var cr_zero = () => {
return 0;
};
function heap_by(f) {
// Builds a binary heap within the specified array a[lo:hi]. The heap has the
// property such that the parent a[lo+i] is always less than or equal to its
// two children: a[lo+2*i+1] and a[lo+2*i+2].
function heap(a, lo, hi) {
var n = hi - lo,
i = (n >>> 1) + 1;
while (--i > 0) sift(a, i, n, lo);
return a;
}
// Sorts the specified array a[lo:hi] in descending order, assuming it is
// already a heap.
function sort(a, lo, hi) {
var n = hi - lo,
t;
while (--n > 0) t = a[lo], a[lo] = a[lo + n], a[lo + n] = t, sift(a, 1, n, lo);
return a;
}
// Sifts the element a[lo+i-1] down the heap, where the heap is the contiguous
// slice of array a[lo:lo+n]. This method can also be used to update the heap
// incrementally, without incurring the full cost of reconstructing the heap.
function sift(a, i, n, lo) {
var d = a[--lo + i],
x = f(d),
child;
while ((child = i << 1) <= n) {
if (child < n && f(a[lo + child]) > f(a[lo + child + 1])) child++;
if (x <= f(a[lo + child])) break;
a[lo + i] = a[lo + child];
i = child;
}
a[lo + i] = d;
}
heap.sort = sort;
return heap;
}
const h = heap_by(cr_identity);
h.by = heap_by;
function heapselect_by(f) {
var heap = h.by(f);
// Returns a new array containing the top k elements in the array a[lo:hi].
// The returned array is not sorted, but maintains the heap property. If k is
// greater than hi - lo, then fewer than k elements will be returned. The
// order of elements in a is unchanged by this operation.
function heapselect(a, lo, hi, k) {
var queue = new Array(k = Math.min(hi - lo, k)),
min,
i,
d;
for (i = 0; i < k; ++i) queue[i] = a[lo++];
heap(queue, 0, k);
if (lo < hi) {
min = f(queue[0]);
do {
if (f(d = a[lo]) > min) {
queue[0] = d;
min = f(heap(queue, 0, k)[0]);
}
} while (++lo < hi);
}
return queue;
}
return heapselect;
}
const h$1 = heapselect_by(cr_identity);
h$1.by = heapselect_by; // assign the raw function to the export as well
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;
}
const bisect = bisect_by(cr_identity);
bisect.by = bisect_by; // assign the raw function to the export as well
var permute = (array, index, deep) => {
for (var i = 0, n = index.length, copy = deep ? JSON.parse(JSON.stringify(array)) : new Array(n); i < n; ++i) {
copy[i] = array[index[i]];
}
return copy;
};
const reduceIncrement = p => {
return p + 1;
};
const reduceDecrement = p => {
return p - 1;
};
const reduceAdd = f => {
return function(p, v) {
return p + +f(v);
};
};
const reduceSubtract = f => {
return function(p, v) {
return p - f(v);
};
};
var xfilterReduce = {
reduceIncrement,
reduceDecrement,
reduceAdd,
reduceSubtract
};
function deep(t,e,i,n,r){for(r in n=(i=i.split(".")).splice(-1,1),i)e=e[i[r]]=e[i[r]]||{};return t(e,n)}
// Note(cg): result was previsouly using lodash.result, not ESM compatible.
const get = (obj, prop) => {
const value = obj[prop];
return (typeof value === 'function') ? value.call(obj) : value;
};
/**
* get value of object at a deep path.
* if the resolved value is a function,
* it's invoked with the `this` binding of
* its parent object and its result is returned.
*
* @param {Object} obj the object (e.g. { 'a': [{ 'b': { 'c1': 3, 'c2': 4} }], 'd': {e:1} }; )
* @param {String} path deep path (e.g. `d.e`` or `a[0].b.c1`. Dot notation (a.0.b)is also supported)
* @return {Any} the resolved value
*/
const reg = /\[([\w\d]+)\]/g;
var result = (obj, path) => {
return deep(get, obj, path.replace(reg, '.$1'))
};
// constants
var REMOVED_INDEX = -1;
crossfilter.heap = h;
crossfilter.heapselect = h$1;
crossfilter.bisect = bisect;
crossfilter.permute = permute;
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.
f