mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-22 06:08:11 +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;
|
||||
}
|
||||
Reference in New Issue
Block a user