mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-27 14:08:11 +08:00
draw labels marking the centroids of category value clusters (#809)
* Connect mouse over events to reducer actions * Change Styling on hover * Rename reducer actions to be more descriptive * Reorder reducer in cascade * Create centroid calculation util * Whitespace * Typo fix, use correct action * Create centroid calc util * Create centroid svg setup * Refactor existing svg layer to toolSVG * Change calcCentroid signature and centroidXY to match mapPointToScreen * Add id and styling * Run prettier * Set z-index to 999 * Draw the label * Introduce the centroid SVG, refactor code to allow both SVG layers * Add text label and compute radius based on population * Implement optional chaining * Update font family * Optimize calcMeanCentroid() * Create and utilize calcMedianCentroid() * Remove mass circle from label * Remove styling change on hover * Remove reducer action logs * Prettier * Swap out binds for arrow functions * Style text * switch from selectAll() to select() * Reflect centroid container's purpose in id * Remove mass from the output * Swap to obj * Add finite check * Don't draw centroid if no finite values * Fix finite check * Remove log * Toggle label coloring based on colorBy state * Pass cursor events through centroid svg
This commit is contained in:
@@ -13,23 +13,41 @@ import * as globals from "../../globals";
|
||||
world: state.world
|
||||
}))
|
||||
class CategoryValue extends React.Component {
|
||||
toggleOff() {
|
||||
toggleOff = () => {
|
||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||
dispatch({
|
||||
type: "categorical metadata filter deselect",
|
||||
metadataField,
|
||||
categoryIndex
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
toggleOn() {
|
||||
toggleOn = () => {
|
||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||
dispatch({
|
||||
type: "categorical metadata filter select",
|
||||
metadataField,
|
||||
categoryIndex
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseEnter = () => {
|
||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||
dispatch({
|
||||
type: "category value mouse hover start",
|
||||
metadataField,
|
||||
categoryIndex
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseExit = () => {
|
||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||
dispatch({
|
||||
type: "category value mouse hover end",
|
||||
metadataField,
|
||||
categoryIndex
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -79,6 +97,8 @@ class CategoryValue extends React.Component {
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
data-testclass="categorical-row"
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseExit}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -92,9 +112,7 @@ class CategoryValue extends React.Component {
|
||||
>
|
||||
<label className="bp3-control bp3-checkbox">
|
||||
<input
|
||||
onChange={
|
||||
selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)
|
||||
}
|
||||
onChange={selected ? this.toggleOff : this.toggleOn}
|
||||
data-testclass="categorical-value-select"
|
||||
data-testid={`categorical-value-select-${metadataField}-${displayString}`}
|
||||
checked={selected}
|
||||
|
||||
@@ -8,6 +8,8 @@ import memoize from "memoize-one";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import setupSVGandBrushElements from "./setupSVGandBrush";
|
||||
import setupCentroidSVG from "./setupCentroidSVG";
|
||||
import actions from "../../actions";
|
||||
import _camera from "../../util/camera";
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import scaleLinear from "../../util/scaleLinear";
|
||||
@@ -22,7 +24,9 @@ import scaleLinear from "../../util/scaleLinear";
|
||||
selectionTool: state.graphSelection.tool,
|
||||
currentSelection: state.graphSelection.selection,
|
||||
layoutChoice: state.layoutChoice,
|
||||
graphInteractionMode: state.controls.graphInteractionMode
|
||||
centroidLabel: state.centroidLabel,
|
||||
graphInteractionMode: state.controls.graphInteractionMode,
|
||||
colorAccessor: state.colors.colorAccessor
|
||||
}))
|
||||
class Graph extends React.Component {
|
||||
computePointPositions = memoize((X, Y, scaleX, scaleY) => {
|
||||
@@ -70,7 +74,8 @@ class Graph extends React.Component {
|
||||
sizes: null
|
||||
};
|
||||
this.state = {
|
||||
svg: null,
|
||||
toolSVG: null,
|
||||
centroidSVG: null,
|
||||
tool: null,
|
||||
container: null
|
||||
};
|
||||
@@ -139,9 +144,11 @@ class Graph extends React.Component {
|
||||
selectionTool,
|
||||
currentSelection,
|
||||
layoutChoice,
|
||||
graphInteractionMode
|
||||
graphInteractionMode,
|
||||
colorAccessor,
|
||||
centroidLabel
|
||||
} = this.props;
|
||||
const { reglRender, regl, svg } = this.state;
|
||||
const { reglRender, mode, regl, toolSVG, centroidSVG } = this.state;
|
||||
let stateChanges = {};
|
||||
|
||||
if (reglRender) {
|
||||
@@ -216,16 +223,10 @@ class Graph extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.responsive.height !== responsive.height ||
|
||||
prevProps.responsive.width !== responsive.width ||
|
||||
/* first time */
|
||||
(responsive.height && responsive.width && !svg) ||
|
||||
selectionTool !== prevProps.selectionTool
|
||||
) {
|
||||
const createToolSVG = () => {
|
||||
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
|
||||
d3.select("#graphAttachPoint")
|
||||
.selectAll("svg")
|
||||
.select("#tool")
|
||||
.remove();
|
||||
|
||||
let handleStart;
|
||||
@@ -241,16 +242,63 @@ class Graph extends React.Component {
|
||||
handleEnd = this.handleLassoEnd.bind(this);
|
||||
handleCancel = this.handleLassoCancel.bind(this);
|
||||
}
|
||||
const { svg: newSvg, tool, container } = setupSVGandBrushElements(
|
||||
|
||||
const { svg: newToolSVG, tool, container } = setupSVGandBrushElements(
|
||||
selectionTool,
|
||||
handleStart,
|
||||
handleDrag,
|
||||
handleEnd,
|
||||
handleCancel,
|
||||
responsive,
|
||||
this.graphPaddingRight
|
||||
this.graphPaddingRight,
|
||||
graphInteractionMode
|
||||
);
|
||||
stateChanges = { ...stateChanges, svg: newSvg, tool, container };
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
if (
|
||||
prevProps.responsive.height !== responsive.height ||
|
||||
prevProps.responsive.width !== responsive.width
|
||||
) {
|
||||
// If the window size has changed we want to recreate all SVGs
|
||||
createToolSVG();
|
||||
createCentroidSVG();
|
||||
} else if (
|
||||
(responsive.height && responsive.width && !toolSVG) ||
|
||||
selectionTool !== prevProps.selectionTool ||
|
||||
prevProps.graphInteractionMode !== graphInteractionMode
|
||||
) {
|
||||
// first time or change of selection tool6
|
||||
createToolSVG();
|
||||
} else if (
|
||||
centroidLabel !== prevProps.centroidLabel ||
|
||||
(responsive.height && responsive.width && !centroidSVG)
|
||||
) {
|
||||
// First time for centroid or label change
|
||||
createCentroidSVG();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -260,7 +308,7 @@ class Graph extends React.Component {
|
||||
if (
|
||||
currentSelection !== prevProps.currentSelection ||
|
||||
graphInteractionMode !== prevProps.graphInteractionMode ||
|
||||
stateChanges.svg
|
||||
stateChanges.toolSVG
|
||||
) {
|
||||
const { tool, container } = this.state;
|
||||
this.selectionToolUpdate(
|
||||
@@ -588,12 +636,7 @@ class Graph extends React.Component {
|
||||
right: 0
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: graphInteractionMode === "select" ? "inherit" : "none"
|
||||
}}
|
||||
id="graphAttachPoint"
|
||||
/>
|
||||
<div id="graphAttachPoint" />
|
||||
<div style={{ padding: 0, margin: 0 }}>
|
||||
<canvas
|
||||
width={responsive.width - this.graphPaddingRight}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as d3 from "d3";
|
||||
import styles from "./graph.css";
|
||||
|
||||
export default (responsive, graphPaddingRight, xy, text, colorBy) => {
|
||||
const containerWidth = responsive.width - graphPaddingRight;
|
||||
|
||||
const svg = d3
|
||||
.select("#graphAttachPoint")
|
||||
.append("svg")
|
||||
.attr("id", "centroid-container")
|
||||
.attr("data-testid", "centroid-overlay")
|
||||
.attr("width", containerWidth)
|
||||
.attr("height", responsive.height)
|
||||
.attr("class", `${styles.graphSVG}`)
|
||||
.style("z-index", 998)
|
||||
.style("pointer-events", "none");
|
||||
// TODO: Create own styles, ask Colin for an explanation on the css
|
||||
// For now I'm going to put centroid z-index at 998 and lasso on 999
|
||||
|
||||
const label = svg
|
||||
.append("g")
|
||||
.attr("transform", `translate(${xy[0]}, ${xy[1]})`);
|
||||
|
||||
label
|
||||
.append("text")
|
||||
.attr("text-anchor", "middle")
|
||||
.text(text)
|
||||
.style("font-family", "Roboto Condensed")
|
||||
.style("font-size", "18px")
|
||||
.style("font-weight", "700")
|
||||
.style("fill", colorBy ? "black" : "rgb(32, 178, 212)");
|
||||
|
||||
return svg;
|
||||
};
|
||||
@@ -16,15 +16,19 @@ export default (
|
||||
handleEndAction,
|
||||
handleCancelAction,
|
||||
responsive,
|
||||
graphPaddingRight
|
||||
graphPaddingRight,
|
||||
graphInteractionMode
|
||||
) => {
|
||||
const svg = d3
|
||||
.select("#graphAttachPoint")
|
||||
.append("svg")
|
||||
.attr("id", "tool")
|
||||
.attr("data-testid", "layout-overlay")
|
||||
.attr("width", responsive.width - graphPaddingRight)
|
||||
.attr("height", responsive.height)
|
||||
.attr("class", `${styles.graphSVG}`);
|
||||
.attr("class", `${styles.graphSVG}`)
|
||||
.style("z-index", 999)
|
||||
.style("display", graphInteractionMode === "select" ? "inherit" : "none");
|
||||
|
||||
if (selectionToolType === "brush") {
|
||||
const brush = d3
|
||||
|
||||
Reference in New Issue
Block a user