Skip to content

Commit

Permalink
feat: add quick gene lookup functionality (#2250)
Browse files Browse the repository at this point in the history
* add/remove gene functionality back with geneset style gene

* styling and expansion

* memo gene list to prevent re render
  • Loading branch information
seve authored Jun 23, 2021
1 parent 023ae10 commit 82de417
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 21 deletions.
28 changes: 17 additions & 11 deletions client/src/components/geneExpression/gene.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -94,16 +96,18 @@ class Gene extends React.Component {
}}
>
<div>
<Icon
icon="drag-handle-horizontal"
iconSize={12}
style={{
marginRight: 7,
cursor: "grab",
position: "relative",
top: -1,
}}
/>
{!quickGene && (
<Icon
icon="drag-handle-horizontal"
iconSize={12}
style={{
marginRight: 7,
cursor: "grab",
position: "relative",
top: -1,
}}
/>
)}
<Truncate
tooltipAddendum={geneDescription && `: ${geneDescription}`}
>
Expand Down Expand Up @@ -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={<Icon icon="trash" iconSize={10} />}
Expand Down
68 changes: 58 additions & 10 deletions client/src/components/geneExpression/index.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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;
Expand All @@ -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 (
<div>
<QuickGene />
<div>
<div style={{ marginBottom: 10, position: "relative", top: -2 }}>
<Button
data-testid="open-create-geneset-dialog"
onClick={this.handleActivateCreateGenesetMode}
intent="primary"
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<H4
role="menuitem"
tabIndex="0"
data-testclass="category-expand"
onKeyPress={this.handleExpandGeneSets}
style={{
cursor: "pointer",
}}
onClick={this.handleExpandGeneSets}
>
Create new <strong>gene set</strong>
</Button>
Gene Sets{" "}
{geneSetsExpanded ? (
<Icon icon={IconNames.CHEVRON_DOWN} />
) : (
<Icon icon={IconNames.CHEVRON_RIGHT} />
)}
</H4>

<div style={{ marginBottom: 10, position: "relative", top: -2 }}>
<Button
data-testid="open-create-geneset-dialog"
onClick={this.handleActivateCreateGenesetMode}
intent="primary"
>
Create new
</Button>
</div>
</div>
<CreateGenesetDialogue />
</div>
<div>{this.renderGeneSets()}</div>

{geneSetsExpanded && <div>{this.renderGeneSets()}</div>}
</div>
);
}
Expand Down
176 changes: 176 additions & 0 deletions client/src/components/geneExpression/quickGene.js
Original file line number Diff line number Diff line change
@@ -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 (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
data-testid={`suggest-menu-item-${geneName}`}
key={geneName}
onClick={(g) =>
/* 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 (
<Gene
key={`quick=${gene}`}
gene={gene}
removeGene={removeGene}
quickGene
/>
);
});
}, [userDefinedGenes]);

return (
<div style={{ width: "100%", marginBottom: "16px" }}>
<H4
role="menuitem"
tabIndex="0"
data-testclass="category-expand"
onKeyPress={handleExpand}
style={{
cursor: "pointer",
}}
onClick={handleExpand}
>
Genes{" "}
{isExpanded ? (
<Icon icon={IconNames.CHEVRON_DOWN} />
) : (
<Icon icon={IconNames.CHEVRON_RIGHT} />
)}
</H4>
{isExpanded && (
<>
<div style={{ marginBottom: "8px" }}>
<Suggest
resetOnSelect
closeOnSelect
resetOnClose
itemDisabled={userDefinedGenesLoading ? () => true : () => false}
noResults={<MenuItem disabled text="No matching genes." />}
onItemSelect={(g) => {
/* this happens on 'enter' */
handleClick(g);
}}
initialContent={<MenuItem disabled text="Enter a gene…" />}
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
/>
</div>
{QuickGenes}
</>
)}
</div>
);
}

export default QuickGene;

0 comments on commit 82de417

Please sign in to comment.