mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-09 06:28:13 +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) {
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user