diff --git a/client/__tests__/util/stateManager/sampleResponses.js b/client/__tests__/util/stateManager/sampleResponses.js new file mode 100644 index 00000000..22d36bc5 --- /dev/null +++ b/client/__tests__/util/stateManager/sampleResponses.js @@ -0,0 +1,116 @@ +/* eslint no-bitwise: "off" */ +import _ from "lodash"; + +/* +test data mocking REST 0.2 API responses. Used in several tests. +*/ +const nObs = 10; +const nVar = 32; +const field4Categories = [83, true, "foo", 2.222222]; +const fieldDCategories = [99, false, "mumble", 3.1415]; + +const aConfigResponse = { + config: { + features: [ + { method: "POST", path: "/cluster/", available: false }, + { method: "POST", path: "/layout/", available: false }, + { method: "POST", path: "/diffexp/", available: false }, + { method: "POST", path: "/saveLocal/", available: false } + ], + displayNames: { + engine: "the little engine that could", + dataset: "all your zeros are mine" + } + } +}; + +const aSchemaResponse = { + schema: { + dataframe: { + nObs, + nVar, + type: "float32" + }, + annotations: { + obs: [ + { name: "name", type: "string" }, + { name: "field1", type: "int32" }, + { name: "field2", type: "float32" }, + { name: "field3", type: "boolean" }, + { + name: "field4", + type: "categorical", + categories: field4Categories + } + ], + var: [ + { name: "name", type: "string" }, + { name: "fieldA", type: "int32" }, + { name: "fieldB", type: "float32" }, + { name: "fieldC", type: "boolean" }, + { + name: "fieldD", + type: "categorical", + categories: fieldDCategories + } + ] + } + } +}; + +const anAnnotationsObsResponse = { + names: ["name", "field1", "field2", "field3", "field4"], + data: _() + .range(nObs) + .map(idx => [ + idx, + `obs${idx}`, + 2 * idx, + idx + 0.0133, + !!(idx & 1), + field4Categories[idx % field4Categories.length] + ]) + .value() +}; + +const anAnnotationsVarResponse = { + names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"], + data: _() + .range(nVar) + .map(idx => [ + idx, + 10 * idx, + idx + 2.90143, + !!(idx & 1), + fieldDCategories[idx % fieldDCategories.length], + `var${idx}` + ]) + .value() +}; + +const aLayoutResponse = { + layout: { + ndims: 2, + coordinates: _() + .range(nObs) + .map(idx => [idx, Math.random(), Math.random()]) + .value() + } +}; + +const aDataObsResponse = { + var: [2, 4, 29], + obs: _() + .range(nObs) + .map(idx => [idx, Math.random(), Math.random(), Math.random()]) + .value() +}; + +export { + aLayoutResponse as layoutObs, + aDataObsResponse as dataObs, + anAnnotationsVarResponse as annotationsVar, + anAnnotationsObsResponse as annotationsObs, + aSchemaResponse as schema, + aConfigResponse as config +}; diff --git a/client/__tests__/util/stateManager/universe.test.js b/client/__tests__/util/stateManager/universe.test.js index 4dd6eb78..b0612d8e 100644 --- a/client/__tests__/util/stateManager/universe.test.js +++ b/client/__tests__/util/stateManager/universe.test.js @@ -1,112 +1,6 @@ -/* eslint no-bitwise: "off" */ - import _ from "lodash"; import * as Universe from "../../../src/util/stateManager/universe"; - -/* -test data mocking REST 0.2 API responses -*/ -const nObs = 10; -const nVar = 32; -const field4Categories = [83, true, "foo", 2.222222]; -const fieldDCategories = [99, false, "mumble", 3.1415]; - -const aConfigResponse = { - config: { - features: [ - { method: "POST", path: "/cluster/", available: false }, - { method: "POST", path: "/layout/", available: false }, - { method: "POST", path: "/diffexp/", available: false }, - { method: "POST", path: "/saveLocal/", available: false } - ], - displayNames: { - engine: "the little engine that could", - dataset: "all your zeros are mine" - } - } -}; - -const aSchemaResponse = { - schema: { - dataframe: { - nObs, - nVar, - type: "float32" - }, - annotations: { - obs: [ - { name: "name", type: "string" }, - { name: "field1", type: "int32" }, - { name: "field2", type: "float32" }, - { name: "field3", type: "boolean" }, - { - name: "field4", - type: "categorical", - categories: field4Categories - } - ], - var: [ - { name: "name", type: "string" }, - { name: "fieldA", type: "int32" }, - { name: "fieldB", type: "float32" }, - { name: "fieldC", type: "boolean" }, - { - name: "fieldD", - type: "categorical", - categories: fieldDCategories - } - ] - } - } -}; - -const anAnnotationsObsResponse = { - names: ["name", "field1", "field2", "field3", "field4"], - data: _() - .range(nObs) - .map(idx => [ - idx, - `obs${idx}`, - 2 * idx, - idx + 0.0133, - idx & 1, - field4Categories[idx % field4Categories.length] - ]) - .value() -}; - -const anAnnotationsVarResponse = { - names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"], - data: _() - .range(nVar) - .map(idx => [ - idx, - 10 * idx, - idx + 2.90143, - idx & 1, - fieldDCategories[idx % fieldDCategories.length], - `var${idx}` - ]) - .value() -}; - -const aLayoutResponse = { - layout: { - ndims: 2, - coordinates: _() - .range(nObs) - .map(idx => [idx, Math.random(), Math.random()]) - .value() - } -}; - -const aDataObsResponse = { - var: [2, 4, 29], - obs: _() - .range(nObs) - .map(idx => [idx, Math.random(), Math.random(), Math.random()]) - .value() -}; +import * as REST from "./sampleResponses"; describe("createUniverseFromRestV02Response", () => { /* @@ -135,13 +29,14 @@ describe("createUniverseFromRestV02Response", () => { /* create a universe from sample data nad validate its shape & contents */ + const { nObs, nVar } = REST.schema.schema.dataframe; const universe = Universe.createUniverseFromRestV02Response( - aConfigResponse, - aSchemaResponse, - anAnnotationsObsResponse, - anAnnotationsVarResponse, - aLayoutResponse + REST.config, + REST.schema, + REST.annotationsObs, + REST.annotationsVar, + REST.layoutObs ); expect(universe).toBeDefined(); @@ -150,7 +45,7 @@ describe("createUniverseFromRestV02Response", () => { api: "0.2", nObs, nVar, - schema: aSchemaResponse.schema, + schema: REST.schema.schema, obsAnnotations: expect.any(Array), varAnnotations: expect.any(Array), obsNameToIndexMap: expect.any(Object), @@ -163,12 +58,12 @@ describe("createUniverseFromRestV02Response", () => { }) ); - expect(_.size(universe.obsAnnotations)).toBe(nObs); - expect(_.size(universe.obsNameToIndexMap)).toBe(nObs); - expect(_.size(universe.obsLayout.X)).toBe(nObs); - expect(_.size(universe.obsLayout.Y)).toBe(nObs); - expect(_.size(universe.varAnnotations)).toBe(nVar); - expect(_.size(universe.varNameToIndexMap)).toBe(nVar); + expect(universe.obsAnnotations).toHaveLength(nObs); + expect(_.keys(universe.obsNameToIndexMap)).toHaveLength(nObs); + expect(universe.obsLayout.X).toHaveLength(nObs); + expect(universe.obsLayout.Y).toHaveLength(nObs); + expect(universe.varAnnotations).toHaveLength(nVar); + expect(_.keys(universe.varNameToIndexMap)).toHaveLength(nVar); }); }); @@ -191,36 +86,32 @@ describe("convertExpressionRESTv02ToObject", () => { */ test("create from response data", () => { const universe = Universe.createUniverseFromRestV02Response( - aConfigResponse, - aSchemaResponse, - anAnnotationsObsResponse, - anAnnotationsVarResponse, - aLayoutResponse + REST.config, + REST.schema, + REST.annotationsObs, + REST.annotationsVar, + REST.layoutObs ); const expression = Universe.convertExpressionRESTv02ToObject( universe, - aDataObsResponse + REST.dataObs ); /* Check that the expected keys are present */ const expectedGeneNames = _.map( - aDataObsResponse.var, - v => anAnnotationsVarResponse.data[v][5] + REST.dataObs.var, + v => REST.annotationsVar.data[v][5] ); expect(Object.keys(expression)).toEqual( expect.arrayContaining(expectedGeneNames) ); const expectedExpressionValues = _.map( - _.unzip(aDataObsResponse.obs), + _.unzip(REST.dataObs.obs), a => new Float32Array(a) ); - // console.log(aDataObsResponse); - // console.log(expression); - // console.log(_.unzip(aDataObsResponse.obs)); - - _.forEach(aDataObsResponse.var, (varIdx, idx) => { + _.forEach(REST.dataObs.var, (varIdx, idx) => { const varName = universe.varAnnotations[varIdx].name; expect(varName).toBeDefined(); expect(varIdx).toBe(universe.varNameToIndexMap[varName]); diff --git a/client/__tests__/util/stateManager/world.test.js b/client/__tests__/util/stateManager/world.test.js new file mode 100644 index 00000000..c05c98d4 --- /dev/null +++ b/client/__tests__/util/stateManager/world.test.js @@ -0,0 +1,129 @@ +import _ from "lodash"; +import * as Universe from "../../../src/util/stateManager/universe"; +import * as World from "../../../src/util/stateManager/world"; +import Crossfilter from "../../../src/util/typedCrossfilter"; +import * as REST from "./sampleResponses"; + +/* +TODO: endpoints to test: + +createObsDimensionMap(crossfilter, world) +subsetVarData(world, universe, varData) + +*/ + +describe("createWorldFromEntireUniverse", () => { + test("create from REST sample", () => { + const universe = Universe.createUniverseFromRestV02Response( + 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({ + api: "0.2", + nObs: universe.nObs, + nVar: universe.nVar, + schema: universe.schema, + obsAnnotations: universe.obsAnnotations, + varAnnotations: universe.varAnnotations, + obsLayout: universe.obsLayout, + + summary: expect.objectContaining({ + obs: _(REST.schema.schema.annotations.obs) + .keyBy("name") + .mapValues(() => expect.any(Object)) + .value(), + var: {} // TODO: currently unimplemneted + }), + + varDataCache: expect.any(Object), + + worldObsIndex: null // indicating full universe + }) + ); + }); +}); + +describe("createWorldFromCurrentSelection", () => { + test("create from REST sample", () => { + /* create universe */ + const universe = Universe.createUniverseFromRestV02Response( + REST.config, + REST.schema, + REST.annotationsObs, + REST.annotationsVar, + REST.layoutObs + ); + /* create world */ + const originalWorld = World.createWorldFromEntireUniverse(universe); + /* create crossfilter */ + const crossfilter = Crossfilter(originalWorld.obsAnnotations); + + /* fake a selection */ + const dimensionMap = World.createObsDimensionMap( + crossfilter, + originalWorld + ); + dimensionMap.field1.filterRange([0, 5]); + dimensionMap.field3.filterExact(false); + + /* create the world from the selection */ + const world = World.createWorldFromCurrentSelection( + universe, + originalWorld, + crossfilter + ); + expect(world).toBeDefined(); + expect(world.nObs).toEqual(crossfilter.countFiltered()); + + /* + calculate expected values and match against result + */ + + /* matchFilter must match the dimension filters above */ + const matchFilter = val => val.field1 >= 0 && val.field1 < 5 && !val.field3; + const universeIndices = _() + .range(universe.nObs) + .filter(idx => matchFilter(universe.obsAnnotations[idx])) + .value(); + + const expected = { + nObs: universeIndices.length, + obsAnnotations: _.map(universeIndices, i => universe.obsAnnotations[i]), + obsLayout: { + X: _.map(universeIndices, i => universe.obsLayout.X[i]), + Y: _.map(universeIndices, i => universe.obsLayout.Y[i]) + }, + worldObsIndex: _.transform( + universeIndices, + (result, univIdx, worldIdx) => { + result[univIdx] = worldIdx; + }, + new Array(universe.nObs).fill(-1) + ) + }; + + expect(world).toMatchObject( + expect.objectContaining({ + api: "0.2", + nObs: expected.nObs, + nVar: universe.nVar, + schema: universe.schema, + obsAnnotations: expected.obsAnnotations, + varAnnotations: universe.varAnnotations, + obsLayout: expected.obsLayout, + summary: expect.any(Object) /* we could do better! */, + varDataCache: expect.any(Object), + worldObsIndex: expected.worldObsIndex + }) + ); + }); +});