mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-09 20:30:57 +08:00
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:
@@ -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;
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user