Add hover-over highlighting to the scatterplot (#900)

* small perf improvement - do not recreate functions each time component is updated

* add hover-over highlighting to scatterplot
This commit is contained in:
Bruce Martin
2019-08-27 14:01:30 -07:00
committed by GitHub
parent b359a610da
commit b309367bb0
3 changed files with 121 additions and 89 deletions
+77 -58
View File
@@ -53,6 +53,12 @@ function createModelTF() {
}
function renderThrottle(callback) {
/*
This wraps a call to requestAnimationFrame(), enforcing a single
render callback at any given time (ie, you can call this any number
of times, and it will coallesce multiple inter-frame calls into a
single render).
*/
let rafCurrentlyInProgress = null;
return function f() {
if (rafCurrentlyInProgress) return;
@@ -326,62 +332,6 @@ class Graph extends React.Component {
}
}
const createToolSVG = () => {
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
d3.select("#graphAttachPoint")
.select("#tool")
.remove();
let handleStart;
let handleDrag;
let handleEnd;
let handleCancel;
if (selectionTool === "brush") {
handleStart = this.handleBrushStartAction.bind(this);
handleDrag = this.handleBrushDragAction.bind(this);
handleEnd = this.handleBrushEndAction.bind(this);
} else {
handleStart = this.handleLassoStart.bind(this);
handleEnd = this.handleLassoEnd.bind(this);
handleCancel = this.handleLassoCancel.bind(this);
}
const { svg: newToolSVG, tool, container } = setupSVGandBrushElements(
selectionTool,
handleStart,
handleDrag,
handleEnd,
handleCancel,
responsive,
this.graphPaddingRight,
graphInteractionMode
);
stateChanges = { ...stateChanges, toolSVG: newToolSVG, tool, container };
};
const createCentroidSVG = () => {
d3.select("#graphAttachPoint")
.select("#centroid-container")
.remove();
if (centroidLabel.metadataField === "" || !centroidLabel.centroidXY) {
return;
}
const centroidScreen = this.mapPointToScreen(centroidLabel.centroidXY);
const newCentroidSVG = setupCentroidSVG(
responsive,
this.graphPaddingRight,
centroidScreen,
centroidLabel.categoryField,
colorAccessor
);
stateChanges = { ...stateChanges, centroidSVG: newCentroidSVG };
};
// Centroid SVG creation is disabled for now but should go into the
// first and third cases if enabled
if (
@@ -389,14 +339,14 @@ class Graph extends React.Component {
prevProps.responsive.width !== responsive.width
) {
// If the window size has changed we want to recreate all SVGs
createToolSVG();
stateChanges = { ...stateChanges, ...this.createToolSVG() };
} else if (
(responsive.height && responsive.width && !toolSVG) ||
selectionTool !== prevProps.selectionTool ||
prevProps.graphInteractionMode !== graphInteractionMode
) {
// first time or change of selection tool6
createToolSVG();
stateChanges = { ...stateChanges, ...this.createToolSVG() };
} else if (
centroidLabel !== prevProps.centroidLabel ||
(responsive.height && responsive.width && !centroidSVG)
@@ -433,6 +383,75 @@ class Graph extends React.Component {
}
};
createToolSVG() {
/*
Called from componentDidUpdate. Create the tool SVG, and return any
state changes that should be passed to setState().
*/
const { responsive, selectionTool, graphInteractionMode } = this.props;
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
d3.select("#graphAttachPoint")
.select("#tool")
.remove();
let handleStart;
let handleDrag;
let handleEnd;
let handleCancel;
if (selectionTool === "brush") {
handleStart = this.handleBrushStartAction.bind(this);
handleDrag = this.handleBrushDragAction.bind(this);
handleEnd = this.handleBrushEndAction.bind(this);
} else {
handleStart = this.handleLassoStart.bind(this);
handleEnd = this.handleLassoEnd.bind(this);
handleCancel = this.handleLassoCancel.bind(this);
}
const { svg: newToolSVG, tool, container } = setupSVGandBrushElements(
selectionTool,
handleStart,
handleDrag,
handleEnd,
handleCancel,
responsive,
this.graphPaddingRight,
graphInteractionMode
);
return { toolSVG: newToolSVG, tool, container };
}
createCentroidSVG() {
/*
Called from componentDidUpdate. Create the centroid SVG, and return any
state changes that should be passed to setState().
CURRENTLY UNUSED
*/
const { responsive, centroidLabel, colorAccessor } = this.props;
d3.select("#graphAttachPoint")
.select("#centroid-container")
.remove();
if (centroidLabel.metadataField === "" || !centroidLabel.centroidXY) {
return {};
}
const centroidScreen = this.mapPointToScreen(centroidLabel.centroidXY);
const newCentroidSVG = setupCentroidSVG(
responsive,
this.graphPaddingRight,
centroidScreen,
centroidLabel.categoryField,
colorAccessor
);
return { centroidSVG: newCentroidSVG };
}
brushToolUpdate(tool, container) {
/*
this is called from componentDidUpdate(), so be very careful using
@@ -26,8 +26,7 @@ export default function(regl) {
bool isNaN, isSelected, isHighlight;
getFlags(flag, isNaN, isSelected, isHighlight);
float size = isHighlight ? 8. : isSelected ? 4. : 1.;
gl_PointSize = size;
gl_PointSize = isHighlight ? 8. : isSelected ? 4. : 1.;
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle);
vec3 xy = projection * vec3(position, 1.);
@@ -25,12 +25,12 @@ function createProjectionTF(viewportWidth, viewportHeight) {
@connect(state => {
const { world, crossfilter, universe } = state;
const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls;
const expressionX =
scatterplotXXaccessor &&
world.varData.col(scatterplotXXaccessor)?.asArray();
const expressionY =
scatterplotYYaccessor &&
world.varData.col(scatterplotYYaccessor)?.asArray();
const expressionX = scatterplotXXaccessor
? world.varData.col(scatterplotXXaccessor)?.asArray()
: null;
const expressionY = scatterplotYYaccessor
? world.varData.col(scatterplotYYaccessor)?.asArray()
: null;
return {
world,
@@ -40,6 +40,8 @@ function createProjectionTF(viewportWidth, viewportHeight) {
colorScale: state.colors.scale,
colorAccessor: state.colors.colorAccessor,
centroidLabel: state.centroidLabel,
// Accessors are var/gene names (strings)
scatterplotXXaccessor,
scatterplotYYaccessor,
@@ -85,32 +87,42 @@ class Scatterplot extends React.Component {
}
);
computePointFlags = memoize((world, crossfilter, colorAccessor) => {
const flagSelected = 1;
const flagNaN = 2;
// XXX - coming soon.
// const flagHighlight = 4;
computePointFlags = memoize(
(world, crossfilter, colorAccessor, centroidLabel) => {
const flagSelected = 1;
const flagNaN = 2;
const flagHighlight = 4;
const flags = this.computeSelectedFlags(
crossfilter,
flagSelected,
0
).slice();
const flags = this.computeSelectedFlags(
crossfilter,
flagSelected,
0
).slice();
const colorByColumn = colorAccessor
? world.obsAnnotations.col(colorAccessor)?.asArray() ||
world.varData.col(colorAccessor)?.asArray()
: null;
const colorByData =
colorByColumn && isTypedArray(colorByColumn) ? colorByColumn : null;
const { metadataField, categoryField } = centroidLabel;
const highlightData = metadataField
? world.obsAnnotations.col(metadataField)?.asArray()
: null;
const colorByColumn = colorAccessor
? world.obsAnnotations.col(colorAccessor)?.asArray() ||
world.varData.col(colorAccessor)?.asArray()
: null;
const colorByData =
colorByColumn && isTypedArray(colorByColumn) ? colorByColumn : null;
if (colorByData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
if (colorByData || highlightData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (highlightData) {
flags[i] += highlightData[i] === categoryField ? flagHighlight : 0;
}
if (colorByData) {
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
}
}
}
return flags;
}
return flags;
});
);
constructor(props) {
super(props);
@@ -183,7 +195,8 @@ class Scatterplot extends React.Component {
expressionX,
expressionY,
colorRGB,
colorAccessor
colorAccessor,
centroidLabel
} = this.props;
const {
regl,
@@ -233,7 +246,8 @@ class Scatterplot extends React.Component {
const newFlags = this.computePointFlags(
world,
crossfilter,
colorAccessor
colorAccessor,
centroidLabel
);
if (renderCache.flags !== newFlags) {
renderCache.flags = newFlags;