diff --git a/client/src/util/parseRGB.js b/client/src/util/parseRGB.js index 9d0f9f16..7033aa50 100644 --- a/client/src/util/parseRGB.js +++ b/client/src/util/parseRGB.js @@ -1,31 +1,31 @@ -// jshint esversion: 6 -import { scaleRGB } from "./scaleRGB"; - -// maintain a cache of already parsed RGB names, as it is reasonably expensive -// to do this operation. This lets us have speed, but keep the pleasant ability -// to talk about colors by their text description eg, 'rgb(0,0,1)' -// -const colorCache = new Object(null); // no prototype - -function parseColorName(c) { - if (c[0] !== "#") { - const _c = c.replace(/[^\d,.]/g, "").split(","); - return [scaleRGB(+_c[0]), scaleRGB(+_c[1]), scaleRGB(+_c[2])]; - } else { - var parsedHex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(c); - return [ - scaleRGB(parseInt(parsedHex[1], 16)), - scaleRGB(parseInt(parsedHex[2], 16)), - scaleRGB(parseInt(parsedHex[3], 16)) - ]; - } -} - -export const parseRGB = c => { - var cv = colorCache[c]; - if (!cv) { - cv = parseColorName(c); - colorCache[c] = cv; - } - return cv; -}; +// jshint esversion: 6 +import { scaleRGB } from "./scaleRGB"; + +// maintain a cache of already parsed RGB names, as it is reasonably expensive +// to do this operation. This lets us have speed, but keep the pleasant ability +// to talk about colors by their text description eg, 'rgb(0,0,1)' +// +const colorCache = new Object(null); // no prototype + +function parseColorName(c) { + if (c[0] !== "#") { + const _c = c.replace(/[^\d,.]/g, "").split(","); + return [scaleRGB(+_c[0]), scaleRGB(+_c[1]), scaleRGB(+_c[2])]; + } else { + var parsedHex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(c); + return [ + scaleRGB(parseInt(parsedHex[1], 16)), + scaleRGB(parseInt(parsedHex[2], 16)), + scaleRGB(parseInt(parsedHex[3], 16)) + ]; + } +} + +export const parseRGB = c => { + var cv = colorCache[c]; + if (!cv) { + cv = parseColorName(c); + colorCache[c] = cv; + } + return cv; +}; diff --git a/client/src/util/scaleLinear.js b/client/src/util/scaleLinear.js index 15aab8b9..a675011d 100644 --- a/client/src/util/scaleLinear.js +++ b/client/src/util/scaleLinear.js @@ -1,18 +1,18 @@ -// jshint esversion: 6 - -// Substitute for a d3 linear scale - less flexible, more performant. -// Returns a function which will scale a value. -// -// Example will scale [0,1] to [-1,1] -// var myScale = scaleLinear([0, 1], [-1, 1]); -// myScale(0) === -1 -// this is is equivalent to d3.scaleLinear().domain([0,1]).range([-1,1]) - -export const scaleLinear = (domain, range) => { - const domainStart = domain[0]; - const scale = (range[1] - range[0]) / (domain[1] - domain[0]); - const rangeStart = range[0]; - return function(value) { - return (value - domainStart) * scale + rangeStart; - }; -}; +// jshint esversion: 6 + +// Substitute for a d3 linear scale - less flexible, more performant. +// Returns a function which will scale a value. +// +// Example will scale [0,1] to [-1,1] +// var myScale = scaleLinear([0, 1], [-1, 1]); +// myScale(0) === -1 +// this is is equivalent to d3.scaleLinear().domain([0,1]).range([-1,1]) + +export const scaleLinear = (domain, range) => { + const domainStart = domain[0]; + const scale = (range[1] - range[0]) / (domain[1] - domain[0]); + const rangeStart = range[0]; + return function(value) { + return (value - domainStart) * scale + rangeStart; + }; +}; diff --git a/client/src/util/schema.js b/client/src/util/schema.js index 3027ad16..6d6161d1 100644 --- a/client/src/util/schema.js +++ b/client/src/util/schema.js @@ -1,41 +1,41 @@ -// jshint esversion: 6 - -// In the case where the REST server does not implement data schema -// declaration, we attempt to deduce it by sniffing the data. -// -export function createSchemaByDataSniffing(ranges) { - let schema = {}; - _.forEach(ranges, (value, key) => { - schema[key] = { - displayname: key, - variabletype: value.options ? "categorical" : "continuous" - }; - - // Metadata field type is inferred by sniffing the data. This has some risks. - // Caveats: - // * Values have been converted to native JS objects by the JSON parser. - // * Lots of assumptions about he REST API behaving properly (eg, min/max - // are the same type, etc). - let type; - if (schema[key].variabletype === "continuous" && value.range) { - // Use min/max as a proxy for all data. - const min = value.range.min; - const max = value.range.max; - type = - typeof min !== "number" || typeof max !== "number" - ? "string" - : Number.isSafeInteger(min) && Number.isSafeInteger(max) - ? "int" - : "float"; - } else { - // use an option value as a proxy for all data - const aVal = value.options[0]; - type = - typeof aVal !== "number" - ? "string" - : Number.isSafeInteger(aVal) ? "int" : "float"; - } - schema[key].type = type; - }); - return schema; -} +// jshint esversion: 6 + +// In the case where the REST server does not implement data schema +// declaration, we attempt to deduce it by sniffing the data. +// +export function createSchemaByDataSniffing(ranges) { + let schema = {}; + _.forEach(ranges, (value, key) => { + schema[key] = { + displayname: key, + variabletype: value.options ? "categorical" : "continuous" + }; + + // Metadata field type is inferred by sniffing the data. This has some risks. + // Caveats: + // * Values have been converted to native JS objects by the JSON parser. + // * Lots of assumptions about he REST API behaving properly (eg, min/max + // are the same type, etc). + let type; + if (schema[key].variabletype === "continuous" && value.range) { + // Use min/max as a proxy for all data. + const min = value.range.min; + const max = value.range.max; + type = + typeof min !== "number" || typeof max !== "number" + ? "string" + : Number.isSafeInteger(min) && Number.isSafeInteger(max) + ? "int" + : "float"; + } else { + // use an option value as a proxy for all data + const aVal = value.options[0]; + type = + typeof aVal !== "number" + ? "string" + : Number.isSafeInteger(aVal) ? "int" : "float"; + } + schema[key].type = type; + }); + return schema; +} diff --git a/client/src/util/typedCrossfilter/bitArray.js b/client/src/util/typedCrossfilter/bitArray.js index 2fa46acb..63f61826 100644 --- a/client/src/util/typedCrossfilter/bitArray.js +++ b/client/src/util/typedCrossfilter/bitArray.js @@ -1,254 +1,254 @@ -"use strict"; -// jshint esversion: 6 - -// BitArray is a 2D bitarray with size [length, nBitWidth]. -// Each bit is referred to as a `dimension`. Dimensions may be -// dynamically allocated and deallocated. The overall length -// of the BitArray is fixed at creation time (for simplicity). -// -// Organization of the bitarray is dimension-major. As dimensions -// are added, the underlying store is grown 32 bits at a time. -// NOTE: currently does not deallocate / shrink. -// -// Primary operations on the BitArray are: -// - set & clear dimension -// - test dimension -// - various performance or convenience operations to optimize bulk ops -// -// The underlying data structure uses TypedArrays for performance. -// -class BitArray { - constructor(length) { - // Initially allocate a 32 bit wide array. allocDimension() will expand - // as necessary. - // - // Int32Array is (counterintuitively) used to accomadate JS numeric casting - // (to/from primitive number type). - // - - // Fixed for the life of this object. - this.length = length; - - // Bitarray width. width is always greater than 32*dimensionCount. - this.width = 1; // underlying number of 32 bit arrays - this.dimensionCount = 0; // num allocated dimensions - - this.bitmask = new Int32Array(this.width); // dimension allocation mask - this.bitarray = new Int32Array(this.width * this.length); - } - - // Return the number of records that are selected, ie, have a one bit in - // all allocated dimensions. - // - get selectionCount() { - return this.countAllOnes(); - } - - // Count all records that have a 'one' bit in allocated dimensions. - // - countAllOnes() { - let count = 0; - for (let i = 0; i < this.width; i++) { - const bitmask = this.bitmask[i]; - for (let j = i * this.length, len = j + this.length; j < len; j++) { - if (this.bitarray[i * this.length + j] === bitmask) count++; - } - } - return count; - } - - // count trailing zeros - hard to do fast in JS! - // https://en.wikipedia.org/wiki/Find_first_set#CTZ - static ctz(v) { - let c = 32; - v &= -v; // isolate lowest non-zero bit - if (v) c--; - if (v & 0x0000ffff) c -= 16; - if (v & 0x00ff00ff) c -= 8; - if (v & 0x0f0f0f0f) c -= 4; - if (v & 0x33333333) c -= 2; - if (v & 0x55555555) c -= 1; - return c; - } - - // find a free dimension. Return undefined if none - _findFreeDimension() { - let dim; - for (let col = 0; col < this.width; col++) { - const bitmask = this.bitmask[col]; - const lowestZeroBit = ~this.bitmask[col] & -~this.bitmask[col]; - if (lowestZeroBit) { - this.bitmask[col] |= lowestZeroBit; - dim = 32 * col + BitArray.ctz(lowestZeroBit); - } - } - return dim; - } - - // allocate and return the dimension ID (bit position) - // - allocDimension() { - let dim = this._findFreeDimension(); - - // if we did not find free dimension, expand the bitarray. - if (dim === undefined) { - this.width++; - - const biggerBitArray = new Int32Array(this.width * this.length); - biggerBitArray.set(this.bitarray); - this.bitarray = biggerBitArray; - - const biggerBitmask = new Int32Array(this.width); - biggerBitmask.set(this.bitmask); - this.bitmask = biggerBitmask; - - dim = this._findFreeDimension(); - } - - this.dimensionCount++; - return dim; - } - - // free a dimension for later use. MUST deselect the dimension, as other - // code assume the column will be zero valued. - // - freeDimension(dim) { - // all selection tests assume unallocated dimensions are zero valued. - this.deselectAll(dim); - const col = dim >>> 5; - this.bitmask[col] &= ~(1 << dim % 32); - this.dimensionCount--; - } - - // return true if this index is selected in ALL dimensions. - // - isSelected(index) { - const width = this.width; - const length = this.length; - const bitarray = this.bitarray; - - for (let w = 0; w < width; w++) { - const bitmask = this.bitmask[w]; - if (!bitmask || bitarray[w * length + index] !== bitmask) return false; - } - return true; - } - - // return true if this index is selected in ALL dimensions IGNORING dim - // - isSelectedIgnoringDim(index, dim) { - const ignoreOffset = dim >>> 5; - const ignoreMask = ~(1 << dim % 32); - - const width = this.width; - const length = this.length; - const bitarray = this.bitarray; - - for (let w = 0; w < width; w++) { - const bitmask = this.bitmask[w]; - if (w === ignoreOffset) { - if ( - bitmask && - (bitarray[w * length + index] & ignoreMask) !== (bitmask & ignoreMask) - ) - return false; - } else { - if (bitmask && bitarray[w * length + index] !== bitmask) return false; - } - } - return true; - } - - // select index on dimension - // - selectOne(dim, index) { - const col = dim >>> 5; - const before = this.bitarray[col * this.length + index]; - const after = before | (1 << dim % 32); - this.bitarray[col * this.length + index] = after; - } - - // deselect index on dimension - // - deselectOne(dim, index) { - const col = dim >>> 5; - const before = this.bitarray[col * this.length + index]; - const after = before & ~(1 << dim % 32); - this.bitarray[col * this.length + index] = after; - } - - // select all indices on dimension. - // - selectAll(dim) { - let col = dim >> 5; - const bitmask = this.bitmask[col]; - const bitarray = this.bitarray; - const one = 1 << dim % 32; - for (let i = col * this.length, len = i + this.length; i < len; i++) { - bitarray[i] |= one; - } - } - - // deselect all indices on dimension - // - deselectAll(dim) { - let col = dim >> 5; - const bitmask = this.bitmask[col]; - const bitarray = this.bitarray; - const zero = ~(1 << dim % 32); - for (let i = col * this.length, len = i + this.length; i < len; i++) { - bitarray[i] &= zero; - } - } - - // select range of indices on a dimension, indirect through a sort map. - // Indirect functions are used to map between sort and natural order. - // - selectIndirectFromRange(dim, indirect, range) { - const col = dim >>> 5; - const first = range[0]; - const last = range[1]; - const bitarray = this.bitarray; - const one = 1 << dim % 32; - const offset = col * this.length; - for (let i = first; i < last; i++) { - bitarray[offset + indirect[i]] |= one; - } - } - - // deselect range of indices on a dimension, indirect through a sort map. - // - deselectIndirectFromRange(dim, indirect, range) { - const col = dim >>> 5; - const first = range[0]; - const last = range[1]; - const bitarray = this.bitarray; - const zero = ~(1 << dim % 32); - const offset = col * this.length; - for (let i = first; i < last; i++) { - bitarray[offset + indirect[i]] &= zero; - } - } - - // Fill the array with selected|deselected value based upon the - // current selection state. - // - fillBySelection(result, selectedValue, deselectedValue) { - // special case (width === 1) for performance - if (this.width === 1) { - const bitmask = this.bitmask[0]; - const bitarray = this.bitarray; - for (let i = 0, len = this.length; i < len; i++) { - result[i] = - bitmask && bitarray[i] === bitmask ? selectedValue : deselectedValue; - } - } else { - for (let i = 0, len = this.length; i < len; i++) { - result[i] = this.isSelected(i) ? selectedValue : deselectedValue; - } - } - return result; - } -} - -export default BitArray; +"use strict"; +// jshint esversion: 6 + +// BitArray is a 2D bitarray with size [length, nBitWidth]. +// Each bit is referred to as a `dimension`. Dimensions may be +// dynamically allocated and deallocated. The overall length +// of the BitArray is fixed at creation time (for simplicity). +// +// Organization of the bitarray is dimension-major. As dimensions +// are added, the underlying store is grown 32 bits at a time. +// NOTE: currently does not deallocate / shrink. +// +// Primary operations on the BitArray are: +// - set & clear dimension +// - test dimension +// - various performance or convenience operations to optimize bulk ops +// +// The underlying data structure uses TypedArrays for performance. +// +class BitArray { + constructor(length) { + // Initially allocate a 32 bit wide array. allocDimension() will expand + // as necessary. + // + // Int32Array is (counterintuitively) used to accomadate JS numeric casting + // (to/from primitive number type). + // + + // Fixed for the life of this object. + this.length = length; + + // Bitarray width. width is always greater than 32*dimensionCount. + this.width = 1; // underlying number of 32 bit arrays + this.dimensionCount = 0; // num allocated dimensions + + this.bitmask = new Int32Array(this.width); // dimension allocation mask + this.bitarray = new Int32Array(this.width * this.length); + } + + // Return the number of records that are selected, ie, have a one bit in + // all allocated dimensions. + // + get selectionCount() { + return this.countAllOnes(); + } + + // Count all records that have a 'one' bit in allocated dimensions. + // + countAllOnes() { + let count = 0; + for (let i = 0; i < this.width; i++) { + const bitmask = this.bitmask[i]; + for (let j = i * this.length, len = j + this.length; j < len; j++) { + if (this.bitarray[i * this.length + j] === bitmask) count++; + } + } + return count; + } + + // count trailing zeros - hard to do fast in JS! + // https://en.wikipedia.org/wiki/Find_first_set#CTZ + static ctz(v) { + let c = 32; + v &= -v; // isolate lowest non-zero bit + if (v) c--; + if (v & 0x0000ffff) c -= 16; + if (v & 0x00ff00ff) c -= 8; + if (v & 0x0f0f0f0f) c -= 4; + if (v & 0x33333333) c -= 2; + if (v & 0x55555555) c -= 1; + return c; + } + + // find a free dimension. Return undefined if none + _findFreeDimension() { + let dim; + for (let col = 0; col < this.width; col++) { + const bitmask = this.bitmask[col]; + const lowestZeroBit = ~this.bitmask[col] & -~this.bitmask[col]; + if (lowestZeroBit) { + this.bitmask[col] |= lowestZeroBit; + dim = 32 * col + BitArray.ctz(lowestZeroBit); + } + } + return dim; + } + + // allocate and return the dimension ID (bit position) + // + allocDimension() { + let dim = this._findFreeDimension(); + + // if we did not find free dimension, expand the bitarray. + if (dim === undefined) { + this.width++; + + const biggerBitArray = new Int32Array(this.width * this.length); + biggerBitArray.set(this.bitarray); + this.bitarray = biggerBitArray; + + const biggerBitmask = new Int32Array(this.width); + biggerBitmask.set(this.bitmask); + this.bitmask = biggerBitmask; + + dim = this._findFreeDimension(); + } + + this.dimensionCount++; + return dim; + } + + // free a dimension for later use. MUST deselect the dimension, as other + // code assume the column will be zero valued. + // + freeDimension(dim) { + // all selection tests assume unallocated dimensions are zero valued. + this.deselectAll(dim); + const col = dim >>> 5; + this.bitmask[col] &= ~(1 << dim % 32); + this.dimensionCount--; + } + + // return true if this index is selected in ALL dimensions. + // + isSelected(index) { + const width = this.width; + const length = this.length; + const bitarray = this.bitarray; + + for (let w = 0; w < width; w++) { + const bitmask = this.bitmask[w]; + if (!bitmask || bitarray[w * length + index] !== bitmask) return false; + } + return true; + } + + // return true if this index is selected in ALL dimensions IGNORING dim + // + isSelectedIgnoringDim(index, dim) { + const ignoreOffset = dim >>> 5; + const ignoreMask = ~(1 << dim % 32); + + const width = this.width; + const length = this.length; + const bitarray = this.bitarray; + + for (let w = 0; w < width; w++) { + const bitmask = this.bitmask[w]; + if (w === ignoreOffset) { + if ( + bitmask && + (bitarray[w * length + index] & ignoreMask) !== (bitmask & ignoreMask) + ) + return false; + } else { + if (bitmask && bitarray[w * length + index] !== bitmask) return false; + } + } + return true; + } + + // select index on dimension + // + selectOne(dim, index) { + const col = dim >>> 5; + const before = this.bitarray[col * this.length + index]; + const after = before | (1 << dim % 32); + this.bitarray[col * this.length + index] = after; + } + + // deselect index on dimension + // + deselectOne(dim, index) { + const col = dim >>> 5; + const before = this.bitarray[col * this.length + index]; + const after = before & ~(1 << dim % 32); + this.bitarray[col * this.length + index] = after; + } + + // select all indices on dimension. + // + selectAll(dim) { + let col = dim >> 5; + const bitmask = this.bitmask[col]; + const bitarray = this.bitarray; + const one = 1 << dim % 32; + for (let i = col * this.length, len = i + this.length; i < len; i++) { + bitarray[i] |= one; + } + } + + // deselect all indices on dimension + // + deselectAll(dim) { + let col = dim >> 5; + const bitmask = this.bitmask[col]; + const bitarray = this.bitarray; + const zero = ~(1 << dim % 32); + for (let i = col * this.length, len = i + this.length; i < len; i++) { + bitarray[i] &= zero; + } + } + + // select range of indices on a dimension, indirect through a sort map. + // Indirect functions are used to map between sort and natural order. + // + selectIndirectFromRange(dim, indirect, range) { + const col = dim >>> 5; + const first = range[0]; + const last = range[1]; + const bitarray = this.bitarray; + const one = 1 << dim % 32; + const offset = col * this.length; + for (let i = first; i < last; i++) { + bitarray[offset + indirect[i]] |= one; + } + } + + // deselect range of indices on a dimension, indirect through a sort map. + // + deselectIndirectFromRange(dim, indirect, range) { + const col = dim >>> 5; + const first = range[0]; + const last = range[1]; + const bitarray = this.bitarray; + const zero = ~(1 << dim % 32); + const offset = col * this.length; + for (let i = first; i < last; i++) { + bitarray[offset + indirect[i]] &= zero; + } + } + + // Fill the array with selected|deselected value based upon the + // current selection state. + // + fillBySelection(result, selectedValue, deselectedValue) { + // special case (width === 1) for performance + if (this.width === 1) { + const bitmask = this.bitmask[0]; + const bitarray = this.bitarray; + for (let i = 0, len = this.length; i < len; i++) { + result[i] = + bitmask && bitarray[i] === bitmask ? selectedValue : deselectedValue; + } + } else { + for (let i = 0, len = this.length; i < len; i++) { + result[i] = this.isSelected(i) ? selectedValue : deselectedValue; + } + } + return result; + } +} + +export default BitArray; diff --git a/client/src/util/typedCrossfilter/index.js b/client/src/util/typedCrossfilter/index.js index 7c19d10b..f12d4027 100644 --- a/client/src/util/typedCrossfilter/index.js +++ b/client/src/util/typedCrossfilter/index.js @@ -1,602 +1,602 @@ -"use strict"; -// jshint esversion: 6 - -/* -Typedarray Crossfilter - a re-implementation of a subset of crossfilter, with -time/space optimizations predicated upon the following assumptions: - - dimensions are uniformly typed, and all values must be of that type - - dimension values must be a primitive type (int, float, string). Arrays - or other complex types not supported. - - dimension creation requires call-provided type declaration - - no support for adding/removing data to an existing crossfilter. If you - want to do that, you have to create the new crossfilter, using the new - data, from scratch. - -The actual backing store for a dimension is a TypedArray, enabling significant -performance improvements over the original crossfilter. - -There are also a handful of new methods, primarily to take advantage of the -performance (eg, crossfilter.fillBySelection) - -Helpful documents (this module tries to follow the original API as much -as is feasable): - https://github.com/square/crossfilter/ - http://square.github.io/crossfilter/ - -There is also a newer, community supported fork of crossfilter, with a -more complex API. In a few cases, elements of that API were incorporated. - https://github.com/square/crossfilter/ - -*/ - -import PositiveIntervals from "./positiveIntervals"; -import BitArray from "./bitArray"; -import { - fillRange, - lowerBound, - lowerBoundIndirect, - upperBound, - upperBoundIndirect -} from "./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) { - this.data = data; - - // filters: array of { id, dimension } - this.filters = []; - this.selection = new BitArray(data.length); - } - - size() { - return this.data.length; - } - - all() { - return this.data; - } - - dimension(value, valueArrayType) { - const id = this.selection.allocDimension(); - let dim; - if (valueArrayType === "enum") { - dim = new EnumDimension(value, this, id); - } else { - dim = new ScalarDimension(value, valueArrayType, this, id); - } - this.filters.push({ id, dim }); - dim.filterAll(); - return dim; - } - - _freeDimension(id) { - this.selection.freeDimension(id); - this.filters = this.filters.filter(f => f._id != id); - } - - // return array of all records that are selected/filtered - // by all dimensions. - allFiltered() { - const selection = this.selection; - const res = []; - for (let i = 0, len = this.data.length; i < len; i++) { - if (selection.isSelected(i)) { - res.push(this.data[i]); - } - } - return res; - } - - countFiltered() { - return this.selection.selectionCount; - } - - isElementFiltered(i) { - return this.selection.isSelected(i); - } - - // fill array with one of two values, based upon selection state - fillByIsFiltered(array, selectedValue, deselectedValue) { - return this.selection.fillBySelection( - array, - selectedValue, - deselectedValue - ); - } -} - -// Base dimension type - value must be a scalar type (eg, int, float), -// and value array must be a TypedArray. -// -class ScalarDimension { - constructor(value, valueArrayType, crossfilter, id) { - this.crossfilter = crossfilter; - this._id = id; - - // current selection filter, expressed as PostiveIntervals. - this.currentFilter = []; - - // Create value array - const array = this._createValueArray( - value, - new valueArrayType(this.crossfilter.data.length) - ); - this.value = array; - - // create sort index - this.index = fillRange(new Uint32Array(this.crossfilter.data.length)); - this.index.sort((a, b) => array[a] - array[b]); - - // groups, if any - this.groups = []; - } - - _createValueArray(value, array) { - // create dimension value array - const data = this.crossfilter.data; - const len = data.length; - for (let i = 0; i < len; i++) { - array[i] = value(data[i]); - } - return array; - } - - dispose() { - this.crossfilter._freeDimension(this._id); - return this; - } - - id() { - return this._id; - } - - // Argument is an array of intervals indicating records newly selected/filtered - // - _updateFilters(newFilter) { - newFilter = PositiveIntervals.canonicalize(newFilter); - - const adds = PositiveIntervals.difference(newFilter, this.currentFilter); - const dels = PositiveIntervals.difference(this.currentFilter, newFilter); - - this.crossfilter.filters.forEach(f => - f.dim.groups.forEach(grp => grp._updateReduceDel(this, dels)) - ); - - dels.forEach(interval => - this.crossfilter.selection.deselectIndirectFromRange( - this._id, - this.index, - interval - ) - ); - - adds.forEach(interval => - this.crossfilter.selection.selectIndirectFromRange( - this._id, - this.index, - interval - ) - ); - - this.crossfilter.filters.forEach(f => - f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds)) - ); - - this.currentFilter = newFilter; - } - - // filter by value - exact match - filterExact(value) { - const newFilter = [ - lowerBoundIndirect(this.value, this.index, value, 0, this.value.length), - upperBoundIndirect(this.value, this.index, value, 0, this.value.length) - ]; - if (newFilter[0] <= newFilter[1]) { - this._updateFilters([newFilter]); - } else { - this._updateFilters([]); - } - return this; - } - - // filter by a set of values, eg. enum. - filterEnum(values) { - const newFilter = []; - for (let v = 0, len = values.length; v < len; v++) { - const intv = [ - lowerBoundIndirect( - this.value, - this.index, - values[v], - 0, - this.value.length - ), - upperBoundIndirect( - this.value, - this.index, - values[v], - 0, - this.value.length - ) - ]; - if (intv[0] <= intv[1]) newFilter.push(intv); - } - this._updateFilters(newFilter); - return this; - } - - // filter by value range [lo, hi) - // lo: inclusive, hi: exclusive - filterRange(range) { - const newFilter = []; - const intv = [ - lowerBoundIndirect( - this.value, - this.index, - range[0], - 0, - this.value.length - ), - upperBoundIndirect(this.value, this.index, range[1], 0, this.value.length) - ]; - if (intv[0] < intv[1]) newFilter.push(intv); - this._updateFilters(newFilter); - return this; - } - - // select all - equivalent of selecting all in this dimension - filterAll() { - this._updateFilters([[0, this.value.length]]); - return this; - } - - // select none - filterNone() { - this._updateFilters([]); - } - - // return top k records, starting with offset, in descending order. - // Order is this dimension's sort order - top(k, offset = 0) { - const data = this.crossfilter.data; - const selection = this.crossfilter.selection; - const index = this.index; - const len = index.length; - const ret = []; - let i = 0; - let skip = 0; - let found = 0; - - // skip up to offset records - for (i = len - 1; 0 <= i && skip < offset; i--) { - if (selection.isSelected(index[i])) { - skip++; - } - } - - // grab up to k records - for (; 0 <= i && found < k; i--) { - if (selection.isSelected(index[i])) { - ret.push(data[index[i]]); - found++; - } - } - - return ret; - } - - // return bottom k records, starting with offset, in ascending order. - // Order is this dimension's sort order - bottom(k, offset = 0) { - const data = this.crossfilter.data; - const selection = this.crossfilter.selection; - const index = this.index; - const len = index.length; - const ret = []; - let skip = 0; - let found = 0; - let i = 0; - - // skip up to offset records - for (i = 0; i < len && skip < offset; i++) { - if (selection.isSelected(index[i])) { - skip++; - } - } - - // grab up to k records - for (; i < len && found < k; i++) { - if (selection.isSelected(index[i])) { - ret.push(data[index[i]]); - found++; - } - } - - return ret; - } - - group(groupValue) { - const grp = new ScalarGroup(groupValue, this.value.constructor, this); - this.groups.push(grp); - return grp; - } - - _freeGroup(group) { - this.groups = this.groups.filter(e => e !== group); - } -} - -// Ordered enumeration - supports any sortable enumerable type, eg, -// strings, which can be mapped into an fixed numeric range [0..n). -// -class EnumDimension extends ScalarDimension { - constructor(value, crossfilter, id) { - super(value, Uint32Array, crossfilter, id); - } - - _createValueArray(value, array) { - const data = this.crossfilter.data; - const len = data.length; - - // create enumeration table - mapping between the value - // and the enum. - const s = new Set(); - for (let i = 0; i < len; i++) { - s.add(value(data[i])); - } - this.enumIndex = Array.from(s); - this.enumIndex.sort(); - - // create dimension value array - 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); - array[i] = e; - } - return array; - } - - filterExact(value) { - return super.filterExact( - lowerBound(this.enumIndex, value, 0, this.enumIndex.length) - ); - } - - filterEnum(values) { - return super.filterEnum( - values.map(v => lowerBound(this.enumIndex, v, 0, this.enumIndex.length)) - ); - } - - filterRange(range) { - return super.filterEnum( - range.map(v => lowerBound(this.enumIndex, v, 0, this.enumIndex.length)) - ); - } - - group(groupValue) { - const grp = new EnumGroup(groupValue, this.value.constructor, this); - this.groups.push(grp); - return grp; - } -} - -// Groups! Map/reduce -// -class ScalarGroup { - constructor(groupValue, groupValueType, dimension) { - // parent dimension - this.dimension = dimension; - - // 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); - - // default to counting - this.reduceCount(); - - // Creates this.groups - 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. - // - // Arguments: - // * dim: the dimension that is changing - // * intv: interval list of newly selected values on `dim` (adds) - // - _updateReduceAdd(dim, intv) { - // ignore updates to self, as we don't reduce inclusive of our filter - if (dim === this.dimension || intv.length === 0) return; - - // Each item in the range was just added to `dim`. It was NOT previously - // selected - reduceAdd if it is now selected. - const selection = this.dimension.crossfilter.selection; - const data = this.dimension.crossfilter.data; - intv.forEach(rng => { - for (let r = rng[0]; r < rng[1]; r++) { - const i = dim.index[r]; - if (selection.isSelectedIgnoringDim(i, this.dimension.id())) { - const group = this.groups[this.groupIndex[i]]; - group.value = this.reduceAdd(group.value, data[i]); - } - } - }); - } - - // Update the group reduction incrementally. Called when *any* dimension filter - // changes. Guaranteed to be called BEFORE the crossfilter is updated. - // - // Arguments: - // * dim: the dimension that is changing - // * intv: interval list of previously selected values on `dim` (dels) - // - _updateReduceDel(dim, intv) { - // ignore updates to self, as we don't reduce inclusive of our filter - if (dim === this.dimension || intv.length === 0) return; - - // Each item in the range will be remved from `dim`. reduceRemove if it - // is currently selected. - const selection = this.dimension.crossfilter.selection; - const data = this.dimension.crossfilter.data; - intv.forEach(rng => { - for (let r = rng[0]; r < rng[1]; r++) { - const i = dim.index[r]; - if (selection.isSelectedIgnoringDim(i, this.dimension.id())) { - const group = this.groups[this.groupIndex[i]]; - group.value = this.reduceRemove(group.value, data[i]); - } - } - }); - } - - // Reduce the entire data set, creating both the group index and the - // groups data. - // - _reduce() { - const dimension = this.dimension; - const data = dimension.crossfilter.data; - - // Create groups - const groupNames = new Set(this.mapValue); - this.groups = []; - const groupIndexByName = {}; - groupNames.forEach(name => { - this.groups.push({ key: name, value: this.reduceInitial() }); - groupIndexByName[name] = this.groups.length - 1; - }); - - // Create groupIndex - index map between data record index and group index - for (let i = 0, len = this.mapValue.length; i < len; i++) { - this.groupIndex[i] = groupIndexByName[this.mapValue[i]]; - } - - // reduce all filtered records, IGNORING the current dimension's filter - const selection = dimension.crossfilter.selection; - for (let i = 0, len = data.length; i < len; i++) { - if (selection.isSelectedIgnoringDim(i, dimension.id())) { - const group = this.groups[this.groupIndex[i]]; - group.value = this.reduceAdd(group.value, data[i]); - } - } - } - - dispose() { - this.dimension._freeGroup(this); - return this; - } - - // return number of distinct values in the group, independent of any filters. - // - size() { - return this.groups.length; - } - - // Set the reduce functions and return the grouping. - // - reduce(add, remove, initial) { - this.reduceAdd = add; - this.reduceRemove = remove; - this.reduceInitial = initial; - this._reduce(); - return this; - } - - // set the reduce functions to count records. - reduceCount() { - return this.reduce((p, v) => p + 1, (p, v) => p - 1, () => 0); - } - - // set the reduce functions to sum records using specified value accessor. - // - reduceSum(value) { - return this.reduce((p, v) => p + value(v), (p, v) => p - value(v), () => 0); - } - - all() { - const res = [...this.groups]; - res.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); - return res; - } -} - -class EnumGroup extends ScalarGroup { - constructor(groupValue, groupValueType, dimension) { - 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 - }) - ); - res.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); - return res; - } -} - -// Wrapper for backwards compat with crossfilter. -// -function crossfilter(data) { - return new TypedCrossfilter(data); -} - -crossfilter.PositiveIntervals = PositiveIntervals; -crossfilter.BitArray = BitArray; -crossfilter.TypedCrossfilter = TypedCrossfilter; -crossfilter.ScalarDimension = ScalarDimension; -crossfilter.EnumDimension = EnumDimension; - -export default crossfilter; +"use strict"; +// jshint esversion: 6 + +/* +Typedarray Crossfilter - a re-implementation of a subset of crossfilter, with +time/space optimizations predicated upon the following assumptions: + - dimensions are uniformly typed, and all values must be of that type + - dimension values must be a primitive type (int, float, string). Arrays + or other complex types not supported. + - dimension creation requires call-provided type declaration + - no support for adding/removing data to an existing crossfilter. If you + want to do that, you have to create the new crossfilter, using the new + data, from scratch. + +The actual backing store for a dimension is a TypedArray, enabling significant +performance improvements over the original crossfilter. + +There are also a handful of new methods, primarily to take advantage of the +performance (eg, crossfilter.fillBySelection) + +Helpful documents (this module tries to follow the original API as much +as is feasable): + https://github.com/square/crossfilter/ + http://square.github.io/crossfilter/ + +There is also a newer, community supported fork of crossfilter, with a +more complex API. In a few cases, elements of that API were incorporated. + https://github.com/square/crossfilter/ + +*/ + +import PositiveIntervals from "./positiveIntervals"; +import BitArray from "./bitArray"; +import { + fillRange, + lowerBound, + lowerBoundIndirect, + upperBound, + upperBoundIndirect +} from "./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) { + this.data = data; + + // filters: array of { id, dimension } + this.filters = []; + this.selection = new BitArray(data.length); + } + + size() { + return this.data.length; + } + + all() { + return this.data; + } + + dimension(value, valueArrayType) { + const id = this.selection.allocDimension(); + let dim; + if (valueArrayType === "enum") { + dim = new EnumDimension(value, this, id); + } else { + dim = new ScalarDimension(value, valueArrayType, this, id); + } + this.filters.push({ id, dim }); + dim.filterAll(); + return dim; + } + + _freeDimension(id) { + this.selection.freeDimension(id); + this.filters = this.filters.filter(f => f._id != id); + } + + // return array of all records that are selected/filtered + // by all dimensions. + allFiltered() { + const selection = this.selection; + const res = []; + for (let i = 0, len = this.data.length; i < len; i++) { + if (selection.isSelected(i)) { + res.push(this.data[i]); + } + } + return res; + } + + countFiltered() { + return this.selection.selectionCount; + } + + isElementFiltered(i) { + return this.selection.isSelected(i); + } + + // fill array with one of two values, based upon selection state + fillByIsFiltered(array, selectedValue, deselectedValue) { + return this.selection.fillBySelection( + array, + selectedValue, + deselectedValue + ); + } +} + +// Base dimension type - value must be a scalar type (eg, int, float), +// and value array must be a TypedArray. +// +class ScalarDimension { + constructor(value, valueArrayType, crossfilter, id) { + this.crossfilter = crossfilter; + this._id = id; + + // current selection filter, expressed as PostiveIntervals. + this.currentFilter = []; + + // Create value array + const array = this._createValueArray( + value, + new valueArrayType(this.crossfilter.data.length) + ); + this.value = array; + + // create sort index + this.index = fillRange(new Uint32Array(this.crossfilter.data.length)); + this.index.sort((a, b) => array[a] - array[b]); + + // groups, if any + this.groups = []; + } + + _createValueArray(value, array) { + // create dimension value array + const data = this.crossfilter.data; + const len = data.length; + for (let i = 0; i < len; i++) { + array[i] = value(data[i]); + } + return array; + } + + dispose() { + this.crossfilter._freeDimension(this._id); + return this; + } + + id() { + return this._id; + } + + // Argument is an array of intervals indicating records newly selected/filtered + // + _updateFilters(newFilter) { + newFilter = PositiveIntervals.canonicalize(newFilter); + + const adds = PositiveIntervals.difference(newFilter, this.currentFilter); + const dels = PositiveIntervals.difference(this.currentFilter, newFilter); + + this.crossfilter.filters.forEach(f => + f.dim.groups.forEach(grp => grp._updateReduceDel(this, dels)) + ); + + dels.forEach(interval => + this.crossfilter.selection.deselectIndirectFromRange( + this._id, + this.index, + interval + ) + ); + + adds.forEach(interval => + this.crossfilter.selection.selectIndirectFromRange( + this._id, + this.index, + interval + ) + ); + + this.crossfilter.filters.forEach(f => + f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds)) + ); + + this.currentFilter = newFilter; + } + + // filter by value - exact match + filterExact(value) { + const newFilter = [ + lowerBoundIndirect(this.value, this.index, value, 0, this.value.length), + upperBoundIndirect(this.value, this.index, value, 0, this.value.length) + ]; + if (newFilter[0] <= newFilter[1]) { + this._updateFilters([newFilter]); + } else { + this._updateFilters([]); + } + return this; + } + + // filter by a set of values, eg. enum. + filterEnum(values) { + const newFilter = []; + for (let v = 0, len = values.length; v < len; v++) { + const intv = [ + lowerBoundIndirect( + this.value, + this.index, + values[v], + 0, + this.value.length + ), + upperBoundIndirect( + this.value, + this.index, + values[v], + 0, + this.value.length + ) + ]; + if (intv[0] <= intv[1]) newFilter.push(intv); + } + this._updateFilters(newFilter); + return this; + } + + // filter by value range [lo, hi) + // lo: inclusive, hi: exclusive + filterRange(range) { + const newFilter = []; + const intv = [ + lowerBoundIndirect( + this.value, + this.index, + range[0], + 0, + this.value.length + ), + upperBoundIndirect(this.value, this.index, range[1], 0, this.value.length) + ]; + if (intv[0] < intv[1]) newFilter.push(intv); + this._updateFilters(newFilter); + return this; + } + + // select all - equivalent of selecting all in this dimension + filterAll() { + this._updateFilters([[0, this.value.length]]); + return this; + } + + // select none + filterNone() { + this._updateFilters([]); + } + + // return top k records, starting with offset, in descending order. + // Order is this dimension's sort order + top(k, offset = 0) { + const data = this.crossfilter.data; + const selection = this.crossfilter.selection; + const index = this.index; + const len = index.length; + const ret = []; + let i = 0; + let skip = 0; + let found = 0; + + // skip up to offset records + for (i = len - 1; 0 <= i && skip < offset; i--) { + if (selection.isSelected(index[i])) { + skip++; + } + } + + // grab up to k records + for (; 0 <= i && found < k; i--) { + if (selection.isSelected(index[i])) { + ret.push(data[index[i]]); + found++; + } + } + + return ret; + } + + // return bottom k records, starting with offset, in ascending order. + // Order is this dimension's sort order + bottom(k, offset = 0) { + const data = this.crossfilter.data; + const selection = this.crossfilter.selection; + const index = this.index; + const len = index.length; + const ret = []; + let skip = 0; + let found = 0; + let i = 0; + + // skip up to offset records + for (i = 0; i < len && skip < offset; i++) { + if (selection.isSelected(index[i])) { + skip++; + } + } + + // grab up to k records + for (; i < len && found < k; i++) { + if (selection.isSelected(index[i])) { + ret.push(data[index[i]]); + found++; + } + } + + return ret; + } + + group(groupValue) { + const grp = new ScalarGroup(groupValue, this.value.constructor, this); + this.groups.push(grp); + return grp; + } + + _freeGroup(group) { + this.groups = this.groups.filter(e => e !== group); + } +} + +// Ordered enumeration - supports any sortable enumerable type, eg, +// strings, which can be mapped into an fixed numeric range [0..n). +// +class EnumDimension extends ScalarDimension { + constructor(value, crossfilter, id) { + super(value, Uint32Array, crossfilter, id); + } + + _createValueArray(value, array) { + const data = this.crossfilter.data; + const len = data.length; + + // create enumeration table - mapping between the value + // and the enum. + const s = new Set(); + for (let i = 0; i < len; i++) { + s.add(value(data[i])); + } + this.enumIndex = Array.from(s); + this.enumIndex.sort(); + + // create dimension value array + 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); + array[i] = e; + } + return array; + } + + filterExact(value) { + return super.filterExact( + lowerBound(this.enumIndex, value, 0, this.enumIndex.length) + ); + } + + filterEnum(values) { + return super.filterEnum( + values.map(v => lowerBound(this.enumIndex, v, 0, this.enumIndex.length)) + ); + } + + filterRange(range) { + return super.filterEnum( + range.map(v => lowerBound(this.enumIndex, v, 0, this.enumIndex.length)) + ); + } + + group(groupValue) { + const grp = new EnumGroup(groupValue, this.value.constructor, this); + this.groups.push(grp); + return grp; + } +} + +// Groups! Map/reduce +// +class ScalarGroup { + constructor(groupValue, groupValueType, dimension) { + // parent dimension + this.dimension = dimension; + + // 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); + + // default to counting + this.reduceCount(); + + // Creates this.groups + 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. + // + // Arguments: + // * dim: the dimension that is changing + // * intv: interval list of newly selected values on `dim` (adds) + // + _updateReduceAdd(dim, intv) { + // ignore updates to self, as we don't reduce inclusive of our filter + if (dim === this.dimension || intv.length === 0) return; + + // Each item in the range was just added to `dim`. It was NOT previously + // selected - reduceAdd if it is now selected. + const selection = this.dimension.crossfilter.selection; + const data = this.dimension.crossfilter.data; + intv.forEach(rng => { + for (let r = rng[0]; r < rng[1]; r++) { + const i = dim.index[r]; + if (selection.isSelectedIgnoringDim(i, this.dimension.id())) { + const group = this.groups[this.groupIndex[i]]; + group.value = this.reduceAdd(group.value, data[i]); + } + } + }); + } + + // Update the group reduction incrementally. Called when *any* dimension filter + // changes. Guaranteed to be called BEFORE the crossfilter is updated. + // + // Arguments: + // * dim: the dimension that is changing + // * intv: interval list of previously selected values on `dim` (dels) + // + _updateReduceDel(dim, intv) { + // ignore updates to self, as we don't reduce inclusive of our filter + if (dim === this.dimension || intv.length === 0) return; + + // Each item in the range will be remved from `dim`. reduceRemove if it + // is currently selected. + const selection = this.dimension.crossfilter.selection; + const data = this.dimension.crossfilter.data; + intv.forEach(rng => { + for (let r = rng[0]; r < rng[1]; r++) { + const i = dim.index[r]; + if (selection.isSelectedIgnoringDim(i, this.dimension.id())) { + const group = this.groups[this.groupIndex[i]]; + group.value = this.reduceRemove(group.value, data[i]); + } + } + }); + } + + // Reduce the entire data set, creating both the group index and the + // groups data. + // + _reduce() { + const dimension = this.dimension; + const data = dimension.crossfilter.data; + + // Create groups + const groupNames = new Set(this.mapValue); + this.groups = []; + const groupIndexByName = {}; + groupNames.forEach(name => { + this.groups.push({ key: name, value: this.reduceInitial() }); + groupIndexByName[name] = this.groups.length - 1; + }); + + // Create groupIndex - index map between data record index and group index + for (let i = 0, len = this.mapValue.length; i < len; i++) { + this.groupIndex[i] = groupIndexByName[this.mapValue[i]]; + } + + // reduce all filtered records, IGNORING the current dimension's filter + const selection = dimension.crossfilter.selection; + for (let i = 0, len = data.length; i < len; i++) { + if (selection.isSelectedIgnoringDim(i, dimension.id())) { + const group = this.groups[this.groupIndex[i]]; + group.value = this.reduceAdd(group.value, data[i]); + } + } + } + + dispose() { + this.dimension._freeGroup(this); + return this; + } + + // return number of distinct values in the group, independent of any filters. + // + size() { + return this.groups.length; + } + + // Set the reduce functions and return the grouping. + // + reduce(add, remove, initial) { + this.reduceAdd = add; + this.reduceRemove = remove; + this.reduceInitial = initial; + this._reduce(); + return this; + } + + // set the reduce functions to count records. + reduceCount() { + return this.reduce((p, v) => p + 1, (p, v) => p - 1, () => 0); + } + + // set the reduce functions to sum records using specified value accessor. + // + reduceSum(value) { + return this.reduce((p, v) => p + value(v), (p, v) => p - value(v), () => 0); + } + + all() { + const res = [...this.groups]; + res.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + return res; + } +} + +class EnumGroup extends ScalarGroup { + constructor(groupValue, groupValueType, dimension) { + 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 + }) + ); + res.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + return res; + } +} + +// Wrapper for backwards compat with crossfilter. +// +function crossfilter(data) { + return new TypedCrossfilter(data); +} + +crossfilter.PositiveIntervals = PositiveIntervals; +crossfilter.BitArray = BitArray; +crossfilter.TypedCrossfilter = TypedCrossfilter; +crossfilter.ScalarDimension = ScalarDimension; +crossfilter.EnumDimension = EnumDimension; + +export default crossfilter; diff --git a/client/src/util/typedCrossfilter/positiveIntervals.js b/client/src/util/typedCrossfilter/positiveIntervals.js index 5e54bf7f..66742faa 100644 --- a/client/src/util/typedCrossfilter/positiveIntervals.js +++ b/client/src/util/typedCrossfilter/positiveIntervals.js @@ -1,132 +1,132 @@ -"use strict"; -// jshint esversion: 6 - -// Interval operations - very simple version of interval set relationship -// operators. An interval is a multi-interval list of [min, max), -// where min and max are mandatory. Constraints: -// * min <= max, min >= 0 -// * empty interval groups are OK, ie, [] -// * Legal intervals: [], [ [0, 1], ... ] -// * Not legal: [ [] ] -// -// All intervals are represented by simple JS arrays/numbers. -// -// Code assumes intervals have a low cardinality; many operations are done -// with a brute force scan. Little attempt to reduce GC pressure. -// -class PositiveIntervals { - // Canonicalize - ensure that: - // 1. no overlapping intervals - // 2. sorted in order of interval min. - // - static canonicalize(A) { - if (A.length <= 1) return A; - let copy = A.slice(); - copy.sort((a, b) => a[0] - b[0]); - const res = []; - res.push(copy[0]); - for (let i = 1, len = copy.length; i < len; i++) { - if (copy[i][0] > res[res.length - 1][1]) { - // non-overlapping, add to result - res.push(copy[i]); - } else if (copy[i][1] > res[res.length - 1][1]) { - // merge this into previous - res[res.length - 1][1] = copy[i][1]; - } - } - return res; - } - - // Return interval with values belonging to both A and B. Essentially - // a set union operation. - // - static union(A, B) { - return PositiveIntervals.canonicalize([...A, ...B]); - } - - static _flatten(A, B) { - let points = []; /* point, A, start */ - for (let a = 0; a < A.length; a++) { - points.push([A[a][0], true, true]); - points.push([A[a][1], true, false]); - } - for (let b = 0; b < B.length; b++) { - points.push([B[b][0], false, true]); - points.push([B[b][1], false, false]); - } - // Sort order: point, then start - points.sort((a, b) => (a[0] !== b[0] ? a[0] - b[0] : a[2] ? 1 : -1)); - return points; - } - - // A - B, ie, the interval with all values in A that are not in B. Essentially - // a set difference operation. - // - static difference(A, B) { - // Corner cases - if (A.length === 0 || B.length === 0) { - return PositiveIntervals.canonicalize(A); - } - - A = PositiveIntervals.canonicalize(A); - B = PositiveIntervals.canonicalize(B); - - const points = PositiveIntervals._flatten(A, B); - const res = []; - let aDepth = 0; - let depth = 0; - let intervalStart; - let prevPoint; - for (let i = 0; i < points.length; i++) { - const p = points[i]; - const before = depth; - const delta = p[2] ? 1 : -1; - depth += delta; - if (p[1]) aDepth += delta; - - if (i === points.length - 1 || p[0] !== points[i + 1][0]) { - if (aDepth === 1 && depth === 1) { - intervalStart = p[0]; - } else if (intervalStart !== undefined) { - res.push([intervalStart, p[0]]); - intervalStart = undefined; - } - } - prevPoint = p[0]; - } - // guaranteed to be in canonical form - return res; - } - - // Return interval with values belonging to A or B. Essentially a set - // intersection. - // - static intersection(A, B) { - if (A.length === 0 || B.length === 0) { - return []; - } - - A = PositiveIntervals.canonicalize(A); - B = PositiveIntervals.canonicalize(B); - - const points = PositiveIntervals._flatten(A, B); - const res = []; - let depth = 0; - let intervalStart; - for (let i = 0; i < points.length; i++) { - const p = points[i]; - const before = depth; - depth += p[2] ? 1 : -1; - if (depth === 2) { - intervalStart = p[0]; - } else if (intervalStart !== undefined) { - res.push([intervalStart, p[0]]); - intervalStart = undefined; - } - } - // guaranteed to be in canonical form - return res; - } -} - -export default PositiveIntervals; +"use strict"; +// jshint esversion: 6 + +// Interval operations - very simple version of interval set relationship +// operators. An interval is a multi-interval list of [min, max), +// where min and max are mandatory. Constraints: +// * min <= max, min >= 0 +// * empty interval groups are OK, ie, [] +// * Legal intervals: [], [ [0, 1], ... ] +// * Not legal: [ [] ] +// +// All intervals are represented by simple JS arrays/numbers. +// +// Code assumes intervals have a low cardinality; many operations are done +// with a brute force scan. Little attempt to reduce GC pressure. +// +class PositiveIntervals { + // Canonicalize - ensure that: + // 1. no overlapping intervals + // 2. sorted in order of interval min. + // + static canonicalize(A) { + if (A.length <= 1) return A; + let copy = A.slice(); + copy.sort((a, b) => a[0] - b[0]); + const res = []; + res.push(copy[0]); + for (let i = 1, len = copy.length; i < len; i++) { + if (copy[i][0] > res[res.length - 1][1]) { + // non-overlapping, add to result + res.push(copy[i]); + } else if (copy[i][1] > res[res.length - 1][1]) { + // merge this into previous + res[res.length - 1][1] = copy[i][1]; + } + } + return res; + } + + // Return interval with values belonging to both A and B. Essentially + // a set union operation. + // + static union(A, B) { + return PositiveIntervals.canonicalize([...A, ...B]); + } + + static _flatten(A, B) { + let points = []; /* point, A, start */ + for (let a = 0; a < A.length; a++) { + points.push([A[a][0], true, true]); + points.push([A[a][1], true, false]); + } + for (let b = 0; b < B.length; b++) { + points.push([B[b][0], false, true]); + points.push([B[b][1], false, false]); + } + // Sort order: point, then start + points.sort((a, b) => (a[0] !== b[0] ? a[0] - b[0] : a[2] ? 1 : -1)); + return points; + } + + // A - B, ie, the interval with all values in A that are not in B. Essentially + // a set difference operation. + // + static difference(A, B) { + // Corner cases + if (A.length === 0 || B.length === 0) { + return PositiveIntervals.canonicalize(A); + } + + A = PositiveIntervals.canonicalize(A); + B = PositiveIntervals.canonicalize(B); + + const points = PositiveIntervals._flatten(A, B); + const res = []; + let aDepth = 0; + let depth = 0; + let intervalStart; + let prevPoint; + for (let i = 0; i < points.length; i++) { + const p = points[i]; + const before = depth; + const delta = p[2] ? 1 : -1; + depth += delta; + if (p[1]) aDepth += delta; + + if (i === points.length - 1 || p[0] !== points[i + 1][0]) { + if (aDepth === 1 && depth === 1) { + intervalStart = p[0]; + } else if (intervalStart !== undefined) { + res.push([intervalStart, p[0]]); + intervalStart = undefined; + } + } + prevPoint = p[0]; + } + // guaranteed to be in canonical form + return res; + } + + // Return interval with values belonging to A or B. Essentially a set + // intersection. + // + static intersection(A, B) { + if (A.length === 0 || B.length === 0) { + return []; + } + + A = PositiveIntervals.canonicalize(A); + B = PositiveIntervals.canonicalize(B); + + const points = PositiveIntervals._flatten(A, B); + const res = []; + let depth = 0; + let intervalStart; + for (let i = 0; i < points.length; i++) { + const p = points[i]; + const before = depth; + depth += p[2] ? 1 : -1; + if (depth === 2) { + intervalStart = p[0]; + } else if (intervalStart !== undefined) { + res.push([intervalStart, p[0]]); + intervalStart = undefined; + } + } + // guaranteed to be in canonical form + return res; + } +} + +export default PositiveIntervals; diff --git a/client/src/util/typedCrossfilter/util.js b/client/src/util/typedCrossfilter/util.js index 18b2c21e..1f1f148c 100644 --- a/client/src/util/typedCrossfilter/util.js +++ b/client/src/util/typedCrossfilter/util.js @@ -1,98 +1,98 @@ -"use strict"; -// jshint esversion: 6 - -/* - Utility functions, private to this module. -*/ - -// fill an array or typedarray with a sequential range of numbers, -// starting with `start` -// -export function fillRange(arr, start = 0) { - for (let i = 0, len = arr.length; i < len; i++) { - arr[i] = i + start; - } - return arr; -} - -// Search for `value` in the sorted array `arr`, in the range [first, last). -// Return the first (left most) index where arr[index] >= value. -// -// In other words, return array index I where: -// arr[i] < value for all tarr[lo:I] -// arr[i] >= value for all tarr[I:last] -// -// The same semantics/behavior as: -// C++: lower_bound() -// Python: bisect.bisect_left() -// -// XXX: it is likely that there would be minimal performance hit from creating -// 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) { - // this is just a binary search - while (first < last) { - const middle = (first + last) >>> 1; - if (valueArray[middle] < value) { - first = middle + 1; - } else { - last = middle; - } - } - return first; -} - -// Inlined performance optimization - used to indirect through a sort map. -// -export function lowerBoundIndirect(valueArray, indexArray, value, first, last) { - // this is just a binary search - while (first < last) { - const middle = (first + last) >>> 1; - if (valueArray[indexArray[middle]] < value) { - first = middle + 1; - } else { - last = middle; - } - } - return first; -} - -// Search for `value in the sorted array `arr`, in the range [first, last). -// Return the first value where arr[index] > value. -// -// In other words, return array index I, where: -// arr[i] <= value for all tarr[lo:I] -// arr[i] > value for all tarr[I:last] -// -// The same semantics/behavior as: -// C++: upper_bound() -// Python: bisect.bisect_right() -// -export function upperBound(valueArray, value, first, last) { - // this is just a binary search - while (first < last) { - const middle = (first + last) >>> 1; - if (valueArray[middle] > value) { - last = middle; - } else { - first = middle + 1; - } - } - return first; -} - -// Inline performance optimization -// -export function upperBoundIndirect(valueArray, indexArray, value, first, last) { - // this is just a binary search - while (first < last) { - const middle = (first + last) >>> 1; - if (valueArray[indexArray[middle]] > value) { - last = middle; - } else { - first = middle + 1; - } - } - return first; -} +"use strict"; +// jshint esversion: 6 + +/* + Utility functions, private to this module. +*/ + +// fill an array or typedarray with a sequential range of numbers, +// starting with `start` +// +export function fillRange(arr, start = 0) { + for (let i = 0, len = arr.length; i < len; i++) { + arr[i] = i + start; + } + return arr; +} + +// Search for `value` in the sorted array `arr`, in the range [first, last). +// Return the first (left most) index where arr[index] >= value. +// +// In other words, return array index I where: +// arr[i] < value for all tarr[lo:I] +// arr[i] >= value for all tarr[I:last] +// +// The same semantics/behavior as: +// C++: lower_bound() +// Python: bisect.bisect_left() +// +// XXX: it is likely that there would be minimal performance hit from creating +// 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) { + // this is just a binary search + while (first < last) { + const middle = (first + last) >>> 1; + if (valueArray[middle] < value) { + first = middle + 1; + } else { + last = middle; + } + } + return first; +} + +// Inlined performance optimization - used to indirect through a sort map. +// +export function lowerBoundIndirect(valueArray, indexArray, value, first, last) { + // this is just a binary search + while (first < last) { + const middle = (first + last) >>> 1; + if (valueArray[indexArray[middle]] < value) { + first = middle + 1; + } else { + last = middle; + } + } + return first; +} + +// Search for `value in the sorted array `arr`, in the range [first, last). +// Return the first value where arr[index] > value. +// +// In other words, return array index I, where: +// arr[i] <= value for all tarr[lo:I] +// arr[i] > value for all tarr[I:last] +// +// The same semantics/behavior as: +// C++: upper_bound() +// Python: bisect.bisect_right() +// +export function upperBound(valueArray, value, first, last) { + // this is just a binary search + while (first < last) { + const middle = (first + last) >>> 1; + if (valueArray[middle] > value) { + last = middle; + } else { + first = middle + 1; + } + } + return first; +} + +// Inline performance optimization +// +export function upperBoundIndirect(valueArray, indexArray, value, first, last) { + // this is just a binary search + while (first < last) { + const middle = (first + last) >>> 1; + if (valueArray[indexArray[middle]] > value) { + last = middle; + } else { + first = middle + 1; + } + } + return first; +}