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
This commit is contained in:
Bruce Martin
2019-03-25 15:02:39 -07:00
committed by GitHub
parent df46db965a
commit 153f240c43
3 changed files with 91 additions and 16 deletions
@@ -13,12 +13,13 @@ import memoize from "memoize-one";
import * as globals from "../../globals"; import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
import finiteExtent from "../../util/finiteExtent"; import finiteExtent from "../../util/finiteExtent";
import { makeContinuousDimensionName } from "../../util/nameCreators";
@connect(state => ({ @connect(state => ({
world: state.world, world: state.world,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
crossfilter: state.crossfilter, continuousSelection: state.continuousSelection,
differential: state.differential, differential: state.differential,
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
obsAnnotations: _.get(state.world, "obsAnnotations", null) obsAnnotations: _.get(state.world, "obsAnnotations", null)
@@ -86,12 +87,52 @@ class HistogramBrush extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { field, obsAnnotations } = this.props; const { field, obsAnnotations, continuousSelection } = this.props;
const { x, y, bins, numValues, svgRef } = this._histogram; const { x, y, bins, numValues, svgRef } = this._histogram;
if (obsAnnotations !== prevProps.obsAnnotations) { if (obsAnnotations !== prevProps.obsAnnotations) {
this.renderAxesBrushBins(x, y, bins, numValues, svgRef, field); 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) { onBrush(selection, x, eventType) {
@@ -99,6 +140,9 @@ class HistogramBrush extends React.Component {
return () => { return () => {
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props; const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
// ignore programmatically generated events
if (!d3.event.sourceEvent) return;
if (d3.event.selection) { if (d3.event.selection) {
dispatch({ dispatch({
type, type,
@@ -128,10 +172,13 @@ class HistogramBrush extends React.Component {
onBrushEnd(selection, x) { onBrushEnd(selection, x) {
return () => { return () => {
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props; const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
const { brushX } = this.state; const { brushXselection } = this.state;
const minAllowedBrushSize = 10; const minAllowedBrushSize = 10;
const smallAmountToAvoidInfiniteLoop = 0.1; const smallAmountToAvoidInfiniteLoop = 0.1;
// ignore programmatically generated events
if (!d3.event.sourceEvent) return;
if (d3.event.selection) { if (d3.event.selection) {
let _range; let _range;
@@ -151,7 +198,7 @@ class HistogramBrush extends React.Component {
_range = [x(d3.event.selection[0]), x(procedurallyResizedBrushWidth)]; _range = [x(d3.event.selection[0]), x(procedurallyResizedBrushWidth)];
d3.event.target.move(brushX, [ d3.event.target.move(brushXselection, [
d3.event.selection[0], d3.event.selection[0],
procedurallyResizedBrushWidth procedurallyResizedBrushWidth
]); ]);
@@ -283,21 +330,20 @@ class HistogramBrush extends React.Component {
/* BRUSH */ /* BRUSH */
const brushX = d3 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) .select(svgRef)
.append("g") .append("g")
.attr("class", "brush") .attr("class", "brush")
.attr("data-testid", `${svgRef.id}-brush`) .attr("data-testid", `${svgRef.id}-brush`)
.call( .call(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))
);
/* AXIS */ /* AXIS */
d3.select(svgRef) d3.select(svgRef)
@@ -318,7 +364,7 @@ class HistogramBrush extends React.Component {
.selectAll(".axis--x line") .selectAll(".axis--x line")
.style("stroke", "rgb(230,230,230)"); .style("stroke", "rgb(230,230,230)");
this.setState({ brushX }); this.setState({ brushX, brushXselection });
} }
render() { render() {
@@ -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;
+3
View File
@@ -7,6 +7,7 @@ import config from "./config";
import universe from "./universe"; import universe from "./universe";
import world from "./world"; import world from "./world";
import categoricalSelection from "./categoricalSelection"; import categoricalSelection from "./categoricalSelection";
import continuousSelection from "./continuousSelection";
import crossfilter from "./crossfilter"; import crossfilter from "./crossfilter";
import colors from "./colors"; import colors from "./colors";
import differential from "./differential"; import differential from "./differential";
@@ -63,6 +64,7 @@ const Reducer = undoable(
["universe", universe], ["universe", universe],
["world", world], ["world", world],
["categoricalSelection", categoricalSelection], ["categoricalSelection", categoricalSelection],
["continuousSelection", continuousSelection],
["crossfilter", crossfilter], ["crossfilter", crossfilter],
["colors", colors], ["colors", colors],
["controls", controls], ["controls", controls],
@@ -73,6 +75,7 @@ const Reducer = undoable(
[ [
"world", "world",
"categoricalSelection", "categoricalSelection",
"continuousSelection",
"crossfilter", "crossfilter",
"colors", "colors",
"controls", "controls",