mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-06 11:08:12 +08:00
Experimental - manual annotations (#837)
* icons, partway * redux for values * onChange * cancel * annotations lifecycle for category names * copy categorical * edit category * add Dataframe.withColsFrom * render user annotations; default add/delete annotation category * add label name to actions * category name edit * error checking improvements * change schema field isUserAnnotation to writable * always have an unassigned label; implement delete label * implement add new label and edit label name * label current cell selection * fix select exact bug in crossfilter * clean up categorical reducer * fix tests * remove debugging printf * implement subset/reset for user annotations * undo redo support for user annotations * remove duplicate button from categories * add modal * remove obsolete duplicate annotation reducers * remove old debugging printf * connect modal to annotation create and dup * initial full-stack wiring * finish up end-to-end wiring * fix existing unit tests * fix pytests to match new schema API * remove debugging printfs * add label file rotation * remove obsolete comment * add fbs encode/decode tests * add tests for writable annotations * simplify code * fix hashing bug with FBS encoding * lint * fix smoke tests * improve error checking in Dataframe.withColsFrom * add unit test for Dataframe.withColsFrom * add unit test for Dataframe.columns and Dataframe.renameCol * fix bug in FBS encode, add better error checks, refactor * add FBS encode/decode test * add clarifying comment * clean up action type names; fix state inconsistency in crossfilter update * change autosave timer to 2.5sec * sort categorical metadata render order so it remains consistent * add temporary autogenerated label for add-new-label operation * fix hover-over label menu interference with cell highlighting * remove debugging code * add missing reducer cases & fix typo * make dataframe memoize more general purpose * add dev mode for annos * fix error on select duplicate * handle zero occupancy categories * correctly maintain unclipped AND clipped world * correctly handle zero length FBS matrix and label files * ensure all writable categorical schema contains an unassigned category * handle case where building occupancy stack for category with no members * dialog for creating label, disable button if duplicate or empty * visually separate writeable * edit category * fix edit category name * remove debugging code * fix edit annotation label * visually define unassigned, change options * Pull in requirements.txt from `master` * label currently selected cells * duplicate label * lint * fix pytest merge issues * rename --label-file to --experimental-label-file * remove debugging console log * spelling error fix; fix bug found in PR review. * lint
This commit is contained in:
committed by
Colin Megill
parent
ab2c423006
commit
3660a6cc27
@@ -1,7 +1,22 @@
|
||||
import { World, ControlsHelpers } from "../util/stateManager";
|
||||
import { unassignedCategoryLabel } from "../globals";
|
||||
import {
|
||||
World,
|
||||
ControlsHelpers as CH,
|
||||
AnnotationsHelpers as AH
|
||||
} from "../util/stateManager";
|
||||
import clip from "../util/clip";
|
||||
import quantile from "../util/quantile";
|
||||
|
||||
/*
|
||||
important note: much of this code assumes that wriable (user) annotations
|
||||
will NOT contain scalar data (ie, will only contain categorical labelled
|
||||
data), and therefore will never need to be clipped. Put another way, it
|
||||
assumes that for these annotations, the clipped & unclipped data is equal.
|
||||
|
||||
If we ever start allowing user editable scalar data, this assumption will
|
||||
need to be revisited.
|
||||
*/
|
||||
|
||||
const WorldReducer = (
|
||||
state = null,
|
||||
action,
|
||||
@@ -9,16 +24,13 @@ const WorldReducer = (
|
||||
prevSharedState
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "initial data load complete (universe exists)": {
|
||||
case "initial data load complete (universe exists)":
|
||||
case "reset World to eq Universe": {
|
||||
const { universe } = nextSharedState;
|
||||
const world = World.createWorldFromEntireUniverse(universe);
|
||||
return world;
|
||||
}
|
||||
|
||||
case "reset World to eq Universe": {
|
||||
return prevSharedState.resetCache.world;
|
||||
}
|
||||
|
||||
case "set World to current selection": {
|
||||
/* Set viewable world to be the currently selected data */
|
||||
const world = World.createWorldBySelection(
|
||||
@@ -80,7 +92,7 @@ const WorldReducer = (
|
||||
Object.keys(action.expressionData)
|
||||
)
|
||||
];
|
||||
unclippedVarData = ControlsHelpers.pruneVarDataCache(
|
||||
unclippedVarData = CH.pruneVarDataCache(
|
||||
unclippedVarData,
|
||||
allTheGenesWeNeed
|
||||
);
|
||||
@@ -122,6 +134,135 @@ const WorldReducer = (
|
||||
};
|
||||
}
|
||||
|
||||
case "annotation: create category": {
|
||||
const name = action.data;
|
||||
const { universe } = nextSharedState;
|
||||
const { schema } = universe;
|
||||
|
||||
/*
|
||||
if world !== universe, we have to subset the newly created annotation,
|
||||
else, just use it as is.
|
||||
*/
|
||||
let newAnnotation = null;
|
||||
if (!World.worldEqUniverse(state, universe)) {
|
||||
newAnnotation = universe.obsAnnotations
|
||||
.subset(state.obsAnnotations.rowIndex.keys(), [name], null)
|
||||
.icol(0)
|
||||
.asArray();
|
||||
} else {
|
||||
newAnnotation = universe.obsAnnotations.col(name).asArray();
|
||||
}
|
||||
const obsAnnotations = state.obsAnnotations.withCol(
|
||||
name,
|
||||
newAnnotation,
|
||||
state.obsAnnotations.rowIndex
|
||||
);
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: state.unclipped.obsAnnotations.withCol(
|
||||
name,
|
||||
newAnnotation,
|
||||
state.unclipped.obsAnnotations.rowIndex
|
||||
)
|
||||
};
|
||||
return { ...state, schema, obsAnnotations, unclipped };
|
||||
}
|
||||
|
||||
case "annotation: category edited": {
|
||||
/* change the name of an obs annotation */
|
||||
const name = action.metadataField;
|
||||
const newName = action.newCategoryText;
|
||||
const { schema } = nextSharedState.universe;
|
||||
const obsAnnotations = state.obsAnnotations.renameCol(name, newName);
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: state.unclipped.obsAnnotations.renameCol(name, newName)
|
||||
};
|
||||
return { ...state, schema, obsAnnotations, unclipped };
|
||||
}
|
||||
|
||||
case "annotation: delete category": {
|
||||
/* remove a category from obs annotation */
|
||||
const { schema } = nextSharedState.universe;
|
||||
const name = action.metadataField;
|
||||
const obsAnnotations = state.obsAnnotations.dropCol(name);
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: state.unclipped.obsAnnotations.dropCol(name)
|
||||
};
|
||||
return { ...state, schema, obsAnnotations, unclipped };
|
||||
}
|
||||
|
||||
case "annotation: add new label to category": {
|
||||
/* add a new label to the schema - schema updated by universe reducer, we just need to note it */
|
||||
const { schema } = nextSharedState.universe;
|
||||
return { ...state, schema };
|
||||
}
|
||||
|
||||
case "annotation: label edited": {
|
||||
const { schema } = nextSharedState.universe;
|
||||
const { metadataField } = action;
|
||||
const oldLabelName = action.label;
|
||||
const newLabelName = action.editedLabel;
|
||||
|
||||
/* set all values to to new label */
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: AH.setLabelByValue(
|
||||
state.unclipped.obsAnnotations,
|
||||
metadataField,
|
||||
oldLabelName,
|
||||
newLabelName
|
||||
)
|
||||
};
|
||||
const obsAnnotations = state.obsAnnotations.replaceColData(
|
||||
metadataField,
|
||||
unclipped.obsAnnotations.col(metadataField).asArray()
|
||||
);
|
||||
return { ...state, schema, obsAnnotations, unclipped };
|
||||
}
|
||||
|
||||
case "annotation: delete label": {
|
||||
const { schema } = nextSharedState.universe;
|
||||
const { label, metadataField } = action;
|
||||
|
||||
/* set all values to unassigned in obsAnnotations */
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: AH.setLabelByValue(
|
||||
state.unclipped.obsAnnotations,
|
||||
metadataField,
|
||||
label,
|
||||
unassignedCategoryLabel
|
||||
)
|
||||
};
|
||||
const obsAnnotations = state.obsAnnotations.replaceColData(
|
||||
metadataField,
|
||||
unclipped.obsAnnotations.col(metadataField).asArray()
|
||||
);
|
||||
return { ...state, schema, obsAnnotations, unclipped };
|
||||
}
|
||||
|
||||
case "annotation: label current cell selection": {
|
||||
const { metadataField, label } = action;
|
||||
const { crossfilter } = prevSharedState;
|
||||
const mask = crossfilter.allSelectedMask();
|
||||
const unclipped = {
|
||||
...state.unclipped,
|
||||
obsAnnotations: AH.setLabelByMask(
|
||||
state.unclipped.obsAnnotations,
|
||||
metadataField,
|
||||
mask,
|
||||
label
|
||||
)
|
||||
};
|
||||
const obsAnnotations = state.obsAnnotations.replaceColData(
|
||||
metadataField,
|
||||
unclipped.obsAnnotations.col(metadataField).asArray()
|
||||
);
|
||||
return { ...state, obsAnnotations, unclipped };
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user