refactoring - immutable crossfilter (#647)

* immutable crossfilter

* PR review changes
This commit is contained in:
Bruce Martin
2019-03-20 14:32:35 -07:00
committed by GitHub
parent 571b7387e7
commit 996b06cecc
13 changed files with 1153 additions and 1582 deletions
@@ -3,6 +3,7 @@ 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,
@@ -26,15 +27,15 @@ const defaultBigBang = () => {
/* create world */
const world = World.createWorldFromEntireUniverse(universe);
/* create crossfilter */
const crossfilter = Crossfilter(world.obsAnnotations);
/* create dimension map */
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
const crossfilter = World.createObsDimensions(
new Crossfilter(world.obsAnnotations),
world
);
return {
universe,
world,
crossfilter,
dimensionMap
crossfilter
};
};
@@ -71,13 +72,16 @@ describe("createWorldFromCurrentSelection", () => {
const {
universe,
world: originalWorld,
crossfilter,
dimensionMap
crossfilter: originalCrossfilter
} = defaultBigBang();
/* mock a selection */
dimensionMap[obsAnnoDimensionName("field1")].filterRange([0, 5]);
dimensionMap[obsAnnoDimensionName("field3")].filterExact(false);
let 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.createWorldFromCurrentSelection(
@@ -86,7 +90,7 @@ describe("createWorldFromCurrentSelection", () => {
crossfilter
);
expect(world).toBeDefined();
expect(world.nObs).toEqual(crossfilter.countFiltered());
expect(world.nObs).toEqual(crossfilter.countSelected());
/*
calculate expected values and match against result
@@ -136,43 +140,32 @@ describe("createObsDimensionMap", () => {
- check that dimension typing is sane
*/
const { dimensionMap } = defaultBigBang();
const { crossfilter } = defaultBigBang();
const annotationNames = _.map(
REST.schema.schema.annotations.obs,
c => c.name
);
const schemaByObsName = _.keyBy(REST.schema.schema.annotations.obs, "name");
expect(dimensionMap).toBeDefined();
expect(crossfilter).toBeDefined();
annotationNames.forEach(name => {
const dim = dimensionMap[obsAnnoDimensionName(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).toBeInstanceOf(Crossfilter.EnumDimension);
expect(dim.dim).toBeInstanceOf(DimTypes.enum);
} else {
expect(dim).toBeInstanceOf(Crossfilter.ScalarDimension);
expect(dim.dim).toBeInstanceOf(DimTypes.scalar);
}
}
});
expect(dimensionMap[layoutDimensionName("XY")]).toBeInstanceOf(
Crossfilter.SpatialDimension
);
expect(
crossfilter.dimensions[layoutDimensionName("XY")].dim
).toBeInstanceOf(DimTypes.spatial);
});
});
describe("createVarDataDimension", () => {
/* create default universe */
const { world, crossfilter } = defaultBigBang();
world.varData = world.varData.withCol(
"GENE",
Float32Array.from(_.range(world.nObs))
);
const result = World.createVarDataDimension(world, crossfilter, "GENE");
expect(result).toBeInstanceOf(Crossfilter.ScalarDimension);
});
describe("worldEqUniverse", () => {
const { universe, world } = defaultBigBang();
const result = World.worldEqUniverse(world, universe);
@@ -0,0 +1,330 @@
import _ from "lodash";
import { polygonContains } from "d3";
import Crossfilter from "../../../src/util/typedCrossfilter";
const someData = [
{
date: "2011-11-14T16:17:54Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001"],
coords: [0, 0]
},
{
date: "2011-11-14T16:20:19Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001", "005"],
coords: [0.4, 0.4]
},
{
date: "2011-11-14T16:28:54Z",
quantity: 1,
total: 300,
tip: 200,
type: "visa",
productIDs: ["004", "005"],
coords: [0.3, 0.1]
},
{
date: "2011-11-14T16:30:43Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002"],
coords: [0.392, 0.1]
},
{
date: "2011-11-14T16:48:46Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["005"],
coords: [0.7, 0.0482]
},
{
date: "2011-11-14T16:53:41Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "004", "005"],
coords: [0.9999, 1.0]
},
{
date: "2011-11-14T16:54:06Z",
quantity: 1,
total: 100,
tip: 0,
type: "cash",
productIDs: ["001", "002", "003", "004", "005"],
coords: [0.384, 0.6938]
},
{
date: "2011-11-14T16:58:03Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001"],
coords: [0.4822, 0.482]
},
{
date: "2011-11-14T17:07:21Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["004", "005"],
coords: [0.2234, 0]
},
{
date: "2011-11-14T17:22:59Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002", "004", "005"],
coords: [0.382, 0.38485]
},
{
date: "2011-11-14T17:25:45Z",
quantity: 2,
total: 200,
tip: 0,
type: "cash",
productIDs: ["002"],
coords: [0.998, 0.8472]
},
{
date: "2011-11-14T17:29:52Z",
quantity: 1,
total: 200,
tip: 100,
type: "visa",
productIDs: ["004"],
coords: [0.8273, 0.3384]
}
];
let payments = null;
beforeEach(() => {
payments = new Crossfilter(someData);
});
describe("ImmutableTypedCrossfilter", () => {
test("create crossfilter", () => {
expect(payments).toBeDefined();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
const p = payments
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
.select("quantity", { mode: "all" });
expect(p).toBeDefined();
expect(p.all()).toEqual(someData);
expect(p.size()).toEqual(someData.length);
expect(p.isElementSelected(0)).toBeTruthy();
expect(p.countSelected()).toEqual(someData.length);
expect(p.allSelected()).toEqual(someData);
});
test("immutability", () => {
/*
the following should return a new crossfilter:
- addDimension()
- delDimension()
- select
*/
const p2 = payments.addDimension(
"quantity",
"scalar",
(i, data) => data[i].quantity,
Int32Array
);
expect(payments).not.toBe(p2);
const p3 = p2.select("quantity", { mode: "all" });
expect(p3).not.toBe(p2);
const p4 = p3.delDimension("quantity");
expect(p4).not.toBe(p3);
});
test("select all and none", () => {
let p = payments
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
.addDimension("total", "scalar", (i, d) => d[i].total, Float32Array)
.addDimension("type", "enum", (i, d) => d[i].type);
expect(p).toBeDefined();
/* expect all records to be selected - default init state */
expect(p.allSelected()).toEqual(someData);
expect(p.countSelected()).toEqual(someData.length);
expect(p.allSelectedMask()).toEqual(
new Uint8Array(someData.length).fill(1)
);
expect(p.fillByIsSelected(new Uint8Array(someData.length), 99, 0)).toEqual(
new Uint8Array(someData.length).fill(99)
);
for (let i = 0; i < someData.length; i += 1) {
expect(p.isElementSelected(i)).toBeTruthy();
}
/* expect a selectAll on one dimension to change nothing */
p = p.select("tip", { mode: "all" });
expect(p.allSelected()).toEqual(someData);
/* ditto */
p = p.select("quantity", { mode: "all" });
expect(p.allSelected()).toEqual(someData);
/* select none on one dimension */
p = p.select("type", { mode: "none" });
expect(p.allSelected()).toEqual([]);
expect(p.countSelected()).toEqual(0);
expect(p.allSelectedMask()).toEqual(
new Uint8Array(someData.length).fill(0)
);
expect(p.fillByIsSelected(new Uint8Array(someData.length), 99, 0)).toEqual(
new Uint8Array(someData.length).fill(0)
);
for (let i = 0; i < someData.length; i += 1) {
expect(p.isElementSelected(i)).toBeFalsy();
}
p = p.select("quantity", { mode: "none" });
expect(p.allSelected()).toEqual([]);
// invert the first none; should have no effect because type is
// still not filtered.
p = p.select("quantity", { mode: "all" });
expect(p.allSelected()).toEqual([]);
/* select all of type; should select all records */
p = p.select("type", { mode: "all" });
expect(p.allSelected()).toEqual(someData);
});
describe("scalar dimension", () => {
let p;
beforeEach(() => {
p = payments
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
.select("tip", { mode: "all" });
});
/*
select modes: all, none, exact, range
*/
test("all", () => {
expect(p.select("quantity", { mode: "all" }).countSelected()).toEqual(
someData.length
);
});
test("none", () => {
expect(p.select("quantity", { mode: "none" }).countSelected()).toEqual(0);
});
test.each([[[]], [[2]], [[2, 1]], [[9, 82]], [[0, 1]]])("exact: %p", v =>
expect(
p.select("quantity", { mode: "exact", values: v }).countSelected()
).toEqual(_.filter(someData, d => v.includes(d.quantity)).length)
);
test.each([[0, 1], [1, 2], [0, 99], [99, 100000]])("range %p", (lo, hi) =>
expect(
p.select("quantity", { mode: "range", lo, hi }).countSelected()
).toEqual(
_.filter(someData, d => d.quantity >= lo && d.quantity < hi).length
)
);
test("bad mode", () => {
expect(() => p.select("type", { mode: "bad mode" })).toThrow(Error);
});
});
describe("enum dimension", () => {
let p;
beforeEach(() => {
p = payments.addDimension("type", "enum", (i, d) => d[i].type);
});
test("all", () => {
expect(p.select("type", { mode: "all" }).countSelected()).toEqual(
someData.length
);
});
test("none", () => {
expect(p.select("type", { mode: "none" }).countSelected()).toEqual(0);
});
test.each([
[[]],
[["tab"]],
[["visa"]],
[["visa", "tab"]],
[["cash", "tab", "visa"]]
])("exact: %p", v =>
expect(
p.select("type", { mode: "exact", values: v }).countSelected()
).toEqual(_.filter(someData, d => v.includes(d.type)).length)
);
test("range", () => {
expect(() => p.select("type", { mode: "range", lo: 0, hi: 9 })).toThrow(
Error
);
});
test("bad mode", () => {
expect(() => p.select("type", { mode: "bad mode" })).toThrow(Error);
});
});
describe("spatial dimension", () => {
let p;
beforeEach(() => {
const X = someData.map(r => r.coords[0]);
const Y = someData.map(r => r.coords[1]);
p = payments.addDimension("coords", "spatial", X, Y);
});
test("all", () => {
expect(p.select("coords", { mode: "all" }).countSelected()).toEqual(
someData.length
);
});
test("none", () => {
expect(p.select("coords", { mode: "none" }).countSelected()).toEqual(0);
});
test.each([[0, 0, 1, 1], [0, 0, 0.5, 0.5], [0.5, 0.5, 1, 1]])(
"within-rect %d %d %d %d",
(x0, y0, x1, y1) => {
expect(
p
.select("coords", { mode: "within-rect", x0, y0, x1, y1 })
.allSelected()
).toEqual(
_.filter(someData, d => {
const [x, y] = d.coords;
return x0 <= x && x < x1 && y0 <= y && y < y1;
})
);
}
);
test.each([
[[[0, 0], [0, 1], [1, 1], [1, 0]]],
[[[0, 0], [0, 0.5], [0.5, 0.5], [0.5, 0]]]
])("within-polygon %p", polygon => {
expect(
p.select("coords", { mode: "within-polygon", polygon }).allSelected()
).toEqual(_.filter(someData, d => polygonContains(polygon, d.coords)));
});
});
});
@@ -1,590 +0,0 @@
// jshint esversion: 6
import _ from "lodash";
import crossfilter from "../../../src/util/typedCrossfilter";
const someData = [
{
date: "2011-11-14T16:17:54Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001"]
},
{
date: "2011-11-14T16:20:19Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001", "005"]
},
{
date: "2011-11-14T16:28:54Z",
quantity: 1,
total: 300,
tip: 200,
type: "visa",
productIDs: ["004", "005"]
},
{
date: "2011-11-14T16:30:43Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002"]
},
{
date: "2011-11-14T16:48:46Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["005"]
},
{
date: "2011-11-14T16:53:41Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "004", "005"]
},
{
date: "2011-11-14T16:54:06Z",
quantity: 1,
total: 100,
tip: 0,
type: "cash",
productIDs: ["001", "002", "003", "004", "005"]
},
{
date: "2011-11-14T16:58:03Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001"]
},
{
date: "2011-11-14T17:07:21Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["004", "005"]
},
{
date: "2011-11-14T17:22:59Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002", "004", "005"]
},
{
date: "2011-11-14T17:25:45Z",
quantity: 2,
total: 200,
tip: 0,
type: "cash",
productIDs: ["002"]
},
{
date: "2011-11-14T17:29:52Z",
quantity: 1,
total: 200,
tip: 100,
type: "visa",
productIDs: ["004"]
}
];
function groupReduce(data, valueMap, valueReduce, valueInit) {
return _.reduce(
data,
(acc, value) => {
const k = valueMap(value);
let r = _.find(acc, o => o.key === k);
if (!r) {
r = { key: k, value: valueInit() };
acc.push(r);
}
r.value = valueReduce(r.value, value);
return acc;
},
[]
).sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
}
function groupCount(data, map) {
return groupReduce(data, map, p => p + 1, () => 0);
}
function groupSum(data, map) {
return groupReduce(
data,
map,
(p, v) => {
p += map(v);
return p;
},
() => 0
);
}
let payments = null;
beforeEach(() => {
payments = crossfilter(someData);
});
describe("typedCrossfilter", () => {
test("alloc and free", () => {
expect(payments).toBeDefined();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
const quantity = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].quantity,
Int32Array
);
expect(quantity).toBeDefined();
expect(quantity.id()).toBeDefined();
quantity.dispose();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
});
test("filterAll and filterNone", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].quantity,
Int32Array
);
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Float32Array
);
const total = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total,
Float32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
expect(quantity).toBeDefined();
expect(tip).toBeDefined();
expect(total).toBeDefined();
expect(type).toBeDefined();
// initially, all should be filtered
expect(payments.allFiltered()).toHaveLength(payments.size());
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// filterAll
tip.filterAll(); // should change nothing
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// ditto
total.filterAll();
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// filterNone
type.filterNone();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
quantity.filterNone();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
// invert the first none; should have no effect because type is
// still not filtered
quantity.filterAll();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
// filter all of type; should select all
type.filterAll();
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(payments.size());
});
test("filterExact", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].quantity,
Int32Array
);
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Float32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
quantity.filterExact(1);
expect(payments.countFiltered()).toEqual(
_.countBy(someData, "quantity")[1]
);
expect(payments.allFiltered()).toEqual(_.filter(someData, { quantity: 1 }));
tip.filterExact(0);
expect(payments.allFiltered()).toEqual(
_.filter(someData, { tip: 0, quantity: 1 })
);
type.filterExact("cash");
expect(payments.allFiltered()).toEqual(
_.filter(someData, { tip: 0, quantity: 1, type: "cash" })
);
});
test("filterRange", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].quantity,
Int32Array
);
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Float32Array
);
const total = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total,
Float32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
tip.filterRange([0, 91]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 0 && r.tip < 91)
.value()
);
tip.filterRange([0, 90]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 0 && r.tip < 90)
.value()
);
tip.filterRange([1, 90]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 1 && r.tip < 91)
.value()
);
});
test("filterEnum", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].quantity,
Int32Array
);
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Float32Array
);
const total = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total,
Float32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
type.filterEnum(["tab", "cash"]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.type === "cash" || r.type === "tab")
.value()
);
tip.filterEnum([0, 100]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.type === "cash" || r.type === "tab")
.filter(r => r.tip === 0 || r.tip === 100)
.value()
);
});
test("more than 32 dimensions", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].quantity,
Int32Array
);
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Float32Array
);
const total = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total,
Float32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
// Create a bunch of fake dimensions to ensure we can handle > 32
let dimMap = {};
for (let i = 0; i < 65; i++) {
dimMap[i] = payments.dimension(
crossfilter.ScalarDimension,
() => Math.random(),
Float32Array
);
expect(dimMap[i]).toBeDefined();
expect(dimMap[i].id()).toBeDefined();
}
// everything should start as selected/filtered
expect(payments.countFiltered()).toEqual(someData.length);
dimMap[0].filterAll();
dimMap[64].filterAll();
expect(payments.countFiltered()).toEqual(someData.length);
dimMap[33].filterNone();
expect(payments.allFiltered()).toEqual([]);
dimMap[33].filterAll();
expect(payments.allFiltered()).toEqual(someData);
});
test("group, default mapping, default reducer, no filter", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].quantity,
Int32Array
);
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Int32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
const total = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total,
Int32Array
);
_.each(
{
tip: tip.group(r => r),
type: type.group(),
total: total.group(),
quantity: quantity.group()
},
(grp, k) => {
const whatWeExpect = groupCount(someData, v => v[k]);
expect(grp.all()).toEqual(whatWeExpect);
expect(grp.size()).toEqual(whatWeExpect.length);
expect(grp.dispose()).toEqual(grp);
}
);
});
test("group, custom map, default reducer, no filters", () => {
expect(payments).toBeDefined();
// custom mapping in groups only works for scalar types. Enums do not
// currently implement it.
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Int32Array
);
const totalX10 = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total * 10,
Int32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
const paymentsByTip_A = tip.group();
const paymentsByTip_B = tip.group(r => 10 * r);
const paymentsByType = type.group(); // identity only
const paymentsByTotalX10_A = totalX10.group();
const paymentsByTotalX10_B = totalX10.group(r => r / 10);
expect(paymentsByTip_A.all()).toEqual(groupCount(someData, v => v.tip));
expect(paymentsByTip_B.all()).toEqual(
groupCount(someData, v => 10 * v.tip)
);
expect(paymentsByType.all()).toEqual(groupCount(someData, v => v.type));
expect(paymentsByTotalX10_A.all()).toEqual(
groupCount(someData, v => 10 * v.total)
);
expect(paymentsByTotalX10_B.all()).toEqual(
groupCount(someData, v => (10 * v.total) / 10)
);
for (let i of [
paymentsByTip_A,
paymentsByTip_B,
paymentsByType,
paymentsByTotalX10_A,
paymentsByTotalX10_B,
tip,
totalX10,
type
]) {
expect(i.dispose()).toEqual(i);
}
});
test("group, default map, custom reducer, no filters", () => {
expect(payments).toBeDefined();
const total = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total,
Float32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
const paymentsByTotal = total.group();
const paymentsByType = type.group();
// reduceCount
expect(paymentsByTotal.reduceCount()).toEqual(paymentsByTotal);
expect(paymentsByTotal.all()).toEqual(groupCount(someData, v => v.total));
// reduceSum
expect(paymentsByTotal.reduceSum(v => v.total)).toEqual(paymentsByTotal);
expect(paymentsByTotal.all()).toEqual(groupSum(someData, v => v.total));
// use custom reducers (my reducers) - count by three, init 1
expect(
paymentsByTotal.reduce((p, v) => (p += 3), (p, v) => (p -= 3), () => 1)
).toEqual(paymentsByTotal);
expect(paymentsByTotal.all()).toEqual(
groupReduce(someData, v => v.total, (p, v) => p + 3, () => 1)
);
for (let i of [paymentsByTotal, paymentsByType, type]) {
expect(i.dispose()).toEqual(i);
}
});
test("group, default map, default reducer, filters", () => {
// From the docs:
// Note: a grouping intersects the crossfilter's current filters, except for the
// associated dimension's filter. Thus, group methods consider only records that
// satisfy every filter except this dimension's filter. So, if the crossfilter of
// payments is filtered by type and total, then group by total only observes the
// filter by type.
expect(payments).toBeDefined();
const tip = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].tip,
Int32Array
);
const total = payments.dimension(
crossfilter.ScalarDimension,
(i, data) => data[i].total,
Int32Array
);
const type = payments.dimension(
crossfilter.EnumDimension,
(i, data) => data[i].type
);
const paymentsByTip = tip.group();
const paymentsByTotal = total.group();
const paymentsByType = type.group();
// 1. confirm that changing the filter on a dimension does NOT change that
// dimensions groups.
{
tip.filterAll(), total.filterAll(), type.filterAll();
let before = _.cloneDeep(paymentsByTip.all());
tip.filterExact(0);
expect(paymentsByTip.all()).toEqual(before);
}
// 2. confirm that changing a filter on a different dimension DOES change
// all other groups.
{
tip.filterAll(), total.filterAll(), type.filterAll();
const before = _.cloneDeep([paymentsByTotal.all(), paymentsByType.all()]);
tip.filterExact(0);
const after = [paymentsByTotal.all(), paymentsByType.all()];
expect(after).not.toEqual(before);
expect(after).toEqual([
groupReduce(
someData,
v => v.total,
(p, v) => (v.tip !== 0 ? p : p + 1),
() => 0
),
groupReduce(
someData,
v => v.type,
(p, v) => (v.tip !== 0 ? p : p + 1),
() => 0
)
]);
}
for (let i of [
paymentsByTip,
paymentsByTotal,
paymentsByType,
tip,
total,
type
]) {
expect(i.dispose()).toEqual(i);
}
});
});