Files
cellxgene/client/__tests__/util/stateManager/world.test.js
T
Sidney Bell a08e19bbd0 Clip continuous values based on percentile cutoffs (#672)
* Add numeric inputs for percentiles

* Define initial values for percentile cutoffs in world reducer

* add percentil to crossfilter dimensions

* worldEqUniverse now handles cloned worlds

* add Dataframe.mapColumns

* Wire up handlers for percentile inputs

* World reducer and stateManager know about continuousPercentileMin/Max

* Create world as universe clone (not pointer) to avoid clobbering vals

* Define basic actions for setting continuousPercentileMin/Max

* Under the hood, deal with percentiles between 0 and 1

* Move percentile inputs to visualization settings menu

* Fix padding for undo/redo buttons

* Trigger world rebuild from percentile actions

* BROKEN - pseudocode for clamping dataframe by percentiles upon world rebuild

* fix error handling on clip quantiles; start world clipping implementation

* more unclipped reorg

* rename crossfilter.percentile to quantile

* simplify schema access

* update continuous legend when scale changes

* update color cache when clip changes

* clip obs annotations and var data when clip quantile changes

* use own fromEntries

* fix tests

* stable non-finite float sort/search

* clarify comments

* fix syntax typo

* use new stand-alone clip

* clip expresssion data

* add select tests for non-finite scalars

* basic styles

* clip UI now requires explicit commit

* reset enable/disable accounts for clip percentiles

* better error messages

* fix bug in undo interaction with programatic min brush selection

* small refactoring

* support clipping of int data

* do not perform unnecessary summarizations

* improve caching of dataframe compiled columns

* add percentile precompute to Dataframe.summarize

* use Dataframe.summarize for clip percentiles

* remove obsolete quantile code from corssfilter

* histogram scale and label Y axis, add unclipped X range labels

* layout tweaks

* scatterplot now updates when clip changes

* improve comments

* remove debugging comment

* rework clip number entry validation for usability

* ui tweaks to histogram colors and layout

* enable undo/redo for clip user action

* refine UI on clip value entry

* api cleanup

* update confusing comment

* clarify purpose of isValidDigitKeyEvent

* fix misleading comment

* apply appropriate button-group classes; do not mix span and div

* variable name and comment changes suggested in PR review

* rename sort to sortArray; remove unused and dead code path

* naming changes suggested in PR review

* code review improvements for clarity

* more small changes from PR review

* lint fixes for PR review

* fix spelling error

* clarify that function performs in-place modification of world

* add comment to clarify intent of range operation

* fix bad indents in comments

* clean up __columnsAccessor comments and code

* improve comments around clipPredicate

* field name consistency

* improve comment on quantiles params
2019-04-30 16:20:10 -07:00

184 lines
5.4 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(
REST.config,
REST.schema,
REST.annotationsObs,
REST.annotationsVar,
REST.layoutObs
);
/* create world */
const world = World.createWorldFromEntireUniverse(universe);
/* create crossfilter */
const crossfilter = World.createObsDimensions(
new Crossfilter(world.obsAnnotations),
world
);
return {
universe,
world,
crossfilter
};
};
describe("createWorldFromEntireUniverse", () => {
test("create from REST sample", () => {
const universe = Universe.createUniverseFromResponse(
REST.config,
REST.schema,
REST.annotationsObs,
REST.annotationsVar,
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(["X", "Y"]);
});
});
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,
c => c.name
);
const schemaByObsName = _.keyBy(REST.schema.schema.annotations.obs, "name");
expect(crossfilter).toBeDefined();
annotationNames.forEach(name => {
const dim = crossfilter.dimensions[obsAnnoDimensionName(name)];
if (name === "name") {
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);
});