#130 redux refactor (#208)

* mocks for redux refactor - for discussion

* more API design on redux refactor

* add new reuqired dependencies for build

* change babel target to use modern browser

* remove dead code

* remove dead code - joy plots

* checkpoint on redux refactoring

* checkpoint on redux refactoring

* fix mistaken rebase conflict resolution

* dead code removal; add name to dataframe backmap

* rename dataframe to universe

* update eslint config to more closely match prettier

* lint

* more eslint updates to match prettier

* additional config to make eslint match prettier

* add expression data to Universe/World

* remove obsolete reducers

* lint fixes

* more eslint cleanup

* lint

* lint

* fix but in countAllOnes when dimensions gt 1

* lint; do not display name metadata field

* lint; colors refactor

* lint; colors refactor

* update comments

* first cut at regraph and reset

* enable object-curly-braces consistent mode

* lint, handle regraph with no selection

* fix expression scatterplot bugs

* fix regression legend display

* add expression data cache

* remove console logging

* reset cell color on regraph/reset

* remove obsolete server URLs

* rename UniverseV01 to Universe_REST_API_v01

* add additional comments on the varDataCache

* merge universe reducer into controls reducer; simplify initialization-related actions

* use spread operator

* fix erroneous comment

* convert universe and world state to plain objects, and functionalize supporting code (remove ES6 classes)

* use spread operator

* lint

* improve variable names

* rename obsCrossfilter to crossfilter and obsDimensionMap to dimensionMap

* rename controls2 to controls
This commit is contained in:
Bruce Martin
2018-09-17 20:43:39 -07:00
committed by GitHub
parent 7b00fea44d
commit d4e850d18f
40 changed files with 2008 additions and 1584 deletions
+19
View File
@@ -0,0 +1,19 @@
// jshint esversion: 6
/*
Model manager providing an abstraction for the use of the reducer code.
This module provides several buckets of functionality:
- schema and config driven tranformation of the dataframe wire protocol
into a format that is easy for the UI code to use.
- manage the universe/world abstraction:
+ universe: all of the server-provided, read-only data
+ world: subset of universe
- lazy access and caching of dataframe contents as needed
This is all VERY tightly integrated with reducers and actions, and
exists to support those concepts.
*/
export * as Universe from "./universe";
export * as World from "./world";
export * as kvCache from "./keyvalcache";
@@ -0,0 +1,72 @@
// jshint esversion: 6
import _ from "lodash";
/*
Very simple key/value cache for use by World & Universe.
* constructor(lowWatermark, cachekey):
- lowWatermark defines the number of cache elements below which
flushing will not occur.
- minTTL defines minimum time in MS that cache entries will live.
A value of -1 disables automatic flushing (flush() can still
be called by external user).
- cachekey is a key that will be assigned to any value to track age
* set() - add a key/val pair.
* get() - get a value or undefined if not present.
* flush(minAgeMs) - flush cache entries in excess of lowWatermark if those
entries are older than minAgeMs.
*/
const cachePrivateKey = "__kvcachekey__";
function create(lowWatermark = 32, minTTL = 1000) {
return {
[cachePrivateKey]: {
lowWatermark,
minTTL
}
};
}
function get(kvcache, key) {
const val = kvcache[key];
if (val) {
val[cachePrivateKey] = Date.now();
}
return val;
}
function set(kvcache, key, val) {
const newKvCache = { ...kvcache };
newKvCache[key] = val;
val[cachePrivateKey] = Date.now();
flush(newKvCache, newKvCache[cachePrivateKey].minTTL);
return newKvCache;
}
/*
Flush elements from cache IF cache size is greater than lowWatermark, and
those elements are older than minAgeMS
*/
function flush(kvcache, minAgeMs = 0) {
if (minAgeMs < 0) return kvcache;
const eol = Date.now() - minAgeMs;
const { lowWatermark } = kvcache[cachePrivateKey];
const keys = _(kvcache)
.keys()
.filter(k => k !== cachePrivateKey)
.filter(k => kvcache[k][cachePrivateKey] < eol)
.sortBy([k => kvcache[k][cachePrivateKey]])
.value();
if (keys.length > lowWatermark) {
const numKeysToDelete = keys.length - lowWatermark;
const keysToDelete = _.slice(keys, 0, numKeysToDelete);
_.forEach(keysToDelete, k => delete kvcache[k]);
}
return kvcache;
}
export { create, get, set, flush };
+252
View File
@@ -0,0 +1,252 @@
// jshint esversion: 6
import _ from "lodash";
import * as kvCache from "./keyvalcache";
/*
This module implements functions that support storage of "Universe",
aka all of the var/obs data and annotations.
These functions are used exclusively by the actions and reducers to
build an internal POJO for use by the rendering components.
*/
/*
Cherry pick from /api/v0.1 response format to make somethign similar
to the v0.2 schema, which we use for internal interfaces.
*/
function RESTv01ResponseToSchema(response) {
/*
Annotation schemas in V02 (our target) look like:
annotations: {
obs: [
{ name: "name", type: "string" },
{ name: "num_reads", type: "int32" },
{
name: "clusters",
type: "categorical",
categories=[ 99, 1, "unknown cluster" ]
},
{ name: "QScore", type: "float32" }
],
var: [
{ "name": "name", "type": "string" },
{ "name": "gene", "type": "string" }
]
}
In V01, our source, it looks like:
"schema": {
"CellName": {
"displayname": "Name",
"include": true,
"type": "string",
"variabletype": "categorical"
},
"Cluster_2d": {
"displayname": "Cluster2d",
"include": true,
"type": "string",
"variabletype": "categorical"
},
"ERCC_reads": {
"displayname": "ERCC Reads",
"include": true,
"type": "int",
"variabletype": "continuous"
},
...
}
Mapping between the two assumes:
- V01 only has schema for observations
- CellName is mapped to 'name'
- type conversion: float->float32, int->int32, string->string
*/
return {
annotations: {
obs: _.map(response.data.schema, (val, key) => {
const name = key === "CellName" ? "name" : key;
let { type } = val;
if (type === "int") {
type = "int32";
}
if (type === "float") {
type = "float32";
}
return {
name,
type
};
}),
var: [{ name: "name", type: "string" }]
}
};
}
function RESTv01ResponseToVarAnnotations(response) {
/*
v0.1 initialize response contains 'genes' - names of all genes
in order.
*/
return _.map(response.data.genes, (g, i) => ({ __varIndex__: i, name: g }));
}
function RESTv01ResponseToObsAnnotations(response) {
/*
v0.1 format for metadata:
metadata: [ { key: val, key: val, ... }, ... ]
Target format is essentially the same, except the CellName key becomes name.
*/
return _.map(response.data.metadata, (c, i) => ({
__obsIndex__: i,
name: c.CellName,
...c
}));
}
function RESTv01ResponseToLayout(obsAnnotations, response) {
/*
v0.1 format for the graph is:
[ [ 'cellname', x, y ], [ 'cellname', x, y, ], ... ]
NOTE XXX: this code does not assume any particular array ordering in the V0.1
response. But for Universe initial load, the layout will be in the same
order as annotations, so this extra work isn't really necessary.
*/
const obsAnnotationsByName = _.keyBy(obsAnnotations, "name");
const { graph } = response.data;
const layout = {
X: new Float32Array(graph.length),
Y: new Float32Array(graph.length)
};
for (let i = 0; i < graph.length; i += 1) {
const [name, x, y] = graph[i];
const anno = obsAnnotationsByName[name];
const idx = anno.__obsIndex__;
layout.X[idx] = x;
layout.Y[idx] = y;
}
return layout;
}
function finalize(universe) {
/* A bit of sanity checking! */
const { nObs, nVar } = universe;
if (
nObs !== universe.obsAnnotations.length ||
nObs !== universe.obsLayout.X.length ||
nObs !== universe.obsLayout.Y.length ||
nVar !== universe.varAnnotations.length
) {
throw new Error("Universe dimensionality mismatch - failed to load");
}
universe.obsNameToIndexMap = _.transform(
universe.obsAnnotations,
(acc, value, idx) => {
acc[value.name] = idx;
},
{}
);
universe.varNameToIndexMap = _.transform(
universe.varAnnotations,
(acc, value, idx) => {
acc[value.name] = idx;
},
{}
);
universe.finalized = true;
return universe;
}
function templateUniverse() {
/* default universe template */
const VarDataCacheLowWatermark = 32;
const VarDataCacheTTLMs = 1000;
return {
api: "0.1",
finalized: true, // XXX: may not be needed
nObs: 0,
nVar: 0,
schema: {},
/*
Annotations
*/
obsAnnotations: [] /* all obs annotations, by obs index */,
varAnnotations: [] /* all var annotations, by var index */,
obsNameToIndexMap: {} /* reverse map 'name' to index */,
varNameToIndexMap: {} /* reverse map 'name' to index */,
obsLayout: { X: [], Y: [] } /* xy layout */,
varDataCache: kvCache.create(
VarDataCacheLowWatermark,
VarDataCacheTTLMs
) /* cache of var data (expression) */
};
}
export function createUniverseFromRESTv01Response(initResponse, cellsResponse) {
/*
build & return universe from a REST 0.1 /init and /cells response
*/
const universe = templateUniverse();
/* extract information from init OTA response */
universe.schema = RESTv01ResponseToSchema(initResponse);
universe.varAnnotations = RESTv01ResponseToVarAnnotations(initResponse);
universe.nVar = universe.varAnnotations.length;
/* extract information fron cells REST json response */
/*
NOTE: this code *assumes* that cell order in data.metadata and data.graph
are the same. TODO: error checking.
*/
universe.obsAnnotations = RESTv01ResponseToObsAnnotations(cellsResponse);
universe.nObs = universe.obsAnnotations.length;
universe.obsLayout = RESTv01ResponseToLayout(
universe.obsAnnotations,
cellsResponse
);
return finalize(universe);
}
export function convertExpressionRESTv01ToObject(universe, response) {
/*
v0.1 ota looks like:
{
genes: [ "name1", "name2", ... ],
cells: [
{ cellname: 'cell1', e: [ 3, 4, n, x, y, ... ] },
...
]
}
convert expression to a simple Float32Array, and return
[ [geneName, array], [geneName, array], ... ]
*/
const result = {};
const { genes, cells } = response.data;
for (let idx = 0; idx < genes.length; idx += 1) {
const gene = genes[idx];
const data = new Float32Array(universe.nObs);
for (let c = 0; c < cells.length; c += 1) {
const obsIndex = universe.obsNameToIndexMap[cells[c].cellname];
data[obsIndex] = cells[c].e[idx];
}
result[gene] = data;
}
return result;
}
+315
View File
@@ -0,0 +1,315 @@
// jshint esversion: 6
import _ from "lodash";
import * as kvCache from "./keyvalcache";
/*
World is a subset of universe. Most code should use world, and should
(generally) not use Universe. World contains any per-obs or per-var data
that must be consisstent acorss the app when we view/manipulate subsets
of Universe.
Private API indicated by leading underscore in key name (eg, _foo). Anything else
is public.
World contains several public keys, obsAnnotations, and obsLayout, which are
arrays contianing information about an OBS in the same order/offset. In
other words, world.obsAnnotations[0] and world.obsLayout.X[0] refer to the same
obs/cell.
* obsAnnotations:
obsAnnotations will return an array of objects. Each object contains all annotation
values for a given observation/cell, keyed by annotation name, PLUS a key
'__cellId__', containing a REST API ID for this obs/cell (referred to as the
obsIndex in the REST 0.2 spec or cellIndex in the 0.1 spec.
Example: [ { __cellId__: 99, cluster: 'blue', numReads: 93933 } ]
NOTE: world.obsAnnotation should be identical to the old state.cells value,
EXCEPT that
* __cellIndex__ renamed to __obsIndex__
* __x__ and __y__ are now in world.obsLayout
* __color__ and __colorRBG__ should be moved to controls reducer
* obsLayout:
obsLayout will return an object containing two arrays, containing X and Y
coordinates respectively.
Example: { X: [ 0.33, 0.23, ... ], Y: [ 0.8, 0.777, ... ]}
* crossfilter - a crossfilter object across world.obsAnnotations
* dimensionMap - an object mapping annotation names to dimensions on
the crossfilter
*/
/*
Summary information for each annotation, keyed by annotation name.
Value will be an object, containing either 'range' or 'options' object,
depending on the annotation schema type (categorical or continuous).
Summarize for BOTH obs and var annotations. Result format:
{
obs: {
annotation_name: { ... },
...
},
var: {
annotation_name: { ... },
...
}
}
Example:
{
"Splice_sites_Annotated": {
"range": {
"min": 26,
"max": 1075869
}
},
"Selection": {
"options": {
"Astrocytes(HEPACAM)": 714,
"Endothelial(BSC)": 123,
"Oligodendrocytes(GC)": 294,
"Neurons(Thy1)": 685,
"Microglia(CD45)": 1108,
"Unpanned": 665
}
}
}
*/
function summarizeAnnotations(schema, obsAnnotations) {
/*
Build and return obs/var summary using any annotation in the schema
*/
const obsSummary = _(schema.annotations.obs)
.keyBy("name")
.mapValues(anno => {
const { name, type } = anno;
const continuous = type === "int32" || type === "float32";
if (!continuous) {
return {
options: _.countBy(obsAnnotations, name)
};
}
if (continuous) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
_.forEach(obsAnnotations, obs => {
const val = Number(obs[name]);
min = val < min ? val : min;
max = val > max ? val : max;
});
return { range: { min, max } };
}
throw new Error("incomprehensible schema");
})
.value();
const varSummary = {}; // TODO XXX - not currently used, so skip it
return {
obs: obsSummary,
var: varSummary
};
}
function templateWorld() {
const VarDataCacheLowWatermark = 32;
const VarDataCacheTTLMs = 1000;
return {
// map from universe obsIndex to world offset.
// Undefined / null indicates identity mapping.
worldObsIndex: null,
/* schema/version related */
api: null,
schema: null,
nObs: 0,
nVar: 0,
/* annotations */
obsAnnotations: null,
varAnnotations: null,
/* layout of graph */
obsLayout: null,
/* derived data summaries XXX: consider exploding in place */
summary: null,
varDataCache: kvCache.create(
VarDataCacheLowWatermark,
VarDataCacheTTLMs
) /* cache of var data (expression) */
};
}
export function createWorldFromEntireUniverse(universe) {
if (!universe.finalized) {
throw new Error("World can't be created from an partial Universe");
}
const world = templateWorld();
// map from the universe obsIndex to our world offset.
// undefined/null indicates identity map.
world.worldObsIndex = null;
/*
public interface follows
*/
/* Schema related */
world.api = universe.api;
world.schema = universe.schema;
world.nObs = universe.nObs;
world.nVar = universe.nVar;
/* annotations */
world.obsAnnotations = universe.obsAnnotations;
world.varAnnotations = universe.varAnnotations;
/* layout and display characteristics */
world.obsLayout = universe.obsLayout;
/* derived data & summaries */
world.summary = summarizeAnnotations(world.schema, world.obsAnnotations);
return world;
}
export function createWorldFromCurrentSelection(universe, world, crossfilter) {
const newWorld = templateWorld();
/* these don't change as only OBS are selected in our current implementation */
newWorld.api = world.api;
newWorld.nVar = world.nVar;
newWorld.schema = world.schema;
newWorld.varAnnotations = world.varAnnotations;
/*
Subset world from universe based upon world's current selection. Only those
fields which are subset by observation selection/filtering need to be updated.
*/
const numSelected = crossfilter.countFiltered();
/*
Create a world which is based upon current selection
*/
newWorld.nObs = numSelected;
newWorld.obsAnnotations = new Array(numSelected);
newWorld.obsLayout = {
X: new Array(numSelected),
Y: new Array(numSelected)
};
newWorld.worldObsIndex = new Array(universe.nObs);
for (let i = 0, sel = 0; i < world.nObs; i += 1) {
if (crossfilter.isElementFiltered(i)) {
newWorld.obsAnnotations[sel] = world.obsAnnotations[i];
newWorld.obsLayout.X[sel] = world.obsLayout.X[i];
newWorld.obsLayout.Y[sel] = world.obsLayout.Y[i];
sel += 1;
}
}
// build index to our world offset
newWorld.worldObsIndex.fill(-1); // default - aka unused
for (let i = 0; i < newWorld.nObs; i += 1) {
newWorld.worldObsIndex[newWorld.obsAnnotations[i].__obsIndex__] = i;
}
newWorld.summary = summarizeAnnotations(
newWorld.schema,
newWorld.obsAnnotations
);
return newWorld;
}
/*
Deduce the correct crossfilter dimension type from a metadata
schema description.
*/
function deduceDimensionType(attributes, fieldName) {
let dimensionType;
if (attributes.type === "string") {
dimensionType = "enum";
} else if (attributes.type === "int32") {
dimensionType = Int32Array;
} else if (attributes.type === "float32") {
dimensionType = Float32Array;
} else {
/*
Currently not supporting boolean and categorical types.
*/
console.error(
`Warning - REST API returned unknown metadata schema (${
attributes.type
}) for field ${fieldName}.`
);
// skip it - we don't know what to do with this type
}
return dimensionType;
}
export function createObsDimensionMap(crossfilter, world) {
/*
create and return a crossfilter dimension for every obs annotation
for which we have a supported type.
*/
const { schema, obsLayout, worldObsIndex } = world;
const dimensionMap = _.transform(
schema.annotations.obs,
(result, anno) => {
const dimType = deduceDimensionType(anno, anno.name);
if (dimType) {
result[anno.name] = crossfilter.dimension(r => r[anno.name], dimType);
} // else ignore the annotation
},
{}
);
/*
Add crossfilter dimensions allowing filtering on layout
*/
const worldIndex = worldObsIndex ? idx => worldObsIndex[idx] : idx => idx;
dimensionMap.x = crossfilter.dimension(
r => obsLayout.X[worldIndex(r.__obsIndex__)],
Float32Array
);
dimensionMap.y = crossfilter.dimension(
r => obsLayout.Y[worldIndex(r.__obsIndex__)],
Float32Array
);
return dimensionMap;
}
function worldEqUniverse(world, universe) {
return world.obsAnnotations === universe.obsAnnotations;
}
export function subsetVarData(world, universe, varData) {
// If world === universe, just return the entire varData array
if (worldEqUniverse(world, universe)) {
return varData;
}
const newVarData = new Float32Array(world.nObs);
for (let i = 0; i < world.nObs; i += 1) {
newVarData[i] = varData[world.obsAnnotations[i].__obsIndex__];
}
return newVarData;
}
+42 -44
View File
@@ -1,5 +1,5 @@
"use strict";
// jshint esversion: 6
/* eslint no-bitwise: "off" */
// BitArray is a 2D bitarray with size [length, nBitWidth].
// Each bit is referred to as a `dimension`. Dimensions may be
@@ -29,7 +29,7 @@ class BitArray {
// Fixed for the life of this object.
this.length = length;
// Bitarray width. width is always greater than 32*dimensionCount.
// Bitarray width. width is always greater than dimensionCount/32.
this.width = 1; // underlying number of 32 bit arrays
this.dimensionCount = 0; // num allocated dimensions
@@ -48,10 +48,16 @@ class BitArray {
//
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++;
const { bitarray, bitmask, length, width } = this;
for (let l = 0; l < length; l += 1) {
let dimensionsSet = 0;
for (let w = 0; w < width; w += 1) {
if (bitarray[w * length + l] === bitmask[w]) {
dimensionsSet += 1;
}
}
if (dimensionsSet === width) {
count += 1;
}
}
return count;
@@ -59,10 +65,11 @@ class BitArray {
// count trailing zeros - hard to do fast in JS!
// https://en.wikipedia.org/wiki/Find_first_set#CTZ
static ctz(v) {
static ctz(av) {
let c = 32;
let v = av;
v &= -v; // isolate lowest non-zero bit
if (v) c--;
if (v) c -= 1;
if (v & 0x0000ffff) c -= 16;
if (v & 0x00ff00ff) c -= 8;
if (v & 0x0f0f0f0f) c -= 4;
@@ -74,8 +81,7 @@ class BitArray {
// find a free dimension. Return undefined if none
_findFreeDimension() {
let dim;
for (let col = 0; col < this.width; col++) {
const bitmask = this.bitmask[col];
for (let col = 0; col < this.width; col += 1) {
const lowestZeroBit = ~this.bitmask[col] & -~this.bitmask[col];
if (lowestZeroBit) {
this.bitmask[col] |= lowestZeroBit;
@@ -92,7 +98,7 @@ class BitArray {
// if we did not find free dimension, expand the bitarray.
if (dim === undefined) {
this.width++;
this.width += 1;
const biggerBitArray = new Int32Array(this.width * this.length);
biggerBitArray.set(this.bitarray);
@@ -105,7 +111,7 @@ class BitArray {
dim = this._findFreeDimension();
}
this.dimensionCount++;
this.dimensionCount += 1;
return dim;
}
@@ -117,17 +123,15 @@ class BitArray {
this.deselectAll(dim);
const col = dim >>> 5;
this.bitmask[col] &= ~(1 << dim % 32);
this.dimensionCount--;
this.dimensionCount -= 1;
}
// return true if this index is selected in ALL dimensions.
//
isSelected(index) {
const width = this.width;
const length = this.length;
const bitarray = this.bitarray;
const { width, length, bitarray } = this;
for (let w = 0; w < width; w++) {
for (let w = 0; w < width; w += 1) {
const bitmask = this.bitmask[w];
if (!bitmask || bitarray[w * length + index] !== bitmask) return false;
}
@@ -140,20 +144,19 @@ class BitArray {
const ignoreOffset = dim >>> 5;
const ignoreMask = ~(1 << dim % 32);
const width = this.width;
const length = this.length;
const bitarray = this.bitarray;
const { width, length, bitarray } = this;
for (let w = 0; w < width; w++) {
for (let w = 0; w < width; w += 1) {
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;
}
} else if (bitmask && bitarray[w * length + index] !== bitmask) {
return false;
}
}
return true;
@@ -180,24 +183,20 @@ class BitArray {
// select all indices on dimension.
//
selectAll(dim) {
let col = dim >> 5;
const bitmask = this.bitmask[col];
const bitarray = this.bitarray;
const col = dim >> 5;
const one = 1 << dim % 32;
for (let i = col * this.length, len = i + this.length; i < len; i++) {
bitarray[i] |= one;
for (let i = col * this.length, len = i + this.length; i < len; i += 1) {
this.bitarray[i] |= one;
}
}
// deselect all indices on dimension
//
deselectAll(dim) {
let col = dim >> 5;
const bitmask = this.bitmask[col];
const bitarray = this.bitarray;
const col = dim >> 5;
const zero = ~(1 << dim % 32);
for (let i = col * this.length, len = i + this.length; i < len; i++) {
bitarray[i] &= zero;
for (let i = col * this.length, len = i + this.length; i < len; i += 1) {
this.bitarray[i] &= zero;
}
}
@@ -208,11 +207,10 @@ class BitArray {
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;
for (let i = first; i < last; i += 1) {
this.bitarray[offset + indirect[i]] |= one;
}
}
@@ -222,11 +220,10 @@ class BitArray {
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;
for (let i = first; i < last; i += 1) {
this.bitarray[offset + indirect[i]] &= zero;
}
}
@@ -237,13 +234,14 @@ class BitArray {
// 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++) {
for (let i = 0, len = this.length; i < len; i += 1) {
result[i] =
bitmask && bitarray[i] === bitmask ? selectedValue : deselectedValue;
bitmask && this.bitarray[i] === bitmask
? selectedValue
: deselectedValue;
}
} else {
for (let i = 0, len = this.length; i < len; i++) {
for (let i = 0, len = this.length; i < len; i += 1) {
result[i] = this.isSelected(i) ? selectedValue : deselectedValue;
}
}
+51 -57
View File
@@ -1,4 +1,3 @@
"use strict";
// jshint esversion: 6
/*
@@ -35,7 +34,6 @@ import {
fillRange,
lowerBound,
lowerBoundIndirect,
upperBound,
upperBoundIndirect
} from "./util";
@@ -57,6 +55,7 @@ class TypedCrossfilter {
// filters: array of { id, dimension }
this.filters = [];
this.selection = new BitArray(data.length);
this.updateTime = 0;
}
size() {
@@ -82,15 +81,15 @@ class TypedCrossfilter {
_freeDimension(id) {
this.selection.freeDimension(id);
this.filters = this.filters.filter(f => f._id != 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 { selection } = this;
const res = [];
for (let i = 0, len = this.data.length; i < len; i++) {
for (let i = 0, len = this.data.length; i < len; i += 1) {
if (selection.isSelected(i)) {
res.push(this.data[i]);
}
@@ -120,8 +119,8 @@ class TypedCrossfilter {
// and value array must be a TypedArray.
//
class ScalarDimension {
constructor(value, valueArrayType, crossfilter, id) {
this.crossfilter = crossfilter;
constructor(value, ValueArrayType, xfltr, id) {
this.crossfilter = xfltr;
this._id = id;
// current selection filter, expressed as PostiveIntervals.
@@ -130,7 +129,7 @@ class ScalarDimension {
// Create value array
const array = this._createValueArray(
value,
new valueArrayType(this.crossfilter.data.length)
new ValueArrayType(this.crossfilter.data.length)
);
this.value = array;
@@ -144,12 +143,13 @@ class ScalarDimension {
_createValueArray(value, array) {
// create dimension value array
const data = this.crossfilter.data;
const { data } = this.crossfilter;
const len = data.length;
for (let i = 0; i < len; i++) {
array[i] = value(data[i]);
const larray = array;
for (let i = 0; i < len; i += 1) {
larray[i] = value(data[i]);
}
return array;
return larray;
}
dispose() {
@@ -164,10 +164,10 @@ class ScalarDimension {
// Argument is an array of intervals indicating records newly selected/filtered
//
_updateFilters(newFilter) {
newFilter = PositiveIntervals.canonicalize(newFilter);
const cNewFilter = PositiveIntervals.canonicalize(newFilter);
const adds = PositiveIntervals.difference(newFilter, this.currentFilter);
const dels = PositiveIntervals.difference(this.currentFilter, newFilter);
const adds = PositiveIntervals.difference(cNewFilter, this.currentFilter);
const dels = PositiveIntervals.difference(this.currentFilter, cNewFilter);
this.crossfilter.filters.forEach(f =>
f.dim.groups.forEach(grp => grp._updateReduceDel(this, dels))
@@ -193,7 +193,8 @@ class ScalarDimension {
f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds))
);
this.currentFilter = newFilter;
this.currentFilter = cNewFilter;
this.crossfilter.updateTime += 1;
}
// filter by value - exact match
@@ -213,7 +214,7 @@ class ScalarDimension {
// filter by a set of values, eg. enum.
filterEnum(values) {
const newFilter = [];
for (let v = 0, len = values.length; v < len; v++) {
for (let v = 0, len = values.length; v < len; v += 1) {
const intv = [
lowerBoundIndirect(
this.value,
@@ -269,9 +270,8 @@ class ScalarDimension {
// 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 { data, selection } = this.crossfilter;
const { index } = this;
const len = index.length;
const ret = [];
let i = 0;
@@ -279,17 +279,17 @@ class ScalarDimension {
let found = 0;
// skip up to offset records
for (i = len - 1; 0 <= i && skip < offset; i--) {
for (i = len - 1; 0 <= i && skip < offset; i -= 1) {
if (selection.isSelected(index[i])) {
skip++;
skip += 1;
}
}
// grab up to k records
for (; 0 <= i && found < k; i--) {
for (; 0 <= i && found < k; i -= 1) {
if (selection.isSelected(index[i])) {
ret.push(data[index[i]]);
found++;
found += 1;
}
}
@@ -299,9 +299,8 @@ class ScalarDimension {
// 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 { data, selection } = this.crossfilter;
const { index } = this;
const len = index.length;
const ret = [];
let skip = 0;
@@ -309,17 +308,17 @@ class ScalarDimension {
let i = 0;
// skip up to offset records
for (i = 0; i < len && skip < offset; i++) {
for (i = 0; i < len && skip < offset; i += 1) {
if (selection.isSelected(index[i])) {
skip++;
skip += 1;
}
}
// grab up to k records
for (; i < len && found < k; i++) {
for (; i < len && found < k; i += 1) {
if (selection.isSelected(index[i])) {
ret.push(data[index[i]]);
found++;
found += 1;
}
}
@@ -341,18 +340,19 @@ class ScalarDimension {
// 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);
constructor(value, xfltr, id) {
super(value, Uint32Array, xfltr, id);
}
_createValueArray(value, array) {
const data = this.crossfilter.data;
const { data } = this.crossfilter;
const len = data.length;
const larray = array;
// create enumeration table - mapping between the value
// and the enum.
const s = new Set();
for (let i = 0; i < len; i++) {
for (let i = 0; i < len; i += 1) {
s.add(value(data[i]));
}
this.enumIndex = Array.from(s);
@@ -360,12 +360,12 @@ class EnumDimension extends ScalarDimension {
// create dimension value array
const enumLen = this.enumIndex.length;
for (let i = 0; i < len; i++) {
for (let i = 0; i < len; i += 1) {
const v = value(data[i]);
const e = lowerBound(this.enumIndex, v, 0, enumLen);
array[i] = e;
larray[i] = e;
}
return array;
return larray;
}
filterExact(value) {
@@ -415,7 +415,7 @@ class ScalarGroup {
// internal support function - map all dimension values to group values.
//
_map(groupValue, groupValueType, dimension) {
static _map(groupValue, GroupValueType, dimension) {
// groupValue is optional. Defaults to identity. Used to perform
// initial map operation.
//
@@ -424,8 +424,8 @@ class ScalarGroup {
const data = dimension.value;
const len = data.length;
const mapValue = new groupValueType(dimension.value.length);
for (let i = 0; i < len; i++) {
const mapValue = new GroupValueType(dimension.value.length);
for (let i = 0; i < len; i += 1) {
mapValue[i] = groupValue(data[i]);
}
return mapValue;
@@ -444,10 +444,9 @@ class ScalarGroup {
// 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;
const { data, selection } = this.dimension.crossfilter;
intv.forEach(rng => {
for (let r = rng[0]; r < rng[1]; r++) {
for (let r = rng[0]; r < rng[1]; r += 1) {
const i = dim.index[r];
if (selection.isSelectedIgnoringDim(i, this.dimension.id())) {
const group = this.groups[this.groupIndex[i]];
@@ -470,10 +469,9 @@ class ScalarGroup {
// 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;
const { data, selection } = this.dimension.crossfilter.selection;
intv.forEach(rng => {
for (let r = rng[0]; r < rng[1]; r++) {
for (let r = rng[0]; r < rng[1]; r += 1) {
const i = dim.index[r];
if (selection.isSelectedIgnoringDim(i, this.dimension.id())) {
const group = this.groups[this.groupIndex[i]];
@@ -487,8 +485,8 @@ class ScalarGroup {
// groups data.
//
_reduce() {
const dimension = this.dimension;
const data = dimension.crossfilter.data;
const { dimension } = this;
const { data } = dimension.crossfilter;
// Create groups
const groupNames = new Set(this.mapValue);
@@ -500,13 +498,13 @@ class ScalarGroup {
});
// Create groupIndex - index map between data record index and group index
for (let i = 0, len = this.mapValue.length; i < len; i++) {
for (let i = 0, len = this.mapValue.length; i < len; i += 1) {
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++) {
const { selection } = dimension.crossfilter;
for (let i = 0, len = data.length; i < len; i += 1) {
if (selection.isSelectedIgnoringDim(i, dimension.id())) {
const group = this.groups[this.groupIndex[i]];
group.value = this.reduceAdd(group.value, data[i]);
@@ -554,11 +552,7 @@ class ScalarGroup {
}
class EnumGroup extends ScalarGroup {
constructor(groupValue, groupValueType, dimension) {
super(groupValue, groupValueType, dimension);
}
_map(groupValue, groupValueType, dimension) {
static _map(groupValue, groupValueType, dimension) {
// groupValue is optional. Defaults to identity. Used to perform
// initial map operation.
//
@@ -1,4 +1,3 @@
"use strict";
// jshint esversion: 6
// Interval operations - very simple version of interval set relationship
@@ -21,11 +20,11 @@ class PositiveIntervals {
//
static canonicalize(A) {
if (A.length <= 1) return A;
let copy = A.slice();
const 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++) {
for (let i = 1, len = copy.length; i < len; i += 1) {
if (copy[i][0] > res[res.length - 1][1]) {
// non-overlapping, add to result
res.push(copy[i]);
@@ -45,12 +44,12 @@ class PositiveIntervals {
}
static _flatten(A, B) {
let points = []; /* point, A, start */
for (let a = 0; a < A.length; a++) {
const points = []; /* point, A, start */
for (let a = 0; a < A.length; a += 1) {
points.push([A[a][0], true, true]);
points.push([A[a][1], true, false]);
}
for (let b = 0; b < B.length; b++) {
for (let b = 0; b < B.length; b += 1) {
points.push([B[b][0], false, true]);
points.push([B[b][1], false, false]);
}
@@ -68,18 +67,16 @@ class PositiveIntervals {
return PositiveIntervals.canonicalize(A);
}
A = PositiveIntervals.canonicalize(A);
B = PositiveIntervals.canonicalize(B);
const cA = PositiveIntervals.canonicalize(A);
const cB = PositiveIntervals.canonicalize(B);
const points = PositiveIntervals._flatten(A, B);
const points = PositiveIntervals._flatten(cA, cB);
const res = [];
let aDepth = 0;
let depth = 0;
let intervalStart;
let prevPoint;
for (let i = 0; i < points.length; i++) {
for (let i = 0; i < points.length; i += 1) {
const p = points[i];
const before = depth;
const delta = p[2] ? 1 : -1;
depth += delta;
if (p[1]) aDepth += delta;
@@ -92,7 +89,6 @@ class PositiveIntervals {
intervalStart = undefined;
}
}
prevPoint = p[0];
}
// guaranteed to be in canonical form
return res;
@@ -106,16 +102,15 @@ class PositiveIntervals {
return [];
}
A = PositiveIntervals.canonicalize(A);
B = PositiveIntervals.canonicalize(B);
const cA = PositiveIntervals.canonicalize(A);
const cB = PositiveIntervals.canonicalize(B);
const points = PositiveIntervals._flatten(A, B);
const points = PositiveIntervals._flatten(cA, cB);
const res = [];
let depth = 0;
let intervalStart;
for (let i = 0; i < points.length; i++) {
for (let i = 0; i < points.length; i += 1) {
const p = points[i];
const before = depth;
depth += p[2] ? 1 : -1;
if (depth === 2) {
intervalStart = p[0];
+33 -24
View File
@@ -1,5 +1,5 @@
"use strict";
// jshint esversion: 6
/* eslint no-bitwise: "off" */
/*
Utility functions, private to this module.
@@ -9,10 +9,11 @@
// starting with `start`
//
export function fillRange(arr, start = 0) {
for (let i = 0, len = arr.length; i < len; i++) {
arr[i] = i + start;
const larr = arr;
for (let i = 0, len = larr.length; i < len; i += 1) {
larr[i] = i + start;
}
return arr;
return larr;
}
// Search for `value` in the sorted array `arr`, in the range [first, last).
@@ -31,31 +32,35 @@ export function fillRange(arr, start = 0) {
// a special-cased version for lining the indirection).
//
export function lowerBound(valueArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
while (lfirst < llast) {
const middle = (lfirst + llast) >>> 1;
if (valueArray[middle] < value) {
first = middle + 1;
lfirst = middle + 1;
} else {
last = middle;
llast = middle;
}
}
return first;
return lfirst;
}
// Inlined performance optimization - used to indirect through a sort map.
//
export function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
while (lfirst < llast) {
const middle = (lfirst + llast) >>> 1;
if (valueArray[indexArray[middle]] < value) {
first = middle + 1;
lfirst = middle + 1;
} else {
last = middle;
llast = middle;
}
}
return first;
return lfirst;
}
// Search for `value in the sorted array `arr`, in the range [first, last).
@@ -70,29 +75,33 @@ export function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
// Python: bisect.bisect_right()
//
export function upperBound(valueArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
while (lfirst < llast) {
const middle = (lfirst + llast) >>> 1;
if (valueArray[middle] > value) {
last = middle;
llast = middle;
} else {
first = middle + 1;
lfirst = middle + 1;
}
}
return first;
return lfirst;
}
// Inline performance optimization
//
export function upperBoundIndirect(valueArray, indexArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search
while (first < last) {
const middle = (first + last) >>> 1;
while (lfirst < llast) {
const middle = (lfirst + llast) >>> 1;
if (valueArray[indexArray[middle]] > value) {
last = middle;
llast = middle;
} else {
first = middle + 1;
lfirst = middle + 1;
}
}
return first;
return lfirst;
}