From 153f240c433828f8be858aea5185f61df56879c2 Mon Sep 17 00:00:00 2001 From: Bruce Martin Date: Mon, 25 Mar 2019 15:02:39 -0700 Subject: [PATCH] brushable histogram brush selection responds to state updates (#670) * add continuousSelection reducer and make histo respond to state changes * correctly handle uninitialized state for brush move * PR review tweaks --- .../components/brushableHistogram/index.js | 78 +++++++++++++++---- client/src/reducers/continuousSelection.js | 26 +++++++ client/src/reducers/index.js | 3 + 3 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 client/src/reducers/continuousSelection.js diff --git a/client/src/components/brushableHistogram/index.js b/client/src/components/brushableHistogram/index.js index b8b2f1a6..795b321f 100644 --- a/client/src/components/brushableHistogram/index.js +++ b/client/src/components/brushableHistogram/index.js @@ -13,12 +13,13 @@ import memoize from "memoize-one"; import * as globals from "../../globals"; import actions from "../../actions"; import finiteExtent from "../../util/finiteExtent"; +import { makeContinuousDimensionName } from "../../util/nameCreators"; @connect(state => ({ world: state.world, scatterplotXXaccessor: state.controls.scatterplotXXaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor, - crossfilter: state.crossfilter, + continuousSelection: state.continuousSelection, differential: state.differential, colorAccessor: state.colors.colorAccessor, obsAnnotations: _.get(state.world, "obsAnnotations", null) @@ -86,12 +87,52 @@ class HistogramBrush extends React.Component { } componentDidUpdate(prevProps) { - const { field, obsAnnotations } = this.props; + const { field, obsAnnotations, continuousSelection } = this.props; const { x, y, bins, numValues, svgRef } = this._histogram; if (obsAnnotations !== prevProps.obsAnnotations) { this.renderAxesBrushBins(x, y, bins, numValues, svgRef, field); } + + /* + if the selection has changed, ensure that the brush correctly reflects + the underlying selection. + */ + if (continuousSelection !== prevProps.continuousSelection) { + const { isObs, isUserDefined, isDiffExp } = this.props; + const myName = makeContinuousDimensionName( + { isObs, isUserDefined, isDiffExp }, + field + ); + const range = continuousSelection[myName]; + const { brushXselection, brushX } = this.state; + if (brushXselection) { + const selection = d3.brushSelection(brushXselection.node()); + if (!range && selection) { + /* no active selection - clear brush */ + brushXselection.call(brushX.move, null); + } else if (range && !selection) { + /* there is an active selection, but no brush - set the brush */ + const x0 = x(range[0]); + const x1 = x(range[1]); + brushXselection.call(brushX.move, [x0, x1]); + } else if (range && selection) { + /* there is an active selection and a brush - make sure they match */ + const moveDeltaThreshold = 1; + const x0 = x(range[0]); + const x1 = x(range[1]); + const dX0 = Math.abs(x0 - selection[0]); + const dX1 = Math.abs(x1 - selection[1]); + /* + only update the brush if it is grossly incorrect, + as defined by the moveDeltaThreshold + */ + if (dX0 > moveDeltaThreshold || dX1 > moveDeltaThreshold) { + brushXselection.call(brushX.move, [x0, x1]); + } + } + } + } } onBrush(selection, x, eventType) { @@ -99,6 +140,9 @@ class HistogramBrush extends React.Component { return () => { const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props; + // ignore programmatically generated events + if (!d3.event.sourceEvent) return; + if (d3.event.selection) { dispatch({ type, @@ -128,10 +172,13 @@ class HistogramBrush extends React.Component { onBrushEnd(selection, x) { return () => { const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props; - const { brushX } = this.state; + const { brushXselection } = this.state; const minAllowedBrushSize = 10; const smallAmountToAvoidInfiniteLoop = 0.1; + // ignore programmatically generated events + if (!d3.event.sourceEvent) return; + if (d3.event.selection) { let _range; @@ -151,7 +198,7 @@ class HistogramBrush extends React.Component { _range = [x(d3.event.selection[0]), x(procedurallyResizedBrushWidth)]; - d3.event.target.move(brushX, [ + d3.event.target.move(brushXselection, [ d3.event.selection[0], procedurallyResizedBrushWidth ]); @@ -283,21 +330,20 @@ class HistogramBrush extends React.Component { /* BRUSH */ const brushX = d3 + .brushX() + /* + emit start so that the Undoable history can save an undo point + upon drag start, and ignore the subsequent intermediate drag events. + */ + .on("start", this.onBrush(field, x.invert, "start").bind(this)) + .on("brush", this.onBrush(field, x.invert, "brush").bind(this)) + .on("end", this.onBrushEnd(field, x.invert).bind(this)); + const brushXselection = d3 .select(svgRef) .append("g") .attr("class", "brush") .attr("data-testid", `${svgRef.id}-brush`) - .call( - d3 - .brushX() - /* - emit start so that the Undoable history can save an undo point - upon drag start, and ignore the subsequent intermediate drag events. - */ - .on("start", this.onBrush(field, x.invert, "start").bind(this)) - .on("brush", this.onBrush(field, x.invert, "brush").bind(this)) - .on("end", this.onBrushEnd(field, x.invert).bind(this)) - ); + .call(brushX); /* AXIS */ d3.select(svgRef) @@ -318,7 +364,7 @@ class HistogramBrush extends React.Component { .selectAll(".axis--x line") .style("stroke", "rgb(230,230,230)"); - this.setState({ brushX }); + this.setState({ brushX, brushXselection }); } render() { diff --git a/client/src/reducers/continuousSelection.js b/client/src/reducers/continuousSelection.js new file mode 100644 index 00000000..f0eadea7 --- /dev/null +++ b/client/src/reducers/continuousSelection.js @@ -0,0 +1,26 @@ +import { makeContinuousDimensionName } from "../util/nameCreators"; + +const ContinuousSelection = (state = {}, action) => { + switch (action.type) { + case "reset World to eq Universe": { + return {}; + } + case "continuous metadata histogram start": + case "continuous metadata histogram brush": + case "continuous metadata histogram end": { + const name = makeContinuousDimensionName( + action.continuousNamespace, + action.selection + ); + return { + ...state, + [name]: action.range + }; + } + default: { + return state; + } + } +}; + +export default ContinuousSelection; diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 9d82b645..aad1939a 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -7,6 +7,7 @@ import config from "./config"; import universe from "./universe"; import world from "./world"; import categoricalSelection from "./categoricalSelection"; +import continuousSelection from "./continuousSelection"; import crossfilter from "./crossfilter"; import colors from "./colors"; import differential from "./differential"; @@ -63,6 +64,7 @@ const Reducer = undoable( ["universe", universe], ["world", world], ["categoricalSelection", categoricalSelection], + ["continuousSelection", continuousSelection], ["crossfilter", crossfilter], ["colors", colors], ["controls", controls], @@ -73,6 +75,7 @@ const Reducer = undoable( [ "world", "categoricalSelection", + "continuousSelection", "crossfilter", "colors", "controls",