Files
cellxgene/client/__tests__/util/stateManager/world.test.js
T
Bruce Martin 3660a6cc27 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
2019-09-18 07:33:41 -04:00

191 lines
5.7 KiB
JavaScript

import _ from "lodash";
import * as Universe from "../../../src/util/stateManager/universe";
import * as World from "../../../src/util/stateManager/world";
import * as Dataframe from "../../../src/util/dataframe";
import Crossfilter from "../../../src/util/typedCrossfilter";
import { DimTypes } from "../../../src/util/typedCrossfilter/crossfilter";
import * as REST from "./sampleResponses";
import {
obsAnnoDimensionName,
layoutDimensionName
} from "../../../src/util/nameCreators";
/*
Helper - creates universe, world, corssfilter and dimensionMap from
the default REST test response.
*/
const defaultBigBang = () => {
/* create unverse, world, crossfilter and dimensionMap */
/* create universe */
const universe = Universe.createUniverseFromResponse(
_.cloneDeep(REST.config),
_.cloneDeep(REST.schema),
_.cloneDeep(REST.annotationsObs),
_.cloneDeep(REST.annotationsVar),
_.cloneDeep(REST.layoutObs)
);
/* create world */
const world = World.createWorldFromEntireUniverse(universe);
/* create crossfilter */
const crossfilter = World.createObsDimensions(
new Crossfilter(world.obsAnnotations),
world,
REST.schema.schema.layout.obs[0].dims
);
return {
universe,
world,
crossfilter
};
};
describe("createWorldFromEntireUniverse", () => {
test("create from REST sample", () => {
const universe = Universe.createUniverseFromResponse(
_.cloneDeep(REST.config),
_.cloneDeep(REST.schema),
_.cloneDeep(REST.annotationsObs),
_.cloneDeep(REST.annotationsVar),
_.cloneDeep(REST.layoutObs)
);
expect(universe).toBeDefined();
const world = World.createWorldFromEntireUniverse(universe);
expect(world).toBeDefined();
expect(world).toMatchObject(
expect.objectContaining({
nObs: universe.nObs,
nVar: universe.nVar,
schema: universe.schema,
obsAnnotations: expect.any(Dataframe.Dataframe),
varAnnotations: expect.any(Dataframe.Dataframe),
obsLayout: expect.any(Dataframe.Dataframe),
varData: expect.any(Dataframe.Dataframe),
clipQuantiles: { min: 0, max: 1 },
unclipped: {
obsAnnotations: expect.any(Dataframe.Dataframe),
varData: expect.any(Dataframe.Dataframe)
}
})
);
});
});
describe("createWorldFromCurrentSelection", () => {
test("create from REST sample", () => {
const {
universe,
world: originalWorld,
crossfilter: originalCrossfilter
} = defaultBigBang();
/* mock a selection */
const crossfilter = originalCrossfilter
.select(obsAnnoDimensionName("field1"), { mode: "range", lo: 0, hi: 5 })
.select(obsAnnoDimensionName("field3"), {
mode: "exact",
values: [false]
});
/* create the world from the selection */
const world = World.createWorldBySelection(
universe,
originalWorld,
crossfilter
);
expect(world).toBeDefined();
expect(world.nObs).toEqual(crossfilter.countSelected());
/*
calculate expected values and match against result
*/
/* matchFilter must match the dimension filters above */
const matchFilter = (df, row) => {
const field1 = df.at(row, "field1");
const field3 = df.at(row, "field3");
return field1 >= 0 && field1 < 5 && !field3;
};
const matchingIndices = _()
.range(universe.nObs)
.filter(idx => matchFilter(universe.obsAnnotations, idx))
.value();
expect(world).toMatchObject(
expect.objectContaining({
nObs: matchingIndices.length,
nVar: universe.nVar,
schema: universe.schema,
clipQuantiles: { min: 0, max: 1 },
obsAnnotations: expect.any(Dataframe.Dataframe),
varAnnotations: expect.any(Dataframe.Dataframe),
obsLayout: expect.any(Dataframe.Dataframe),
varData: expect.any(Dataframe.Dataframe),
unclipped: {
obsAnnotations: expect.any(Dataframe.Dataframe),
varData: expect.any(Dataframe.Dataframe)
}
})
);
expect(world.obsAnnotations.rowIndex.keys()).toEqual(
new Int32Array(matchingIndices)
);
expect(world.obsAnnotations.colIndex.keys()).toEqual(
universe.obsAnnotations.colIndex.keys()
);
expect(world.obsLayout.rowIndex.keys()).toEqual(
new Int32Array(matchingIndices)
);
expect(world.obsLayout.colIndex.keys()).toEqual(
world.schema.layout.obs[0].dims
);
});
});
describe("createObsDimensionMap", () => {
test("when universe eq world", () => {
/*
check for:
- creates a dimension for all obsAnnotations, PLUS X/Y layout
- check that dimension typing is sane
*/
const { crossfilter } = defaultBigBang();
const annotationNames = _.map(
REST.schema.schema.annotations.obs.columns,
c => c.name
);
const obsIndexColName = REST.schema.schema.annotations.obs.index;
const schemaByObsName = _.keyBy(
REST.schema.schema.annotations.obs.columns,
"name"
);
expect(crossfilter).toBeDefined();
annotationNames.forEach(name => {
const dim = crossfilter.dimensions[obsAnnoDimensionName(name)];
if (name === obsIndexColName) {
expect(dim).toBeUndefined();
} else {
const { type } = schemaByObsName[name];
if (type === "string" || type === "boolean" || type === "categorical") {
expect(dim.dim).toBeInstanceOf(DimTypes.enum);
} else {
expect(dim.dim).toBeInstanceOf(DimTypes.scalar);
}
}
});
expect(
crossfilter.dimensions[layoutDimensionName("XY")].dim
).toBeInstanceOf(DimTypes.spatial);
});
});
describe("worldEqUniverse", () => {
const { universe, world } = defaultBigBang();
const result = World.worldEqUniverse(world, universe);
expect(result).toBe(true);
});