Redux refactor (#1571)

* refactor categorical controls state

* lint

* fix race condition in tests

* fix typo

* add missing update on subset

* remove obsolete code

* update jest and puppeteer major version; update all minors

* update when label changes

* remove lint from tests; increase timeouts in e2e tests

* initial refactoring to new async annomatrix

* refine error handling

* fix bad merge

* add continuous legend

* lint

* fix memoization in color table creators

* partial implementation of user defined annotations

* add new annotations action creator file

* first pass at user annotations

* additional user annotation bug fixes

* user annotation auto-save

* unit test cleanup

* lint

* refactor into multiple files

* cleanup

* add column GC

* fix several bugs in user annotations

* remove debug code

* no anonymous functions

* undo redo cleanup

* file cleanup

* scatterplot

* performance

* cleanup

* remove old code

* render in parallel with load

* fix race condition

* simply graph rendering

* render throttle DRY

* fix category label order

* fix typo in e2e test setup

* re-fix the e2e test setup

* be more tolerant of races

* anno matrix unit tests

* temp disable reembedding

* pilot port continuous histo to react-async

* name change

* lint

* fix repaint bug

* typo fix

* update snap to match new ids

* world/universe name cleanup

* move annoMatrix to src dir

* use private underscore naming convention

* fix corner case in all selected

* name cleanup

* add layout control

* init edge case

* lint

* port scatterplot

* fix label indexing bug and improve tests

* port category to react-async

* fix user annotation labelling while subset

* select all of prev layout on layout switch

* fix race with crossfilter update

* prettier lint

* fix misleading comment

* fix url composition in loader

* first pass at crossfilter tests

* lint

* lint

* fix typo

* improved error handling for network errors

* fix memoization bug

* add memo

* refactor for performnce

* add missing single-value handling in select exact parser

* small bugs discovered by tests

* lint

* additional crossfilter unit tests

* remove extraneous comment

* add support for automatic category determination

* lint

* fix render bug in category

* take advantage of schema categories guarantee

* lint

* do not clear history when resetting

* enhanced annomatrix gc

* lint

* finish renaming to follow conventions; fix clone race bug

* lint

* add priority based loading to improve initial data load UX

* crossfilter cache perf

* perf tuning

* remove timers

* documentation

* PR review changes

* PR review changes

* more PR review edits

* improve clarity of comment

* more PR review fixes

* port centroidLabels to use react-async

* remove dead code

* pr review updates

* oops, remove logging
This commit is contained in:
Bruce Martin
2020-07-14 13:53:33 -07:00
committed by GitHub
parent f69d141336
commit 1269e188be
95 changed files with 18051 additions and 5382 deletions
@@ -1,4 +1,3 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react";
@@ -34,9 +33,6 @@ const renderGene = (fuzzySortResult, { handleClick, modifiers }) => {
active={modifiers.active}
disabled={modifiers.disabled}
data-testid={`suggest-menu-item-${geneName}`}
// Use of annotations in this way is incorrect and dataset specific.
// See https://github.com/chanzuckerberg/cellxgene/issues/483
// label={gene.n_counts}
key={geneName}
onClick={(g) =>
/* this fires when user clicks a menu item */
@@ -56,11 +52,9 @@ const filterGenes = (query, genes) =>
@connect((state) => {
return {
obsAnnotations: state.world?.obsAnnotations,
annoMatrix: state.annoMatrix,
userDefinedGenes: state.controls.userDefinedGenes,
userDefinedGenesLoading: state.controls.userDefinedGenesLoading,
world: state.world,
colorAccessor: state.colors.colorAccessor,
differential: state.differential,
};
})
@@ -71,9 +65,19 @@ class AddGenes extends React.Component {
bulkAdd: "",
tab: "autosuggest",
activeItem: null,
geneNames: [],
status: "pending",
};
}
componentDidMount() {
this.updateState();
}
componentDidUpdate(prevProps) {
this.updateState(prevProps);
}
_genesToUpper = (listGenes) => {
// Has to be a Map to preserve index
const upperGenes = new Map();
@@ -88,9 +92,8 @@ class AddGenes extends React.Component {
_memoGenesToUpper = memoize(this._genesToUpper, (arr) => arr);
handleBulkAddClick = () => {
const { world, dispatch, userDefinedGenes } = this.props;
const varIndexName = world.schema.annotations.var.index;
const { bulkAdd } = this.state;
const { dispatch, userDefinedGenes } = this.props;
const { bulkAdd, geneNames } = this.state;
/*
test:
@@ -98,18 +101,14 @@ class AddGenes extends React.Component {
*/
if (bulkAdd !== "") {
const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), "");
console.log("geneExpression genes", genes);
if (genes.length === 0) {
return keepAroundErrorToast("Must enter a gene name.");
}
const worldGenes =
world.varAnnotations?.col(varIndexName)?.asArray() || [];
// These gene lists are unique enough where memoization is useless
const upperGenes = this._genesToUpper(genes);
const upperUserDefinedGenes = this._genesToUpper(userDefinedGenes);
const upperWorldGenes = this._memoGenesToUpper(worldGenes);
const upperGeneNames = this._memoGenesToUpper(geneNames);
dispatch({ type: "bulk user defined gene start" });
@@ -119,7 +118,7 @@ class AddGenes extends React.Component {
return keepAroundErrorToast("That gene already exists");
}
const indexOfGene = upperWorldGenes.get(upperGene);
const indexOfGene = upperGeneNames.get(upperGene);
if (indexOfGene === undefined) {
return keepAroundErrorToast(
@@ -129,7 +128,7 @@ class AddGenes extends React.Component {
);
}
return dispatch(
actions.requestUserDefinedGene(worldGenes[indexOfGene])
actions.requestUserDefinedGene(geneNames[indexOfGene])
);
})
).then(
@@ -142,6 +141,27 @@ class AddGenes extends React.Component {
return undefined;
};
async updateState(prevProps) {
const { annoMatrix } = this.props;
if (!annoMatrix) return;
if (annoMatrix !== prevProps?.annoMatrix) {
const { schema } = annoMatrix;
const varIndex = schema.annotations.var.index;
this.setState({ status: "pending" });
try {
const df = await annoMatrix.fetch("var", varIndex);
this.setState({
status: "success",
geneNames: df.col(varIndex).asArray(),
});
} catch (error) {
this.setState({ status: "error" });
throw error;
}
}
}
placeholderGeneNames() {
/*
return a string containing gene name suggestions for use as a user hint.
@@ -151,10 +171,7 @@ class AddGenes extends React.Component {
NOTE: the random selection means it will re-render constantly.
*/
const { world } = this.props;
const { varAnnotations } = world;
const varIndexName = world.schema.annotations.var.index;
const geneNames = varAnnotations.col(varIndexName).asArray();
const { geneNames } = this.state;
if (geneNames.length > 0) {
const placeholder = [];
let len = geneNames.length;
@@ -175,8 +192,8 @@ class AddGenes extends React.Component {
}
handleClick(g) {
const { world, dispatch, userDefinedGenes } = this.props;
const varIndexName = world.schema.annotations.var.index;
const { dispatch, userDefinedGenes } = this.props;
const { geneNames } = this.state;
if (!g) return;
const gene = g.target;
if (userDefinedGenes.indexOf(gene) !== -1) {
@@ -185,27 +202,21 @@ class AddGenes extends React.Component {
postUserErrorToast(
`That's too many genes, you can have at most ${globals.maxUserDefinedGenes} user defined genes`
);
} else if (
world.varAnnotations.col(varIndexName).indexOf(gene) === undefined
) {
} 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)).then(
() => dispatch({ type: "single user defined gene complete" }),
() => dispatch({ type: "single user defined gene error" })
);
dispatch(actions.requestUserDefinedGene(gene));
dispatch({ type: "single user defined gene complete" });
}
}
render() {
const { world, userDefinedGenesLoading } = this.props;
const varIndexName = world?.schema?.annotations?.var?.index;
const varIndex = world?.varAnnotations?.col(varIndexName)?.asArray();
const { tab, bulkAdd, activeItem } = this.state;
const { userDefinedGenesLoading } = this.props;
const { tab, bulkAdd, activeItem, status, geneNames } = this.state;
// may still be loading!
if (!varIndex) return null;
if (status !== "success") return null;
return (
<div>
@@ -263,7 +274,7 @@ class AddGenes extends React.Component {
itemListPredicate={filterGenes}
onActiveItemChange={(item) => this.setState({ activeItem: item })}
itemRenderer={renderGene}
items={varIndex || ["No genes"]}
items={geneNames || ["No genes"]}
popoverProps={{ minimal: true }}
/>
<Button
+4 -28
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react";
@@ -10,23 +9,13 @@ import AddGenes from "./addGenes";
@connect((state) => {
return {
obsAnnotations: state.world?.obsAnnotations,
userDefinedGenes: state.controls.userDefinedGenes,
userDefinedGenesLoading: state.controls.userDefinedGenesLoading,
world: state.world,
colorAccessor: state.colors.colorAccessor,
differential: state.differential,
};
})
class GeneExpression extends React.Component {
render() {
const { world, userDefinedGenes, differential } = this.props;
const varIndexName = world?.schema?.annotations?.var?.index;
const varIndex = world?.varAnnotations?.col(varIndexName)?.asArray();
// may still be loading!
if (!varIndex) return null;
const { userDefinedGenes, differential } = this.props;
return (
<div
style={{
@@ -35,19 +24,13 @@ class GeneExpression extends React.Component {
>
<div>
<AddGenes />
{world && userDefinedGenes.length > 0
{userDefinedGenes.length > 0
? _.map(userDefinedGenes, (geneName, index) => {
const values = world.varData.col(geneName);
if (!values) {
return null;
}
const summary = values.summarize();
return (
<HistogramBrush
key={geneName}
field={geneName}
zebra={index % 2 === 0}
ranges={summary}
isUserDefined
/>
);
@@ -57,18 +40,11 @@ class GeneExpression extends React.Component {
<div>
{differential.diffExp
? _.map(differential.diffExp, (value, index) => {
const name = world.varAnnotations.at(value[0], varIndexName);
const values = world.varData.col(name);
if (!values) {
return null;
}
const summary = values.summarize();
return (
<HistogramBrush
key={name}
field={name}
key={value[0]}
field={value[0]}
zebra={index % 2 === 0}
ranges={summary}
isDiffExp
logFoldChange={value[1]}
pval={value[2]}