diff --git a/client/src/util/typedCrossfilter/bitArray.js b/client/src/util/typedCrossfilter/bitArray.js index 2fa46acb..667f359c 100644 --- a/client/src/util/typedCrossfilter/bitArray.js +++ b/client/src/util/typedCrossfilter/bitArray.js @@ -251,4 +251,4 @@ class BitArray { } } -export default BitArray; +module.exports = BitArray; diff --git a/client/src/util/typedCrossfilter/index.js b/client/src/util/typedCrossfilter/index.js index 3d3b413e..57a4de56 100644 --- a/client/src/util/typedCrossfilter/index.js +++ b/client/src/util/typedCrossfilter/index.js @@ -29,9 +29,20 @@ more complex API. In a few cases, elements of that API were incorporated. */ -import PositiveIntervals from "./positiveIntervals"; -import BitArray from "./bitArray"; -import {fillRange, lowerBound, lowerBoundIndirect, upperBound, upperBoundIndirect} from "./util"; +var PositiveIntervals = require("./positiveIntervals"); +var BitArray = require("./bitArray"); +var Util = require("./util"); + +class NotImplementedError extends Error { + constructor(...params) { + super(...params); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, NotImplementedError); + } + } +} class TypedCrossfilter { constructor(data) { @@ -118,7 +129,7 @@ class ScalarDimension { this.value = array; // create sort index - this.index = fillRange(new Uint32Array(this.crossfilter.data.length)); + this.index = Util.fillRange(new Uint32Array(this.crossfilter.data.length)); this.index.sort((a, b) => array[a] - array[b]); // groups, if any @@ -149,20 +160,6 @@ class ScalarDimension { _updateFilters(newFilter) { newFilter = PositiveIntervals.canonicalize(newFilter); - // XXX removed optimization for now - // special case optimization - select all/none can bypass - // more complex work and just clobber everything. - // - // if (newFilter.length === 0) { - // this.crossfilter.selection.deselectAll(this._id); - // } else if ( - // newFilter.length === 1 && - // newFilter[0][0] === 0 && - // newFilter[0][1] == this.index.length - // ) { - // this.crossfilter.selection.selectAll(this._id); - // } else { - const adds = PositiveIntervals.difference(newFilter, this.currentFilter); const dels = PositiveIntervals.difference(this.currentFilter, newFilter); @@ -190,23 +187,20 @@ class ScalarDimension { f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds)) ); - // XXX removed optimization - // } - this.currentFilter = newFilter; } // filter by value - exact match filterExact(value) { const newFilter = [ - lowerBoundIndirect( + Util.lowerBoundIndirect( this.value, this.index, value, 0, this.value.length ), - upperBoundIndirect( + Util.upperBoundIndirect( this.value, this.index, value, @@ -227,14 +221,14 @@ class ScalarDimension { const newFilter = []; for (let v = 0, len = values.length; v < len; v++) { const intv = [ - lowerBoundIndirect( + Util.lowerBoundIndirect( this.value, this.index, values[v], 0, this.value.length ), - upperBoundIndirect( + Util.upperBoundIndirect( this.value, this.index, values[v], @@ -253,14 +247,14 @@ class ScalarDimension { filterRange(range) { const newFilter = []; const intv = [ - lowerBoundIndirect( + Util.lowerBoundIndirect( this.value, this.index, range[0], 0, this.value.length ), - upperBoundIndirect( + Util.upperBoundIndirect( this.value, this.index, range[1], @@ -380,7 +374,7 @@ class EnumDimension extends ScalarDimension { const enumLen = this.enumIndex.length; for (let i = 0; i < len; i++) { const v = value(data[i]); - const e = lowerBound(this.enumIndex, v, 0, enumLen); + const e = Util.lowerBound(this.enumIndex, v, 0, enumLen); array[i] = e; } return array; @@ -388,14 +382,14 @@ class EnumDimension extends ScalarDimension { filterExact(value) { return super.filterExact( - lowerBound(this.enumIndex, value, 0, this.enumIndex.length) + Util.lowerBound(this.enumIndex, value, 0, this.enumIndex.length) ); } filterEnum(values) { return super.filterEnum( values.map(v => - lowerBound(this.enumIndex, v, 0, this.enumIndex.length) + Util.lowerBound(this.enumIndex, v, 0, this.enumIndex.length) ) ); } @@ -403,7 +397,7 @@ class EnumDimension extends ScalarDimension { filterRange(range) { return super.filterEnum( range.map(v => - lowerBound(this.enumIndex, v, 0, this.enumIndex.length) + Util.lowerBound(this.enumIndex, v, 0, this.enumIndex.length) ) ); } @@ -417,31 +411,13 @@ class EnumDimension extends ScalarDimension { // Groups! Map/reduce // -// XXX: potential optimizations not implemented -// - groupValue may be identity - could skip creating separate group map -// - only works for scalars so far (no enum) -// class ScalarGroup { constructor(groupValue, groupValueType, dimension) { // parent dimension this.dimension = dimension; - // groupValue is optional. Defaults to identity. Used to perform - // initial map operation. - // - if (groupValue === undefined) { - this.mapValue = dimension.value; - } else { - const data = dimension.value; - const len = data.length; - const array = new groupValueType(dimension.value.length); - for (let i = 0; i < len; i++) { - array[i] = groupValue(data[i]); - } - this.mapValue = array; - } - - this.groups = []; + // generate group names from dimension values + this.mapValue = this._map(groupValue, groupValueType, dimension); // group index is mapping from data record index to group index this.groupIndex = new Uint32Array(dimension.crossfilter.data.length); @@ -453,6 +429,24 @@ class ScalarGroup { this._reduce(); } + // internal support function - map all dimension values to group values. + // + _map(groupValue, groupValueType, dimension) { + // groupValue is optional. Defaults to identity. Used to perform + // initial map operation. + // + // identity: save some memory... + if (groupValue === undefined) return dimension.value; + + const data = dimension.value; + const len = data.length; + const mapValue = new groupValueType(dimension.value.length); + for (let i = 0; i < len; i++) { + mapValue[i] = groupValue(data[i]); + } + return mapValue; + } + // Update the group reduction incrementally. Called when *any* dimension filter // changes. Guaranteed to be called AFTER the crossfilter is updated. // @@ -461,8 +455,6 @@ class ScalarGroup { // * intv: interval list of newly selected values on `dim` (adds) // _updateReduceAdd(dim, intv) { - // console.log("_updateReduceAdd", dim, intv); - // ignore updates to self, as we don't reduce inclusive of our filter if (dim === this.dimension || intv.length === 0) return; @@ -489,8 +481,6 @@ class ScalarGroup { // * intv: interval list of previously selected values on `dim` (dels) // _updateReduceDel(dim, intv) { - // console.log("_updateReduceDel", dim, intv); - // ignore updates to self, as we don't reduce inclusive of our filter if (dim === this.dimension || intv.length === 0) return; @@ -584,10 +574,26 @@ class EnumGroup extends ScalarGroup { super(groupValue, groupValueType, dimension); } + _map(groupValue, groupValueType, dimension) { + // groupValue is optional. Defaults to identity. Used to perform + // initial map operation. + // + // identity: save some memory + if (groupValue === undefined) return dimension.value; + + // non-identity mapping unsupported for EnumDimension/EnumGroup. + // XXX: this could be implemented, but would require another index + // array to map from the group names/keys back to the dimension values. + // With this, we just rely on the dimensions `enumIndex` to map from + // enumeration value to the record. + throw new NotImplementedError("enumerated group mapping not implemented"); + } + all() { const res = []; this.groups.forEach(e => res.push({ + // XXX: assumes identity group map - see comment in _map() key: this.dimension.enumIndex[e.key], value: e.value }) @@ -609,4 +615,4 @@ crossfilter.TypedCrossfilter = TypedCrossfilter; crossfilter.ScalarDimension = ScalarDimension; crossfilter.EnumDimension = EnumDimension; -export default crossfilter; +module.exports = crossfilter; diff --git a/client/src/util/typedCrossfilter/positiveIntervals.js b/client/src/util/typedCrossfilter/positiveIntervals.js index 5e54bf7f..6e5d074a 100644 --- a/client/src/util/typedCrossfilter/positiveIntervals.js +++ b/client/src/util/typedCrossfilter/positiveIntervals.js @@ -129,4 +129,4 @@ class PositiveIntervals { } } -export default PositiveIntervals; +module.exports = PositiveIntervals; diff --git a/client/src/util/typedCrossfilter/util.js b/client/src/util/typedCrossfilter/util.js index 18b2c21e..deffcdec 100644 --- a/client/src/util/typedCrossfilter/util.js +++ b/client/src/util/typedCrossfilter/util.js @@ -8,7 +8,7 @@ // fill an array or typedarray with a sequential range of numbers, // starting with `start` // -export function fillRange(arr, start = 0) { +function fillRange(arr, start = 0) { for (let i = 0, len = arr.length; i < len; i++) { arr[i] = i + start; } @@ -30,7 +30,7 @@ export function fillRange(arr, start = 0) { // a factory version of lowerBound that takes an accessor (rather than having // a special-cased version for lining the indirection). // -export function lowerBound(valueArray, value, first, last) { +function lowerBound(valueArray, value, first, last) { // this is just a binary search while (first < last) { const middle = (first + last) >>> 1; @@ -45,7 +45,7 @@ export function lowerBound(valueArray, value, first, last) { // Inlined performance optimization - used to indirect through a sort map. // -export function lowerBoundIndirect(valueArray, indexArray, value, first, last) { +function lowerBoundIndirect(valueArray, indexArray, value, first, last) { // this is just a binary search while (first < last) { const middle = (first + last) >>> 1; @@ -69,7 +69,7 @@ export function lowerBoundIndirect(valueArray, indexArray, value, first, last) { // C++: upper_bound() // Python: bisect.bisect_right() // -export function upperBound(valueArray, value, first, last) { +function upperBound(valueArray, value, first, last) { // this is just a binary search while (first < last) { const middle = (first + last) >>> 1; @@ -84,7 +84,7 @@ export function upperBound(valueArray, value, first, last) { // Inline performance optimization // -export function upperBoundIndirect(valueArray, indexArray, value, first, last) { +function upperBoundIndirect(valueArray, indexArray, value, first, last) { // this is just a binary search while (first < last) { const middle = (first + last) >>> 1; @@ -96,3 +96,11 @@ export function upperBoundIndirect(valueArray, indexArray, value, first, last) { } return first; } + +module.exports = { + fillRange, + lowerBound, + lowerBoundIndirect, + upperBound, + upperBoundIndirect +};