diff --git a/client/__tests__/util/bitArray.test.js b/client/__tests__/util/bitArray.test.js new file mode 100644 index 00000000..72e91882 --- /dev/null +++ b/client/__tests__/util/bitArray.test.js @@ -0,0 +1,184 @@ +// jshint esversion: 6 + +const BitArray = require("../../src/util/typedCrossfilter/bitArray"); +const defaultTestLength = 8; + +describe("default select state", () => { + test("newly created Bitarray should be deselected", () => { + const ba = new BitArray(defaultTestLength); + expect(ba).toBeDefined(); + + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + const dim = ba.allocDimension(); + expect(dim).toBeDefined(); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + ba.freeDimension(dim); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + }); +}); + +describe("select and deselect", () => { + test("selectAll and deselectAll", () => { + const ba = new BitArray(defaultTestLength); + expect(ba).toBeDefined(); + const dim1 = ba.allocDimension(); + expect(dim1).toBeDefined(); + ba.selectAll(dim1); + + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(true); + } + + const dim2 = ba.allocDimension(); + expect(dim2).toBeDefined(); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + ba.selectAll(dim2); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(true); + } + + ba.deselectAll(dim1); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + ba.deselectAll(dim2); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + ba.selectAll(dim1); + ba.selectAll(dim2); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(true); + } + + ba.freeDimension(dim1); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(true); + } + + ba.freeDimension(dim2); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + }); + + test("selectOne and deselectOne", () => { + const ba = new BitArray(defaultTestLength); + expect(ba).toBeDefined(); + const dim = ba.allocDimension(); + expect(dim).toBeDefined(); + + ba.selectOne(dim, 0); + expect(ba.isSelected(0)).toEqual(true); + for (let i = 1; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + ba.deselectOne(dim, 0); + for (let i = 0; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + ba.selectOne(dim, 1); + expect(ba.isSelected(1)).toEqual(true); + expect(ba.isSelected(0)).toEqual(false); + for (let i = 2; i < defaultTestLength; i++) { + expect(ba.isSelected(i)).toEqual(false); + } + + ba.selectAll(dim); + ba.deselectOne(dim, defaultTestLength - 1); + expect(ba.isSelected(defaultTestLength - 1)).toEqual(false); + for (let i = 0; i < defaultTestLength - 1; i++) { + expect(ba.isSelected(i)).toEqual(true); + } + }); +}); + +describe("selectionCount", () => { + test("simple", () => { + const ba = new BitArray(defaultTestLength); + expect(ba).toBeDefined(); + const dim1 = ba.allocDimension(); + expect(dim1).toBeDefined(); + const dim2 = ba.allocDimension(); + expect(dim2).toBeDefined(); + + expect(ba.selectionCount).toEqual(0); + ba.selectAll(dim1); + expect(ba.selectionCount).toEqual(0); + ba.selectAll(dim2); + expect(ba.selectionCount).toEqual(defaultTestLength); + + for (let i = 0; i < defaultTestLength; i++) { + ba.deselectOne(dim1, i); + expect(ba.selectionCount).toEqual(defaultTestLength - i - 1); + expect(ba.selectionCount).toEqual(ba.countAllOnes()); + } + + ba.freeDimension(dim1); + ba.freeDimension(dim2); + }); +}); + +describe("fillBySelection", () => { + test("sets values correctly", () => { + const ba = new BitArray(defaultTestLength); + expect(ba).toBeDefined(); + const dim = ba.allocDimension(); + expect(dim).toBeDefined(); + + const arr = new Int32Array(defaultTestLength); + arr.fill(0); + const truth = new Int32Array(defaultTestLength); + truth.fill(0); + + // initial state should be deselected + ba.fillBySelection(arr, 1, 0); + expect(arr).toEqual(expect.not.arrayContaining([1])); + + // selectAll + ba.selectAll(dim); + ba.fillBySelection(arr, 1, 0); + expect(arr).toEqual(expect.not.arrayContaining([0])); + + // deselectOne + ba.deselectOne(dim, 3); + ba.fillBySelection(arr, 1, 0); + truth.fill(1); + truth[3] = 0; + expect(arr).toEqual(truth); + + // deselectAll + ba.deselectAll(dim); + ba.fillBySelection(arr, 1, 0); + truth.fill(0); + expect(arr).toEqual(truth); + + // selectOne + ba.selectOne(dim, 5); + ba.fillBySelection(arr, 6, 1); + truth.fill(1); + truth[5] = 6; + expect(arr).toEqual(truth); + + // should be deselected after dimension disposal + ba.freeDimension(dim); + ba.fillBySelection(arr, 3, 9); + truth.fill(9); + expect(arr).toEqual(truth); + }); +}); diff --git a/client/__tests__/util/positiveInterval.test.js b/client/__tests__/util/positiveInterval.test.js new file mode 100644 index 00000000..81c44496 --- /dev/null +++ b/client/__tests__/util/positiveInterval.test.js @@ -0,0 +1,140 @@ +// jshint esversion: 6 + +const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals"); + +describe("canonicalize", () => { + test("empty", () => { + expect(PositiveIntervals.canonicalize([])).toEqual([]); + }); + + test("simple, already correct", () => { + expect(PositiveIntervals.canonicalize([[0, 1]])).toEqual([[0, 1]]); + expect(PositiveIntervals.canonicalize([[0, 1], [2, 3]])).toEqual([ + [0, 1], + [2, 3] + ]); + }); + + test("non-canonical, need to be canonicalized", () => { + expect(PositiveIntervals.canonicalize([[0, 1], [1, 2]])).toEqual([[0, 2]]); + expect(PositiveIntervals.canonicalize([[1, 2], [2, 3]])).toEqual([[1, 3]]); + }); +}); + +describe("union", () => { + test("empty range", () => { + expect(PositiveIntervals.union([], [])).toEqual([]); + expect(PositiveIntervals.union([], [[1, 2]])).toEqual([[1, 2]]); + expect(PositiveIntervals.union([], [[1, 2], [3, 4]])).toEqual([ + [1, 2], + [3, 4] + ]); + expect(PositiveIntervals.union([[3, 4]], [])).toEqual([[3, 4]]); + expect(PositiveIntervals.union([[1, 2], [3, 4]], [])).toEqual([ + [1, 2], + [3, 4] + ]); + expect(PositiveIntervals.union([[3, 3]], [])).toEqual([[3, 3]]); + expect(PositiveIntervals.union([], [[3, 3]])).toEqual([[3, 3]]); + }); + + test("simple ranges", () => { + expect(PositiveIntervals.union([[1, 2]], [[2, 3]])).toEqual([[1, 3]]); + expect(PositiveIntervals.union([[2, 3]], [[1, 2]])).toEqual([[1, 3]]); + expect(PositiveIntervals.union([[1, 2]], [[3, 4]])).toEqual([ + [1, 2], + [3, 4] + ]); + expect( + PositiveIntervals.union([[1, 2], [3, 4]], [[6, 7], [19, 40]]) + ).toEqual([[1, 2], [3, 4], [6, 7], [19, 40]]); + expect(PositiveIntervals.union([[1, 4]], [[1, 1], [3, 4]])).toEqual([ + [1, 4] + ]); + expect(PositiveIntervals.union([[3, 3]], [[4, 4]])).toEqual([ + [3, 3], + [4, 4] + ]); + }); +}); + +describe("intersection", () => { + test("empty range", () => { + expect(PositiveIntervals.intersection([], [])).toEqual([]); + expect(PositiveIntervals.intersection([], [[1, 2]])).toEqual([]); + expect(PositiveIntervals.intersection([[1, 2]], [])).toEqual([]); + }); + + test("simple", () => { + expect(PositiveIntervals.intersection([[1, 2]], [[2, 3]])).toEqual([]); + expect(PositiveIntervals.intersection([[2, 3]], [[1, 2]])).toEqual([]); + expect(PositiveIntervals.intersection([[1, 10]], [[1, 10]])).toEqual([ + [1, 10] + ]); + expect(PositiveIntervals.intersection([[1, 10]], [[2, 8]])).toEqual([ + [2, 8] + ]); + expect(PositiveIntervals.intersection([[2, 8]], [[1, 10]])).toEqual([ + [2, 8] + ]); + expect(PositiveIntervals.intersection([[1, 10]], [[2, 12]])).toEqual([ + [2, 10] + ]); + expect(PositiveIntervals.intersection([[2, 12]], [[1, 10]])).toEqual([ + [2, 10] + ]); + expect(PositiveIntervals.intersection([[1, 10]], [[1, 8]])).toEqual([ + [1, 8] + ]); + expect(PositiveIntervals.intersection([[1, 8]], [[1, 10]])).toEqual([ + [1, 8] + ]); + expect(PositiveIntervals.intersection([[1, 10]], [[1, 2], [6, 9]])).toEqual( + [[1, 2], [6, 9]] + ); + expect(PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])).toEqual( + [[1363, 2638]] + ); + expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([ + [1, 2] + ]); + }); +}); + +describe("difference", () => { + test("empty", () => { + expect(PositiveIntervals.difference([], [])).toEqual([]); + expect(PositiveIntervals.difference([], [[1, 10]])).toEqual([]); + expect(PositiveIntervals.difference([[1, 10]], [])).toEqual([[1, 10]]); + }); + + test("simple", () => { + expect(PositiveIntervals.difference([[1, 2], [3, 4]], [])).toEqual([ + [1, 2], + [3, 4] + ]); + expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[5, 10]])).toEqual([ + [1, 2], + [3, 5] + ]); + expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[0, 5]])).toEqual([ + [5, 10] + ]); + expect( + PositiveIntervals.difference([[0, 2638]], [[0, 1363], [2055, 2638]]) + ).toEqual([[1363, 2055]]); + expect( + PositiveIntervals.difference([[0, 1363], [2055, 2638]], [[0, 2638]]) + ).toEqual([]); + expect(PositiveIntervals.difference([[0, 10]], [[0, 1]])).toEqual([ + [1, 10] + ]); + expect(PositiveIntervals.difference([[0, 10]], [[1, 2]])).toEqual([ + [0, 1], + [2, 10] + ]); + expect(PositiveIntervals.difference([[0, 10]], [[9, 10]])).toEqual([ + [0, 9] + ]); + }); +}); diff --git a/client/__tests__/util/typedCrossfilter.test.js b/client/__tests__/util/typedCrossfilter.test.js new file mode 100644 index 00000000..00e68495 --- /dev/null +++ b/client/__tests__/util/typedCrossfilter.test.js @@ -0,0 +1,276 @@ +// jshint esversion: 6 +const _ = require("lodash"); +const crossfilter = require("../../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"] + } +]; + +var 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(r => r.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(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"); + + expect(quantity).toBeDefined(); + expect(tip).toBeDefined(); + expect(total).toBeDefined(); + expect(type).toBeDefined(); + + // initially, all should be filtered + expect(payments.allFiltered().length).toEqual(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(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"); + + 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(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"); + + 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(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"); + + 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(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"); + + // 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); + 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); + }); +}); diff --git a/client/package.json b/client/package.json index 03568a9c..712021ed 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,8 @@ "clean": "rimraf build", "start": "node server/development.js", "lint": "eslint src", - "prod": "cross-env NODE_ENV=production PORT=3000 node server/production.js" + "prod": "cross-env NODE_ENV=production PORT=3000 node server/production.js", + "test": "jest" }, "engineStrict": true, "engines": { @@ -90,6 +91,7 @@ "gzip-size": "^3.0.0", "html-webpack-inline-source-plugin": "0.0.6", "html-webpack-plugin": "^2.22.0", + "jest": "^23.0.1", "jsdom": "^9.4.1", "json-loader": "^0.5.4", "nyc": "^10.0.0", @@ -106,5 +108,10 @@ "webpack-dev-middleware": "^1.6.1", "webpack-hot-middleware": "^2.12.2", "whatwg-fetch": "^2.0.1" + }, + "jest": { + "testMatch": [ + "**/__tests__/**/?(*.)(spec|test).js?(x)" + ] } } diff --git a/client/src/components/categorical/categorical.js b/client/src/components/categorical/categorical.js index 1b04c388..997b3889 100644 --- a/client/src/components/categorical/categorical.js +++ b/client/src/components/categorical/categorical.js @@ -15,7 +15,8 @@ import FaPaintBrush from "react-icons/lib/fa/paint-brush"; @connect(state => { return { - colorAccessor: state.controls.colorAccessor + colorAccessor: state.controls.colorAccessor, + categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap }; }) class Category extends React.Component { @@ -26,7 +27,24 @@ class Category extends React.Component { isExpanded: false }; } + componentDidUpdate() { + const valuesAsBool = _.values( + this.props.categoricalAsBooleansMap[this.props.metadataField] + ) + /* count categories toggled on by counting true values */ + const categoriesToggledOn = _.values(valuesAsBool).filter(v => v).length; + if (categoriesToggledOn === valuesAsBool.length) { + /* everything is on, so not indeterminate */ + this.checkbox.indeterminate = false; + } else if (categoriesToggledOn === 0) { + /* nothing is on, so no */ + this.checkbox.indeterminate = false; + } else if (categoriesToggledOn < valuesAsBool.length) { + /* to be explicit... */ + this.checkbox.indeterminate = true; + } + } handleColorChange() { this.props.dispatch({ type: "color by categorical metadata", @@ -61,6 +79,16 @@ class Category extends React.Component { ); }); } + handleToggleAllClick() { + // || this.checkbox.indeterminate === false + if (this.state.isChecked) { + console.log('checked, firing toggle none') + this.toggleNone(); + } else if (!this.state.isChecked) { + console.log('!checked, firing toggle all') + this.toggleAll() + } + } render() { return (
{this.props.metadataField} this.checkbox = el} checked={this.state.isChecked} type="checkbox" /> diff --git a/client/src/components/continuous/histogramBrush.js b/client/src/components/continuous/histogramBrush.js index 1b67e687..17ab9cec 100644 --- a/client/src/components/continuous/histogramBrush.js +++ b/client/src/components/continuous/histogramBrush.js @@ -7,8 +7,11 @@ https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771 import React from "react"; import _ from "lodash"; import { connect } from "react-redux"; +import FaPaintBrush from "react-icons/lib/fa/paint-brush"; +import * as globals from "../../globals"; @connect(state => { + console.log("state in histo brush", state) const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges @@ -26,7 +29,8 @@ import { connect } from "react-redux"; return { colorAccessor: state.controls.colorAccessor, colorScale: state.controls.colorScale, - cellsMetadata: state.controls.cellsMetadata + cellsMetadata: state.controls.cellsMetadata, + initializeRanges }; }) class HistogramBrush extends React.Component { @@ -154,7 +158,7 @@ class HistogramBrush extends React.Component { ) .call(d3.axisBottom(x).ticks(5)) .append("text") - .attr("x", 300) + .attr("x", this.width - 2) .attr("y", -6) .attr("fill", "#000") .attr("text-anchor", "end") @@ -164,13 +168,40 @@ class HistogramBrush extends React.Component { this.setState({ brush, xAxis }); } } - + handleColorAction() { + this.props.dispatch({ + type: "color by continuous metadata", + colorAccessor: this.props.metadataField, + rangeMaxForColorAccessor: this.props.initializeRanges[this.props.metadataField].range.max + }); + } render() { return (
+ + + cell coordinates - const invert = pin => { - const x = - 2 * pin[0] / (this.props.responsive.height - this.graphPaddingTop) - 1; - const y = - 2 * - (1 - pin[1] / (this.props.responsive.height - this.graphPaddingTop)) - - 1; - const pout = [x + inverse[12], y + inverse[13]]; - return [(pout[0] + 1) / 2, (pout[1] + 1) / 2]; - }; + // transform screen coordinates -> cell coordinates + const invert = pin => { + const x = + (2 * pin[0]) / (this.props.responsive.height - this.graphPaddingTop) - + 1; + const y = + 2 * + (1 - + pin[1] / (this.props.responsive.height - this.graphPaddingTop)) - + 1; + const pout = [ + x * inverse[14] + inverse[12], + y * inverse[14] + inverse[13] + ]; + return [(pout[0] + 1) / 2, (pout[1] + 1) / 2]; + }; - const brushCoords = { - northwest: invert([s[0][0], s[0][1]]), - southeast: invert([s[1][0], s[1][1]]) - }; + const brushCoords = { + northwest: invert([s[0][0], s[0][1]]), + southeast: invert([s[1][0], s[1][1]]) + }; - this.props.dispatch({ - type: "graph brush selection change", - brushCoords - }); + this.props.dispatch({ + type: "graph brush selection change", + brushCoords + }); + } } handleBrushDeselectAction() { - if (!d3.event.selection) { + if (d3.event && !d3.event.selection) { + this.props.dispatch({ + type: "graph brush deselect" + }); + } + + if (!d3.event) { + /* this line clears the brush procedurally, ie., zoom button clicked, not a click away from brush on svg */ + this.state.svg.select(".graph_brush").call(this.state.brush.move, null); this.props.dispatch({ type: "graph brush deselect" }); @@ -302,6 +317,7 @@ class Graph extends React.Component {