diff --git a/client/__tests__/util/stateManager/world.test.js b/client/__tests__/util/stateManager/world.test.js index 187e51be..321ccafa 100644 --- a/client/__tests__/util/stateManager/world.test.js +++ b/client/__tests__/util/stateManager/world.test.js @@ -187,11 +187,8 @@ describe("createObsDimensionMap", () => { } } }); - expect(dimensionMap[layoutDimensionName("X")]).toBeInstanceOf( - Crossfilter.ScalarDimension - ); - expect(dimensionMap[layoutDimensionName("Y")]).toBeInstanceOf( - Crossfilter.ScalarDimension + expect(dimensionMap[layoutDimensionName("XY")]).toBeInstanceOf( + Crossfilter.SpatialDimension ); }); }); diff --git a/client/__tests__/util/typedCrossfilter/typedCrossfilter.test.js b/client/__tests__/util/typedCrossfilter/typedCrossfilter.test.js index 27096861..6e72ce1a 100644 --- a/client/__tests__/util/typedCrossfilter/typedCrossfilter.test.js +++ b/client/__tests__/util/typedCrossfilter/typedCrossfilter.test.js @@ -102,22 +102,20 @@ const someData = [ ]; 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)); + 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) { @@ -139,7 +137,11 @@ describe("typedCrossfilter", () => { expect(payments.size()).toEqual(someData.length); expect(payments.all()).toEqual(someData); - const quantity = payments.dimension(r => r.quantity, Int32Array); + const quantity = payments.dimension( + crossfilter.ScalarDimension, + r => r.quantity, + Int32Array + ); expect(quantity).toBeDefined(); expect(quantity.id()).toBeDefined(); @@ -150,10 +152,22 @@ describe("typedCrossfilter", () => { test("filterAll and filterNone", () => { expect(payments).toBeDefined(); - const quantity = payments.dimension(r => r.quantity, Int32Array); - const tip = payments.dimension(r => r.tip, Float32Array); - const total = payments.dimension(r => r.total, Float32Array); - const type = payments.dimension(r => r.type, "enum"); + const quantity = payments.dimension( + crossfilter.ScalarDimension, + r => r.quantity, + Int32Array + ); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Float32Array + ); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Float32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); expect(quantity).toBeDefined(); expect(tip).toBeDefined(); @@ -198,10 +212,22 @@ describe("typedCrossfilter", () => { test("filterExact", () => { expect(payments).toBeDefined(); - const quantity = payments.dimension(r => r.quantity, Int32Array); - const tip = payments.dimension(r => r.tip, Float32Array); - const total = payments.dimension(r => r.total, Float32Array); - const type = payments.dimension(r => r.type, "enum"); + const quantity = payments.dimension( + crossfilter.ScalarDimension, + r => r.quantity, + Int32Array + ); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Float32Array + ); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Float32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); quantity.filterExact(1); expect(payments.countFiltered()).toEqual( @@ -222,10 +248,22 @@ describe("typedCrossfilter", () => { test("filterRange", () => { expect(payments).toBeDefined(); - const quantity = payments.dimension(r => r.quantity, Int32Array); - const tip = payments.dimension(r => r.tip, Float32Array); - const total = payments.dimension(r => r.total, Float32Array); - const type = payments.dimension(r => r.type, "enum"); + const quantity = payments.dimension( + crossfilter.ScalarDimension, + r => r.quantity, + Int32Array + ); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Float32Array + ); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Float32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); tip.filterRange([0, 91]); expect(payments.allFiltered()).toEqual( @@ -251,10 +289,22 @@ describe("typedCrossfilter", () => { test("filterEnum", () => { expect(payments).toBeDefined(); - const quantity = payments.dimension(r => r.quantity, Int32Array); - const tip = payments.dimension(r => r.tip, Float32Array); - const total = payments.dimension(r => r.total, Float32Array); - const type = payments.dimension(r => r.type, "enum"); + const quantity = payments.dimension( + crossfilter.ScalarDimension, + r => r.quantity, + Int32Array + ); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Float32Array + ); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Float32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); type.filterEnum(["tab", "cash"]); expect(payments.allFiltered()).toEqual( @@ -274,15 +324,31 @@ describe("typedCrossfilter", () => { test("more than 32 dimensions", () => { expect(payments).toBeDefined(); - const quantity = payments.dimension(r => r.quantity, Int32Array); - const tip = payments.dimension(r => r.tip, Float32Array); - const total = payments.dimension(r => r.total, Float32Array); - const type = payments.dimension(r => r.type, "enum"); + const quantity = payments.dimension( + crossfilter.ScalarDimension, + r => r.quantity, + Int32Array + ); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Float32Array + ); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Float32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.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(r => Math.random(), Float32Array); + dimMap[i] = payments.dimension( + crossfilter.ScalarDimension, + r => Math.random(), + Float32Array + ); expect(dimMap[i]).toBeDefined(); expect(dimMap[i].id()).toBeDefined(); } @@ -304,10 +370,22 @@ describe("typedCrossfilter", () => { test("group, default mapping, default reducer, no filter", () => { expect(payments).toBeDefined(); - var quantity = payments.dimension(r => r.quantity, Int32Array); - var tip = payments.dimension(r => r.tip, Int32Array); - var type = payments.dimension(r => r.type, "enum"); - var total = payments.dimension(r => r.total, Int32Array); + const quantity = payments.dimension( + crossfilter.ScalarDimension, + r => r.quantity, + Int32Array + ); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Int32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Int32Array + ); _.each( { @@ -331,9 +409,17 @@ describe("typedCrossfilter", () => { // custom mapping in groups only works for scalar types. Enums do not // currently implement it. - const tip = payments.dimension(r => r.tip, Int32Array); - const totalX10 = payments.dimension(r => r.total * 10, Int32Array); - const type = payments.dimension(r => r.type, "enum"); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Int32Array + ); + const totalX10 = payments.dimension( + crossfilter.ScalarDimension, + r => r.total * 10, + Int32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); const paymentsByTip_A = tip.group(); const paymentsByTip_B = tip.group(r => 10 * r); @@ -370,8 +456,12 @@ describe("typedCrossfilter", () => { test("group, default map, custom reducer, no filters", () => { expect(payments).toBeDefined(); - const total = payments.dimension(r => r.total, Float32Array); - const type = payments.dimension(r => r.type, "enum"); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Float32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); const paymentsByTotal = total.group(); const paymentsByType = type.group(); @@ -407,9 +497,17 @@ describe("typedCrossfilter", () => { expect(payments).toBeDefined(); - const tip = payments.dimension(r => r.tip, Int32Array); - const total = payments.dimension(r => r.total, Int32Array); - const type = payments.dimension(r => r.type, "enum"); + const tip = payments.dimension( + crossfilter.ScalarDimension, + r => r.tip, + Int32Array + ); + const total = payments.dimension( + crossfilter.ScalarDimension, + r => r.total, + Int32Array + ); + const type = payments.dimension(crossfilter.EnumDimension, r => r.type); const paymentsByTip = tip.group(); const paymentsByTotal = total.group(); diff --git a/client/src/components/graph/graph.js b/client/src/components/graph/graph.js index cc29a77a..3251dd3c 100644 --- a/client/src/components/graph/graph.js +++ b/client/src/components/graph/graph.js @@ -40,7 +40,7 @@ class Graph extends React.Component { this.state = { svg: null, brush: null, - mode: "brush" + mode: "lasso" }; } @@ -202,7 +202,9 @@ class Graph extends React.Component { this.handleBrushSelectAction.bind(this), this.handleBrushDeselectAction.bind(this), responsive, - this.graphPaddingRight + this.graphPaddingRight, + this.handleLassoStart.bind(this), + this.handleLassoEnd.bind(this) ); this.setState({ svg: newSvg, brush }); } @@ -251,54 +253,54 @@ class Graph extends React.Component { }); } + invertPoint(pin) { + const { responsive } = this.props; + const { regl, camera, offset } = this.state; + + const gl = regl._gl; + + // get aspect ratio + const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight; + + // compute inverse view matrix + const inverse = mat4.invert([], camera.view()); + + // transform screen coordinates -> cell coordinates + const x = (2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1; + const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1; + const pout = [ + x * inverse[14] * aspect + inverse[12], + y * inverse[14] + inverse[13] + ]; + return [(pout[0] + 1) / 2 + offset[0], (pout[1] + 1) / 2 + offset[1]]; + } + handleBrushSelectAction() { /* - This conditional handles procedural brush deselect. Brush emits - an event on procedural deselect because it is move: null + This conditional handles procedural brush deselect. Brush emits + an event on procedural deselect because it is move: null */ - - const { camera, offset } = this.state; - const { dispatch, responsive } = this.props; - - if (d3.event.sourceEvent !== null) { - /* - No idea why d3 event scope works like this - but apparently - it does - https://bl.ocks.org/EfratVil/0e542f5fc426065dd1d4b6daaa345a9f - */ - const s = d3.event.selection; - const gl = this.state.regl._gl; - /* + /* event describing brush position: @-------| | | | | |-------@ */ + /* + No idea why d3 event scope works like this + but apparently + it does + https://bl.ocks.org/EfratVil/0e542f5fc426065dd1d4b6daaa345a9f + */ + const { dispatch } = this.props; - // get aspect ratio - const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight; - - // compute inverse view matrix - const inverse = mat4.invert([], camera.view()); - - // transform screen coordinates -> cell coordinates - const invert = pin => { - const x = - (2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1; - const y = - 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1; - const pout = [ - x * inverse[14] * aspect + inverse[12], - y * inverse[14] + inverse[13] - ]; - return [(pout[0] + 1) / 2 + offset[0], (pout[1] + 1) / 2 + offset[1]]; - }; + if (d3.event.sourceEvent !== null) { + const s = d3.event.selection; const brushCoords = { - northwest: invert([s[0][0], s[0][1]]), - southeast: invert([s[1][0], s[1][1]]) + northwest: this.invertPoint([s[0][0], s[0][1]]), + southeast: this.invertPoint([s[1][0], s[1][1]]) }; dispatch({ @@ -330,6 +332,25 @@ class Graph extends React.Component { } } + handleLassoStart() { + const { dispatch } = this.props; + // reset selected points when starting a new polygon + // making it easier for the user to make the next selection + dispatch({ + type: "lasso started" + }); + } + + // when a lasso is completed, filter to the points within the lasso polygon + handleLassoEnd(polygon) { + const { dispatch } = this.props; + + dispatch({ + type: "lasso selection", + polygon: polygon.map(xy => this.invertPoint(xy)) // transform the polygon + }); + } + handleOpacityRangeChange(e) { const { dispatch } = this.props; dispatch({ @@ -412,13 +433,18 @@ class Graph extends React.Component {
- +