mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-22 05:18:12 +08:00
* 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:
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user