mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-17 05:47:58 +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:
@@ -17,13 +17,16 @@ import * as globals from "../../globals";
|
||||
import styles from "./categorical.css";
|
||||
import { Tooltip } from "@blueprintjs/core";
|
||||
|
||||
import { AnnotationsHelpers } from "../../util/stateManager";
|
||||
|
||||
@connect(state => ({
|
||||
categoricalSelection: state.categoricalSelection,
|
||||
annotations: state.annotations,
|
||||
colorScale: state.colors.scale,
|
||||
colorAccessor: state.colors.colorAccessor,
|
||||
schema: state.world?.schema,
|
||||
world: state.world
|
||||
world: state.world,
|
||||
crossfilter: state.crossfilter
|
||||
}))
|
||||
class CategoryValue extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -119,6 +122,7 @@ class CategoryValue extends React.Component {
|
||||
* world state
|
||||
* the color accessor (what is currently being colored by)
|
||||
* if this catagorical value's selection status has changed
|
||||
* the crossfilter (ie, global selection state)
|
||||
|
||||
If and only if true, update the component
|
||||
*/
|
||||
@@ -137,12 +141,14 @@ class CategoryValue extends React.Component {
|
||||
const worldChange = props.world !== nextProps.world;
|
||||
const colorAccessorChange = props.colorAccessor !== nextProps.colorAccessor;
|
||||
const annotationsChange = props.annotations !== nextProps.annotations;
|
||||
const crossfilterChange = props.crossfilter !== nextProps.crossfilter;
|
||||
|
||||
return (
|
||||
valueSelectionChange ||
|
||||
worldChange ||
|
||||
colorAccessorChange ||
|
||||
annotationsChange
|
||||
annotationsChange ||
|
||||
crossfilterChange
|
||||
);
|
||||
};
|
||||
|
||||
@@ -173,6 +179,34 @@ class CategoryValue extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
isAddCurrentSelectionDisabled(category, value) {
|
||||
/*
|
||||
disable "add current selection to label", if one of the following is true:
|
||||
1. no cells are selected
|
||||
2. all currently selected cells already have this label, on this category
|
||||
*/
|
||||
const { crossfilter, world } = this.props;
|
||||
|
||||
// 1. no cells selected?
|
||||
if (crossfilter.countSelected() === 0) {
|
||||
return true;
|
||||
}
|
||||
// 2. all selected cells already have the label
|
||||
const mask = crossfilter.allSelectedMask();
|
||||
if (
|
||||
AnnotationHelpers.allHaveLabelByMask(
|
||||
world.obsAnnotations,
|
||||
category,
|
||||
value,
|
||||
mask
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// else, don't disable
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
categoricalSelection,
|
||||
@@ -407,7 +441,26 @@ class CategoryValue extends React.Component {
|
||||
data-testclass="handleAddCurrentSelectionToThisLabel"
|
||||
data-testid={`handleAddCurrentSelectionToThisLabel-${metadataField}`}
|
||||
onClick={this.handleAddCurrentSelectionToThisLabel}
|
||||
text={`Re-label currently selected cells as ${displayString}`}
|
||||
text={
|
||||
<span>
|
||||
Re-label currently selected cells as
|
||||
<span
|
||||
style={{
|
||||
fontStyle:
|
||||
displayString ===
|
||||
globals.unassignedCategoryLabel
|
||||
? "italic"
|
||||
: "auto"
|
||||
}}
|
||||
>
|
||||
{` ${displayString}`}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
disabled={this.isAddCurrentSelectionDisabled(
|
||||
metadataField,
|
||||
value
|
||||
)}
|
||||
/>
|
||||
{displayString !== globals.unassignedCategoryLabel ? (
|
||||
<MenuItem
|
||||
@@ -425,7 +478,9 @@ class CategoryValue extends React.Component {
|
||||
data-testclass="handleDeleteValue"
|
||||
data-testid={`handleDeleteValue-${metadataField}`}
|
||||
onClick={this.handleDeleteValue}
|
||||
text="Delete this label, and reassign all cells to type 'unknown'"
|
||||
text={`Delete this label, and reassign all cells to type '${
|
||||
globals.unassignedCategoryLabel
|
||||
}'`}
|
||||
/>
|
||||
) : null}
|
||||
</Menu>
|
||||
|
||||
@@ -3,8 +3,8 @@ import _ from "lodash";
|
||||
import Crossfilter from "../util/typedCrossfilter";
|
||||
import {
|
||||
World,
|
||||
ControlsHelpers as CH,
|
||||
AnnotationsHelpers as AH
|
||||
ControlsHelpers,
|
||||
AnnotationsHelpers
|
||||
} from "../util/stateManager";
|
||||
import {
|
||||
layoutDimensionName,
|
||||
@@ -37,13 +37,16 @@ const CrossfilterReducerBase = (
|
||||
const { userDefinedGenes, diffexpGenes } = prevSharedState.controls;
|
||||
const { world } = nextSharedState;
|
||||
let { crossfilter } = prevSharedState.resetCache;
|
||||
crossfilter = CH.createGeneDimensions(
|
||||
crossfilter = ControlsHelpers.createGeneDimensions(
|
||||
userDefinedGenes,
|
||||
diffexpGenes,
|
||||
world,
|
||||
crossfilter
|
||||
);
|
||||
crossfilter = AH.createWritableAnnotationDimensions(world, crossfilter);
|
||||
crossfilter = AnnotationsHelpers.createWritableAnnotationDimensions(
|
||||
world,
|
||||
crossfilter
|
||||
);
|
||||
return crossfilter;
|
||||
}
|
||||
|
||||
@@ -57,7 +60,7 @@ const CrossfilterReducerBase = (
|
||||
world,
|
||||
layoutChoice.currentDimNames
|
||||
);
|
||||
crossfilter = CH.createGeneDimensions(
|
||||
crossfilter = ControlsHelpers.createGeneDimensions(
|
||||
userDefinedGenes,
|
||||
diffexpGenes,
|
||||
world,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { unassignedCategoryLabel } from "../globals";
|
||||
import {
|
||||
World,
|
||||
ControlsHelpers as CH,
|
||||
AnnotationsHelpers as AH
|
||||
ControlsHelpers,
|
||||
AnnotationsHelpers
|
||||
} from "../util/stateManager";
|
||||
|
||||
const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
@@ -35,7 +35,7 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
Object.keys(action.expressionData)
|
||||
)
|
||||
];
|
||||
varData = CH.pruneVarDataCache(varData, allTheGenesWeNeed);
|
||||
varData = ControlsHelpers.pruneVarDataCache(varData, allTheGenesWeNeed);
|
||||
return { ...state, varData };
|
||||
}
|
||||
|
||||
@@ -63,9 +63,14 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
let data;
|
||||
if (categoryToDuplicate) {
|
||||
/* duplicate the named annotation */
|
||||
schema = AH.dupObsAnnoSchema(state.schema, categoryToDuplicate, name, {
|
||||
writable: true
|
||||
});
|
||||
schema = AnnotationsHelpers.dupObsAnnoSchema(
|
||||
state.schema,
|
||||
categoryToDuplicate,
|
||||
name,
|
||||
{
|
||||
writable: true
|
||||
}
|
||||
);
|
||||
/* if we are duplicating a non-writable annotation, it may not have an unassigned category */
|
||||
const s = schema.annotations.obsByName[categoryToDuplicate];
|
||||
if (s.categories.indexOf(unassignedCategoryLabel) === -1) {
|
||||
@@ -75,7 +80,7 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
} else {
|
||||
/* else, all are unassined */
|
||||
const categories = [unassignedCategoryLabel];
|
||||
schema = AH.addObsAnnoSchema(state.schema, name, {
|
||||
schema = AnnotationsHelpers.addObsAnnoSchema(state.schema, name, {
|
||||
name,
|
||||
categories,
|
||||
type: "categorical",
|
||||
@@ -92,7 +97,7 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
/* change the name of an obs annotation category */
|
||||
const name = action.metadataField;
|
||||
const newName = action.newCategoryText;
|
||||
if (!AH.isUserAnnotation(state, name))
|
||||
if (!AnnotationsHelpers.isUserAnnotation(state, name))
|
||||
throw new Error("unable to edit read-only annotation");
|
||||
if (typeof newName !== "string" || newName.length === 0)
|
||||
throw new Error("user annotations require string name");
|
||||
@@ -101,8 +106,8 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
...state.schema.annotations.obsByName[name],
|
||||
name: newName
|
||||
};
|
||||
const schema = AH.addObsAnnoSchema(
|
||||
AH.removeObsAnnoSchema(state.schema, name),
|
||||
const schema = AnnotationsHelpers.addObsAnnoSchema(
|
||||
AnnotationsHelpers.removeObsAnnoSchema(state.schema, name),
|
||||
newName,
|
||||
colSchema
|
||||
);
|
||||
@@ -113,10 +118,10 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
case "annotation: delete category": {
|
||||
/* delete annotation category from schema and obsAnnotations */
|
||||
const name = action.metadataField;
|
||||
if (!AH.isUserAnnotation(state, name))
|
||||
if (!AnnotationsHelpers.isUserAnnotation(state, name))
|
||||
throw new Error("unable to delete read-only annotation");
|
||||
|
||||
const schema = AH.removeObsAnnoSchema(state.schema, name);
|
||||
const schema = AnnotationsHelpers.removeObsAnnoSchema(state.schema, name);
|
||||
const obsAnnotations = state.obsAnnotations.dropCol(name);
|
||||
return { ...state, schema, obsAnnotations };
|
||||
}
|
||||
@@ -124,7 +129,7 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
case "annotation: add new label to category": {
|
||||
const annotationName = action.metadataField;
|
||||
const newLabelName = action.newLabelText;
|
||||
if (!AH.isUserAnnotation(state, annotationName))
|
||||
if (!AnnotationsHelpers.isUserAnnotation(state, annotationName))
|
||||
throw new Error("unable to modify read-only annotation");
|
||||
if (typeof newLabelName !== "string" || newLabelName.length === 0)
|
||||
throw new Error(
|
||||
@@ -132,7 +137,7 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
);
|
||||
|
||||
/* add the new label to the annotation */
|
||||
const schema = AH.addObsAnnoCategory(
|
||||
const schema = AnnotationsHelpers.addObsAnnoCategory(
|
||||
state.schema,
|
||||
annotationName,
|
||||
newLabelName
|
||||
@@ -144,7 +149,7 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
const annotationName = action.metadataField;
|
||||
const oldLabelName = action.label;
|
||||
const newLabelName = action.editedLabel;
|
||||
if (!AH.isUserAnnotation(state, annotationName))
|
||||
if (!AnnotationsHelpers.isUserAnnotation(state, annotationName))
|
||||
throw new Error("unable to modify read-only annotation");
|
||||
if (typeof newLabelName !== "string" || newLabelName.length === 0)
|
||||
throw new Error(
|
||||
@@ -152,14 +157,18 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
);
|
||||
|
||||
/* remove old label, add new label */
|
||||
const schema = AH.addObsAnnoCategory(
|
||||
AH.removeObsAnnoCategory(state.schema, annotationName, oldLabelName),
|
||||
const schema = AnnotationsHelpers.addObsAnnoCategory(
|
||||
AnnotationsHelpers.removeObsAnnoCategory(
|
||||
state.schema,
|
||||
annotationName,
|
||||
oldLabelName
|
||||
),
|
||||
annotationName,
|
||||
newLabelName
|
||||
);
|
||||
|
||||
/* change all values in obsAnnotation */
|
||||
const obsAnnotations = AH.setLabelByValue(
|
||||
const obsAnnotations = AnnotationsHelpers.setLabelByValue(
|
||||
state.obsAnnotations,
|
||||
annotationName,
|
||||
oldLabelName,
|
||||
@@ -173,20 +182,20 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
/* delete the label from the annotation, and set all cells with this value to unassigned */
|
||||
const annotationName = action.metadataField;
|
||||
const labelName = action.label;
|
||||
if (!AH.isUserAnnotation(state, annotationName))
|
||||
if (!AnnotationsHelpers.isUserAnnotation(state, annotationName))
|
||||
throw new Error("unable to modify read-only annotation");
|
||||
if (labelName === unassignedCategoryLabel)
|
||||
throw new Error("may not remove the unassigned label");
|
||||
|
||||
/* remove the category from the schema */
|
||||
const schema = AH.removeObsAnnoCategory(
|
||||
const schema = AnnotationsHelpers.removeObsAnnoCategory(
|
||||
state.schema,
|
||||
annotationName,
|
||||
labelName
|
||||
);
|
||||
|
||||
/* set all values to unassigned in obsAnnotations */
|
||||
const obsAnnotations = AH.setLabelByValue(
|
||||
const obsAnnotations = AnnotationsHelpers.setLabelByValue(
|
||||
state.obsAnnotations,
|
||||
annotationName,
|
||||
labelName,
|
||||
@@ -207,8 +216,12 @@ const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
||||
const worldMask = crossfilter.allSelectedMask();
|
||||
const mask = World.worldEqUniverse(world, state)
|
||||
? worldMask
|
||||
: AH.worldToUniverseMask(worldMask, world.obsAnnotations, state.nObs);
|
||||
const obsAnnotations = AH.setLabelByMask(
|
||||
: AnnotationsHelpers.worldToUniverseMask(
|
||||
worldMask,
|
||||
world.obsAnnotations,
|
||||
state.nObs
|
||||
);
|
||||
const obsAnnotations = AnnotationsHelpers.setLabelByMask(
|
||||
state.obsAnnotations,
|
||||
metadataField,
|
||||
mask,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { unassignedCategoryLabel } from "../globals";
|
||||
import {
|
||||
World,
|
||||
ControlsHelpers as CH,
|
||||
AnnotationsHelpers as AH
|
||||
ControlsHelpers,
|
||||
AnnotationsHelpers
|
||||
} from "../util/stateManager";
|
||||
import clip from "../util/clip";
|
||||
import quantile from "../util/quantile";
|
||||
@@ -92,7 +92,7 @@ const WorldReducer = (
|
||||
Object.keys(action.expressionData)
|
||||
)
|
||||
];
|
||||
unclippedVarData = CH.pruneVarDataCache(
|
||||
unclippedVarData = ControlsHelpers.pruneVarDataCache(
|
||||
unclippedVarData,
|
||||
allTheGenesWeNeed
|
||||
);
|
||||
@@ -208,7 +208,7 @@ const WorldReducer = (
|
||||
/* set all values to to new label */
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: AH.setLabelByValue(
|
||||
obsAnnotations: AnnotationsHelpers.setLabelByValue(
|
||||
state.unclipped.obsAnnotations,
|
||||
metadataField,
|
||||
oldLabelName,
|
||||
@@ -229,7 +229,7 @@ const WorldReducer = (
|
||||
/* set all values to unassigned in obsAnnotations */
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: AH.setLabelByValue(
|
||||
obsAnnotations: AnnotationsHelpers.setLabelByValue(
|
||||
state.unclipped.obsAnnotations,
|
||||
metadataField,
|
||||
label,
|
||||
@@ -249,7 +249,7 @@ const WorldReducer = (
|
||||
const mask = crossfilter.allSelectedMask();
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: AH.setLabelByMask(
|
||||
obsAnnotations: AnnotationsHelpers.setLabelByMask(
|
||||
state.unclipped.obsAnnotations,
|
||||
metadataField,
|
||||
mask,
|
||||
|
||||
@@ -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