mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-24 07:18:11 +08:00
Embedding button to lower left, cell selection (#1658)
* embedding * menu bottom left * button * change gutters to support lower toolbar * fix scatterplot layout * fix tests to match new layout * fix smoke tests to match new layout * better sentence, dataset.nObs to top * scatterplot position Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
This commit is contained in:
co-authored by
bkmartinjr
parent
a44da11f3f
commit
03bad04436
@@ -11,6 +11,7 @@ import Legend from "./continuousLegend";
|
||||
import Graph from "./graph/graph";
|
||||
import MenuBar from "./menubar";
|
||||
import Autosave from "./autosave";
|
||||
import Embedding from "./embedding";
|
||||
import TermsOfServicePrompt from "./termsPrompt";
|
||||
|
||||
import actions from "../actions";
|
||||
@@ -73,6 +74,7 @@ class App extends React.Component {
|
||||
{(viewportRef) => (
|
||||
<>
|
||||
<MenuBar />
|
||||
<Embedding />
|
||||
<Autosave />
|
||||
<TermsOfServicePrompt />
|
||||
<Legend viewportRef={viewportRef} />
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Popover,
|
||||
Button,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tooltip,
|
||||
Position,
|
||||
} from "@blueprintjs/core";
|
||||
import * as globals from "../../globals";
|
||||
import actions from "../../actions";
|
||||
|
||||
@connect((state) => {
|
||||
return {
|
||||
layoutChoice: state.layoutChoice,
|
||||
schema: state.annoMatrix?.schema,
|
||||
crossfilter: state.obsCrossfilter,
|
||||
};
|
||||
})
|
||||
class Embedding extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
handleLayoutChoiceChange = (e) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(actions.layoutChoiceAction(e.currentTarget.value));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { layoutChoice, schema, crossfilter } = this.props;
|
||||
return (
|
||||
<ButtonGroup
|
||||
style={{
|
||||
position: "absolute",
|
||||
display: "inherit",
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Popover
|
||||
target={
|
||||
<Tooltip
|
||||
content="Select embedding for visualization"
|
||||
position="top"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="layout-choice"
|
||||
icon="heatmap"
|
||||
// minimal
|
||||
id="embedding"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{layoutChoice?.current}: {crossfilter.countSelected()} out of{" "}
|
||||
{crossfilter.size()} cells
|
||||
{/* BRUCE to extend 1559 */}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
// minimal /* removes arrow */
|
||||
position={Position.TOP_LEFT}
|
||||
content={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
padding: 10,
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<h1>Embedding Choice</h1>
|
||||
<p style={{ fontStyle: "italic" }}>
|
||||
There are {schema?.dataframe?.nObs} cells in the entire dataset.
|
||||
</p>
|
||||
<RadioGroup
|
||||
onChange={this.handleLayoutChoiceChange}
|
||||
selectedValue={layoutChoice.current}
|
||||
>
|
||||
{layoutChoice.available.map((name) => (
|
||||
<Radio
|
||||
label={`${name} ${schema?.dataframe?.nObs} cells`}
|
||||
value={name}
|
||||
key={name}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Embedding;
|
||||
@@ -34,8 +34,10 @@ function createProjectionTF(viewportWidth, viewportHeight) {
|
||||
the projection transform accounts for the screen size & other layout
|
||||
*/
|
||||
const fractionToUse = 0.95; // fraction of min dimension to use
|
||||
const topGutterSizePx = 32; // toolbar box height
|
||||
const heightMinusGutter = viewportHeight - topGutterSizePx;
|
||||
const topGutterSizePx = 32; // top gutter for tools
|
||||
const bottomGutterSizePx = 32; // bottom gutter for tools
|
||||
const heightMinusGutter =
|
||||
viewportHeight - topGutterSizePx - bottomGutterSizePx;
|
||||
const minDim = Math.min(viewportWidth, heightMinusGutter);
|
||||
const aspectScale = [
|
||||
(fractionToUse * minDim) / viewportWidth,
|
||||
@@ -44,7 +46,7 @@ function createProjectionTF(viewportWidth, viewportHeight) {
|
||||
const m = mat3.create();
|
||||
mat3.fromTranslation(m, [
|
||||
0,
|
||||
-topGutterSizePx / viewportHeight / aspectScale[1],
|
||||
(bottomGutterSizePx - topGutterSizePx) / viewportHeight / aspectScale[1],
|
||||
]);
|
||||
mat3.scale(m, m, aspectScale);
|
||||
return m;
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Popover,
|
||||
Button,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tooltip,
|
||||
Position,
|
||||
} from "@blueprintjs/core";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
import actions from "../../actions";
|
||||
|
||||
@connect((state) => ({
|
||||
layoutChoice: state.layoutChoice,
|
||||
// disabled temporarily. TODO - issue #1606
|
||||
// reembedController: state.reembedController,
|
||||
// enableReembedding: state.config?.parameters?.["enable-reembedding"] ?? false,
|
||||
enableReembedding: false,
|
||||
}))
|
||||
class Embedding extends React.PureComponent {
|
||||
handleLayoutChoiceChange = (e) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(actions.layoutChoiceAction(e.currentTarget.value));
|
||||
};
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this -- temporary disable
|
||||
renderReembedding() {
|
||||
return null;
|
||||
/* disabled pending rewrite. TODO - issue #1606
|
||||
const {
|
||||
enableReembedding,
|
||||
world,
|
||||
universe,
|
||||
dispatch,
|
||||
reembedController,
|
||||
} = this.props;
|
||||
|
||||
if (!enableReembedding) return null;
|
||||
|
||||
const loading = !!reembedController?.pendingFetch;
|
||||
const disabled = World.worldEqUniverse(world, universe);
|
||||
const tipContent = disabled
|
||||
? "Subset cells first, then click to recompute UMAP embedding."
|
||||
: "Click to recompute UMAP embedding on the current cell subset.";
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tipContent}
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
icon="new-object"
|
||||
style={{ marginRight: 10 }}
|
||||
disabled={disabled}
|
||||
onClick={() => dispatch(actions.requestReembed())}
|
||||
loading={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layoutChoice } = this.props;
|
||||
|
||||
return (
|
||||
<ButtonGroup className={styles.menubarButton}>
|
||||
<Popover
|
||||
target={
|
||||
<Tooltip
|
||||
content="Select embedding for visualization"
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="layout-choice"
|
||||
icon="heatmap"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
content={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<RadioGroup
|
||||
label="Embedding Choice"
|
||||
onChange={this.handleLayoutChoiceChange}
|
||||
selectedValue={layoutChoice.current}
|
||||
>
|
||||
{layoutChoice.available.map((name) => (
|
||||
<Radio label={name} value={name} key={name} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{this.renderReembedding()}
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Embedding;
|
||||
@@ -6,7 +6,6 @@ import * as globals from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
import actions from "../../actions";
|
||||
import Clip from "./clip";
|
||||
import Embedding from "./embedding";
|
||||
import InformationMenu from "./infoMenu";
|
||||
import Subset from "./subset";
|
||||
import UndoRedoReset from "./undoRedo";
|
||||
@@ -264,7 +263,6 @@ class MenuBar extends React.PureComponent {
|
||||
this.handleClipPercentileMinValueChange
|
||||
}
|
||||
/>
|
||||
<Embedding />
|
||||
<Tooltip
|
||||
content="When a category is colored by, show labels on the graph"
|
||||
position="bottom"
|
||||
|
||||
@@ -438,18 +438,19 @@ class Scatterplot extends React.PureComponent {
|
||||
pointDilation,
|
||||
} = this.props;
|
||||
const { minimized, regl, viewport } = this.state;
|
||||
const bottomToolbarGutter = 48; // gutter for bottom tool bar
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: minimized ? -height + -margin.top - 2 : 0,
|
||||
bottom: bottomToolbarGutter,
|
||||
borderRadius: "3px 3px 0px 0px",
|
||||
left: globals.leftSidebarWidth + globals.scatterplotMarginLeft,
|
||||
padding: "0px 20px 20px 0px",
|
||||
background: "white",
|
||||
/* x y blur spread color */
|
||||
boxShadow: "0px 0px 6px 2px rgba(153,153,153,0.4)",
|
||||
boxShadow: "0px 0px 3px 2px rgba(153,153,153,0.2)",
|
||||
zIndex: 2,
|
||||
}}
|
||||
id="scatterplot_wrapper"
|
||||
@@ -488,7 +489,9 @@ class Scatterplot extends React.PureComponent {
|
||||
id="scatterplot"
|
||||
style={{
|
||||
width: `${width + margin.left + margin.right}px`,
|
||||
height: `${height + margin.top + margin.bottom}px`,
|
||||
height: `${
|
||||
(minimized ? 0 : height + margin.top) + margin.bottom
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
@@ -498,6 +501,7 @@ class Scatterplot extends React.PureComponent {
|
||||
style={{
|
||||
marginLeft: margin.left,
|
||||
marginTop: margin.top,
|
||||
display: minimized ? "none" : null,
|
||||
}}
|
||||
ref={this.setReglCanvas}
|
||||
/>
|
||||
@@ -523,9 +527,7 @@ class Scatterplot extends React.PureComponent {
|
||||
}
|
||||
return (
|
||||
<ScatterplotAxis
|
||||
width={width}
|
||||
height={height}
|
||||
margin={margin}
|
||||
minimized={minimized}
|
||||
scatterplotYYaccessor={scatterplotXXaccessor}
|
||||
scatterplotXXaccessor={scatterplotYYaccessor}
|
||||
xScale={asyncProps.xScale}
|
||||
@@ -544,7 +546,13 @@ class Scatterplot extends React.PureComponent {
|
||||
export default Scatterplot;
|
||||
|
||||
const ScatterplotAxis = React.memo(
|
||||
({ scatterplotYYaccessor, scatterplotXXaccessor, xScale, yScale }) => {
|
||||
({
|
||||
minimized,
|
||||
scatterplotYYaccessor,
|
||||
scatterplotXXaccessor,
|
||||
xScale,
|
||||
yScale,
|
||||
}) => {
|
||||
/*
|
||||
Axis for the scatterplot, rendered with SVG/D3. Props:
|
||||
* scatterplotXXaccessor - name of X axis
|
||||
@@ -559,7 +567,7 @@ const ScatterplotAxis = React.memo(
|
||||
const svgRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current) return;
|
||||
if (!svgRef.current || minimized) return;
|
||||
const svg = d3.select(svgRef.current);
|
||||
|
||||
svg.selectAll("*").remove();
|
||||
@@ -608,6 +616,9 @@ const ScatterplotAxis = React.memo(
|
||||
width={width + margin.left + margin.right}
|
||||
height={height + margin.top + margin.bottom}
|
||||
data-testid="scatterplot-svg"
|
||||
style={{
|
||||
display: minimized ? "none" : null,
|
||||
}}
|
||||
>
|
||||
<g ref={svgRef} transform={`translate(${margin.left},${margin.top})`} />
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user