From 82de4178d99e15c3f00cf4f4e3c3a3b02eb7bbe2 Mon Sep 17 00:00:00 2001 From: Severiano Badajoz Date: Wed, 23 Jun 2021 10:59:10 -0700 Subject: [PATCH] feat: add quick gene lookup functionality (#2250) * add/remove gene functionality back with geneset style gene * styling and expansion * memo gene list to prevent re render --- client/src/components/geneExpression/gene.js | 28 +-- client/src/components/geneExpression/index.js | 68 ++++++- .../components/geneExpression/quickGene.js | 176 ++++++++++++++++++ 3 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 client/src/components/geneExpression/quickGene.js diff --git a/client/src/components/geneExpression/gene.js b/client/src/components/geneExpression/gene.js index 5912e912..7d0834eb 100644 --- a/client/src/components/geneExpression/gene.js +++ b/client/src/components/geneExpression/gene.js @@ -64,6 +64,8 @@ class Gene extends React.Component { isColorAccessor, isScatterplotXXaccessor, isScatterplotYYaccessor, + quickGene, + removeGene, } = this.props; const { geneIsExpanded } = this.state; const geneSymbolWidth = 60 + (geneIsExpanded ? MINI_HISTOGRAM_WIDTH : 0); @@ -94,16 +96,18 @@ class Gene extends React.Component { }} >
- + {!quickGene && ( + + )} @@ -132,7 +136,9 @@ class Gene extends React.Component { minimal small data-testid={`delete-from-geneset-${gene}`} - onClick={this.handleDeleteGeneFromSet} + onClick={ + quickGene ? removeGene(gene) : this.handleDeleteGeneFromSet + } intent="none" style={{ fontWeight: 700, marginRight: 2 }} icon={} diff --git a/client/src/components/geneExpression/index.js b/client/src/components/geneExpression/index.js index b7990627..0670e751 100644 --- a/client/src/components/geneExpression/index.js +++ b/client/src/components/geneExpression/index.js @@ -1,8 +1,10 @@ import React from "react"; import { connect } from "react-redux"; -import { Button } from "@blueprintjs/core"; -import GeneSet from "./geneSet"; +import { Button, H4, Icon } from "@blueprintjs/core"; +import { IconNames } from "@blueprintjs/icons"; +import GeneSet from "./geneSet"; +import QuickGene from "./quickGene"; import CreateGenesetDialogue from "./menus/createGenesetDialogue"; @connect((state) => { @@ -11,6 +13,11 @@ import CreateGenesetDialogue from "./menus/createGenesetDialogue"; }; }) class GeneExpression extends React.Component { + constructor(props) { + super(props); + this.state = { geneSetsExpanded: true }; + } + renderGeneSets = () => { const sets = []; const { genesets } = this.props; @@ -31,25 +38,66 @@ class GeneExpression extends React.Component { handleActivateCreateGenesetMode = () => { const { dispatch } = this.props; + const { geneSetsExpanded } = this.state; dispatch({ type: "geneset: activate add new geneset mode" }); + if (!geneSetsExpanded) { + this.setState((state) => { + return { ...state, geneSetsExpanded: true }; + }); + } + }; + + handleExpandGeneSets = () => { + this.setState((state) => { + return { ...state, geneSetsExpanded: !state.geneSetsExpanded }; + }); }; render() { + const { geneSetsExpanded } = this.state; return (
+
-
- + Gene Sets{" "} + {geneSetsExpanded ? ( + + ) : ( + + )} + + +
+ +
-
{this.renderGeneSets()}
+ + {geneSetsExpanded &&
{this.renderGeneSets()}
}
); } diff --git a/client/src/components/geneExpression/quickGene.js b/client/src/components/geneExpression/quickGene.js new file mode 100644 index 00000000..219345f1 --- /dev/null +++ b/client/src/components/geneExpression/quickGene.js @@ -0,0 +1,176 @@ +import { H4, Icon, MenuItem } from "@blueprintjs/core"; +import { IconNames } from "@blueprintjs/icons"; +import React, { useState, useEffect, useRef, useMemo } from "react"; +import fuzzysort from "fuzzysort"; +import { Suggest } from "@blueprintjs/select"; +import { useSelector, useDispatch } from "react-redux"; + +import Gene from "./gene"; + +import { postUserErrorToast } from "../framework/toasters"; +import actions from "../../actions"; + +const usePrevious = (value) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; + +function QuickGene() { + const dispatch = useDispatch(); + + const [isExpanded, setIsExpanded] = useState(true); + const [geneNames, setGeneNames] = useState([]); + const [, setStatus] = useState("pending"); + + const { annoMatrix, userDefinedGenes, userDefinedGenesLoading } = useSelector( + (state) => { + return { + annoMatrix: state.annoMatrix, + userDefinedGenes: state.controls.userDefinedGenes, + userDefinedGenesLoading: state.controls.userDefinedGenesLoading, + }; + } + ); + + const prevProps = usePrevious({ annoMatrix }); + + useEffect(async () => { + if (!annoMatrix) return; + if (annoMatrix !== prevProps?.annoMatrix) { + const { schema } = annoMatrix; + const varIndex = schema.annotations.var.index; + + setStatus("pending"); + try { + const df = await annoMatrix.fetch("var", varIndex); + setStatus("success"); + setGeneNames(df.col(varIndex).asArray()); + } catch (error) { + setStatus("error"); + throw error; + } + } + }, [annoMatrix, prevProps]); + + const handleExpand = () => setIsExpanded(!isExpanded); + + 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 handleClick = (g) => { + if (!g) return; + const gene = g.target; + if (userDefinedGenes.indexOf(gene) !== -1) { + postUserErrorToast("That gene already exists"); + } 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" }); + } + }; + + 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 + }); + + const removeGene = (gene) => () => { + dispatch({ type: "clear user defined gene", data: gene }); + }; + + const QuickGenes = useMemo(() => { + return userDefinedGenes.map((gene) => { + return ( + + ); + }); + }, [userDefinedGenes]); + + return ( +
+

+ Genes{" "} + {isExpanded ? ( + + ) : ( + + )} +

+ {isExpanded && ( + <> +
+ true : () => false} + noResults={} + onItemSelect={(g) => { + /* this happens on 'enter' */ + handleClick(g); + }} + initialContent={} + inputProps={{ + "data-testid": "gene-search", + placeholder: "Quick Gene Search", + leftIcon: IconNames.SEARCH, + fill: true, + }} + inputValueRenderer={() => { + return ""; + }} + itemListPredicate={filterGenes} + itemRenderer={renderGene} + items={geneNames || ["No genes"]} + popoverProps={{ minimal: true }} + fill + /> +
+ {QuickGenes} + + )} +
+ ); +} + +export default QuickGene;