Ontologies (#1110)

* add sample ontologies file

* add ontologies reducer

* Move select category to own component

* Dialog and Input factored out

* refactoring categorical, partway

* validationn

* anno

* suggest  populates input

* frontend for ontology working

* initial implementation of back-end support for ontologies

* edit is now dialog again

* autosuggest working on edit

* part way through create arbitrary label

* handle choice in function

* pass duplicate cat  prop

* editing works

* update test to match new CLI params

* fix occupancy alignment

* edit category as dialogue

* secondary button

* remove stubbed out ontologies

* add label setting upon new label creation

* Update legal characters for labels (#1119)

* Allow any term in the ontology (bypass legal name check)

* Add hyphens and parens to legal characters in names

* improve performance for large ontologies

* correctly handle case where ontologies are disabled

* fix logic error in CLI

Co-authored-by: Bruce Martin <bruce@chanzuckerberg.com>

* PR cleanup 1

* lint

* validate user generated labels

* finish hooking up connected suggest component

* protect against undefined callbacks

* Fix illegal characters error message

* break out npm run commands

* fix error detection on label edit

Co-authored-by: Bruce Martin <bruce@chanzuckerberg.com>
Co-authored-by: Sidney Bell <sidneymbell@users.noreply.github.com>
This commit is contained in:
Colin Megill
2020-01-23 17:04:17 -05:00
committed by GitHub
co-authored by Bruce Martin Sidney Bell
parent 8d725b1ad9
commit d48647a655
20 changed files with 964 additions and 532 deletions
@@ -0,0 +1,159 @@
import React from "react";
import { connect } from "react-redux";
import { InputGroup, MenuItem } from "@blueprintjs/core";
import { Suggest } from "@blueprintjs/select";
import fuzzysort from "fuzzysort";
const filterOntology = (query, ontology) =>
/* fires on load, once, and then for each character typed into the input */
fuzzysort.go(query, ontology, {
limit: 100,
threshold: -10000 // don't return bad results
});
const renderListItem = (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}`}
// 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 */
handleClick(g)
}
text={geneName}
/>
);
};
const AnnoSuggest = props => {
const {
ontologyLoading,
handleItemChange,
handleChoice,
ontology,
handleCreateArbitraryLabel,
handleTextChange,
isTextInvalid,
isTextInvalidErrorMessage
} = props;
return (
<Suggest
fill
resetOnSelect
closeOnSelect
resetOnClose
createNewItemFromQuery={() => {}}
createNewItemRenderer={userInputStr => {
return isTextInvalid?.(userInputStr) ? (
<MenuItem disabled text={isTextInvalidErrorMessage(userInputStr)} />
) : (
<MenuItem
icon="add"
text="Create a label not in the ontology"
active
onClick={() => {
handleCreateArbitraryLabel(userInputStr);
}}
shouldDismissPopover={false}
/>
);
}}
itemDisabled={ontologyLoading ? () => true : () => false}
noResults={<MenuItem disabled text="No matching ontology identifier" />}
onItemSelect={handleChoice}
initialContent={
<MenuItem disabled text="Enter an ontology identifier…" />
}
inputProps={{ "data-testid": "gene-search" }}
inputValueRenderer={t => t.target}
itemListPredicate={filterOntology}
onActiveItemChange={handleItemChange}
itemRenderer={renderListItem.bind(this)}
items={!ontologyLoading && ontology ? ontology : ["No ontology loaded"]}
popoverProps={{ minimal: true }}
onQueryChange={(s, e) => {
// undefined event means resetOnSelect
if (e !== undefined) handleTextChange?.(s);
}}
/>
);
};
const VanillaInput = props => {
const { text, handleTextChange } = props;
return (
<InputGroup
autoFocus
value={text}
intent="none"
onChange={e => handleTextChange?.(e.target.value)}
leftIcon="tag"
/>
);
};
@connect(state => ({
colorAccessor: state.colors.colorAccessor,
categoricalSelection: state.categoricalSelection,
annotations: state.annotations,
universe: state.universe,
world: state.world,
ontology: state.ontology?.terms,
ontologyLoading: state.ontology?.loading
}))
class AnnoInputs extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const {
useSuggest,
handleTextChange,
text,
ontologyLoading,
world,
handleChoice,
handleItemChange,
handleCreateArbitraryLabel,
ontology,
isTextInvalid,
isTextInvalidErrorMessage
} = this.props;
return (
<div>
{/* Le sigh https://github.com/palantir/blueprint/issues/2864 */}
{useSuggest ? (
<AnnoSuggest
world={world}
text={text}
handleChoice={handleChoice}
ontologyLoading={ontologyLoading}
handleCreateArbitraryLabel={handleCreateArbitraryLabel}
handleItemChange={handleItemChange}
ontology={ontology}
handleTextChange={handleTextChange}
isTextInvalid={isTextInvalid}
isTextInvalidErrorMessage={isTextInvalidErrorMessage}
/>
) : (
<VanillaInput text={text} handleTextChange={handleTextChange} />
)}
</div>
);
}
}
export default AnnoInputs;