mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 21:08:11 +08:00
centroid labels (#872)
* refactor reducer to no longer support hover state and hold many labels * refactor to generate centroidCoordinates for all values of a category * create hash for function and memoize export * create button to display all labels for a category * clear state * create label for each thing * calculate on each value * change to in place modification of map * switch to for loop with iterator instead of forEach * use map from centroidLabel instead of creating copy * adapt for map * utilize tarrays * begin documentation * disable centroids if in zoom mode * clean up * persist uncalc coordinates * document * clean up and document * cleanup and document * fix * fix undefined labels and document changes * fix first element skip * fix conditional recalc * rename centroidLabel -> centroidLabels * break out dilation on hover to new reducer * numerous styling changes for readability * change centroid icon * remove colorAccessor from parameters * recalc centroids on world change * make label toggle undoable * remove unused import * highlight labels on hover * remove special characters from svg id * lighten backdrop * only generate new centroids if they pre-exist * fix issue with spaces in catagorical value name * add label buttons to menubar * change reducer to use colorAccessor and have single toggle * fix check to see if svg should be rendered * move svg overlays onto a single svg layer * dilate on label hover * remove logs * allow centroid to update along side regl renders * allow actions to pass through svg if in zoom mode * remove artifact from circle * remove comment * remove disabling of centroid button * fix conditional map to screen * make styling label conditions stricter * prettier * refactor onto master * refactor computePointFlags() to use pointDilation store * notify when viewport changes * move svg attributes out of lasso setup and prevent rerenders/writes * begin playing with transform matrix * first solution for camera interaction * create transform using nested groups * semi-working method using nested groups with transforms * inversely scale text * properly do final transform * cleanup dead / test code * reinstate original functionality * breakout centroid labels labels into separate component * default toggle on for testing * separate lasso and centroid layers * remove unnecessary attributes, working hover * dilation on label hover * fix dilation on scatterplot * add dilation on label hover * break overlay into separate component * make overlay agnostic to children * move label mouse actions to centroidlabels component, add overlay state * remove lasso on switch to camera * disallow user selection * fix reducer * fix subset with continuous color error * reset labels on color by continuous * revert centroids on by default * refactor for nested restructuring * remove update checking * remove unused method * readd deleted hover delay * remove old centroid setup * remove centroid from undoable * cleanup dead code * remove dead code * rollback unnecessary changes * begin adding annotation functionality * add annotation functionality * add reset and undo functionality * change centroids on layout change * don't create label for unassigned * add comment pointing out POI for performance * touch up matrix transform comment * add comment explaining coordinate space and children's assumed space * remove dead code * switch to pure component * connect centroidLabels to redux * clean up camera check and null result * tool tip change * rename centroid toggle and the like * fix the misalignment of buttons, also make blueprint use consistent * fix comment spelling mistakes * introduce variable for cleaner logic expressions and state assignment * alter tooltip text to back color by interaction * remove manual iterator manipulation for forEach() * remove debounce * nit fix * tooltip wording fix * lint
This commit is contained in:
@@ -323,6 +323,9 @@ const resetInterface = () => (dispatch, getState) => {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: "reset colorscale"
|
type: "reset colorscale"
|
||||||
});
|
});
|
||||||
|
dispatch({
|
||||||
|
type: "reset centroid labels"
|
||||||
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "clear scatterplot"
|
type: "clear scatterplot"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
:local(.hover),
|
||||||
:local(.value):hover {
|
:local(.value):hover {
|
||||||
background: rgba(167, 182, 194, 0.3);
|
background: rgba(167, 182, 194, 0.3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { AnnotationsHelpers } from "../../util/stateManager";
|
|||||||
annotations: state.annotations,
|
annotations: state.annotations,
|
||||||
colorScale: state.colors.scale,
|
colorScale: state.colors.scale,
|
||||||
colorAccessor: state.colors.colorAccessor,
|
colorAccessor: state.colors.colorAccessor,
|
||||||
|
pointDilation: state.pointDilation,
|
||||||
schema: state.world?.schema,
|
schema: state.world?.schema,
|
||||||
world: state.world,
|
world: state.world,
|
||||||
crossfilter: state.crossfilter
|
crossfilter: state.crossfilter
|
||||||
@@ -194,6 +195,7 @@ class CategoryValue extends React.Component {
|
|||||||
const crossfilterChange =
|
const crossfilterChange =
|
||||||
props.isUserAnno && props.crossfilter !== nextProps.crossfilter;
|
props.isUserAnno && props.crossfilter !== nextProps.crossfilter;
|
||||||
const editingLabel = state.editedLabelText !== nextState.editedLabelText;
|
const editingLabel = state.editedLabelText !== nextState.editedLabelText;
|
||||||
|
const dilationChange = props.pointDilation !== nextProps.pointDilation;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
valueSelectionChange ||
|
valueSelectionChange ||
|
||||||
@@ -201,7 +203,8 @@ class CategoryValue extends React.Component {
|
|||||||
colorAccessorChange ||
|
colorAccessorChange ||
|
||||||
annotationsChange ||
|
annotationsChange ||
|
||||||
crossfilterChange ||
|
crossfilterChange ||
|
||||||
editingLabel
|
editingLabel ||
|
||||||
|
dilationChange
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,7 +291,8 @@ class CategoryValue extends React.Component {
|
|||||||
annotations,
|
annotations,
|
||||||
// flippedProps is potentially brittle, their docs want {...flippedProps} on our div,
|
// flippedProps is potentially brittle, their docs want {...flippedProps} on our div,
|
||||||
// our lint doesn't like jsx spread, we are version pinned to prevent api change on their part
|
// our lint doesn't like jsx spread, we are version pinned to prevent api change on their part
|
||||||
flippedProps
|
flippedProps,
|
||||||
|
pointDilation
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { editedLabelText } = this.state;
|
const { editedLabelText } = this.state;
|
||||||
@@ -341,7 +345,15 @@ class CategoryValue extends React.Component {
|
|||||||
data-flip-config={flippedProps["data-flip-config"]}
|
data-flip-config={flippedProps["data-flip-config"]}
|
||||||
data-flip-id={flippedProps["data-flip-id"]}
|
data-flip-id={flippedProps["data-flip-id"]}
|
||||||
data-portal-key={flippedProps["data-portal-key"]}
|
data-portal-key={flippedProps["data-portal-key"]}
|
||||||
className={styles.value}
|
className={
|
||||||
|
/* This code is to change the styles on centroid label hover is causing over-rendering */
|
||||||
|
`${styles.value}${
|
||||||
|
pointDilation.metadataField === metadataField &&
|
||||||
|
pointDilation.categoryField === displayString
|
||||||
|
? ` ${styles.hover}`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
}
|
||||||
data-testclass="categorical-row"
|
data-testclass="categorical-row"
|
||||||
style={{
|
style={{
|
||||||
padding: "4px 7px",
|
padding: "4px 7px",
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import memoize from "memoize-one";
|
|||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import setupSVGandBrushElements from "./setupSVGandBrush";
|
import setupSVGandBrushElements from "./setupSVGandBrush";
|
||||||
import setupCentroidSVG from "./setupCentroidSVG";
|
|
||||||
import _camera from "../../util/camera";
|
import _camera from "../../util/camera";
|
||||||
import _drawPoints from "./drawPointsRegl";
|
import _drawPoints from "./drawPointsRegl";
|
||||||
import { isTypedArray } from "../../util/typeHelpers";
|
import { isTypedArray } from "../../util/typeHelpers";
|
||||||
|
import styles from "./graph.css";
|
||||||
|
|
||||||
|
import GraphOverlayLayer from "./overlays/graphOverlayLayer";
|
||||||
|
import CentroidLabels from "./overlays/centroidLabels";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Simple 2D transforms control all point painting. There are three:
|
Simple 2D transforms control all point painting. There are three:
|
||||||
@@ -79,9 +82,10 @@ function renderThrottle(callback) {
|
|||||||
selectionTool: state.graphSelection.tool,
|
selectionTool: state.graphSelection.tool,
|
||||||
currentSelection: state.graphSelection.selection,
|
currentSelection: state.graphSelection.selection,
|
||||||
layoutChoice: state.layoutChoice,
|
layoutChoice: state.layoutChoice,
|
||||||
centroidLabel: state.centroidLabel,
|
centroidLabels: state.centroidLabels,
|
||||||
graphInteractionMode: state.controls.graphInteractionMode,
|
graphInteractionMode: state.controls.graphInteractionMode,
|
||||||
colorAccessor: state.colors.colorAccessor
|
colorAccessor: state.colors.colorAccessor,
|
||||||
|
pointDilation: state.pointDilation
|
||||||
}))
|
}))
|
||||||
class Graph extends React.PureComponent {
|
class Graph extends React.PureComponent {
|
||||||
computePointPositions = memoize((X, Y, modelTF) => {
|
computePointPositions = memoize((X, Y, modelTF) => {
|
||||||
@@ -121,7 +125,7 @@ class Graph extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
computePointFlags = memoize(
|
computePointFlags = memoize(
|
||||||
(world, crossfilter, colorAccessor, centroidLabel) => {
|
(world, crossfilter, colorAccessor, pointDilation) => {
|
||||||
/*
|
/*
|
||||||
We communicate with the shader using three flags:
|
We communicate with the shader using three flags:
|
||||||
- isNaN -- the value is a NaN. Only makes sense when we have a colorAccessor
|
- isNaN -- the value is a NaN. Only makes sense when we have a colorAccessor
|
||||||
@@ -146,7 +150,7 @@ class Graph extends React.PureComponent {
|
|||||||
0
|
0
|
||||||
).slice();
|
).slice();
|
||||||
|
|
||||||
const { metadataField, categoryField } = centroidLabel;
|
const { metadataField, categoryField } = pointDilation;
|
||||||
const highlightData = metadataField
|
const highlightData = metadataField
|
||||||
? world.obsAnnotations.col(metadataField)?.asArray()
|
? world.obsAnnotations.col(metadataField)?.asArray()
|
||||||
: null;
|
: null;
|
||||||
@@ -186,9 +190,9 @@ class Graph extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
this.state = {
|
this.state = {
|
||||||
toolSVG: null,
|
toolSVG: null,
|
||||||
centroidSVG: null,
|
|
||||||
tool: null,
|
tool: null,
|
||||||
container: null
|
container: null,
|
||||||
|
cameraRender: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,23 +249,17 @@ class Graph extends React.PureComponent {
|
|||||||
currentSelection,
|
currentSelection,
|
||||||
layoutChoice,
|
layoutChoice,
|
||||||
graphInteractionMode,
|
graphInteractionMode,
|
||||||
colorAccessor,
|
pointDilation,
|
||||||
centroidLabel
|
colorAccessor
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { regl, toolSVG, centroidSVG } = this.state;
|
const { regl, toolSVG, camera, modelTF } = this.state;
|
||||||
let stateChanges = {};
|
let stateChanges = {};
|
||||||
|
|
||||||
if (regl && world) {
|
if (regl && world) {
|
||||||
/* update the regl and point rendering state */
|
/* update the regl and point rendering state */
|
||||||
const { obsLayout, nObs } = world;
|
const { obsLayout, nObs } = world;
|
||||||
const {
|
const { drawPoints, pointBuffer, colorBuffer, flagBuffer } = this.state;
|
||||||
drawPoints,
|
|
||||||
camera,
|
|
||||||
pointBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
flagBuffer,
|
|
||||||
modelTF
|
|
||||||
} = this.state;
|
|
||||||
let { projectionTF } = this.state;
|
let { projectionTF } = this.state;
|
||||||
let needsRepaint = false;
|
let needsRepaint = false;
|
||||||
|
|
||||||
@@ -305,12 +303,12 @@ class Graph extends React.PureComponent {
|
|||||||
world,
|
world,
|
||||||
crossfilter,
|
crossfilter,
|
||||||
colorAccessor,
|
colorAccessor,
|
||||||
centroidLabel
|
pointDilation
|
||||||
);
|
);
|
||||||
if (renderCache.flags !== newFlags) {
|
if (renderCache.flags !== newFlags) {
|
||||||
renderCache.flags = newFlags;
|
renderCache.flags = newFlags;
|
||||||
flagBuffer({ data: newFlags, dimension: 1 });
|
|
||||||
needsRepaint = true;
|
needsRepaint = true;
|
||||||
|
flagBuffer({ data: newFlags, dimension: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.count = nObs;
|
this.count = nObs;
|
||||||
@@ -328,26 +326,27 @@ class Graph extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centroid SVG creation is disabled for now but should go into the
|
|
||||||
// first and third cases if enabled
|
|
||||||
if (
|
if (
|
||||||
prevProps.responsive.height !== responsive.height ||
|
prevProps.responsive.height !== responsive.height ||
|
||||||
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
|
||||||
stateChanges = { ...stateChanges, ...this.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
|
|
||||||
) {
|
) {
|
||||||
// first time or change of selection tool6
|
// first time or change of selection tool
|
||||||
stateChanges = { ...stateChanges, ...this.createToolSVG() };
|
stateChanges = { ...stateChanges, ...this.createToolSVG(true) };
|
||||||
} else if (
|
} else if (prevProps.graphInteractionMode !== graphInteractionMode) {
|
||||||
centroidLabel !== prevProps.centroidLabel ||
|
// If lasso/zoom is switched
|
||||||
(responsive.height && responsive.width && !centroidSVG)
|
stateChanges = {
|
||||||
) {
|
...stateChanges,
|
||||||
// First time for centroid or label change
|
...this.createToolSVG()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -365,7 +364,6 @@ class Graph extends React.PureComponent {
|
|||||||
stateChanges.container ? stateChanges.container : container
|
stateChanges.container ? stateChanges.container : container
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(stateChanges).length > 0) {
|
if (Object.keys(stateChanges).length > 0) {
|
||||||
this.setState(stateChanges);
|
this.setState(stateChanges);
|
||||||
}
|
}
|
||||||
@@ -376,10 +374,13 @@ class Graph extends React.PureComponent {
|
|||||||
if (e.type !== "wheel") e.preventDefault();
|
if (e.type !== "wheel") e.preventDefault();
|
||||||
if (camera.handleEvent(e, projectionTF)) {
|
if (camera.handleEvent(e, projectionTF)) {
|
||||||
this.renderCanvas();
|
this.renderCanvas();
|
||||||
|
this.setState(state => {
|
||||||
|
return { ...state, updateOverlay: !state.updateOverlay };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createToolSVG() {
|
createToolSVG = () => {
|
||||||
/*
|
/*
|
||||||
Called from componentDidUpdate. Create the tool SVG, and return any
|
Called from componentDidUpdate. Create the tool SVG, and return any
|
||||||
state changes that should be passed to setState().
|
state changes that should be passed to setState().
|
||||||
@@ -387,10 +388,14 @@ class Graph extends React.PureComponent {
|
|||||||
const { responsive, selectionTool, graphInteractionMode } = this.props;
|
const { responsive, selectionTool, graphInteractionMode } = this.props;
|
||||||
|
|
||||||
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
|
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
|
||||||
d3.select("#graphAttachPoint")
|
|
||||||
.select("#tool")
|
d3.select("#lasso-layer")
|
||||||
|
.selectAll(".lasso-group")
|
||||||
.remove();
|
.remove();
|
||||||
|
|
||||||
|
// Don't render or recreate toolSVG if currently in zoom mode
|
||||||
|
if (graphInteractionMode !== "select") return { toolSVG: undefined };
|
||||||
|
|
||||||
let handleStart;
|
let handleStart;
|
||||||
let handleDrag;
|
let handleDrag;
|
||||||
let handleEnd;
|
let handleEnd;
|
||||||
@@ -417,36 +422,7 @@ class Graph extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return { toolSVG: newToolSVG, tool, container };
|
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.graphPaddingRightLeft,
|
|
||||||
centroidScreen,
|
|
||||||
centroidLabel.categoryField,
|
|
||||||
colorAccessor
|
|
||||||
);
|
|
||||||
|
|
||||||
return { centroidSVG: newCentroidSVG };
|
|
||||||
}
|
|
||||||
|
|
||||||
brushToolUpdate(tool, container) {
|
brushToolUpdate(tool, container) {
|
||||||
/*
|
/*
|
||||||
@@ -745,7 +721,10 @@ class Graph extends React.PureComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { responsive } = this.props;
|
const { responsive, graphInteractionMode } = this.props;
|
||||||
|
const { modelTF, projectionTF, camera } = this.state;
|
||||||
|
|
||||||
|
const cameraTF = camera?.view();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="graphWrapper">
|
<div id="graphWrapper">
|
||||||
@@ -757,7 +736,30 @@ class Graph extends React.PureComponent {
|
|||||||
right: globals.leftSidebarWidth
|
right: globals.leftSidebarWidth
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div id="graphAttachPoint" />
|
<div id="graphAttachPoint">
|
||||||
|
<GraphOverlayLayer
|
||||||
|
cameraTF={cameraTF}
|
||||||
|
modelTF={modelTF}
|
||||||
|
projectionTF={projectionTF}
|
||||||
|
graphPaddingRightLeft={this.graphPaddingRightLeft}
|
||||||
|
graphPaddingTop={this.graphPaddingTop}
|
||||||
|
responsive={responsive}
|
||||||
|
>
|
||||||
|
<CentroidLabels />
|
||||||
|
</GraphOverlayLayer>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
id="lasso-layer"
|
||||||
|
data-testid="layout-overlay"
|
||||||
|
className={styles.graphSVG}
|
||||||
|
width={responsive.width - this.graphPaddingRightLeft}
|
||||||
|
height={responsive.height}
|
||||||
|
pointerEvents={
|
||||||
|
graphInteractionMode === "select" ? "auto" : "none"
|
||||||
|
}
|
||||||
|
style={{ zIndex: 89 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div style={{ padding: 0, margin: 0 }}>
|
<div style={{ padding: 0, margin: 0 }}>
|
||||||
<canvas
|
<canvas
|
||||||
width={responsive.width - this.graphPaddingRightLeft}
|
width={responsive.width - this.graphPaddingRightLeft}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* eslint-disable jsx-a11y/mouse-events-have-key-events */
|
||||||
|
import React, { PureComponent } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
export default
|
||||||
|
@connect(state => ({
|
||||||
|
colorAccessor: state.colors.colorAccessor,
|
||||||
|
dilatedValue: state.pointDilation.categoryField,
|
||||||
|
labels: state.centroidLabels.labels
|
||||||
|
}))
|
||||||
|
class CentroidLabels extends PureComponent {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
labels,
|
||||||
|
inverseTransform,
|
||||||
|
dilatedValue,
|
||||||
|
dispatch,
|
||||||
|
colorAccessor
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const labelSVGS = [];
|
||||||
|
let fontSize = "15px";
|
||||||
|
let fontWeight = null;
|
||||||
|
labels.forEach((value, key) => {
|
||||||
|
fontSize = "15px";
|
||||||
|
fontWeight = null;
|
||||||
|
if (key === dilatedValue) {
|
||||||
|
fontSize = "18px";
|
||||||
|
fontWeight = "800";
|
||||||
|
}
|
||||||
|
labelSVGS.push(
|
||||||
|
<g
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={key}
|
||||||
|
className="centroid-label"
|
||||||
|
transform={`translate(${value[0]}, ${value[1]})`}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
transform={inverseTransform}
|
||||||
|
textAnchor="middle"
|
||||||
|
data-label={key}
|
||||||
|
style={{
|
||||||
|
fontFamily: "Roboto Condensed",
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
fill: "black",
|
||||||
|
userSelect: "none"
|
||||||
|
}}
|
||||||
|
onMouseEnter={e =>
|
||||||
|
dispatch({
|
||||||
|
type: "category value mouse hover start",
|
||||||
|
metadataField: colorAccessor,
|
||||||
|
categoryField: e.target.getAttribute("data-label")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMouseOut={e =>
|
||||||
|
dispatch({
|
||||||
|
type: "category value mouse hover end",
|
||||||
|
metadataField: colorAccessor,
|
||||||
|
categoryField: e.target.getAttribute("data-label")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pointerEvents="visiblePainted"
|
||||||
|
>
|
||||||
|
{key.length > 20 ? `${key.substr(0, 20)}...` : key}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>{labelSVGS}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { PureComponent, cloneElement } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import styles from "../graph.css";
|
||||||
|
|
||||||
|
export default
|
||||||
|
@connect(state => ({
|
||||||
|
responsive: state.responsive
|
||||||
|
}))
|
||||||
|
class GraphOverlayLayer extends PureComponent {
|
||||||
|
/*
|
||||||
|
This component takes its children (assumed in the data coordinate space ([0, 1] range, origin in bottom left corner))
|
||||||
|
and transforms itself multiple times resulting in screen space ([0, screenWidth/Height] range, origin in top left corner)
|
||||||
|
|
||||||
|
Children are assigned in the graph component
|
||||||
|
*/
|
||||||
|
matrixToTransformString = m => {
|
||||||
|
/*
|
||||||
|
Translates the gl-matrix mat3 to SVG matrix transform style
|
||||||
|
|
||||||
|
mat3 SVG Transform Function
|
||||||
|
a c e
|
||||||
|
b d f / [a, b, 0, c, d, 0, e, f, 1] => matrix(a, b, c, d, e, f) / matrix(sx, 0, 0, sy, tx, ty) / matrix(m[0] m[3] m[1] m[4] m[6] m[7])
|
||||||
|
0 0 1
|
||||||
|
*/
|
||||||
|
return `matrix(${m[0]} ${m[1]} ${m[3]} ${m[4]} ${m[6]} ${m[7]})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
reverseMatrixScaleTransformString = m => {
|
||||||
|
return `matrix(${1 / m[0]} 0 0 ${1 / m[4]} 0 0)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
cameraTF,
|
||||||
|
modelTF,
|
||||||
|
projectionTF,
|
||||||
|
responsive,
|
||||||
|
graphPaddingRightLeft,
|
||||||
|
graphPaddingTop,
|
||||||
|
children
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!cameraTF) return null;
|
||||||
|
|
||||||
|
const inverseTransform = `${this.reverseMatrixScaleTransformString(
|
||||||
|
modelTF
|
||||||
|
)} ${this.reverseMatrixScaleTransformString(
|
||||||
|
cameraTF
|
||||||
|
)} ${this.reverseMatrixScaleTransformString(
|
||||||
|
projectionTF
|
||||||
|
)} scale(1 2) scale(1 ${1 /
|
||||||
|
-(responsive.height - graphPaddingTop)}) scale(2 1) scale(${1 /
|
||||||
|
(responsive.width - graphPaddingRightLeft)} 1)`;
|
||||||
|
|
||||||
|
const newChildren = React.Children.toArray(children);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={styles.graphSVG}
|
||||||
|
width={responsive.width - graphPaddingRightLeft}
|
||||||
|
height={responsive.height}
|
||||||
|
pointerEvents="none"
|
||||||
|
style={{ zIndex: 99 }}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="canvas-transformation-group-x"
|
||||||
|
transform={`scale(${responsive.width -
|
||||||
|
graphPaddingRightLeft} 1) scale(.5 1) translate(1 0)`}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="canvas-transformation-group-y"
|
||||||
|
transform={`scale(1 ${-(
|
||||||
|
responsive.height - graphPaddingTop
|
||||||
|
)}) translate(0 -1) scale(1 .5) translate(0 1)`}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="projection-transformation-group"
|
||||||
|
transform={this.matrixToTransformString(projectionTF)}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="camera-transformation-group"
|
||||||
|
transform={this.matrixToTransformString(cameraTF)}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="model-transformation-group"
|
||||||
|
transform={this.matrixToTransformString(modelTF)}
|
||||||
|
>
|
||||||
|
{newChildren.map(child =>
|
||||||
|
cloneElement(child, { inverseTransform })
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import styles from "./graph.css";
|
|
||||||
import Lasso from "./setupLasso";
|
import Lasso from "./setupLasso";
|
||||||
|
|
||||||
/******************************************
|
/******************************************
|
||||||
@@ -16,19 +15,9 @@ export default (
|
|||||||
handleEndAction,
|
handleEndAction,
|
||||||
handleCancelAction,
|
handleCancelAction,
|
||||||
responsive,
|
responsive,
|
||||||
graphPaddingRight,
|
graphPaddingRight
|
||||||
graphInteractionMode
|
|
||||||
) => {
|
) => {
|
||||||
const svg = d3
|
const svg = d3.select("#graphAttachPoint").select("#lasso-layer");
|
||||||
.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}`)
|
|
||||||
.style("z-index", 999)
|
|
||||||
.style("display", graphInteractionMode === "select" ? "inherit" : "none");
|
|
||||||
|
|
||||||
if (selectionToolType === "brush") {
|
if (selectionToolType === "brush") {
|
||||||
const brush = d3
|
const brush = d3
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function Clip(props) {
|
|||||||
<div
|
<div
|
||||||
className="bp3-button-group"
|
className="bp3-button-group"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 10
|
marginRight: 10
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from "react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
AnchorButton,
|
AnchorButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Popover,
|
Popover,
|
||||||
@@ -42,7 +43,8 @@ import * as globals from "../../globals";
|
|||||||
redoDisabled: state["@@undoable/future"].length === 0,
|
redoDisabled: state["@@undoable/future"].length === 0,
|
||||||
aboutLink: state.config?.links?.["about-dataset"],
|
aboutLink: state.config?.links?.["about-dataset"],
|
||||||
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
|
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
|
||||||
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false
|
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
|
||||||
|
showCentroidLabels: state.centroidLabels.showLabels
|
||||||
}))
|
}))
|
||||||
class MenuBar extends React.Component {
|
class MenuBar extends React.Component {
|
||||||
static isValidDigitKeyEvent(e) {
|
static isValidDigitKeyEvent(e) {
|
||||||
@@ -258,6 +260,15 @@ class MenuBar extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleCentroidChange = () => {
|
||||||
|
const { dispatch, showCentroidLabels } = this.props;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "show centroid labels for category",
|
||||||
|
showLabels: !showCentroidLabels
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderDiffExp() {
|
renderDiffExp() {
|
||||||
/* diffexp-related buttons may be disabled */
|
/* diffexp-related buttons may be disabled */
|
||||||
const { disableDiffexp, differential, diffexpMayBeSlow } = this.props;
|
const { disableDiffexp, differential, diffexpMayBeSlow } = this.props;
|
||||||
@@ -272,7 +283,7 @@ class MenuBar extends React.Component {
|
|||||||
const tipMessage = `See top 10 differentially expressed genes${slowMsg}`;
|
const tipMessage = `See top 10 differentially expressed genes${slowMsg}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bp3-button-group" style={{ marginRight: 10 }}>
|
<ButtonGroup style={{ marginRight: 10 }}>
|
||||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
|
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
|
||||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
|
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
|
||||||
{!differential.diffExp ? (
|
{!differential.diffExp ? (
|
||||||
@@ -288,7 +299,6 @@ class MenuBar extends React.Component {
|
|||||||
loading={differential.loading}
|
loading={differential.loading}
|
||||||
icon="left-join"
|
icon="left-join"
|
||||||
fill
|
fill
|
||||||
type="button"
|
|
||||||
onClick={this.computeDiffExp}
|
onClick={this.computeDiffExp}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -310,7 +320,7 @@ class MenuBar extends React.Component {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,19 +337,20 @@ class MenuBar extends React.Component {
|
|||||||
clipPercentileMax,
|
clipPercentileMax,
|
||||||
layoutChoice,
|
layoutChoice,
|
||||||
graphInteractionMode,
|
graphInteractionMode,
|
||||||
aboutLink
|
aboutLink,
|
||||||
|
showCentroidLabels
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { pendingClipPercentiles } = this.state;
|
const { pendingClipPercentiles } = this.state;
|
||||||
|
|
||||||
// constants used to create selection tool button
|
// constants used to create selection tool button
|
||||||
let selectionTooltip;
|
let selectionTooltip;
|
||||||
let selectionButtonClass;
|
let selectionButtonIcon;
|
||||||
if (selectionTool === "brush") {
|
if (selectionTool === "brush") {
|
||||||
selectionTooltip = "Brush selection";
|
selectionTooltip = "Brush selection";
|
||||||
selectionButtonClass = "bp3-icon-select";
|
selectionButtonIcon = "select";
|
||||||
} else {
|
} else {
|
||||||
selectionTooltip = "Lasso selection";
|
selectionTooltip = "Lasso selection";
|
||||||
selectionButtonClass = "bp3-icon-polygon-filter";
|
selectionButtonIcon = "polygon-filter";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -347,7 +358,8 @@ class MenuBar extends React.Component {
|
|||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
right: globals.leftSidebarWidth + 8,
|
right: globals.leftSidebarWidth + 8,
|
||||||
top: 8
|
top: 8,
|
||||||
|
display: "flex"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.renderDiffExp()}
|
{this.renderDiffExp()}
|
||||||
@@ -357,7 +369,6 @@ class MenuBar extends React.Component {
|
|||||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||||
>
|
>
|
||||||
<AnchorButton
|
<AnchorButton
|
||||||
type="button"
|
|
||||||
data-testid="subset-button"
|
data-testid="subset-button"
|
||||||
disabled={
|
disabled={
|
||||||
crossfilter &&
|
crossfilter &&
|
||||||
@@ -375,16 +386,16 @@ class MenuBar extends React.Component {
|
|||||||
<Icon icon="double-chevron-down" />
|
<Icon icon="double-chevron-down" />
|
||||||
</AnchorButton>
|
</AnchorButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className="bp3-button-group">
|
<ButtonGroup style={{ marginRight: "10px" }}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={selectionTooltip}
|
content={selectionTooltip}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||||
>
|
>
|
||||||
<Button
|
<AnchorButton
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="mode-lasso"
|
data-testid="mode-lasso"
|
||||||
className={`bp3-button ${selectionButtonClass}`}
|
icon={selectionButtonIcon}
|
||||||
active={graphInteractionMode === "select"}
|
active={graphInteractionMode === "select"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -402,10 +413,10 @@ class MenuBar extends React.Component {
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||||
>
|
>
|
||||||
<Button
|
<AnchorButton
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="mode-pan-zoom"
|
data-testid="mode-pan-zoom"
|
||||||
className="bp3-button bp3-icon-zoom-in"
|
icon="zoom-in"
|
||||||
active={graphInteractionMode === "zoom"}
|
active={graphInteractionMode === "zoom"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -418,11 +429,25 @@ class MenuBar extends React.Component {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</ButtonGroup>
|
||||||
<div
|
<Tooltip
|
||||||
className="bp3-button-group"
|
content="When a category is colored by, show labels on the graph"
|
||||||
|
position="bottom"
|
||||||
|
disabled={graphInteractionMode === "zoom"}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon="property"
|
||||||
|
onClick={this.handleCentroidChange}
|
||||||
|
active={showCentroidLabels}
|
||||||
|
intent={showCentroidLabels ? "primary" : "none"}
|
||||||
|
style={{
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<ButtonGroup
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 10
|
marginRight: 10
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -435,7 +460,7 @@ class MenuBar extends React.Component {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="layout-choice"
|
data-testid="layout-choice"
|
||||||
className="bp3-button bp3-icon-heatmap"
|
icon="heatmap"
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer"
|
cursor: "pointer"
|
||||||
}}
|
}}
|
||||||
@@ -465,7 +490,7 @@ class MenuBar extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</ButtonGroup>
|
||||||
<Clip
|
<Clip
|
||||||
pendingClipPercentiles={pendingClipPercentiles}
|
pendingClipPercentiles={pendingClipPercentiles}
|
||||||
clipPercentileMin={clipPercentileMin}
|
clipPercentileMin={clipPercentileMin}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
|
|||||||
function InformationMenu(props) {
|
function InformationMenu(props) {
|
||||||
const { libraryVersions, aboutLink } = props;
|
const { libraryVersions, aboutLink } = props;
|
||||||
return (
|
return (
|
||||||
<div style={{ marginLeft: 10 }} className="bp3-button-group">
|
<div style={{}} className="bp3-button-group">
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
<Menu>
|
<Menu>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function InformationMenu(props) {
|
|||||||
dispatch
|
dispatch
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<div style={{ marginLeft: 10 }} className="bp3-button-group">
|
<div style={{ marginRight: 10 }} className="bp3-button-group">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="Undo"
|
content="Undo"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function createProjectionTF(viewportWidth, viewportHeight) {
|
|||||||
colorScale: state.colors.scale,
|
colorScale: state.colors.scale,
|
||||||
colorAccessor: state.colors.colorAccessor,
|
colorAccessor: state.colors.colorAccessor,
|
||||||
|
|
||||||
centroidLabel: state.centroidLabel,
|
pointDilation: state.pointDilation,
|
||||||
|
|
||||||
// Accessors are var/gene names (strings)
|
// Accessors are var/gene names (strings)
|
||||||
scatterplotXXaccessor,
|
scatterplotXXaccessor,
|
||||||
@@ -90,7 +90,7 @@ class Scatterplot extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
computePointFlags = memoize(
|
computePointFlags = memoize(
|
||||||
(world, crossfilter, colorAccessor, centroidLabel) => {
|
(world, crossfilter, colorAccessor, pointDilation) => {
|
||||||
const flagSelected = 1;
|
const flagSelected = 1;
|
||||||
const flagNaN = 2;
|
const flagNaN = 2;
|
||||||
const flagHighlight = 4;
|
const flagHighlight = 4;
|
||||||
@@ -101,7 +101,7 @@ class Scatterplot extends React.PureComponent {
|
|||||||
0
|
0
|
||||||
).slice();
|
).slice();
|
||||||
|
|
||||||
const { metadataField, categoryField } = centroidLabel;
|
const { metadataField, categoryField } = pointDilation;
|
||||||
const highlightData = metadataField
|
const highlightData = metadataField
|
||||||
? world.obsAnnotations.col(metadataField)?.asArray()
|
? world.obsAnnotations.col(metadataField)?.asArray()
|
||||||
: null;
|
: null;
|
||||||
@@ -198,7 +198,7 @@ class Scatterplot extends React.PureComponent {
|
|||||||
expressionY,
|
expressionY,
|
||||||
colorRGB,
|
colorRGB,
|
||||||
colorAccessor,
|
colorAccessor,
|
||||||
centroidLabel
|
pointDilation
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
regl,
|
regl,
|
||||||
@@ -249,7 +249,7 @@ class Scatterplot extends React.PureComponent {
|
|||||||
world,
|
world,
|
||||||
crossfilter,
|
crossfilter,
|
||||||
colorAccessor,
|
colorAccessor,
|
||||||
centroidLabel
|
pointDilation
|
||||||
);
|
);
|
||||||
if (renderCache.flags !== newFlags) {
|
if (renderCache.flags !== newFlags) {
|
||||||
renderCache.flags = newFlags;
|
renderCache.flags = newFlags;
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import calcCentroid from "../util/centroid";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
labels: [],
|
||||||
|
showLabels: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const centroidLabels = (state = initialState, action, sharedNextState) => {
|
||||||
|
const {
|
||||||
|
world,
|
||||||
|
layoutChoice,
|
||||||
|
categoricalSelection,
|
||||||
|
colors: { colorAccessor }
|
||||||
|
} = sharedNextState;
|
||||||
|
|
||||||
|
const showLabels = action.showLabels ?? state.showLabels;
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case "annotation: label current cell selection":
|
||||||
|
case "annotation: label edited":
|
||||||
|
case "annotation: delete label":
|
||||||
|
case "set layout choice":
|
||||||
|
case "set World to current selection":
|
||||||
|
case "reset World to eq Universe":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
labels:
|
||||||
|
!!colorAccessor && showLabels && !!categoricalSelection[colorAccessor]
|
||||||
|
? calcCentroid(
|
||||||
|
world.obsAnnotations,
|
||||||
|
world.obsLayout,
|
||||||
|
colorAccessor,
|
||||||
|
layoutChoice.currentDimNames,
|
||||||
|
categoricalSelection,
|
||||||
|
world.schema.annotations.obsByName
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
|
||||||
|
case "color by categorical metadata":
|
||||||
|
case "show centroid labels for category":
|
||||||
|
// If colorby is not enabled or labels are not toggled to show
|
||||||
|
// then clear the labels and make sure the toggle is off
|
||||||
|
if (!colorAccessor || !showLabels) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
labels: [],
|
||||||
|
showLabels
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
labels: calcCentroid(
|
||||||
|
world.obsAnnotations,
|
||||||
|
world.obsLayout,
|
||||||
|
colorAccessor,
|
||||||
|
layoutChoice.currentDimNames,
|
||||||
|
categoricalSelection,
|
||||||
|
world.schema.annotations.obsByName
|
||||||
|
),
|
||||||
|
showLabels
|
||||||
|
};
|
||||||
|
|
||||||
|
case "color by continuous metadata":
|
||||||
|
return { ...state, labels: [] };
|
||||||
|
|
||||||
|
case "reset centroid labels":
|
||||||
|
return initialState;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default centroidLabels;
|
||||||
@@ -16,45 +16,48 @@ import layoutChoice from "./layoutChoice";
|
|||||||
import responsive from "./responsive";
|
import responsive from "./responsive";
|
||||||
import controls from "./controls";
|
import controls from "./controls";
|
||||||
import resetCache from "./resetCache";
|
import resetCache from "./resetCache";
|
||||||
import centroidLabel from "./centroidLabel";
|
|
||||||
import annotations from "./annotations";
|
import annotations from "./annotations";
|
||||||
import autosave from "./autosave";
|
import autosave from "./autosave";
|
||||||
|
import centroidLabels from "./centroidLabels";
|
||||||
|
import pointDialation from "./pointDilation";
|
||||||
|
|
||||||
import undoableConfig from "./undoableConfig";
|
import undoableConfig from "./undoableConfig";
|
||||||
|
|
||||||
const Reducer = undoable(
|
const Reducer = undoable(
|
||||||
cascadeReducers([
|
cascadeReducers([
|
||||||
["config", config],
|
["config", config],
|
||||||
["universe", universe],
|
["universe", universe],
|
||||||
["world", world],
|
["world", world],
|
||||||
["annotations", annotations],
|
["annotations", annotations],
|
||||||
["layoutChoice", layoutChoice],
|
["layoutChoice", layoutChoice],
|
||||||
["categoricalSelection", categoricalSelection],
|
["categoricalSelection", categoricalSelection],
|
||||||
["continuousSelection", continuousSelection],
|
["continuousSelection", continuousSelection],
|
||||||
["graphSelection", graphSelection],
|
["graphSelection", graphSelection],
|
||||||
["crossfilter", crossfilter],
|
["crossfilter", crossfilter],
|
||||||
["colors", colors],
|
["colors", colors],
|
||||||
["controls", controls],
|
["controls", controls],
|
||||||
["differential", differential],
|
["differential", differential],
|
||||||
["responsive", responsive],
|
["responsive", responsive],
|
||||||
["centroidLabel", centroidLabel],
|
["centroidLabels", centroidLabels],
|
||||||
["autosave", autosave],
|
["pointDilation", pointDialation],
|
||||||
["resetCache", resetCache]
|
["autosave", autosave],
|
||||||
]),
|
["resetCache", resetCache]
|
||||||
[
|
]),
|
||||||
"universe",
|
[
|
||||||
"world",
|
"universe",
|
||||||
"categoricalSelection",
|
"categoricalSelection",
|
||||||
"continuousSelection",
|
"world",
|
||||||
"graphSelection",
|
"continuousSelection",
|
||||||
"crossfilter",
|
"graphSelection",
|
||||||
"colors",
|
"crossfilter",
|
||||||
"controls",
|
"layoutChoice",
|
||||||
"differential",
|
"controls",
|
||||||
"layoutChoice",
|
"differential",
|
||||||
"annotations"
|
"colors",
|
||||||
],
|
"centroidLabels",
|
||||||
undoableConfig
|
"annotations"
|
||||||
|
],
|
||||||
|
undoableConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = createStore(Reducer, applyMiddleware(thunk));
|
const store = createStore(Reducer, applyMiddleware(thunk));
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
metadataField: "",
|
metadataField: "",
|
||||||
categoryIndex: -1,
|
categoryField: ""
|
||||||
categoryField: "",
|
|
||||||
centroidXY: [-1, -1]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CentroidLabel = (state = initialState, action, sharedNextState) => {
|
const pointDialation = (state = initialState, action, sharedNextState) => {
|
||||||
const { categoricalSelection } = sharedNextState;
|
const { categoricalSelection } = sharedNextState;
|
||||||
const { metadataField, categoryIndex } = action;
|
const { metadataField, categoryIndex } = action;
|
||||||
const categoryField =
|
const categoryField =
|
||||||
|
action.categoryField ||
|
||||||
categoricalSelection?.[metadataField]?.categoryValues[categoryIndex];
|
categoricalSelection?.[metadataField]?.categoryValues[categoryIndex];
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "category value mouse hover start":
|
case "category value mouse hover start":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
metadataField,
|
metadataField,
|
||||||
categoryIndex,
|
categoryField
|
||||||
categoryField,
|
|
||||||
centroidXY: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
case "category value mouse hover end":
|
case "category value mouse hover end":
|
||||||
if (
|
if (
|
||||||
metadataField === state.metadataField &&
|
metadataField === state.metadataField &&
|
||||||
categoryIndex === state.categoryIndex
|
categoryField === state.categoryField
|
||||||
) {
|
) {
|
||||||
return initialState;
|
return initialState;
|
||||||
}
|
}
|
||||||
@@ -34,4 +32,4 @@ const CentroidLabel = (state = initialState, action, sharedNextState) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CentroidLabel;
|
export default pointDialation;
|
||||||
@@ -16,6 +16,7 @@ const skipOnActions = new Set([
|
|||||||
"window resize",
|
"window resize",
|
||||||
"user reset start",
|
"user reset start",
|
||||||
"reset colorscale",
|
"reset colorscale",
|
||||||
|
"reset centroid labels",
|
||||||
|
|
||||||
"graph brush change",
|
"graph brush change",
|
||||||
"continuous metadata histogram brush",
|
"continuous metadata histogram brush",
|
||||||
@@ -68,6 +69,8 @@ const saveOnActions = new Set([
|
|||||||
"color by continuous metadata",
|
"color by continuous metadata",
|
||||||
"color by expression",
|
"color by expression",
|
||||||
|
|
||||||
|
"show centroid labels for category",
|
||||||
|
|
||||||
"set scatterplot x",
|
"set scatterplot x",
|
||||||
"set scatterplot y",
|
"set scatterplot y",
|
||||||
|
|
||||||
|
|||||||
+101
-44
@@ -1,61 +1,118 @@
|
|||||||
import quantile from "./quantile";
|
import quantile from "./quantile";
|
||||||
|
import { memoize } from "./dataframe/util";
|
||||||
|
import { unassignedCategoryLabel } from "../globals";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Centroid coordinate calculation
|
Centroid coordinate calculation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Unused - please cleanup
|
/*
|
||||||
const calcMeanCentroid = (world, annoName, annoValue, layoutDimNames) => {
|
calcMedianCentroid goes through a given metadata category
|
||||||
const centroid = { x: 0, y: 0, size: 0 };
|
fetches each cell's coordinates grouping by category value.
|
||||||
const annoArray = world.obsAnnotations.col(annoName).asArray();
|
|
||||||
const layoutXArray = world.obsLayout.col(layoutDimNames[0]).asArray();
|
|
||||||
const layoutYArray = world.obsLayout.col(layoutDimNames[1]).asArray();
|
|
||||||
|
|
||||||
for (let i = 0, len = annoArray.length; i < len; i += 1) {
|
It then calculates the median value and puts that in the array
|
||||||
if (annoArray[i] === annoValue) {
|
|
||||||
centroid.x += layoutXArray[i];
|
|
||||||
centroid.y += layoutYArray[i];
|
|
||||||
centroid.size += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (centroid[2] !== 0) {
|
|
||||||
centroid.x /= centroid.size;
|
|
||||||
centroid.y /= centroid.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [centroid.x, centroid.y];
|
|
||||||
};
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const calcMedianCentroid = (world, annoName, annoValue, layoutDimNames) => {
|
const calcMedianCentroid = (
|
||||||
const centroidX = [];
|
obsAnnotations,
|
||||||
const centroidY = [];
|
obsLayout,
|
||||||
let hasFinite = false;
|
categoryName,
|
||||||
|
layoutDimNames,
|
||||||
|
categoricalSelection,
|
||||||
|
schemaObsByName
|
||||||
|
) => {
|
||||||
|
const categoryArray = obsAnnotations.col(categoryName).asArray();
|
||||||
|
|
||||||
const annoArray = world.obsAnnotations.col(annoName).asArray();
|
const layoutXArray = obsLayout.col(layoutDimNames[0]).asArray();
|
||||||
const layoutXArray = world.obsLayout.col(layoutDimNames[0]).asArray();
|
const layoutYArray = obsLayout.col(layoutDimNames[1]).asArray();
|
||||||
const layoutYArray = world.obsLayout.col(layoutDimNames[1]).asArray();
|
const coordinates = new Map();
|
||||||
|
|
||||||
|
// Iterate over all the cells in the category
|
||||||
|
for (let i = 0, len = categoryArray.length; i < len; i += 1) {
|
||||||
|
const categoryValue = categoryArray[i];
|
||||||
|
|
||||||
|
// Get the index of the categoryValue within the category
|
||||||
|
// If the category is truncated and this value is removed,
|
||||||
|
// it will not be assigned a category value and will not be
|
||||||
|
// labeled on the graph
|
||||||
|
const categoryValueIndex = categoricalSelection[
|
||||||
|
categoryName
|
||||||
|
].categoryValueIndices.get(categoryValue);
|
||||||
|
|
||||||
|
// Check to see if the current category is a user created annotation
|
||||||
|
// if the user created this category, do not create a label for the `unassigned` value
|
||||||
|
const isUserAnno = schemaObsByName[categoryName].writable;
|
||||||
|
|
||||||
|
if (
|
||||||
|
categoryValueIndex !== undefined &&
|
||||||
|
!(isUserAnno && categoryValue === unassignedCategoryLabel)
|
||||||
|
) {
|
||||||
|
// Get the number of cells which are in the category value
|
||||||
|
const numInCategoryValue =
|
||||||
|
categoricalSelection[categoryName].categoryValueCounts[
|
||||||
|
categoryValueIndex
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create/fetch the valueArray,
|
||||||
|
// which is what the key points to in the `coordinates` hashmap
|
||||||
|
const valueArray = coordinates.get(categoryValue) || [
|
||||||
|
false, // hasFinite
|
||||||
|
0, // index
|
||||||
|
new Float32Array(numInCategoryValue), // x coordinates
|
||||||
|
new Float32Array(numInCategoryValue) // y coordinates
|
||||||
|
];
|
||||||
|
const index = valueArray[1];
|
||||||
|
let hasFinite = valueArray[0];
|
||||||
|
|
||||||
for (let i = 0, len = annoArray.length; i < len; i += 1) {
|
|
||||||
if (annoArray[i] === annoValue) {
|
|
||||||
hasFinite =
|
hasFinite =
|
||||||
Number.isFinite(layoutXArray[i]) || Number.isFinite(layoutYArray[i])
|
hasFinite ||
|
||||||
? true
|
(Number.isFinite(layoutXArray[i]) && Number.isFinite(layoutYArray[i]));
|
||||||
: hasFinite;
|
|
||||||
centroidX.push(layoutXArray[i]);
|
valueArray[0] = hasFinite;
|
||||||
centroidY.push(layoutYArray[i]);
|
valueArray[1] = index + 1;
|
||||||
|
valueArray[2][index] = layoutXArray[i];
|
||||||
|
valueArray[3][index] = layoutYArray[i];
|
||||||
|
|
||||||
|
coordinates.set(categoryValue, valueArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFinite) {
|
// Iterate over the recently created map
|
||||||
const medianX = quantile([0.5], Float64Array.from(centroidX));
|
coordinates.forEach((value, key) => {
|
||||||
const medianY = quantile([0.5], Float64Array.from(centroidY));
|
// If there are coordinates for this cateogrical value,
|
||||||
|
// and there is a finite coordinate for the category value
|
||||||
return [medianX, medianY];
|
if (value[2].length > 0 && value[3].length > 0 && value[0]) {
|
||||||
}
|
// Find the median x and y coordinate
|
||||||
|
// and insert them into the first two indices
|
||||||
return null;
|
value[0] = quantile([0.5], value[2])[0];
|
||||||
|
value[1] = quantile([0.5], value[3])[0];
|
||||||
|
// Remove the last two elements (where the arrays of coordinates were)
|
||||||
|
value.pop();
|
||||||
|
value.pop();
|
||||||
|
} else {
|
||||||
|
// remove the entry if not
|
||||||
|
coordinates.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// return the map: categoricalValue -> [medianXCoordinate, medianYCoordinate]
|
||||||
|
return coordinates;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default calcMedianCentroid;
|
// A simple function to hash the parameters
|
||||||
|
// (not 100% on world hash, Bruce will have to check this one out)
|
||||||
|
const hashMedianCentroid = (
|
||||||
|
obsAnnotations,
|
||||||
|
obsLayout,
|
||||||
|
categoryName,
|
||||||
|
layoutDimNames,
|
||||||
|
categorySelection,
|
||||||
|
schemaObsByName
|
||||||
|
) => {
|
||||||
|
return `${obsAnnotations.__id}+${
|
||||||
|
obsLayout.__id
|
||||||
|
}:${categoryName}:${layoutDimNames}:${Object.keys(
|
||||||
|
categorySelection
|
||||||
|
)}:${Object.keys(schemaObsByName)}`;
|
||||||
|
};
|
||||||
|
// export the mmemoized calculation function
|
||||||
|
export default memoize(calcMedianCentroid, hashMedianCentroid);
|
||||||
|
|||||||
Reference in New Issue
Block a user