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) { 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; let rafCurrentlyInProgress = null;
return function f() { return function f() {
if (rafCurrentlyInProgress) return; 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 // Centroid SVG creation is disabled for now but should go into the
// first and third cases if enabled // first and third cases if enabled
if ( if (
@@ -389,14 +339,14 @@ class Graph extends React.Component {
prevProps.responsive.width !== responsive.width prevProps.responsive.width !== responsive.width
) { ) {
// If the window size has changed we want to recreate all SVGs // If the window size has changed we want to recreate all SVGs
createToolSVG(); stateChanges = { ...stateChanges, ...this.createToolSVG() };
} else if ( } else if (
(responsive.height && responsive.width && !toolSVG) || (responsive.height && responsive.width && !toolSVG) ||
selectionTool !== prevProps.selectionTool || selectionTool !== prevProps.selectionTool ||
prevProps.graphInteractionMode !== graphInteractionMode prevProps.graphInteractionMode !== graphInteractionMode
) { ) {
// first time or change of selection tool6 // first time or change of selection tool6
createToolSVG(); stateChanges = { ...stateChanges, ...this.createToolSVG() };
} else if ( } else if (
centroidLabel !== prevProps.centroidLabel || centroidLabel !== prevProps.centroidLabel ||
(responsive.height && responsive.width && !centroidSVG) (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) { brushToolUpdate(tool, container) {
/* /*
this is called from componentDidUpdate(), so be very careful using this is called from componentDidUpdate(), so be very careful using
@@ -26,8 +26,7 @@ export default function(regl) {
bool isNaN, isSelected, isHighlight; bool isNaN, isSelected, isHighlight;
getFlags(flag, isNaN, isSelected, isHighlight); getFlags(flag, isNaN, isSelected, isHighlight);
float size = isHighlight ? 8. : isSelected ? 4. : 1.; gl_PointSize = isHighlight ? 8. : isSelected ? 4. : 1.;
gl_PointSize = size;
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle); float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle);
vec3 xy = projection * vec3(position, 1.); vec3 xy = projection * vec3(position, 1.);
@@ -25,12 +25,12 @@ function createProjectionTF(viewportWidth, viewportHeight) {
@connect(state => { @connect(state => {
const { world, crossfilter, universe } = state; const { world, crossfilter, universe } = state;
const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls; const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls;
const expressionX = const expressionX = scatterplotXXaccessor
scatterplotXXaccessor && ? world.varData.col(scatterplotXXaccessor)?.asArray()
world.varData.col(scatterplotXXaccessor)?.asArray(); : null;
const expressionY = const expressionY = scatterplotYYaccessor
scatterplotYYaccessor && ? world.varData.col(scatterplotYYaccessor)?.asArray()
world.varData.col(scatterplotYYaccessor)?.asArray(); : null;
return { return {
world, world,
@@ -40,6 +40,8 @@ function createProjectionTF(viewportWidth, viewportHeight) {
colorScale: state.colors.scale, colorScale: state.colors.scale,
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
centroidLabel: state.centroidLabel,
// Accessors are var/gene names (strings) // Accessors are var/gene names (strings)
scatterplotXXaccessor, scatterplotXXaccessor,
scatterplotYYaccessor, scatterplotYYaccessor,
@@ -85,32 +87,42 @@ class Scatterplot extends React.Component {
} }
); );
computePointFlags = memoize((world, crossfilter, colorAccessor) => { computePointFlags = memoize(
const flagSelected = 1; (world, crossfilter, colorAccessor, centroidLabel) => {
const flagNaN = 2; const flagSelected = 1;
// XXX - coming soon. const flagNaN = 2;
// const flagHighlight = 4; const flagHighlight = 4;
const flags = this.computeSelectedFlags( const flags = this.computeSelectedFlags(
crossfilter, crossfilter,
flagSelected, flagSelected,
0 0
).slice(); ).slice();
const colorByColumn = colorAccessor const { metadataField, categoryField } = centroidLabel;
? world.obsAnnotations.col(colorAccessor)?.asArray() || const highlightData = metadataField
world.varData.col(colorAccessor)?.asArray() ? world.obsAnnotations.col(metadataField)?.asArray()
: null; : null;
const colorByData = const colorByColumn = colorAccessor
colorByColumn && isTypedArray(colorByColumn) ? colorByColumn : null; ? world.obsAnnotations.col(colorAccessor)?.asArray() ||
world.varData.col(colorAccessor)?.asArray()
: null;
const colorByData =
colorByColumn && isTypedArray(colorByColumn) ? colorByColumn : null;
if (colorByData) { if (colorByData || highlightData) {
for (let i = 0, len = flags.length; i < len; i += 1) { for (let i = 0, len = flags.length; i < len; i += 1) {
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN; 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) { constructor(props) {
super(props); super(props);
@@ -183,7 +195,8 @@ class Scatterplot extends React.Component {
expressionX, expressionX,
expressionY, expressionY,
colorRGB, colorRGB,
colorAccessor colorAccessor,
centroidLabel
} = this.props; } = this.props;
const { const {
regl, regl,
@@ -233,7 +246,8 @@ class Scatterplot extends React.Component {
const newFlags = this.computePointFlags( const newFlags = this.computePointFlags(
world, world,
crossfilter, crossfilter,
colorAccessor colorAccessor,
centroidLabel
); );
if (renderCache.flags !== newFlags) { if (renderCache.flags !== newFlags) {
renderCache.flags = newFlags; renderCache.flags = newFlags;