mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-25 17:38:12 +08:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user