gene set summary progress (#2127)

* revert removal of cache control headers

* checkpoint work on revising summary route

* add summary query support to annoMatrix

* summarize route cleanup

* add mising file

* clean up summarize route

* add summary histogram

* update deps

* lint

* more lint

* lint

* manage crossfiler during gene set state changes

* remove obsolete debugging code

* correctly perform async watch in histogram

* better error handling
This commit is contained in:
Bruce Martin
2021-03-30 14:43:53 -07:00
committed by GitHub
parent bfb9e1edcc
commit ae30b66123
47 changed files with 28628 additions and 9109 deletions
@@ -18,7 +18,7 @@ import {
userInfo: state.userInfo,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
writableGenesetsEnabled: !(
state.config?.parameters?.["annotations_genesets_readonly"] ?? true
state.config?.parameters?.annotations_genesets_readonly ?? true
),
}))
class FilenameDialog extends React.Component {
+1 -1
View File
@@ -11,7 +11,7 @@ import FilenameDialog from "./filenameDialog";
error: state.autosave?.error,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
writableGenesetsEnabled: !(
state.config?.parameters?.["annotations_genesets_readonly"] ?? true
state.config?.parameters?.annotations_genesets_readonly ?? true
),
annoMatrix: state.annoMatrix,
genesets: state.genesets,
@@ -1,5 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import { connect, shallowEqual } from "react-redux";
import * as d3 from "d3";
import Async from "react-async";
import memoize from "memoize-one";
@@ -14,9 +14,9 @@ import StillLoading from "./loading";
import ErrorLoading from "./error";
@connect((state, ownProps) => {
const { isObs, isUserDefined, isDiffExp, field } = ownProps;
const { isObs, isUserDefined, isDiffExp, isGeneSetSummary, field } = ownProps;
const myName = makeContinuousDimensionName(
{ isObs, isUserDefined, isDiffExp },
{ isObs, isUserDefined, isDiffExp, isGeneSetSummary },
field
);
return {
@@ -28,6 +28,10 @@ import ErrorLoading from "./error";
};
})
class HistogramBrush extends React.PureComponent {
static watchAsync(props, prevProps) {
return !shallowEqual(props.watchProps, prevProps.watchProps);
}
/* memoized closure to prevent HistogramHeader unecessary repaint */
handleColorAction = memoize((dispatch) => (field, isObs) => {
if (isObs) {
@@ -71,7 +75,14 @@ class HistogramBrush extends React.PureComponent {
onBrush = (selection, x, eventType) => {
const type = `continuous metadata histogram ${eventType}`;
return () => {
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
const {
dispatch,
field,
isObs,
isUserDefined,
isDiffExp,
isGeneSetSummary,
} = this.props;
// ignore programmatically generated events
if (!d3.event.sourceEvent) return;
@@ -88,6 +99,7 @@ class HistogramBrush extends React.PureComponent {
isObs,
isUserDefined,
isDiffExp,
isGeneSetSummary,
},
};
dispatch(
@@ -98,7 +110,14 @@ class HistogramBrush extends React.PureComponent {
onBrushEnd = (selection, x) => {
return () => {
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
const {
dispatch,
field,
isObs,
isUserDefined,
isDiffExp,
isGeneSetSummary,
} = this.props;
const minAllowedBrushSize = 10;
const smallAmountToAvoidInfiniteLoop = 0.1;
@@ -138,6 +157,7 @@ class HistogramBrush extends React.PureComponent {
isObs,
isUserDefined,
isDiffExp,
isGeneSetSummary,
},
};
dispatch(
@@ -300,19 +320,37 @@ class HistogramBrush extends React.PureComponent {
}
createQuery() {
const { isObs, field, annoMatrix } = this.props;
const { isObs, isGeneSetSummary, field, setGenes, annoMatrix } = this.props;
const { schema } = annoMatrix;
if (isObs) {
return ["obs", field];
}
const varIndex = schema?.annotations?.var?.index;
if (!varIndex) return null;
if (isGeneSetSummary) {
return [
"X",
{
summarize: {
method: "mean",
field: "var",
column: varIndex,
values: setGenes,
},
},
];
}
// else, we assume it is a gene expression
return [
"X",
{
field: "var",
column: varIndex,
value: field,
where: {
field: "var",
column: varIndex,
value: field,
},
},
];
}
@@ -333,6 +371,7 @@ class HistogramBrush extends React.PureComponent {
continuousSelectionRange,
isObs,
mini,
setGenes,
} = this.props;
const {
margin,
@@ -346,7 +385,11 @@ class HistogramBrush extends React.PureComponent {
const showScatterPlot = isDiffExp || isUserDefined;
return (
<Async watch={annoMatrix} promiseFn={this.fetchAsyncProps}>
<Async
watchFn={HistogramBrush.watchAsync}
promiseFn={this.fetchAsyncProps}
watchProps={{ annoMatrix, setGenes }}
>
<Async.Pending initial>
<StillLoading displayName={field} zebra={zebra} />
</Async.Pending>
+1 -6
View File
@@ -53,12 +53,7 @@ class Gene extends React.Component {
handleDeleteGeneFromSet = () => {
const { dispatch, gene, geneset } = this.props;
dispatch({
type: "geneset: delete genes",
genesetName: geneset,
geneSymbols: [gene],
});
dispatch(actions.genesetDeleteGenes(geneset, [gene]));
};
render() {
+19 -11
View File
@@ -9,6 +9,7 @@ import { memoize } from "../../util/dataframe/util";
import Truncate from "../util/truncate";
import * as globals from "../../globals";
import GenesetMenus from "./menus/genesetMenus";
import HistogramBrush from "../brushableHistogram";
@connect((state, ownProps) => {
return {
@@ -163,18 +164,25 @@ class GeneSet extends React.Component {
) : null}
</div>
{isOpen && !toggleSummaryHisto
? _.map(setGenes, (gene) => {
return (
<Gene
key={gene}
gene={gene}
geneset={setName}
isDiffexp={isDiffexp}
{isOpen &&
(!toggleSummaryHisto
? _.map(setGenes, (gene) => {
return (
<Gene
key={gene}
gene={gene}
geneset={setName}
isDiffexp={isDiffexp}
/>
);
})
: setGenes.length > 0 && (
<HistogramBrush
isGeneSetSummary
field={setName}
setGenes={setGenes}
/>
);
})
: null}
))}
</div>
);
}
@@ -3,6 +3,7 @@ import { connect } from "react-redux";
import AnnoDialog from "../../annoDialog";
import LabelInput from "../../labelInput";
import parseBulkGeneString from "../../../util/parseBulkGeneString";
import actions from "../../../actions";
@connect((state) => ({
genesetsUI: state.genesetsUI,
@@ -36,11 +37,7 @@ class AddGeneToGenesetDialogue extends React.PureComponent {
});
});
dispatch({
type: "geneset: add genes",
genesetName: geneset,
genes: genesTmpHardcodedFormat,
});
dispatch(actions.genesetAddGenes(geneset, genesTmpHardcodedFormat));
dispatch({
type: "geneset: disable add new genes mode",
});
@@ -77,6 +77,26 @@ class AddGenes extends React.Component {
this.updateState(prevProps);
}
handleClick(g) {
const { dispatch, userDefinedGenes } = this.props;
const { geneNames } = this.state;
if (!g) return;
const gene = g.target;
if (userDefinedGenes.indexOf(gene) !== -1) {
postUserErrorToast("That gene already exists");
} else if (userDefinedGenes.length > globals.maxUserDefinedGenes) {
postUserErrorToast(
`That's too many genes, you can have at most ${globals.maxUserDefinedGenes} user defined genes`
);
} else if (geneNames.indexOf(gene) === undefined) {
postUserErrorToast("That doesn't appear to be a valid gene name.");
} else {
dispatch({ type: "single user defined gene start" });
dispatch(actions.requestUserDefinedGene(gene));
dispatch({ type: "single user defined gene complete" });
}
}
_genesToUpper = (listGenes) => {
// Has to be a Map to preserve index
const upperGenes = new Map();
@@ -190,26 +210,6 @@ class AddGenes extends React.Component {
return "Apod, Cd74, ...";
}
handleClick(g) {
const { dispatch, userDefinedGenes } = this.props;
const { geneNames } = this.state;
if (!g) return;
const gene = g.target;
if (userDefinedGenes.indexOf(gene) !== -1) {
postUserErrorToast("That gene already exists");
} else if (userDefinedGenes.length > globals.maxUserDefinedGenes) {
postUserErrorToast(
`That's too many genes, you can have at most ${globals.maxUserDefinedGenes} user defined genes`
);
} else if (geneNames.indexOf(gene) === undefined) {
postUserErrorToast("That doesn't appear to be a valid gene name.");
} else {
dispatch({ type: "single user defined gene start" });
dispatch(actions.requestUserDefinedGene(gene));
dispatch({ type: "single user defined gene complete" });
}
}
render() {
const { userDefinedGenesLoading } = this.props;
const { tab, bulkAdd, activeItem, status, geneNames } = this.state;
@@ -3,6 +3,7 @@ import React from "react";
import { connect } from "react-redux";
import AnnoDialog from "../../annoDialog";
import LabelInput from "../../labelInput";
import actions from "../../../actions";
@connect((state) => ({
annotations: state.annotations,
@@ -56,11 +57,7 @@ class CreateGenesetDialogue extends React.PureComponent {
});
});
dispatch({
type: "geneset: add genes",
genesetName,
genes: genesTmpHardcodedFormat,
});
dispatch(actions.genesetAddGenes(genesetName, genesTmpHardcodedFormat));
}
dispatch({
type: "geneset: disable create geneset mode",
@@ -12,6 +12,7 @@ import {
} from "@blueprintjs/core";
import * as globals from "../../../globals";
import actions from "../../../actions";
import AddGeneToGenesetDialogue from "./addGeneToGenesetDialogue";
@connect((state) => {
@@ -44,7 +45,7 @@ class GenesetMenus extends React.PureComponent {
handleDeleteCategory = () => {
const { dispatch, geneset } = this.props;
dispatch({ type: "geneset: delete", genesetName: geneset });
dispatch(actions.genesetDelete(geneset));
};
render() {
+137 -137
View File
@@ -303,13 +303,6 @@ class Graph extends React.Component {
window.removeEventListener("resize", this.handleResize);
}
setReglCanvas = (canvas) => {
this.reglCanvas = canvas;
this.setState({
...Graph.createReglState(canvas),
});
};
handleResize = () => {
const { state } = this.state;
const viewport = this.getViewportDimensions();
@@ -321,14 +314,6 @@ class Graph extends React.Component {
});
};
getViewportDimensions = () => {
const { viewportRef } = this.props;
return {
height: viewportRef.clientHeight,
width: viewportRef.clientWidth,
};
};
handleCanvasEvent = (e) => {
const { camera, projectionTF } = this.state;
if (e.type !== "wheel") e.preventDefault();
@@ -340,6 +325,143 @@ class Graph extends React.Component {
}
};
handleBrushDragAction() {
/*
event describing brush position:
@-------|
| |
| |
|-------@
*/
// ignore programatically generated events
if (d3.event.sourceEvent === null || !d3.event.selection) return;
const { dispatch, layoutChoice } = this.props;
const s = d3.event.selection;
const northwest = this.mapScreenToPoint(s[0]);
const southeast = this.mapScreenToPoint(s[1]);
const [minX, maxY] = northwest;
const [maxX, minY] = southeast;
dispatch(
actions.graphBrushChangeAction(layoutChoice.current, {
minX,
minY,
maxX,
maxY,
northwest,
southeast,
})
);
}
handleBrushStartAction() {
// Ignore programatically generated events.
if (!d3.event.sourceEvent) return;
const { dispatch } = this.props;
dispatch(actions.graphBrushStartAction());
}
handleBrushEndAction() {
// Ignore programatically generated events.
if (!d3.event.sourceEvent) return;
/*
coordinates will be included if selection made, null
if selection cleared.
*/
const { dispatch, layoutChoice } = this.props;
const s = d3.event.selection;
if (s) {
const northwest = this.mapScreenToPoint(s[0]);
const southeast = this.mapScreenToPoint(s[1]);
const [minX, maxY] = northwest;
const [maxX, minY] = southeast;
dispatch(
actions.graphBrushEndAction(layoutChoice.current, {
minX,
minY,
maxX,
maxY,
northwest,
southeast,
})
);
} else {
dispatch(actions.graphBrushDeselectAction(layoutChoice.current));
}
}
handleBrushDeselectAction() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphBrushDeselectAction(layoutChoice.current));
}
handleLassoStart() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphLassoStartAction(layoutChoice.current));
}
// when a lasso is completed, filter to the points within the lasso polygon
handleLassoEnd(polygon) {
const minimumPolygonArea = 10;
const { dispatch, layoutChoice } = this.props;
if (
polygon.length < 3 ||
Math.abs(d3.polygonArea(polygon)) < minimumPolygonArea
) {
// if less than three points, or super small area, treat as a clear selection.
dispatch(actions.graphLassoDeselectAction(layoutChoice.current));
} else {
dispatch(
actions.graphLassoEndAction(
layoutChoice.current,
polygon.map((xy) => this.mapScreenToPoint(xy))
)
);
}
}
handleLassoCancel() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphLassoCancelAction(layoutChoice.current));
}
handleLassoDeselectAction() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphLassoDeselectAction(layoutChoice.current));
}
handleDeselectAction() {
const { selectionTool } = this.props;
if (selectionTool === "brush") this.handleBrushDeselectAction();
if (selectionTool === "lasso") this.handleLassoDeselectAction();
}
handleOpacityRangeChange(e) {
const { dispatch } = this.props;
dispatch({
type: "change opacity deselected cells in 2d graph background",
data: e.target.value,
});
}
setReglCanvas = (canvas) => {
this.reglCanvas = canvas;
this.setState({
...Graph.createReglState(canvas),
});
};
getViewportDimensions = () => {
const { viewportRef } = this.props;
return {
height: viewportRef.clientHeight,
width: viewportRef.clientWidth,
};
};
createToolSVG = () => {
/*
Called from componentDidUpdate. Create the tool SVG, and return any
@@ -589,128 +711,6 @@ class Graph extends React.Component {
];
}
handleBrushDragAction() {
/*
event describing brush position:
@-------|
| |
| |
|-------@
*/
// ignore programatically generated events
if (d3.event.sourceEvent === null || !d3.event.selection) return;
const { dispatch, layoutChoice } = this.props;
const s = d3.event.selection;
const northwest = this.mapScreenToPoint(s[0]);
const southeast = this.mapScreenToPoint(s[1]);
const [minX, maxY] = northwest;
const [maxX, minY] = southeast;
dispatch(
actions.graphBrushChangeAction(layoutChoice.current, {
minX,
minY,
maxX,
maxY,
northwest,
southeast,
})
);
}
handleBrushStartAction() {
// Ignore programatically generated events.
if (!d3.event.sourceEvent) return;
const { dispatch } = this.props;
dispatch(actions.graphBrushStartAction());
}
handleBrushEndAction() {
// Ignore programatically generated events.
if (!d3.event.sourceEvent) return;
/*
coordinates will be included if selection made, null
if selection cleared.
*/
const { dispatch, layoutChoice } = this.props;
const s = d3.event.selection;
if (s) {
const northwest = this.mapScreenToPoint(s[0]);
const southeast = this.mapScreenToPoint(s[1]);
const [minX, maxY] = northwest;
const [maxX, minY] = southeast;
dispatch(
actions.graphBrushEndAction(layoutChoice.current, {
minX,
minY,
maxX,
maxY,
northwest,
southeast,
})
);
} else {
dispatch(actions.graphBrushDeselectAction(layoutChoice.current));
}
}
handleBrushDeselectAction() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphBrushDeselectAction(layoutChoice.current));
}
handleLassoStart() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphLassoStartAction(layoutChoice.current));
}
// when a lasso is completed, filter to the points within the lasso polygon
handleLassoEnd(polygon) {
const minimumPolygonArea = 10;
const { dispatch, layoutChoice } = this.props;
if (
polygon.length < 3 ||
Math.abs(d3.polygonArea(polygon)) < minimumPolygonArea
) {
// if less than three points, or super small area, treat as a clear selection.
dispatch(actions.graphLassoDeselectAction(layoutChoice.current));
} else {
dispatch(
actions.graphLassoEndAction(
layoutChoice.current,
polygon.map((xy) => this.mapScreenToPoint(xy))
)
);
}
}
handleLassoCancel() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphLassoCancelAction(layoutChoice.current));
}
handleLassoDeselectAction() {
const { dispatch, layoutChoice } = this.props;
dispatch(actions.graphLassoDeselectAction(layoutChoice.current));
}
handleDeselectAction() {
const { selectionTool } = this.props;
if (selectionTool === "brush") this.handleBrushDeselectAction();
if (selectionTool === "lasso") this.handleLassoDeselectAction();
}
handleOpacityRangeChange(e) {
const { dispatch } = this.props;
dispatch({
type: "change opacity deselected cells in 2d graph background",
data: e.target.value,
});
}
renderCanvas = renderThrottle(() => {
const {
regl,
@@ -11,7 +11,7 @@ import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers
datasetTitle: state.config?.displayNames?.dataset ?? "",
aboutURL: state.config?.links?.["about-dataset"],
isOpen: state.controls.datasetDrawer,
dataPortalProps: state.config?.["corpora_props"],
dataPortalProps: state.config?.corpora_props,
};
})
class InfoDrawer extends PureComponent {
@@ -169,7 +169,7 @@ const InfoFormat = React.memo(
({ datasetTitle, singleValueCategories, aboutURL, dataPortalProps = {} }) => {
if (
["1.0.0", "1.1.0"].indexOf(
dataPortalProps.version?.["corpora_schema_version"]
dataPortalProps.version?.corpora_schema_version
) === -1
) {
dataPortalProps = {};
@@ -13,15 +13,14 @@ const DATASET_TITLE_FONT_SIZE = 14;
@connect((state) => {
const { corpora_props: corporaProps } = state.config;
const correctVersion =
["1.0.0", "1.1.0"].indexOf(
corporaProps?.version?.["corpora_schema_version"]
) > -1;
["1.0.0", "1.1.0"].indexOf(corporaProps?.version?.corpora_schema_version) >
-1;
return {
datasetTitle: state.config?.displayNames?.dataset ?? "",
libraryVersions: state.config?.["library_versions"],
libraryVersions: state.config?.library_versions,
aboutLink: state.config?.links?.["about-dataset"],
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
tosURL: state.config?.parameters?.about_legal_tos,
privacyURL: state.config?.parameters?.about_legal_privacy,
title: correctVersion ? corporaProps?.title : undefined,
};
})
@@ -11,7 +11,7 @@ import CellSetButton from "./cellSetButtons";
celllist1: state.differential?.celllist1,
celllist2: state.differential?.celllist2,
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
diffexpCellcountMax: state.config?.limits?.["diffexp_cellcount_max"],
diffexpCellcountMax: state.config?.limits?.diffexp_cellcount_max,
}))
class DiffexpButtons extends React.PureComponent {
computeDiffExp = () => {
+3 -3
View File
@@ -40,7 +40,7 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
celllist1: state.differential.celllist1,
celllist2: state.differential.celllist2,
libraryVersions: state.config?.["library_versions"],
libraryVersions: state.config?.library_versions,
auth: state.config?.authentication,
userInfo: state.userInfo,
undoDisabled: state["@@undoable/past"].length === 0,
@@ -50,8 +50,8 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
diffexpMayBeSlow:
state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
tosURL: state.config?.parameters?.about_legal_tos,
privacyURL: state.config?.parameters?.about_legal_privacy,
categoricalSelection: state.categoricalSelection,
enableReembedding:
state.config?.parameters?.["enable-reembedding"] ?? false,
@@ -303,9 +303,11 @@ class Scatterplot extends React.PureComponent {
return [
"X",
{
field: "var",
column: varIndex,
value: geneName,
where: {
field: "var",
column: varIndex,
value: geneName,
},
},
];
}
+2 -2
View File
@@ -11,8 +11,8 @@ import {
import { storageGet, storageSet, KEYS } from "../util/localStorage";
@connect((state) => ({
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
tosURL: state.config?.parameters?.about_legal_tos,
privacyURL: state.config?.parameters?.about_legal_privacy,
}))
class TermsPrompt extends React.PureComponent {
constructor(props) {