mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-24 15:28:11 +08:00
add context sensitivity to annotation "label" menu item (#1015)
* cache common results to reduce jank * add util function in support of annotation labelling * enable/disable annotation edit menus per issue 972; improve formatting * PR review requests
This commit is contained in:
@@ -132,6 +132,22 @@ export function setLabelByMask(df, colName, mask, label) {
|
||||
return ndf;
|
||||
}
|
||||
|
||||
export function allHaveLabelByMask(df, colName, label, mask) {
|
||||
// return true if all rows as indicated by mask have the colname set to label.
|
||||
// False if not.
|
||||
const col = df.col(colName);
|
||||
if (!col) return false;
|
||||
if (df.length !== mask.length)
|
||||
throw new InternalError("mismatch on mask length");
|
||||
|
||||
for (let i = 0; i < df.length; i += 1) {
|
||||
if (mask[i]) {
|
||||
if (col.iget(i) !== label) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function worldToUniverseMask(worldMask, worldObsAnnotations, nObs) {
|
||||
/*
|
||||
given world seleciton mask, return a selection mask for entire universe
|
||||
|
||||
@@ -23,7 +23,7 @@ class NotImplementedError extends Error {
|
||||
}
|
||||
|
||||
export default class ImmutableTypedCrossfilter {
|
||||
constructor(data, dimensions = {}, selectionCache = null) {
|
||||
constructor(data, dimensions = {}, selectionCache = {}) {
|
||||
/*
|
||||
Typically, parameter 'data' is one of:
|
||||
- Array of objects/records
|
||||
@@ -32,12 +32,14 @@ export default class ImmutableTypedCrossfilter {
|
||||
|
||||
Object field description:
|
||||
- data: reference to the array of records in the crossfilter
|
||||
- selectionBitArray: bit array containing the flatted selection state
|
||||
of all dimensions. This is lazily created and is effectively
|
||||
- selectionCache: object which may contains a bit array indicating
|
||||
the flatted selection state for all dimensions, plus other state
|
||||
summarizing the selection. This is lazily created and is effectively
|
||||
a perfomance cache. Methods which return a new crossfilter,
|
||||
such as select(), addDimention() and delDimension(), will pass
|
||||
the cache forward to the new object, as the typical "immutable API"
|
||||
usage pattern is to retain the new crossfilter and discard the old.
|
||||
If the cache object is empty, it will be rebuilt.
|
||||
- dimensions: contains each dimension and its current state:
|
||||
- id: bit offset in the cached bit array
|
||||
- dim: the dimension object
|
||||
@@ -45,7 +47,7 @@ export default class ImmutableTypedCrossfilter {
|
||||
- selection: the dimension's current selection
|
||||
*/
|
||||
this.data = data;
|
||||
this.selectionCache = selectionCache; /* BitArray */
|
||||
this.selectionCache = selectionCache; /* { BitArray, ... }*/
|
||||
this.dimensions = dimensions; /* name: { id, dim, name, selection } */
|
||||
Object.preventExtensions(this);
|
||||
}
|
||||
@@ -59,11 +61,9 @@ export default class ImmutableTypedCrossfilter {
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
return new ImmutableTypedCrossfilter(
|
||||
data,
|
||||
this.dimensions,
|
||||
this.selectionCache
|
||||
);
|
||||
const { selectionCache } = this;
|
||||
this.selectionCache = null;
|
||||
return new ImmutableTypedCrossfilter(data, this.dimensions, selectionCache);
|
||||
}
|
||||
|
||||
dimensionNames() {
|
||||
@@ -80,18 +80,19 @@ export default class ImmutableTypedCrossfilter {
|
||||
Add a new dimension to this crossfilter, of type DimensionType.
|
||||
Remainder of parameters are dimension-type-specific.
|
||||
*/
|
||||
const { data, selectionCache } = this;
|
||||
const { data } = this;
|
||||
const { bitArray } = this.selectionCache;
|
||||
|
||||
if (this.dimensions[name] !== undefined) {
|
||||
throw new Error(`Adding duplicate dimension name ${name}`);
|
||||
}
|
||||
|
||||
this.selectionCache = null; // pass ownership to new crossfilter
|
||||
this._clearSelectionCache();
|
||||
|
||||
let id;
|
||||
if (selectionCache) {
|
||||
id = selectionCache.allocDimension();
|
||||
selectionCache.selectAll(id);
|
||||
if (bitArray) {
|
||||
id = bitArray.allocDimension();
|
||||
bitArray.selectAll(id);
|
||||
}
|
||||
const DimensionType = DimTypes[type];
|
||||
const dim = new DimensionType(name, data, ...rest);
|
||||
@@ -105,11 +106,15 @@ export default class ImmutableTypedCrossfilter {
|
||||
selection: dim.select({ mode: "all" })
|
||||
}
|
||||
};
|
||||
return new ImmutableTypedCrossfilter(data, dimensions, selectionCache);
|
||||
|
||||
return new ImmutableTypedCrossfilter(data, dimensions, {
|
||||
bitArray: bitArray
|
||||
});
|
||||
}
|
||||
|
||||
delDimension(name) {
|
||||
const { data, selectionCache } = this;
|
||||
const { data } = this;
|
||||
const { bitArray } = this.selectionCache;
|
||||
const dimensions = { ...this.dimensions };
|
||||
if (dimensions[name] === undefined) {
|
||||
throw new ReferenceError(`Unable to delete unknown dimension ${name}`);
|
||||
@@ -117,11 +122,14 @@ export default class ImmutableTypedCrossfilter {
|
||||
|
||||
const { id } = dimensions[name];
|
||||
delete dimensions[name];
|
||||
this.selectionCache = null; // pass ownership to new crossfilter
|
||||
if (selectionCache) {
|
||||
selectionCache.freeDimension(id);
|
||||
this._clearSelectionCache();
|
||||
if (bitArray) {
|
||||
bitArray.freeDimension(id);
|
||||
}
|
||||
return new ImmutableTypedCrossfilter(data, dimensions, selectionCache);
|
||||
|
||||
return new ImmutableTypedCrossfilter(data, dimensions, {
|
||||
bitArray: bitArray
|
||||
});
|
||||
}
|
||||
|
||||
renameDimension(oldName, newName) {
|
||||
@@ -157,13 +165,13 @@ export default class ImmutableTypedCrossfilter {
|
||||
const newSelection = dim.select(spec);
|
||||
newSelection.ranges = PositiveIntervals.canonicalize(newSelection.ranges);
|
||||
dimensions[name] = { id, dim, name, selection: newSelection };
|
||||
ImmutableTypedCrossfilter._dimSelnHasUpdated(
|
||||
const newSelectionCache = ImmutableTypedCrossfilter._dimSelnHasUpdated(
|
||||
selectionCache,
|
||||
id,
|
||||
newSelection,
|
||||
oldSelection
|
||||
);
|
||||
return new ImmutableTypedCrossfilter(data, dimensions, selectionCache);
|
||||
return new ImmutableTypedCrossfilter(data, dimensions, newSelectionCache);
|
||||
}
|
||||
|
||||
static _dimSelnHasUpdated(selectionCache, id, newSeln, oldSeln) {
|
||||
@@ -172,81 +180,96 @@ export default class ImmutableTypedCrossfilter {
|
||||
bit array if it exists. If not, we will lazy create it when
|
||||
needed.
|
||||
*/
|
||||
if (selectionCache) {
|
||||
/*
|
||||
if (!selectionCache || !selectionCache.bitArray) return {};
|
||||
|
||||
const { bitArray } = selectionCache;
|
||||
|
||||
/*
|
||||
if both new and old selection use the same index, we can
|
||||
perform an incremental update. If the index changed, we have
|
||||
to do a suboptimal full deselect/select.
|
||||
*/
|
||||
let adds;
|
||||
let dels;
|
||||
if (newSeln.index === oldSeln.index) {
|
||||
adds = PositiveIntervals.difference(newSeln.ranges, oldSeln.ranges);
|
||||
dels = PositiveIntervals.difference(oldSeln.ranges, newSeln.ranges);
|
||||
} else {
|
||||
// console.log("suboptimal selection update - index changed");
|
||||
adds = newSeln.ranges;
|
||||
dels = oldSeln.ranges;
|
||||
}
|
||||
let adds;
|
||||
let dels;
|
||||
if (newSeln.index === oldSeln.index) {
|
||||
adds = PositiveIntervals.difference(newSeln.ranges, oldSeln.ranges);
|
||||
dels = PositiveIntervals.difference(oldSeln.ranges, newSeln.ranges);
|
||||
} else {
|
||||
// console.log("suboptimal selection update - index changed");
|
||||
adds = newSeln.ranges;
|
||||
dels = oldSeln.ranges;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
allow dimensions to return selected ranges in either dimension sort
|
||||
order (indirect via index), or in original record order.
|
||||
|
||||
If sort index exists in the dimension, assume sort ordered ranges.
|
||||
*/
|
||||
if (oldSeln.index) {
|
||||
dels.forEach(interval =>
|
||||
selectionCache.deselectIndirectFromRange(id, oldSeln.index, interval)
|
||||
);
|
||||
} else {
|
||||
dels.forEach(interval =>
|
||||
selectionCache.deselectFromRange(id, interval)
|
||||
);
|
||||
}
|
||||
|
||||
if (newSeln.index) {
|
||||
adds.forEach(interval =>
|
||||
selectionCache.selectIndirectFromRange(id, newSeln.index, interval)
|
||||
);
|
||||
} else {
|
||||
adds.forEach(interval => selectionCache.selectFromRange(id, interval));
|
||||
}
|
||||
if (oldSeln.index) {
|
||||
dels.forEach(interval =>
|
||||
bitArray.deselectIndirectFromRange(id, oldSeln.index, interval)
|
||||
);
|
||||
} else {
|
||||
dels.forEach(interval => bitArray.deselectFromRange(id, interval));
|
||||
}
|
||||
|
||||
if (newSeln.index) {
|
||||
adds.forEach(interval =>
|
||||
bitArray.selectIndirectFromRange(id, newSeln.index, interval)
|
||||
);
|
||||
} else {
|
||||
adds.forEach(interval => bitArray.selectFromRange(id, interval));
|
||||
}
|
||||
|
||||
return { bitArray };
|
||||
}
|
||||
|
||||
_getSelectionCache() {
|
||||
if (!this.selectionCache) {
|
||||
if (!this.selectionCache) this.selectionCache = {};
|
||||
|
||||
if (!this.selectionCache.bitArray) {
|
||||
// console.log("...rebuilding crossfilter cache...");
|
||||
const selectionCache = new BitArray(this.data.length);
|
||||
const bitArray = new BitArray(this.data.length);
|
||||
Object.keys(this.dimensions).forEach(name => {
|
||||
const { selection } = this.dimensions[name];
|
||||
const id = selectionCache.allocDimension();
|
||||
const id = bitArray.allocDimension();
|
||||
this.dimensions[name].id = id;
|
||||
const { ranges, index } = selection;
|
||||
ranges.forEach(range => {
|
||||
if (index) {
|
||||
selectionCache.selectIndirectFromRange(id, index, range);
|
||||
bitArray.selectIndirectFromRange(id, index, range);
|
||||
} else {
|
||||
selectionCache.selectFromRange(id, range);
|
||||
bitArray.selectFromRange(id, range);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.selectionCache = selectionCache;
|
||||
this.selectionCache.bitArray = bitArray;
|
||||
}
|
||||
return this.selectionCache;
|
||||
}
|
||||
|
||||
_clearSelectionCache() {
|
||||
this.selectionCache = {};
|
||||
return this.selectionCache;
|
||||
}
|
||||
|
||||
_setSelectionCache(vals = {}) {
|
||||
Object.assign(this.selectionCache, vals);
|
||||
return this.selectionCache;
|
||||
}
|
||||
|
||||
allSelected() {
|
||||
/*
|
||||
return array of all records currently selected by all dimensions
|
||||
*/
|
||||
const selectionCache = this._getSelectionCache();
|
||||
const { bitArray } = selectionCache;
|
||||
const { data } = this;
|
||||
if (Array.isArray(data)) {
|
||||
const res = [];
|
||||
for (let i = 0, len = data.length; i < len; i += 1) {
|
||||
if (selectionCache.isSelected(i)) {
|
||||
if (bitArray.isSelected(i)) {
|
||||
res.push(data[i]);
|
||||
}
|
||||
}
|
||||
@@ -261,11 +284,17 @@ export default class ImmutableTypedCrossfilter {
|
||||
return Uint8Array containing selection state (truthy/falsey) for each record.
|
||||
*/
|
||||
const selectionCache = this._getSelectionCache();
|
||||
return selectionCache.fillBySelection(
|
||||
let { allSelectedMask } = selectionCache;
|
||||
|
||||
if (allSelectedMask !== undefined) return allSelectedMask;
|
||||
|
||||
allSelectedMask = selectionCache.bitArray.fillBySelection(
|
||||
new Uint8Array(this.data.length),
|
||||
1,
|
||||
0
|
||||
);
|
||||
this._setSelectionCache({ allSelectedMask });
|
||||
return allSelectedMask;
|
||||
}
|
||||
|
||||
countSelected() {
|
||||
@@ -273,7 +302,13 @@ export default class ImmutableTypedCrossfilter {
|
||||
return number of records selected on all dimensions
|
||||
*/
|
||||
const selectionCache = this._getSelectionCache();
|
||||
return selectionCache.selectionCount();
|
||||
let { countSelected } = selectionCache;
|
||||
|
||||
if (countSelected !== undefined) return countSelected;
|
||||
|
||||
countSelected = selectionCache.bitArray.selectionCount();
|
||||
this._setSelectionCache({ countSelected });
|
||||
return countSelected;
|
||||
}
|
||||
|
||||
isElementSelected(i) {
|
||||
@@ -281,7 +316,7 @@ export default class ImmutableTypedCrossfilter {
|
||||
return truthy/falsey if this record is selected on all dimensions
|
||||
*/
|
||||
const selectionCache = this._getSelectionCache();
|
||||
return selectionCache.isSelected(i);
|
||||
return selectionCache.bitArray.isSelected(i);
|
||||
}
|
||||
|
||||
fillByIsSelected(array, selectedValue, deselectedValue) {
|
||||
@@ -289,7 +324,7 @@ export default class ImmutableTypedCrossfilter {
|
||||
fill array with one of two values, based upon selection state.
|
||||
*/
|
||||
const selectionCache = this._getSelectionCache();
|
||||
return selectionCache.fillBySelection(
|
||||
return selectionCache.bitArray.fillBySelection(
|
||||
array,
|
||||
selectedValue,
|
||||
deselectedValue
|
||||
|
||||
Reference in New Issue
Block a user