mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-28 09:58:12 +08:00
* 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
169 lines
5.1 KiB
JavaScript
169 lines
5.1 KiB
JavaScript
/*
|
|
Helper functions for user-editable nnotations state management.
|
|
See also reducers/annotations.js
|
|
*/
|
|
import { unassignedCategoryLabel } from "../../globals";
|
|
import * as SchemaHelpers from "./schemaHelpers";
|
|
import { obsAnnoDimensionName } from "../nameCreators";
|
|
|
|
/*
|
|
There are a number of state constraints assumed throughout the
|
|
application:
|
|
- all obs annotations are in {world|universe}.obsAnnotations,
|
|
regardless of whether or not they are user editable.
|
|
- the {world|universe}.schema is always up to date and matches
|
|
the data
|
|
- the schema flag `writable` correctly indicates whether
|
|
the annotation is editable/mutable.
|
|
|
|
In addition, the current state management only allows for
|
|
categorical annotations to be writable.
|
|
*/
|
|
|
|
export function isCategoricalAnnotation(schema, name) {
|
|
/* we treat any string, categorical or boolean as a categorical */
|
|
const { type } = schema.annotations.obsByName[name];
|
|
return type === "string" || type === "boolean" || type === "categorical";
|
|
}
|
|
|
|
export function isContinuousAnnotation(schema, name) {
|
|
return !isCategoricalAnnotation(schema, name);
|
|
}
|
|
|
|
function _isUserAnnotation(schema, name) {
|
|
return schema.annotations.obsByName[name]?.writable;
|
|
}
|
|
|
|
export function isUserAnnotation(worldOrUniverse, name) {
|
|
return _isUserAnnotation(worldOrUniverse.schema, name);
|
|
}
|
|
|
|
export function removeObsAnnoSchema(schema, name) {
|
|
/*
|
|
remove named annotation from obs annotation schema
|
|
*/
|
|
|
|
/* only remove if it exists and is a user annotation */
|
|
if (!_isUserAnnotation(schema, name))
|
|
throw new Error("removing non-user-defined schema");
|
|
return SchemaHelpers.removeObsAnnoColumn(schema, name);
|
|
}
|
|
|
|
export function addObsAnnoSchema(schema, name, colSchema) {
|
|
/*
|
|
add a categorical type to the obs annotation schema
|
|
*/
|
|
|
|
/* collision detection */
|
|
if (schema.annotations.obs.columns.some(v => v.name === name))
|
|
throw Error("annotations may not contain duplicate category names");
|
|
if (name !== colSchema.name) throw Error("column schema does not match");
|
|
return SchemaHelpers.addObsAnnoColumn(schema, name, colSchema);
|
|
}
|
|
|
|
export function dupObsAnnoSchema(schema, sourceName, dupName, defaultSchema) {
|
|
/*
|
|
duplicate the obs annotation `sourceName` schema, but with the name `dupName`
|
|
*/
|
|
const colSchema = {
|
|
...schema.annotations.obsByName[sourceName],
|
|
...defaultSchema,
|
|
name: dupName
|
|
};
|
|
/* existance check */
|
|
if (!colSchema) throw Error("source annotation does not exist");
|
|
/* collision detection */
|
|
if (schema.annotations.obs.columns.some(v => v.name === dupName))
|
|
throw Error("annotations may not contain duplicate category names");
|
|
return SchemaHelpers.addObsAnnoColumn(schema, dupName, colSchema);
|
|
}
|
|
|
|
export function removeObsAnnoCategory(schema, name, category) {
|
|
/* don't allow deletion of unassigned category on writable annotations */
|
|
|
|
if (!_isUserAnnotation(schema, name))
|
|
throw new Error("unable to modify read-only schema");
|
|
if (category === unassignedCategoryLabel)
|
|
throw new Error("may not remove unassigned category label");
|
|
|
|
return SchemaHelpers.removeObsAnnoCategory(schema, name, category);
|
|
}
|
|
|
|
export function addObsAnnoCategory(schema, name, category) {
|
|
if (!_isUserAnnotation(schema, name))
|
|
throw new Error("unable to modify read-only schema");
|
|
|
|
return SchemaHelpers.addObsAnnoCategory(schema, name, category);
|
|
}
|
|
|
|
export function setLabelByValue(df, colName, fromLabel, toLabel) {
|
|
/*
|
|
in the dataframe column `colName`, set any value of `fromLabel` to `toLabel`
|
|
*/
|
|
const keys = df.colIndex.keys();
|
|
const ndf = df.mapColumns((col, colIdx) => {
|
|
if (colName !== keys[colIdx]) return col;
|
|
|
|
/* clone data and return it. */
|
|
const newCol = col.slice();
|
|
for (let i = 0, l = newCol.length; i < l; i += 1) {
|
|
if (newCol[i] === fromLabel) newCol[i] = toLabel;
|
|
}
|
|
return newCol;
|
|
});
|
|
return ndf;
|
|
}
|
|
|
|
export function setLabelByMask(df, colName, mask, label) {
|
|
/*
|
|
in the dataframe column `colName`, set the masked rows to 'label'
|
|
*/
|
|
const keys = df.colIndex.keys();
|
|
const ndf = df.mapColumns((col, colIdx) => {
|
|
if (colName !== keys[colIdx]) return col;
|
|
|
|
/* clone data and return it. */
|
|
const newCol = col.slice();
|
|
for (let i = 0, l = newCol.length; i < l; i += 1) {
|
|
if (mask[i]) newCol[i] = label;
|
|
}
|
|
return newCol;
|
|
});
|
|
return ndf;
|
|
}
|
|
|
|
export function worldToUniverseMask(worldMask, worldObsAnnotations, nObs) {
|
|
/*
|
|
given world seleciton mask, return a selection mask for entire universe
|
|
that has same selection state.
|
|
*/
|
|
const mask = new Uint8Array(nObs);
|
|
const { rowIndex } = worldObsAnnotations;
|
|
|
|
for (let i = 0, l = worldMask.length; i < l; i += 1) {
|
|
if (worldMask[i]) {
|
|
const label = rowIndex.getLabel(i);
|
|
mask[label] = 1;
|
|
}
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
export function createWritableAnnotationDimensions(world, crossfilter) {
|
|
const { obsAnnotations, schema } = world;
|
|
const writableAnnotations = schema.annotations.obs.columns
|
|
.filter(s => s.writable)
|
|
.map(s => s.name);
|
|
|
|
crossfilter = writableAnnotations.reduce((xflt, anno) => {
|
|
const dimName = obsAnnoDimensionName(anno);
|
|
if (xflt.hasDimension(dimName)) xflt = xflt.delDimension(dimName);
|
|
return xflt.addDimension(
|
|
dimName,
|
|
"enum",
|
|
obsAnnotations.col(anno).asArray()
|
|
);
|
|
}, crossfilter);
|
|
return crossfilter;
|
|
}
|