UNPKG

crossfilter2

Version:

Fast multidimensional filtering for coordinated views.

1,499 lines (1,298 loc) 48 kB
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; }