Merge branch 'main' into colinmegill/geneset-prototype

This commit is contained in:
Colin Megill
2020-08-17 10:49:57 -04:00
271 changed files with 4546 additions and 1766 deletions
+2
View File
@@ -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} />
@@ -13,6 +13,7 @@ import {
@connect((state) => ({
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
annotations: state.annotations,
auth: state.config?.authentication,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
}))
class FilenameDialog extends React.Component {
@@ -90,12 +91,13 @@ class FilenameDialog extends React.Component {
};
render() {
const { writableCategoriesEnabled, annotations, idhash } = this.props;
const { writableCategoriesEnabled, annotations, idhash, auth } = this.props;
const { filenameText } = this.state;
return writableCategoriesEnabled &&
!annotations.dataCollectionNameIsReadOnly &&
!annotations.dataCollectionName ? (
!annotations.dataCollectionName &&
auth.is_authenticated ? (
<Dialog
icon="tag"
title="Annotations Collection"
@@ -1,7 +1,7 @@
import React, { useRef, useEffect } from "react";
import { connect, shallowEqual } from "react-redux";
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
import { AnchorButton, Button, Tooltip } from "@blueprintjs/core";
import { AnchorButton, Button, Tooltip, Position } from "@blueprintjs/core";
import { Flipper, Flipped } from "react-flip-toolkit";
import Async from "react-async";
import memoize from "memoize-one";
@@ -438,9 +438,13 @@ const CategoryHeader = React.memo(
? `Coloring by ${metadataField} is disabled, as it exceeds the limit of ${globals.maxCategoricalOptionsToDisplay} labels`
: "Use as color scale"
}
position="bottom"
usePortal={false}
position={Position.LEFT}
usePortal
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<AnchorButton
data-testclass="colorby"
@@ -453,7 +453,9 @@ class CategoryValue extends React.Component {
label,
CHART_WIDTH,
VALUE_HEIGHT
) ?? {};
) ?? {}; // if createHistogramBins returns empty object assign null to deconstructed
if (!xScale || !yScale || !bins) return null;
return (
<MiniHistogram
@@ -16,6 +16,7 @@ class Continuous extends React.PureComponent {
const allContinuousNames = schema.annotations.obs.columns
.filter((col) => col.type === "int32" || col.type === "float32")
.filter((col) => col.name !== obsIndex)
.filter((col) => !col.writable) // skip user annotations - they will be treated as categorical
.map((col) => col.name);
return (
+153
View File
@@ -0,0 +1,153 @@
import React from "react";
import { connect } from "react-redux";
import { useAsync } from "react-async";
import {
ButtonGroup,
Popover,
Button,
Radio,
RadioGroup,
Tooltip,
Position,
} from "@blueprintjs/core";
import * as globals from "../../globals";
import actions from "../../actions";
import { getDiscreteCellEmbeddingRowIndex } from "../../util/stateManager/viewStackHelpers";
@connect((state) => {
return {
layoutChoice: state.layoutChoice, // TODO: really should clean up naming, s/layout/embedding/g
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;
const { annoMatrix } = crossfilter;
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
</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>
<EmbeddingChoices
onChange={this.handleLayoutChoiceChange}
annoMatrix={annoMatrix}
layoutChoice={layoutChoice}
/>
</div>
}
/>
</ButtonGroup>
);
}
}
export default Embedding;
const loadAllEmbeddingCounts = async ({ annoMatrix, available }) => {
const embeddings = await Promise.all(
available.map((name) => annoMatrix.base().fetch("emb", name))
);
return available.map((name, idx) => ({
embeddingName: name,
embedding: embeddings[idx],
discreteCellIndex: getDiscreteCellEmbeddingRowIndex(embeddings[idx]),
}));
};
const EmbeddingChoices = ({ onChange, annoMatrix, layoutChoice }) => {
const { available } = layoutChoice;
const { data, error, isPending } = useAsync({
promiseFn: loadAllEmbeddingCounts,
annoMatrix,
available,
});
if (error) {
/* log, as this is unexpected */
console.error(error);
}
if (error || isPending) {
/* still loading, or errored out - just omit counts (TODO: spinner?) */
return (
<RadioGroup onChange={onChange} selectedValue={layoutChoice.current}>
{layoutChoice.available.map((name) => (
<Radio label={`${name}`} value={name} key={name} />
))}
</RadioGroup>
);
}
if (data) {
return (
<RadioGroup onChange={onChange} selectedValue={layoutChoice.current}>
{data.map((summary) => {
const { discreteCellIndex, embeddingName } = summary;
const sizeHint = `${discreteCellIndex.size()} cells`;
return (
<Radio
label={`${embeddingName}: ${sizeHint}`}
value={embeddingName}
key={embeddingName}
/>
);
})}
</RadioGroup>
);
}
return null;
};
+5 -3
View File
@@ -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;
@@ -0,0 +1,32 @@
import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core";
import * as globals from "../../globals";
import styles from "./menubar.css";
const Auth = React.memo((props) => {
const { auth } = props;
if (!auth || (auth && !auth.requires_client_login)) return null;
return (
<div className={`bp3-button-group ${styles.menubarButton}`}>
<Tooltip
content="Log in or log out of cellxgene"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="auth-button"
disabled={false}
icon={!auth.is_authenticated ? "log-in" : "log-out"}
href={!auth.is_authenticated ? auth.login : auth.logout}
>
{!auth.is_authenticated ? "Log In" : "Log Out"}
</AnchorButton>
</Tooltip>
</div>
);
});
export default Auth;
-118
View File
@@ -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;
+15 -5
View File
@@ -6,11 +6,13 @@ import * as globals from "../../globals";
import styles from "./menubar.css";
import actions from "../../actions";
import Clip from "./clip";
import Embedding from "./embedding";
import AuthButtons from "./authButtons";
import InformationMenu from "./infoMenu";
import Subset from "./subset";
import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons";
import Reembedding from "./reembedding";
import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
@connect((state) => {
const { annoMatrix } = state;
@@ -18,9 +20,11 @@ import DiffexpButtons from "./diffexpButtons";
const selectedCount = crossfilter.countSelected();
const subsetPossible =
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all are selected
const subsetResetPossible =
annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs;
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all and not none are selected
const embSubsetView = getEmbSubsetView(annoMatrix);
const subsetResetPossible = !embSubsetView
? annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs
: annoMatrix.nObs !== embSubsetView.nObs;
return {
subsetPossible,
@@ -37,6 +41,7 @@ import DiffexpButtons from "./diffexpButtons";
celllist1: state.differential.celllist1,
celllist2: state.differential.celllist2,
libraryVersions: state.config?.["library_versions"],
auth: state.config?.authentication,
undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"],
@@ -47,6 +52,8 @@ import DiffexpButtons from "./diffexpButtons";
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
categoricalSelection: state.categoricalSelection,
enableReembedding:
state.config?.parameters?.["enable-reembedding"] ?? false,
};
})
class MenuBar extends React.PureComponent {
@@ -212,6 +219,8 @@ class MenuBar extends React.PureComponent {
colorAccessor,
subsetPossible,
subsetResetPossible,
enableReembedding,
auth,
} = this.props;
const { pendingClipPercentiles } = this.state;
@@ -237,6 +246,7 @@ class MenuBar extends React.PureComponent {
zIndex: 3,
}}
>
<AuthButtons auth={auth} />
<InformationMenu
libraryVersions={libraryVersions}
aboutLink={aboutLink}
@@ -264,7 +274,7 @@ class MenuBar extends React.PureComponent {
this.handleClipPercentileMinValueChange
}
/>
<Embedding />
{enableReembedding ? <Reembedding /> : null}
<Tooltip
content="When a category is colored by, show labels on the graph"
position="bottom"
@@ -0,0 +1,40 @@
import React from "react";
import { connect } from "react-redux";
import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core";
import * as globals from "../../globals";
import actions from "../../actions";
import styles from "./menubar.css";
@connect((state) => ({
reembedController: state.reembedController,
annoMatrix: state.annoMatrix,
}))
class Reembedding extends React.PureComponent {
render() {
const { dispatch, annoMatrix, reembedController } = this.props;
const loading = !!reembedController?.pendingFetch;
const disabled = annoMatrix.nObs === annoMatrix.schema.dataframe.nObs;
const tipContent = disabled
? "Subset cells first, then click to recompute UMAP embedding."
: "Click to recompute UMAP embedding on the current cell subset.";
return (
<ButtonGroup className={styles.menubarButton}>
<Tooltip
content={tipContent}
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
icon="new-object"
disabled={disabled}
onClick={() => dispatch(actions.requestReembed())}
loading={loading}
/>
</Tooltip>
</ButtonGroup>
);
}
}
export default Reembedding;
+3 -2
View File
@@ -40,10 +40,11 @@ export default class MiniHistogram extends React.PureComponent {
};
componentDidUpdate = (prevProps) => {
const { obsOrVarContinuousFieldDisplayName } = this.props;
const { obsOrVarContinuousFieldDisplayName, bins } = this.props;
if (
prevProps.obsOrVarContinuousFieldDisplayName !==
obsOrVarContinuousFieldDisplayName
obsOrVarContinuousFieldDisplayName ||
prevProps.bins !== bins
)
this.drawHistogram();
};
@@ -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>