mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-09 03:38:12 +08:00
crossfilters: reverse ES6 module change; add group support for enumerated dimensions
This commit is contained in:
@@ -251,4 +251,4 @@ class BitArray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BitArray;
|
module.exports = BitArray;
|
||||||
|
|||||||
@@ -29,9 +29,20 @@ more complex API. In a few cases, elements of that API were incorporated.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import PositiveIntervals from "./positiveIntervals";
|
var PositiveIntervals = require("./positiveIntervals");
|
||||||
import BitArray from "./bitArray";
|
var BitArray = require("./bitArray");
|
||||||
import {fillRange, lowerBound, lowerBoundIndirect, upperBound, upperBoundIndirect} from "./util";
|
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 {
|
class TypedCrossfilter {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
@@ -118,7 +129,7 @@ class ScalarDimension {
|
|||||||
this.value = array;
|
this.value = array;
|
||||||
|
|
||||||
// create sort index
|
// 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]);
|
this.index.sort((a, b) => array[a] - array[b]);
|
||||||
|
|
||||||
// groups, if any
|
// groups, if any
|
||||||
@@ -149,20 +160,6 @@ class ScalarDimension {
|
|||||||
_updateFilters(newFilter) {
|
_updateFilters(newFilter) {
|
||||||
newFilter = PositiveIntervals.canonicalize(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 adds = PositiveIntervals.difference(newFilter, this.currentFilter);
|
||||||
const dels = PositiveIntervals.difference(this.currentFilter, newFilter);
|
const dels = PositiveIntervals.difference(this.currentFilter, newFilter);
|
||||||
|
|
||||||
@@ -190,23 +187,20 @@ class ScalarDimension {
|
|||||||
f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds))
|
f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds))
|
||||||
);
|
);
|
||||||
|
|
||||||
// XXX removed optimization
|
|
||||||
// }
|
|
||||||
|
|
||||||
this.currentFilter = newFilter;
|
this.currentFilter = newFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter by value - exact match
|
// filter by value - exact match
|
||||||
filterExact(value) {
|
filterExact(value) {
|
||||||
const newFilter = [
|
const newFilter = [
|
||||||
lowerBoundIndirect(
|
Util.lowerBoundIndirect(
|
||||||
this.value,
|
this.value,
|
||||||
this.index,
|
this.index,
|
||||||
value,
|
value,
|
||||||
0,
|
0,
|
||||||
this.value.length
|
this.value.length
|
||||||
),
|
),
|
||||||
upperBoundIndirect(
|
Util.upperBoundIndirect(
|
||||||
this.value,
|
this.value,
|
||||||
this.index,
|
this.index,
|
||||||
value,
|
value,
|
||||||
@@ -227,14 +221,14 @@ class ScalarDimension {
|
|||||||
const newFilter = [];
|
const newFilter = [];
|
||||||
for (let v = 0, len = values.length; v < len; v++) {
|
for (let v = 0, len = values.length; v < len; v++) {
|
||||||
const intv = [
|
const intv = [
|
||||||
lowerBoundIndirect(
|
Util.lowerBoundIndirect(
|
||||||
this.value,
|
this.value,
|
||||||
this.index,
|
this.index,
|
||||||
values[v],
|
values[v],
|
||||||
0,
|
0,
|
||||||
this.value.length
|
this.value.length
|
||||||
),
|
),
|
||||||
upperBoundIndirect(
|
Util.upperBoundIndirect(
|
||||||
this.value,
|
this.value,
|
||||||
this.index,
|
this.index,
|
||||||
values[v],
|
values[v],
|
||||||
@@ -253,14 +247,14 @@ class ScalarDimension {
|
|||||||
filterRange(range) {
|
filterRange(range) {
|
||||||
const newFilter = [];
|
const newFilter = [];
|
||||||
const intv = [
|
const intv = [
|
||||||
lowerBoundIndirect(
|
Util.lowerBoundIndirect(
|
||||||
this.value,
|
this.value,
|
||||||
this.index,
|
this.index,
|
||||||
range[0],
|
range[0],
|
||||||
0,
|
0,
|
||||||
this.value.length
|
this.value.length
|
||||||
),
|
),
|
||||||
upperBoundIndirect(
|
Util.upperBoundIndirect(
|
||||||
this.value,
|
this.value,
|
||||||
this.index,
|
this.index,
|
||||||
range[1],
|
range[1],
|
||||||
@@ -380,7 +374,7 @@ class EnumDimension extends ScalarDimension {
|
|||||||
const enumLen = this.enumIndex.length;
|
const enumLen = this.enumIndex.length;
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const v = value(data[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;
|
array[i] = e;
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
@@ -388,14 +382,14 @@ class EnumDimension extends ScalarDimension {
|
|||||||
|
|
||||||
filterExact(value) {
|
filterExact(value) {
|
||||||
return super.filterExact(
|
return super.filterExact(
|
||||||
lowerBound(this.enumIndex, value, 0, this.enumIndex.length)
|
Util.lowerBound(this.enumIndex, value, 0, this.enumIndex.length)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
filterEnum(values) {
|
filterEnum(values) {
|
||||||
return super.filterEnum(
|
return super.filterEnum(
|
||||||
values.map(v =>
|
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) {
|
filterRange(range) {
|
||||||
return super.filterEnum(
|
return super.filterEnum(
|
||||||
range.map(v =>
|
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
|
// 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 {
|
class ScalarGroup {
|
||||||
constructor(groupValue, groupValueType, dimension) {
|
constructor(groupValue, groupValueType, dimension) {
|
||||||
// parent dimension
|
// parent dimension
|
||||||
this.dimension = dimension;
|
this.dimension = dimension;
|
||||||
|
|
||||||
// groupValue is optional. Defaults to identity. Used to perform
|
// generate group names from dimension values
|
||||||
// initial map operation.
|
this.mapValue = this._map(groupValue, groupValueType, dimension);
|
||||||
//
|
|
||||||
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 = [];
|
|
||||||
|
|
||||||
// group index is mapping from data record index to group index
|
// group index is mapping from data record index to group index
|
||||||
this.groupIndex = new Uint32Array(dimension.crossfilter.data.length);
|
this.groupIndex = new Uint32Array(dimension.crossfilter.data.length);
|
||||||
@@ -453,6 +429,24 @@ class ScalarGroup {
|
|||||||
this._reduce();
|
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
|
// Update the group reduction incrementally. Called when *any* dimension filter
|
||||||
// changes. Guaranteed to be called AFTER the crossfilter is updated.
|
// 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)
|
// * intv: interval list of newly selected values on `dim` (adds)
|
||||||
//
|
//
|
||||||
_updateReduceAdd(dim, intv) {
|
_updateReduceAdd(dim, intv) {
|
||||||
// console.log("_updateReduceAdd", dim, intv);
|
|
||||||
|
|
||||||
// ignore updates to self, as we don't reduce inclusive of our filter
|
// ignore updates to self, as we don't reduce inclusive of our filter
|
||||||
if (dim === this.dimension || intv.length === 0) return;
|
if (dim === this.dimension || intv.length === 0) return;
|
||||||
|
|
||||||
@@ -489,8 +481,6 @@ class ScalarGroup {
|
|||||||
// * intv: interval list of previously selected values on `dim` (dels)
|
// * intv: interval list of previously selected values on `dim` (dels)
|
||||||
//
|
//
|
||||||
_updateReduceDel(dim, intv) {
|
_updateReduceDel(dim, intv) {
|
||||||
// console.log("_updateReduceDel", dim, intv);
|
|
||||||
|
|
||||||
// ignore updates to self, as we don't reduce inclusive of our filter
|
// ignore updates to self, as we don't reduce inclusive of our filter
|
||||||
if (dim === this.dimension || intv.length === 0) return;
|
if (dim === this.dimension || intv.length === 0) return;
|
||||||
|
|
||||||
@@ -584,10 +574,26 @@ class EnumGroup extends ScalarGroup {
|
|||||||
super(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() {
|
all() {
|
||||||
const res = [];
|
const res = [];
|
||||||
this.groups.forEach(e =>
|
this.groups.forEach(e =>
|
||||||
res.push({
|
res.push({
|
||||||
|
// XXX: assumes identity group map - see comment in _map()
|
||||||
key: this.dimension.enumIndex[e.key],
|
key: this.dimension.enumIndex[e.key],
|
||||||
value: e.value
|
value: e.value
|
||||||
})
|
})
|
||||||
@@ -609,4 +615,4 @@ crossfilter.TypedCrossfilter = TypedCrossfilter;
|
|||||||
crossfilter.ScalarDimension = ScalarDimension;
|
crossfilter.ScalarDimension = ScalarDimension;
|
||||||
crossfilter.EnumDimension = EnumDimension;
|
crossfilter.EnumDimension = EnumDimension;
|
||||||
|
|
||||||
export default crossfilter;
|
module.exports = crossfilter;
|
||||||
|
|||||||
@@ -129,4 +129,4 @@ class PositiveIntervals {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PositiveIntervals;
|
module.exports = PositiveIntervals;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// fill an array or typedarray with a sequential range of numbers,
|
// fill an array or typedarray with a sequential range of numbers,
|
||||||
// starting with `start`
|
// starting with `start`
|
||||||
//
|
//
|
||||||
export function fillRange(arr, start = 0) {
|
function fillRange(arr, start = 0) {
|
||||||
for (let i = 0, len = arr.length; i < len; i++) {
|
for (let i = 0, len = arr.length; i < len; i++) {
|
||||||
arr[i] = i + start;
|
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 factory version of lowerBound that takes an accessor (rather than having
|
||||||
// a special-cased version for lining the indirection).
|
// 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
|
// this is just a binary search
|
||||||
while (first < last) {
|
while (first < last) {
|
||||||
const middle = (first + last) >>> 1;
|
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.
|
// 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
|
// this is just a binary search
|
||||||
while (first < last) {
|
while (first < last) {
|
||||||
const middle = (first + last) >>> 1;
|
const middle = (first + last) >>> 1;
|
||||||
@@ -69,7 +69,7 @@ export function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
|
|||||||
// C++: upper_bound()
|
// C++: upper_bound()
|
||||||
// Python: bisect.bisect_right()
|
// Python: bisect.bisect_right()
|
||||||
//
|
//
|
||||||
export function upperBound(valueArray, value, first, last) {
|
function upperBound(valueArray, value, first, last) {
|
||||||
// this is just a binary search
|
// this is just a binary search
|
||||||
while (first < last) {
|
while (first < last) {
|
||||||
const middle = (first + last) >>> 1;
|
const middle = (first + last) >>> 1;
|
||||||
@@ -84,7 +84,7 @@ export function upperBound(valueArray, value, first, last) {
|
|||||||
|
|
||||||
// Inline performance optimization
|
// Inline performance optimization
|
||||||
//
|
//
|
||||||
export function upperBoundIndirect(valueArray, indexArray, value, first, last) {
|
function upperBoundIndirect(valueArray, indexArray, value, first, last) {
|
||||||
// this is just a binary search
|
// this is just a binary search
|
||||||
while (first < last) {
|
while (first < last) {
|
||||||
const middle = (first + last) >>> 1;
|
const middle = (first + last) >>> 1;
|
||||||
@@ -96,3 +96,11 @@ export function upperBoundIndirect(valueArray, indexArray, value, first, last) {
|
|||||||
}
|
}
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fillRange,
|
||||||
|
lowerBound,
|
||||||
|
lowerBoundIndirect,
|
||||||
|
upperBound,
|
||||||
|
upperBoundIndirect
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user