diff --git a/client/src/components/geneExpression/addGenes.js b/client/src/components/geneExpression/addGenes.js new file mode 100644 index 00000000..aeb3001e --- /dev/null +++ b/client/src/components/geneExpression/addGenes.js @@ -0,0 +1,318 @@ +// jshint esversion: 6 +/* rc slider https://www.npmjs.com/package/rc-slider */ + +import React from "react"; +import _ from "lodash"; +import fuzzysort from "fuzzysort"; +import { connect } from "react-redux"; +import { Suggest } from "@blueprintjs/select"; +import { + MenuItem, + Button, + FormGroup, + InputGroup, + ControlGroup, +} from "@blueprintjs/core"; +import * as globals from "../../globals"; +import actions from "../../actions"; +import { + postUserErrorToast, + keepAroundErrorToast, +} from "../framework/toasters"; + +import { memoize } from "../../util/dataframe/util"; + +const renderGene = (fuzzySortResult, { handleClick, modifiers }) => { + if (!modifiers.matchesPredicate) { + return null; + } + /* the fuzzysort wraps the object with other properties, like a score */ + const geneName = fuzzySortResult.target; + + return ( + + /* this fires when user clicks a menu item */ + handleClick(g) + } + text={geneName} + /> + ); +}; + +const filterGenes = (query, genes) => + /* fires on load, once, and then for each character typed into the input */ + fuzzysort.go(query, genes, { + limit: 5, + threshold: -10000, // don't return bad results + }); + +@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 AddGenes extends React.Component { + constructor(props) { + super(props); + this.state = { + bulkAdd: "", + tab: "autosuggest", + activeItem: null, + }; + } + + _genesToUpper = (listGenes) => { + // Has to be a Map to preserve index + const upperGenes = new Map(); + for (let i = 0, { length } = listGenes; i < length; i += 1) { + upperGenes.set(listGenes[i].toUpperCase(), i); + } + + return upperGenes; + }; + + // eslint-disable-next-line react/sort-comp + _memoGenesToUpper = memoize(this._genesToUpper, (arr) => arr); + + handleBulkAddClick = () => { + const { world, dispatch, userDefinedGenes } = this.props; + const varIndexName = world.schema.annotations.var.index; + const { bulkAdd } = this.state; + + /* + test: + Apod,,, Cd74,, ,,, Foo, Bar-2,, + */ + 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); + + dispatch({ type: "bulk user defined gene start" }); + + Promise.all( + [...upperGenes.keys()].map((upperGene) => { + if (upperUserDefinedGenes.get(upperGene) !== undefined) { + return keepAroundErrorToast("That gene already exists"); + } + + const indexOfGene = upperWorldGenes.get(upperGene); + + if (indexOfGene === undefined) { + return keepAroundErrorToast( + `${ + genes[upperGenes.get(upperGene)] + } doesn't appear to be a valid gene name.` + ); + } + return dispatch( + actions.requestUserDefinedGene(worldGenes[indexOfGene]) + ); + }) + ).then( + () => dispatch({ type: "bulk user defined gene complete" }), + () => dispatch({ type: "bulk user defined gene error" }) + ); + } + + this.setState({ bulkAdd: "" }); + return undefined; + }; + + placeholderGeneNames() { + /* + return a string containing gene name suggestions for use as a user hint. + Eg., Apod, Cd74, ... + Will return a max of 3 genes, totalling 15 characters in length. + Randomly selects gene names. + + 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(); + if (geneNames.length > 0) { + const placeholder = []; + let len = geneNames.length; + const maxGeneNameCount = 3; + const maxStrLength = 15; + len = len < maxGeneNameCount ? len : maxGeneNameCount; + for (let i = 0, strLen = 0; i < len && strLen < maxStrLength; i += 1) { + const deal = Math.floor(Math.random() * geneNames.length); + const geneName = geneNames[deal]; + placeholder.push(geneName); + strLen += geneName.length + 2; // '2' is the length of a comma and space + } + placeholder.push("..."); + return placeholder.join(", "); + } + // default - should never happen. + return "Apod, Cd74, ..."; + } + + handleClick(g) { + const { world, dispatch, userDefinedGenes } = this.props; + const varIndexName = world.schema.annotations.var.index; + 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 ( + world.varAnnotations.col(varIndexName).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" }) + ); + } + } + + 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; + + // may still be loading! + if (!varIndex) return null; + + return ( +
+
+ + +
+ {tab === "autosuggest" ? ( + + true : () => false} + noResults={} + onItemSelect={(g) => { + /* this happens on 'enter' */ + this.handleClick(g); + }} + initialContent={} + inputProps={{ "data-testid": "gene-search" }} + inputValueRenderer={() => { + return ""; + }} + itemListPredicate={filterGenes} + onActiveItemChange={(item) => this.setState({ activeItem: item })} + itemRenderer={renderGene.bind(this)} + items={varIndex || ["No genes"]} + popoverProps={{ minimal: true }} + /> + + + ) : null} + {tab === "bulkadd" ? ( +
+
{ + e.preventDefault(); + this.handleBulkAddClick(); + }} + > + + + { + this.setState({ bulkAdd: e.target.value }); + }} + id="text-input-bulk-add" + data-testid="input-bulk-add" + placeholder={this.placeholderGeneNames()} + value={bulkAdd} + /> + + + +
+
+ ) : null} +
+ ); + } +} + +export default AddGenes; diff --git a/client/src/components/geneExpression/index.js b/client/src/components/geneExpression/index.js index 16a91184..a7f7a058 100644 --- a/client/src/components/geneExpression/index.js +++ b/client/src/components/geneExpression/index.js @@ -3,58 +3,10 @@ import React from "react"; import _ from "lodash"; -import fuzzysort from "fuzzysort"; - import { connect } from "react-redux"; -import { - MenuItem, - Button, - FormGroup, - InputGroup, - ControlGroup, -} from "@blueprintjs/core"; -import { Suggest } from "@blueprintjs/select"; import HistogramBrush from "../brushableHistogram"; import * as globals from "../../globals"; -import actions from "../../actions"; -import { - postUserErrorToast, - keepAroundErrorToast, -} from "../framework/toasters"; - -import { memoize } from "../../util/dataframe/util"; - -const renderGene = (fuzzySortResult, { handleClick, modifiers }) => { - if (!modifiers.matchesPredicate) { - return null; - } - /* the fuzzysort wraps the object with other properties, like a score */ - const geneName = fuzzySortResult.target; - - return ( - - /* this fires when user clicks a menu item */ - handleClick(g) - } - text={geneName} - /> - ); -}; - -const filterGenes = (query, genes) => - /* fires on load, once, and then for each character typed into the input */ - fuzzysort.go(query, genes, { - limit: 5, - threshold: -10000, // don't return bad results - }); +import AddGenes from "./addGenes"; @connect((state) => { return { @@ -67,148 +19,10 @@ const filterGenes = (query, genes) => }; }) class GeneExpression extends React.Component { - constructor(props) { - super(props); - this.state = { - bulkAdd: "", - tab: "autosuggest", - activeItem: null, - }; - } - - _genesToUpper = (listGenes) => { - // Has to be a Map to preserve index - const upperGenes = new Map(); - for (let i = 0, { length } = listGenes; i < length; i += 1) { - upperGenes.set(listGenes[i].toUpperCase(), i); - } - - return upperGenes; - }; - - // eslint-disable-next-line react/sort-comp - _memoGenesToUpper = memoize(this._genesToUpper, (arr) => arr); - - handleBulkAddClick = () => { - const { world, dispatch, userDefinedGenes } = this.props; - const varIndexName = world.schema.annotations.var.index; - const { bulkAdd } = this.state; - - /* - test: - Apod,,, Cd74,, ,,, Foo, Bar-2,, - */ - if (bulkAdd !== "") { - const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), ""); - 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); - - dispatch({ type: "bulk user defined gene start" }); - - Promise.all( - [...upperGenes.keys()].map((upperGene) => { - if (upperUserDefinedGenes.get(upperGene) !== undefined) { - return keepAroundErrorToast("That gene already exists"); - } - - const indexOfGene = upperWorldGenes.get(upperGene); - - if (indexOfGene === undefined) { - return keepAroundErrorToast( - `${ - genes[upperGenes.get(upperGene)] - } doesn't appear to be a valid gene name.` - ); - } - return dispatch( - actions.requestUserDefinedGene(worldGenes[indexOfGene]) - ); - }) - ).then( - () => dispatch({ type: "bulk user defined gene complete" }), - () => dispatch({ type: "bulk user defined gene error" }) - ); - } - - this.setState({ bulkAdd: "" }); - return undefined; - }; - - placeholderGeneNames() { - /* - return a string containing gene name suggestions for use as a user hint. - Eg., Apod, Cd74, ... - Will return a max of 3 genes, totalling 15 characters in length. - Randomly selects gene names. - - 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(); - if (geneNames.length > 0) { - const placeholder = []; - let len = geneNames.length; - const maxGeneNameCount = 3; - const maxStrLength = 15; - len = len < maxGeneNameCount ? len : maxGeneNameCount; - for (let i = 0, strLen = 0; i < len && strLen < maxStrLength; i += 1) { - const deal = Math.floor(Math.random() * geneNames.length); - const geneName = geneNames[deal]; - placeholder.push(geneName); - strLen += geneName.length + 2; // '2' is the length of a comma and space - } - placeholder.push("..."); - return placeholder.join(", "); - } - // default - should never happen. - return "Apod, Cd74, ..."; - } - - handleClick(g) { - const { world, dispatch, userDefinedGenes } = this.props; - const varIndexName = world.schema.annotations.var.index; - 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 ( - world.varAnnotations.col(varIndexName).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" }) - ); - } - } - render() { - const { - world, - userDefinedGenes, - userDefinedGenesLoading, - differential, - } = this.props; + const { world, userDefinedGenes, differential } = this.props; const varIndexName = world?.schema?.annotations?.var?.index; const varIndex = world?.varAnnotations?.col(varIndexName)?.asArray(); - const { tab, bulkAdd, activeItem } = this.state; // may still be loading! if (!varIndex) return null; @@ -220,112 +34,7 @@ class GeneExpression extends React.Component { }} >
-
- - -
- - {tab === "autosuggest" ? ( - - true : () => false - } - noResults={} - onItemSelect={(g) => { - /* this happens on 'enter' */ - this.handleClick(g); - }} - initialContent={} - inputProps={{ "data-testid": "gene-search" }} - inputValueRenderer={() => { - return ""; - }} - itemListPredicate={filterGenes} - onActiveItemChange={(item) => - this.setState({ activeItem: item }) - } - itemRenderer={renderGene.bind(this)} - items={varIndex || ["No genes"]} - popoverProps={{ minimal: true }} - /> - - - ) : null} - {tab === "bulkadd" ? ( -
-
{ - e.preventDefault(); - this.handleBulkAddClick(); - }} - > - - - { - this.setState({ bulkAdd: e.target.value }); - }} - id="text-input-bulk-add" - data-testid="input-bulk-add" - placeholder={this.placeholderGeneNames()} - value={bulkAdd} - /> - - - -
-
- ) : null} + {world && userDefinedGenes.length > 0 ? _.map(userDefinedGenes, (geneName, index) => { const values = world.varData.col(geneName);