diff --git a/client/__tests__/util/dataframe/dataframe.test.js b/client/__tests__/util/dataframe/dataframe.test.js index b1139984..a91ca893 100644 --- a/client/__tests__/util/dataframe/dataframe.test.js +++ b/client/__tests__/util/dataframe/dataframe.test.js @@ -129,7 +129,7 @@ describe("simple data access", () => { }); describe("dataframe subsetting", () => { - describe("cutByList", () => { + describe("subset", () => { const sourceDf = new Dataframe.Dataframe( [3, 4], [ @@ -143,7 +143,7 @@ describe("dataframe subsetting", () => { ); test("all rows, one column", () => { - const dfA = sourceDf.cutByList(null, ["colors"]); + const dfA = sourceDf.subset(null, ["colors"]); expect(dfA).toBeDefined(); expect(dfA.dims).toEqual([3, 1]); expect(dfA.iat(0, 0)).toEqual("red"); @@ -158,7 +158,7 @@ describe("dataframe subsetting", () => { }); test("all rows, two columns", () => { - const dfB = sourceDf.cutByList(null, ["colors", "float32"]); + const dfB = sourceDf.subset(null, ["colors", "float32"]); expect(dfB).toBeDefined(); expect(dfB.dims).toEqual([3, 2]); expect(dfB.iat(0, 0)).toBeCloseTo(4.4); @@ -182,7 +182,7 @@ describe("dataframe subsetting", () => { }); test("one row, all columns", () => { - const dfC = sourceDf.cutByList([1], null); + const dfC = sourceDf.subset([1], null); expect(dfC).toBeDefined(); expect(dfC.dims).toEqual([1, 4]); expect(dfC.iat(0, 0)).toEqual(1); @@ -194,7 +194,7 @@ describe("dataframe subsetting", () => { }); test("two rows, all columns", () => { - const dfD = sourceDf.cutByList([0, 2], null); + const dfD = sourceDf.subset([0, 2], null); expect(dfD).toBeDefined(); expect(dfD.dims).toEqual([2, 4]); expect(dfD.icol(0).asArray()).toEqual(new Int32Array([0, 2])); @@ -206,7 +206,7 @@ describe("dataframe subsetting", () => { }); test("all rows, all columns", () => { - const dfE = sourceDf.cutByList(null, null); + const dfE = sourceDf.subset(null, null); expect(dfE).toBeDefined(); expect(dfE.dims).toEqual([3, 4]); expect(dfE.icol(0).asArray()).toEqual(sourceDf.icol(0).asArray()); @@ -218,7 +218,7 @@ describe("dataframe subsetting", () => { }); test("two rows, two colums", () => { - const dfF = sourceDf.cutByList([0, 2], ["int32", "float32"]); + const dfF = sourceDf.subset([0, 2], ["int32", "float32"]); expect(dfF).toBeDefined(); expect(dfF.dims).toEqual([2, 2]); expect(dfF.icol(0).asArray()).toEqual(new Int32Array([0, 2])); @@ -226,9 +226,32 @@ describe("dataframe subsetting", () => { expect(dfF.rowIndex.keys()).toEqual(new Int32Array([0, 2])); expect(dfF.colIndex.keys()).toEqual(["int32", "float32"]); }); + + test("withRowIndex", () => { + const df = sourceDf.subset( + null, + ["int32", "float32"], + new Dataframe.DenseInt32Index([3, 2, 1]) + ); + expect(df.colIndex).toBeInstanceOf(Dataframe.KeyIndex); + expect(df.rowIndex).toBeInstanceOf(Dataframe.DenseInt32Index); + expect(df.at(3, "int32")).toEqual(df.iat(0, 0)); + }); + + test("withRowIndex error checks", () => { + expect(() => + sourceDf.subset(null, ["red"], new Dataframe.IdentityInt32Index(1)) + ).toThrow(RangeError); + expect(() => + sourceDf.subset(null, ["red"], new Dataframe.DenseInt32Index([0, 1])) + ).toThrow(RangeError); + expect(() => + sourceDf.subset(null, ["red"], new Dataframe.KeyIndex([0, 1, 2, 3])) + ).toThrow(RangeError); + }); }); - test("icutByMask", () => { + test("isubsetMask", () => { const sourceDf = new Dataframe.Dataframe( [3, 4], [ @@ -241,7 +264,7 @@ describe("dataframe subsetting", () => { new Dataframe.KeyIndex(["int32", "string", "float32", "colors"]) ); - const dfA = sourceDf.icutByMask( + const dfA = sourceDf.isubsetMask( new Uint8Array([0, 1, 1]), new Uint8Array([1, 0, 0, 1]) ); @@ -293,6 +316,222 @@ describe("dataframe factories", () => { expect(dfB.icol(i).asArray()).toEqual(dfA.icol(i).asArray()); } }); + + describe("withCol", () => { + test("KeyIndex", () => { + const df = new Dataframe.Dataframe( + [2, 2], + [["red", "blue"], [true, false]], + null, + new Dataframe.KeyIndex(["colors", "bools"]) + ); + const dfA = df.withCol("numbers", [1, 0]); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 3]); + expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.icol(1).asArray()).toEqual([true, false]); + expect(dfA.icol(2).asArray()).toEqual([1, 0]); + expect(dfA.col("numbers").asArray()).toEqual([1, 0]); + expect(dfA.colIndex.keys()).toEqual(["colors", "bools", "numbers"]); + expect(df.colIndex.keys()).toEqual(["colors", "bools"]); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + test("DenseInt32Index", () => { + const df = new Dataframe.Dataframe( + [2, 2], + [["red", "blue"], [true, false]], + null, + new Dataframe.DenseInt32Index([74, 75]) + ); + const dfA = df.withCol(72, [1, 0]); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 3]); + expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.icol(1).asArray()).toEqual([true, false]); + expect(dfA.icol(2).asArray()).toEqual([1, 0]); + expect(dfA.col(74).asArray()).toEqual(["red", "blue"]); + expect(dfA.col(75).asArray()).toEqual([true, false]); + expect(dfA.col(72).asArray()).toEqual([1, 0]); + expect(dfA.colIndex.keys()).toEqual(new Int32Array([74, 75, 72])); + expect(df.colIndex.keys()).toEqual(new Int32Array([74, 75])); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + test("DenseInt32Index promote", () => { + const df = new Dataframe.Dataframe( + [2, 2], + [["red", "blue"], [true, false]], + null, + new Dataframe.DenseInt32Index([74, 75]) + ); + const dfA = df.withCol(999, [1, 0]); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 3]); + expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.icol(1).asArray()).toEqual([true, false]); + expect(dfA.icol(2).asArray()).toEqual([1, 0]); + expect(dfA.col(74).asArray()).toEqual(["red", "blue"]); + expect(dfA.col(75).asArray()).toEqual([true, false]); + expect(dfA.col(999).asArray()).toEqual([1, 0]); + expect(dfA.colIndex.keys()).toEqual(new Int32Array([74, 75, 999])); + expect(df.colIndex.keys()).toEqual(new Int32Array([74, 75])); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + test("IdentityInt32Index with last", () => { + const df = new Dataframe.Dataframe( + [2, 2], + [["red", "blue"], [true, false]], + null, + null + ); + const dfA = df.withCol(2, [1, 0]); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 3]); + expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.icol(1).asArray()).toEqual([true, false]); + expect(dfA.icol(2).asArray()).toEqual([1, 0]); + expect(dfA.col(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.col(1).asArray()).toEqual([true, false]); + expect(dfA.col(2).asArray()).toEqual([1, 0]); + expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1, 2])); + expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1])); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + test("IdentityInt32Index promote", () => { + const df = new Dataframe.Dataframe( + [2, 2], + [["red", "blue"], [true, false]], + null, + null + ); + const dfA = df.withCol(99, [1, 0]); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 3]); + expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.icol(1).asArray()).toEqual([true, false]); + expect(dfA.icol(2).asArray()).toEqual([1, 0]); + expect(dfA.col(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.col(1).asArray()).toEqual([true, false]); + expect(dfA.col(99).asArray()).toEqual([1, 0]); + expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1, 99])); + expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1])); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + describe("handle column dimensions correctly", () => { + /* + there are two conditions: + - empty dataframe - will accept an add of any dimensionality + - non-empty dataframe - added column must match row-count dimension + */ + test("empty.withCol", () => { + const edf = Dataframe.Dataframe.empty(); + const df = edf.withCol("foo", [1, 2, 3]); + + expect(edf).toBeDefined(); + expect(df).toBeDefined(); + expect(edf).not.toEqual(df); + expect(df.dims).toEqual([3, 1]); + expect(df.icol(0).asArray()).toEqual([1, 2, 3]); + }); + + test("withCol dimension check", () => { + const dfA = new Dataframe.Dataframe([1, 1], [["a"]]); + expect(() => { + dfA.withCol(1, []); + }).toThrow(RangeError); + }); + }); + }); + + describe("dropCol", () => { + test("KeyIndex", () => { + const df = new Dataframe.Dataframe( + [2, 3], + [["red", "blue"], [true, false], [1, 0]], + null, + new Dataframe.KeyIndex(["colors", "bools", "numbers"]) + ); + const dfA = df.dropCol("colors"); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 2]); + expect(dfA.icol(0).asArray()).toEqual([true, false]); + expect(dfA.icol(1).asArray()).toEqual([1, 0]); + expect(dfA.col("numbers").asArray()).toEqual([1, 0]); + expect(dfA.colIndex.keys()).toEqual(["bools", "numbers"]); + expect(df.colIndex.keys()).toEqual(["colors", "bools", "numbers"]); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + test("IdentityInt32Index drop first", () => { + const df = new Dataframe.Dataframe( + [2, 3], + [["red", "blue"], [true, false], [1, 0]], + null, + null + ); + const dfA = df.dropCol(0); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 2]); + expect(dfA.icol(0).asArray()).toEqual([true, false]); + expect(dfA.icol(1).asArray()).toEqual([1, 0]); + expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray()); + expect(df.col(2).asArray()).toEqual(dfA.col(2).asArray()); + expect(dfA.colIndex.keys()).toEqual(new Int32Array([1, 2])); + expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1, 2])); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + test("IdentityInt32Index drop last", () => { + const df = new Dataframe.Dataframe( + [2, 3], + [["red", "blue"], [true, false], [1, 0]], + null, + null + ); + const dfA = df.dropCol(2); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 2]); + expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.icol(1).asArray()).toEqual([true, false]); + expect(df.col(0).asArray()).toEqual(dfA.col(0).asArray()); + expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray()); + expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1])); + expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1, 2])); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + + test("DenseInt32Index", () => { + const df = new Dataframe.Dataframe( + [2, 3], + [["red", "blue"], [true, false], [1, 0]], + null, + new Dataframe.DenseInt32Index([102, 101, 100]) + ); + const dfA = df.dropCol(101); + + expect(dfA).toBeDefined(); + expect(dfA.dims).toEqual([2, 2]); + expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]); + expect(dfA.icol(1).asArray()).toEqual([1, 0]); + expect(dfA.col(100).asArray()).toEqual([1, 0]); + expect(dfA.col(102).asArray()).toEqual(["red", "blue"]); + expect(dfA.colIndex.keys()).toEqual(new Int32Array([102, 100])); + expect(df.colIndex.keys()).toEqual(new Int32Array([102, 101, 100])); + expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); + }); + }); }); describe("dataframe col", () => { diff --git a/client/__tests__/util/dataframe/summarize.test.js b/client/__tests__/util/dataframe/summarize.test.js new file mode 100644 index 00000000..702d0fae --- /dev/null +++ b/client/__tests__/util/dataframe/summarize.test.js @@ -0,0 +1,253 @@ +import * as Dataframe from "../../../src/util/dataframe"; + +function float32Conversion(f) { + return new Float32Array([f])[0]; +} + +describe("Dataframe column summary", () => { + test("empty column test", () => { + const df = Dataframe.Dataframe.create([0, 1], [[]]); + const summary = df.icol(0).summarize(); + expect(summary).toEqual( + expect.objectContaining({ + categorical: true, + categories: [], + categoryCounts: new Map(), + numCategories: 0 + }) + ); + }); + + test("simple test", () => { + const df = new Dataframe.Dataframe( + [1, 6], + [ + ["n1"], + ["hi"], + [true], + new Float32Array([39.3]), + new Int32Array([99]), + [1] + ], + null, + new Dataframe.KeyIndex([ + "name", + "nameString", + "nameBoolean", + "nameFloat32", + "nameInt32", + "nameCategorical" + ]) + ); + + expect(df.icol(0).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: ["n1"], + categoryCounts: new Map([["n1", 1]]), + numCategories: 1 + }) + ); + expect(df.icol(1).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: ["hi"], + categoryCounts: new Map([["hi", 1]]), + numCategories: 1 + }) + ); + expect(df.icol(2).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: [true], + categoryCounts: new Map([[true, 1]]), + numCategories: 1 + }) + ); + expect(df.icol(3).summarize()).toEqual( + expect.objectContaining({ + categorical: false, + min: float32Conversion(39.3), + max: float32Conversion(39.3), + nan: 0, + ninf: 0, + pinf: 0 + }) + ); + expect(df.icol(4).summarize()).toEqual( + expect.objectContaining({ + categorical: false, + min: 99, + max: 99, + nan: 0, + ninf: 0, + pinf: 0 + }) + ); + expect(df.icol(5).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: [1], + categoryCounts: new Map([[1, 1]]), + numCategories: 1 + }) + ); + }); + + test("multi test", () => { + const df = new Dataframe.Dataframe( + [3, 6], + [ + ["n0", "n1", "n2"], + ["hi", "hi", "bye"], + [false, true, true], + new Float32Array([39.3, 39.3, 0]), + new Int32Array([99, 99, 99]), + [1, false, "0"] + ], + null, + new Dataframe.KeyIndex([ + "name", + "nameString", + "nameBoolean", + "nameFloat32", + "nameInt32", + "nameCategorical" + ]) + ); + + expect(df.icol(0).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining(["n0", "n1", "n2"]), + categoryCounts: new Map([["n0", 1], ["n1", 1], ["n2", 1]]), + numCategories: 3 + }) + ); + expect(df.icol(1).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining(["hi", "bye"]), + categoryCounts: new Map([["hi", 2], ["bye", 1]]), + numCategories: 2 + }) + ); + expect(df.icol(2).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining([true, false]), + categoryCounts: new Map([[true, 2], [false, 1]]), + numCategories: 2 + }) + ); + expect(df.icol(3).summarize()).toEqual( + expect.objectContaining({ + categorical: false, + min: 0, + max: float32Conversion(39.3), + nan: 0, + ninf: 0, + pinf: 0 + }) + ); + expect(df.icol(4).summarize()).toEqual( + expect.objectContaining({ + categorical: false, + min: 99, + max: 99, + nan: 0, + ninf: 0, + pinf: 0 + }) + ); + expect(df.icol(5).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining([1, false, "0"]), + categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]), + numCategories: 3 + }) + ); + }); + + test("non-finite numbers", () => { + const df = new Dataframe.Dataframe( + [4, 6], + [ + ["n0", "n1", "n2", "n2"], + ["hi", "hi", "bye", "bye"], + [false, true, true, true], + new Float32Array([ + 39.3, + Number.NEGATIVE_INFINITY, + Number.NaN, + Number.POSITIVE_INFINITY + ]), + new Int32Array([99, 99, 99, 99]), + [1, false, "0", "0"] + ], + null, + new Dataframe.KeyIndex([ + "name", + "nameString", + "nameBoolean", + "nameFloat32", + "nameInt32", + "nameCategorical" + ]) + ); + + expect(df.icol(0).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining(["n0", "n1", "n2"]), + categoryCounts: new Map([["n0", 1], ["n1", 1], ["n2", 2]]), + numCategories: 3 + }) + ); + expect(df.icol(1).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining(["hi", "bye"]), + categoryCounts: new Map([["hi", 2], ["bye", 1]]), + numCategories: 2 + }) + ); + expect(df.icol(2).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining([true, false]), + categoryCounts: new Map([[true, 2], [false, 1]]), + numCategories: 2 + }) + ); + expect(df.icol(3).summarize()).toEqual( + expect.objectContaining({ + categorical: false, + min: float32Conversion(39.3), + max: float32Conversion(39.3), + nan: 1, + ninf: 1, + pinf: 1 + }) + ); + expect(df.icol(4).summarize()).toEqual( + expect.objectContaining({ + categorical: false, + min: 99, + max: 99, + nan: 0, + ninf: 0, + pinf: 0 + }) + ); + expect(df.icol(5).summarize()).toEqual( + expect.objectContaining({ + categorical: true, + categories: expect.arrayContaining([1, false, "0"]), + categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]), + numCategories: 3 + }) + ); + }); +}); diff --git a/client/__tests__/util/stateManager/keyvalcache.test.js b/client/__tests__/util/stateManager/keyvalcache.test.js deleted file mode 100644 index aa21bc96..00000000 --- a/client/__tests__/util/stateManager/keyvalcache.test.js +++ /dev/null @@ -1,249 +0,0 @@ -import _ from "lodash"; -import * as kvCache from "../../../src/util/stateManager/keyvalcache"; - -/* -This is PRIVATE to keyvalcache and must be kept in sync with -any changs ot that module. Need to Know - to enable error handling test -*/ -const cachePrivateKey = "__kvcachekey__"; - -/* -helper function - promisify setTimeout() -*/ -function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -describe("kvcache API", () => { - /* - test the happy path create/set/get API - */ - - test("simple create", () => { - /* with defaults */ - const kvc = kvCache.create(); - expect(kvc).toBeDefined(); - expect(kvc).toEqual(expect.objectContaining({})); - expect(kvCache.get(kvc, "test")).toBeUndefined(); - - /* with params */ - const kvc1 = kvCache.create(/* lowWatermark */ 99, /* minTTL */ 0); - expect(kvc1).toBeDefined(); - expect(kvc1).toEqual(expect.objectContaining({})); - }); - - test("set/get", () => { - /* - - check basic get/set functionality - - check set does not mutate source cache - */ - const keyName = "foo"; - const kvc1 = kvCache.create(); - expect(kvc1).toBeDefined(); - expect(kvCache.get(kvc1, keyName)).toBeUndefined(); - - const val2 = [2]; - const kvc2 = kvCache.set(kvc1, keyName, val2); - expect(kvc2).toBeDefined(); - expect(kvc2).not.toBe(kvc1); - expect(kvCache.get(kvc1, keyName)).toBeUndefined(); - expect(kvCache.get(kvc2, keyName)).toBe(val2); - - const val3 = [3]; - const kvc3 = kvCache.set(kvc2, keyName, val3); - expect(kvc3).toBeDefined(); - expect(kvc3).not.toBe(kvc1); - expect(kvc3).not.toBe(kvc2); - expect(kvCache.get(kvc1, keyName)).toBeUndefined(); - expect(kvCache.get(kvc2, keyName)).toBe(val2); - expect(kvCache.get(kvc3, keyName)).toBe(val3); - }); -}); - -describe("common error handling", () => { - /* - Test common error handlers - */ - - test("set() protection from namespace pollution", () => { - /* - Test that set() will not allow use of the private cache key - */ - const kvc = kvCache.create(); - expect(() => { - kvCache.set(kvc, cachePrivateKey, {}); - }).toThrow(); - }); - - test("create() does not accept bogus config", () => { - expect(() => { - kvCache.create([], {}); - }).toThrow(); - expect(() => { - kvCache.create(-99, 0); - }).toThrow(); - expect(() => { - kvCache.create(100, -1); - }).toThrow(); - expect(() => { - kvCache.create(1000, "foobar"); - }).toThrow(); - expect(() => { - kvCache.create(null, 8); - }).toThrow(); - }); -}); - -describe("map", () => { - /* - Test kvCache.map() - create new cache that is a transformation of an - existing cache - */ - test("map of empty cache", () => { - const kvc = kvCache.create(); - const callback = jest.fn(); - const kvcMapped = kvCache.map(kvc, callback); - expect(callback).not.toHaveBeenCalled(); - expect(kvcMapped).toBeDefined(); - expect(kvcMapped).not.toBe(kvc); // immutable operation - expect(kvcMapped).toEqual(kvc); - }); - - test("map of non-empty cache", () => { - const key = "aKey"; - const val = [0, 1, 2]; - let kvc = kvCache.create(); - kvc = kvCache.set(kvc, key, val); - const mockCB = jest.fn().mockImplementation(v => [...v]); - const kvcMapped = kvCache.map(kvc, mockCB); - - expect(kvcMapped).toBeDefined(); - expect(kvcMapped).not.toBe(kvc); // immutable operation - expect(_.isEqual(kvc, kvcMapped)).toBe(true); - - expect(mockCB).toHaveBeenCalledTimes(1); - expect(mockCB).toHaveBeenLastCalledWith(val, key); - }); -}); - -describe("flush", () => { - /* - test various cache flush behavior - */ - test("flush - lowWatermark, disable minTTL", () => { - /* - verify lowWatermark functions correctly - */ - - // set lowWatermark to 2, set three times - only the final two - // should remain. - let kvc = kvCache.create(2, 0); - ["a", "b", "c"].forEach(k => { - kvc = kvCache.set(kvc, k, []); - }); - - expect(kvc).toEqual( - expect.objectContaining({ - b: expect.arrayContaining([]), - c: expect.arrayContaining([]) - }) - ); - expect(kvc).toEqual( - expect.not.objectContaining({ - a: expect.arrayContaining([]) - }) - ); - }); - - test("flush - minTTL, disable lowWatermark", async () => { - /* - verify minTTL functions correctly - */ - - // set minTTL to 1 ms - let kvc = kvCache.create(0, 10); - kvc = kvCache.set(kvc, "a", []); - await timeout(20); - ["b", "c"].forEach(k => { - kvc = kvCache.set(kvc, k, []); - }); - - expect(kvc).toEqual( - expect.objectContaining({ - b: expect.arrayContaining([]), - c: expect.arrayContaining([]) - }) - ); - expect(kvc).toEqual( - expect.not.objectContaining({ - a: expect.arrayContaining([]) - }) - ); - }); - - test("flush - minTTL and lowWatermark", async () => { - /* - verify minTTL functions correctly - */ - - // set lowwatermark to 3, minTTL to 1 ms - let kvc = kvCache.create(3, 10); - kvc = kvCache.set(kvc, "a", []); - // delay - await timeout(20); - ["b", "c"].forEach(k => { - kvc = kvCache.set(kvc, k, []); - }); - - expect(kvc).toEqual( - expect.objectContaining({ - a: expect.arrayContaining([]), - b: expect.arrayContaining([]), - c: expect.arrayContaining([]) - }) - ); - - kvc = kvCache.set(kvc, "d", []); - expect(kvc).toEqual( - expect.objectContaining({ - b: expect.arrayContaining([]), - c: expect.arrayContaining([]), - d: expect.arrayContaining([]) - }) - ); - expect(kvc).toEqual( - expect.not.objectContaining({ - a: expect.arrayContaining([]) - }) - ); - }); - - test("manual flush", async () => { - let kvc = kvCache.create(1, 10); - ["a", "b", "c", "d"].forEach(k => { - kvc = kvCache.set(kvc, k, []); - }); - - // Before TTL has expired, should have all values in cache. - expect(kvc).toEqual( - expect.objectContaining({ - a: expect.arrayContaining([]), - b: expect.arrayContaining([]), - c: expect.arrayContaining([]) - }) - ); - - // let TTL expire - await timeout(10); - - // manually flush - const postFlushKvc = kvCache.flush(kvc); - expect(postFlushKvc).toBeDefined(); - expect(postFlushKvc).not.toBe(kvc); - expect(postFlushKvc).toEqual( - expect.objectContaining({ - d: expect.arrayContaining([]) - }) - ); - }); -}); diff --git a/client/__tests__/util/stateManager/summarizeAnnotations.test.js b/client/__tests__/util/stateManager/summarizeAnnotations.test.js deleted file mode 100644 index 2ebc2e4a..00000000 --- a/client/__tests__/util/stateManager/summarizeAnnotations.test.js +++ /dev/null @@ -1,291 +0,0 @@ -import summarizeAnnotations from "../../../src/util/stateManager/summarizeAnnotations"; -import * as Dataframe from "../../../src/util/dataframe"; - -function float32Conversion(f) { - return new Float32Array([39.3])[0]; -} - -describe("summarizeAnnotations", () => { - const schema = { - annotations: { - obs: [ - { name: "name", type: "string" }, - { name: "nameString", type: "string" }, - { name: "nameBoolean", type: "boolean" }, - { name: "nameFloat32", type: "float32" }, - { name: "nameInt32", type: "int32" }, - { - name: "nameCategorical", - type: "categorical", - categories: [true, false, 1, 0, 0.00001, 4383.4833, "test", "", "0"] - } - ], - var: [{ name: "name", type: "string" }] - } - }; - - test("empty test", () => { - const df = Dataframe.Dataframe.empty(); - const summary = summarizeAnnotations(schema, df, df.clone()); - expect(summary).toEqual( - expect.objectContaining({ - obs: { - nameString: { - categorical: true, - categories: [], - categoryCounts: new Map(), - numCategories: 0 - }, - nameBoolean: { - categorical: true, - categories: [], - categoryCounts: new Map(), - numCategories: 0 - }, - nameFloat32: { - categorical: false, - range: { - max: undefined, - min: undefined, - nan: 0, - ninf: 0, - pinf: 0 - } - }, - nameInt32: { - categorical: false, - range: { - max: undefined, - min: undefined, - nan: 0, - ninf: 0, - pinf: 0 - } - }, - nameCategorical: { - categorical: true, - categories: [], - categoryCounts: new Map(), - numCategories: 0 - } - }, - var: {} - }) - ); - }); - - test("simple test", () => { - const obsAnnotations = new Dataframe.Dataframe( - [1, 6], - [ - ["n1"], - ["hi"], - [true], - new Float32Array([39.3]), - new Int32Array([99]), - [1] - ], - null, - new Dataframe.KeyIndex([ - "name", - "nameString", - "nameBoolean", - "nameFloat32", - "nameInt32", - "nameCategorical" - ]) - ); - const varAnnotations = Dataframe.Dataframe.empty(); - - const summary = summarizeAnnotations( - schema, - obsAnnotations, - varAnnotations - ); - - expect(summary).toEqual( - expect.objectContaining({ - obs: { - nameString: { - categorical: true, - categories: ["hi"], - categoryCounts: new Map([["hi", 1]]), - numCategories: 1 - }, - nameBoolean: { - categorical: true, - categories: [true], - categoryCounts: new Map([[true, 1]]), - numCategories: 1 - }, - nameFloat32: { - categorical: false, - range: { - min: float32Conversion(39.3), - max: float32Conversion(39.3), - nan: 0, - ninf: 0, - pinf: 0 - } - }, - nameInt32: { - categorical: false, - range: { min: 99, max: 99, nan: 0, ninf: 0, pinf: 0 } - }, - nameCategorical: { - categorical: true, - categories: [1], - categoryCounts: new Map([[1, 1]]), - numCategories: 1 - } - }, - var: {} - }) - ); - }); - - test("multi test", () => { - const obsAnnotations = new Dataframe.Dataframe( - [3, 6], - [ - ["n0", "n1", "n2"], - ["hi", "hi", "bye"], - [false, true, true], - new Float32Array([39.3, 39.3, 0]), - new Int32Array([99, 99, 99]), - [1, false, "0"] - ], - null, - new Dataframe.KeyIndex([ - "name", - "nameString", - "nameBoolean", - "nameFloat32", - "nameInt32", - "nameCategorical" - ]) - ); - const varAnnotations = Dataframe.Dataframe.empty(); - - const summary = summarizeAnnotations( - schema, - obsAnnotations, - varAnnotations - ); - - expect(summary).toMatchObject( - expect.objectContaining({ - obs: { - nameString: { - categorical: true, - categories: expect.arrayContaining(["hi", "bye"]), - categoryCounts: new Map([["hi", 2], ["bye", 1]]), - numCategories: 2 - }, - nameBoolean: { - categorical: true, - categories: expect.arrayContaining([true, false]), - categoryCounts: new Map([[true, 2], [false, 1]]), - numCategories: 2 - }, - nameFloat32: { - categorical: false, - range: { - min: 0, - max: float32Conversion(39.3), - nan: 0, - ninf: 0, - pinf: 0 - } - }, - nameInt32: { - categorical: false, - range: { min: 99, max: 99, nan: 0, ninf: 0, pinf: 0 } - }, - nameCategorical: { - categorical: true, - categories: expect.arrayContaining([1, false, "0"]), - categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]), - numCategories: 3 - } - }, - var: {} - }) - ); - }); - - test("non-finite numbers", () => { - const obsAnnotations = new Dataframe.Dataframe( - [4, 6], - [ - ["n0", "n1", "n2", "n2"], - ["hi", "hi", "bye", "bye"], - [false, true, true, true], - new Float32Array([ - 39.3, - Number.NEGATIVE_INFINITY, - Number.NaN, - Number.POSITIVE_INFINITY - ]), - new Int32Array([99, 99, 99, 99]), - [1, false, "0", "0"] - ], - null, - new Dataframe.KeyIndex([ - "name", - "nameString", - "nameBoolean", - "nameFloat32", - "nameInt32", - "nameCategorical" - ]) - ); - const varAnnotations = Dataframe.Dataframe.empty(); - - const summary = summarizeAnnotations( - schema, - obsAnnotations, - varAnnotations - ); - - expect(summary).toMatchObject( - expect.objectContaining({ - obs: { - nameString: { - categorical: true, - categories: expect.arrayContaining(["hi", "bye"]), - categoryCounts: new Map([["hi", 2], ["bye", 1]]), - numCategories: 2 - }, - nameBoolean: { - categorical: true, - categories: expect.arrayContaining([true, false]), - categoryCounts: new Map([[true, 2], [false, 1]]), - numCategories: 2 - }, - nameFloat32: { - categorical: false, - range: { - min: float32Conversion(39.3), - max: float32Conversion(39.3), - nan: 1, - ninf: 1, - pinf: 1 - } - }, - nameInt32: { - categorical: false, - range: { min: 99, max: 99, nan: 0, ninf: 0, pinf: 0 } - }, - nameCategorical: { - categorical: true, - categories: expect.arrayContaining([1, false, "0"]), - categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]), - numCategories: 3 - } - }, - var: {} - }) - ); - }); -}); diff --git a/client/__tests__/util/stateManager/universe.test.js b/client/__tests__/util/stateManager/universe.test.js index f45fd7c5..b58e176b 100644 --- a/client/__tests__/util/stateManager/universe.test.js +++ b/client/__tests__/util/stateManager/universe.test.js @@ -41,15 +41,13 @@ describe("createUniverseFromResponse", () => { expect(universe).toBeDefined(); expect(universe).toMatchObject( expect.objectContaining({ - api: "0.2", nObs, nVar, schema: REST.schema.schema, obsAnnotations: expect.any(Dataframe.Dataframe), varAnnotations: expect.any(Dataframe.Dataframe), obsLayout: expect.any(Dataframe.Dataframe), - summary: expect.any(Object), - varDataCache: expect.any(Object) + varData: expect.any(Dataframe.Dataframe) }) ); @@ -63,5 +61,6 @@ describe("createUniverseFromResponse", () => { nVar, REST.schema.schema.annotations.var.length ]); + expect(universe.varData.isEmpty()).toBeTruthy(); }); }); diff --git a/client/__tests__/util/stateManager/world.test.js b/client/__tests__/util/stateManager/world.test.js index c0aafb28..94c1b9c8 100644 --- a/client/__tests__/util/stateManager/world.test.js +++ b/client/__tests__/util/stateManager/world.test.js @@ -8,7 +8,6 @@ import { obsAnnoDimensionName, layoutDimensionName } from "../../../src/util/nameCreators"; -import * as kvCache from "../../../src/util/stateManager/keyvalcache"; /* Helper - creates universe, world, corssfilter and dimensionMap from @@ -55,28 +54,13 @@ describe("createWorldFromEntireUniverse", () => { 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) - .filter(v => v.name !== "name") - .keyBy("name") - .mapValues(() => expect.any(Object)) - .value(), - var: _(REST.schema.schema.annotations.var) - .filter(v => v.name !== "name") - .keyBy("name") - .mapValues(() => expect.any(Object)) - .value() - }), - - varDataCache: expect.any(Object) + varData: expect.any(Dataframe.Dataframe) }) ); }); @@ -121,18 +105,13 @@ describe("createWorldFromCurrentSelection", () => { expect(world).toMatchObject( expect.objectContaining({ - api: "0.2", nObs: matchingIndices.length, nVar: universe.nVar, schema: universe.schema, obsAnnotations: expect.any(Dataframe.Dataframe), varAnnotations: universe.varAnnotations, obsLayout: expect.any(Dataframe.Dataframe), - summary: { - obs: expect.any(Object) /* we could do better! */, - var: expect.any(Object) /* we could do better! */ - }, - varDataCache: expect.any(Object) + varData: expect.any(Dataframe.Dataframe) }) ); @@ -183,60 +162,14 @@ describe("createObsDimensionMap", () => { }); }); -describe("subsetVarData", () => { - test("when world eq universe", () => { - const { universe, world } = defaultBigBang(); - /* create a mock varData array for subsetting */ - const sourceVarData = new Float32Array(universe.nObs); - - /* expect literally the same object back */ - const result = World.subsetVarData(world, universe, sourceVarData); - expect(result).toBe(sourceVarData); - }); - - test("when world neq universe", () => { - const { universe, world, crossfilter, dimensionMap } = defaultBigBang(); - /* create a mock varData array for subsetting */ - const sourceVarData = Float32Array.from(_.range(universe.nObs)); - - /* mock a selection */ - dimensionMap[obsAnnoDimensionName("field1")].filterRange([0, 5]); - dimensionMap[obsAnnoDimensionName("field3")].filterExact(false); - - /* create the world from the selection */ - const newWorld = World.createWorldFromCurrentSelection( - universe, - world, - crossfilter - ); - expect(newWorld.obsAnnotations.rowIndex.keys()).toEqual( - new Int32Array([0, 2]) - ); - - /* expect a subset */ - const result = World.subsetVarData(newWorld, universe, sourceVarData); - expect(result).not.toBe(sourceVarData); - expect(result).toHaveLength(newWorld.nObs); - /* check that we have expected source var content */ - expect(result).toMatchObject(new Float32Array([0, 2])); - }); -}); - -describe("createVarDimension", () => { +describe("createVarDataDimension", () => { /* create default universe */ const { world, crossfilter } = defaultBigBang(); - /* create a mock var data cache */ - const varDataCache = kvCache.set( - kvCache.create(), + world.varData = world.varData.withCol( "GENE", Float32Array.from(_.range(world.nObs)) ); - const result = World.createVarDimension( - world, - varDataCache, - crossfilter, - "GENE" - ); + const result = World.createVarDataDimension(world, crossfilter, "GENE"); expect(result).toBeInstanceOf(Crossfilter.ScalarDimension); }); diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 7964fadf..7a62f3b4 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -1,12 +1,11 @@ // jshint esversion: 6 import _ from "lodash"; import * as globals from "../globals"; -import { Universe, kvCache } from "../util/stateManager"; +import { Universe } from "../util/stateManager"; import { catchErrorsWrap, doJsonRequest, doBinaryRequest, - rangeEncodeIndices, dispatchNetworkErrorMessageToUser } from "../util/actionHelpers"; @@ -130,9 +129,9 @@ async function _doRequestExpressionData(dispatch, getState, genes) { let expressionData = _.transform( genes, (expData, g) => { - const data = kvCache.get(universe.varDataCache, g); + const data = universe.varData.col(g); if (data) { - expData[g] = data; + expData[g] = data.asArray(); } }, {} @@ -170,7 +169,7 @@ function requestSingleGeneExpressionCountsForColoringPOST(gene) { type: "color by expression", gene, data: { - [gene]: kvCache.get(world.varDataCache, gene) + [gene]: world.varData.col(gene).asArray() } }); } catch (error) { @@ -193,7 +192,7 @@ const requestUserDefinedGene = gene => async (dispatch, getState) => { type: "request user defined gene success", data: { genes: [gene], - expression: kvCache.get(world.varDataCache, gene) + expression: world.varData.col(gene).asArray() } }); } catch (error) { @@ -243,12 +242,16 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async ( const state = getState(); const { universe } = state.controls; + // Legal values are null, Array or TypedArray. Null is initial state. + if (!set1) set1 = []; + if (!set2) set2 = []; + // These lines ensure that we convert any TypedArray to an Array. // This is necessary because JSON.stringify() does some very strange // things with TypedArrays (they are marshalled to JSON objects, rather // than being marshalled as a JSON array). - const aset1 = Array.isArray(set1) ? set1 : Array.from(set1); - const aset2 = Array.isArray(set2) ? set2 : Array.from(set2); + set1 = Array.isArray(set1) ? set1 : Array.from(set1); + set2 = Array.isArray(set2) ? set2 : Array.from(set2); const res = await fetch( `${globals.API.prefix}${globals.API.version}diffexp/obs`, @@ -261,8 +264,8 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async ( body: JSON.stringify({ mode: "topN", count: num_genes, - set1: { filter: { obs: { index: aset1 } } }, - set2: { filter: { obs: { index: aset2 } } } + set1: { filter: { obs: { index: set1 } } }, + set2: { filter: { obs: { index: set2 } } } }) } ); diff --git a/client/src/components/brushableHistogram/index.js b/client/src/components/brushableHistogram/index.js index 19bf0f73..657703bf 100644 --- a/client/src/components/brushableHistogram/index.js +++ b/client/src/components/brushableHistogram/index.js @@ -10,7 +10,6 @@ import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core"; import { connect } from "react-redux"; import * as d3 from "d3"; import memoize from "memoize-one"; -import { kvCache } from "../../util/stateManager"; import * as globals from "../../globals"; import actions from "../../actions"; import finiteExtent from "../../util/finiteExtent"; @@ -21,7 +20,6 @@ import finiteExtent from "../../util/finiteExtent"; scatterplotYYaccessor: state.controls.scatterplotYYaccessor, crossfilter: state.controls.crossfilter, differential: state.differential, - initializeRanges: _.get(state.controls.world, "summary.obs"), colorAccessor: state.controls.colorAccessor, colorScale: state.controls.colorScale, obsAnnotations: _.get(state.controls.world, "obsAnnotations", null) @@ -35,7 +33,7 @@ class HistogramBrush extends React.Component { .scaleLinear() .range([this.height - this.marginBottom, 0]); - if (obsAnnotations.col(field)) { + if (obsAnnotations.hasCol(field)) { // recalculate expensive stuff const allValuesForContinuousFieldAsArray = obsAnnotations .col(field) @@ -52,9 +50,8 @@ class HistogramBrush extends React.Component { .thresholds(40)(allValuesForContinuousFieldAsArray); histogramCache.numValues = allValuesForContinuousFieldAsArray.length; - } else if (kvCache.get(world.varDataCache, field)) { - /* it's not in observations, so it's a gene, but let's check to make sure */ - const varValues = kvCache.get(world.varDataCache, field); + } else if (world.varData.hasCol(field)) { + const varValues = world.varData.col(field).asArray(); histogramCache.x = d3 .scaleLinear() @@ -143,21 +140,15 @@ class HistogramBrush extends React.Component { } handleColorAction() { - const { - obsAnnotations, - dispatch, - field, - world, - initializeRanges - } = this.props; + const { obsAnnotations, dispatch, field, world, ranges } = this.props; - if (obsAnnotations.col(field)) { + if (obsAnnotations.hasCol(field)) { dispatch({ type: "color by continuous metadata", colorAccessor: field, - rangeMaxForColorAccessor: initializeRanges[field].range.max + rangeMaxForColorAccessor: ranges.max }); - } else if (kvCache.get(world.varDataCache, field)) { + } else if (world.varData.hasCol(field)) { dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field)); } } diff --git a/client/src/components/continuous/continuous.js b/client/src/components/continuous/continuous.js index 27b2d509..0ea5f022 100644 --- a/client/src/components/continuous/continuous.js +++ b/client/src/components/continuous/continuous.js @@ -9,7 +9,7 @@ import * as globals from "../../globals"; import HistogramBrush from "../brushableHistogram"; @connect(state => ({ - ranges: _.get(state.controls.world, "summary.obs", null), + obsAnnotations: _.get(state.controls.world, "obsAnnotations", null), colorAccessor: state.controls.colorAccessor, colorScale: state.controls.colorScale, selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null), @@ -28,17 +28,18 @@ class Continuous extends React.Component { handleColorAction(key) { return () => { - const { dispatch, ranges } = this.props; + const { dispatch, obsAnnotations } = this.props; + const summary = obsAnnotations.col(key).summarize(); dispatch({ type: "color by continuous metadata", colorAccessor: key, - rangeMaxForColorAccessor: ranges[key].range.max + rangeMaxForColorAccessor: summary.max }); }; } render() { - const { ranges, schema } = this.props; + const { obsAnnotations, schema } = this.props; if (schema && !this.continuousChecked) { this.hasContinuous = _.some( schema.annotations.obs, @@ -62,23 +63,27 @@ class Continuous extends React.Component { Continuous metadata
) : null} - {_.map(ranges, (value, key) => { - const isColorField = key.includes("color") || key.includes("Color"); - zebra += 1; - if (value.range && key !== "name" && !isColorField) { - return ( -