Improve label picking (#1179)

* add simple error message helper

* port all label name pickers to use the new LabelInput component

* use pure components where possible

* cleanup

* more cleanup

* lint

* change new label prompt
This commit is contained in:
Bruce Martin
2020-02-28 15:46:40 -07:00
committed by GitHub
parent 1e4381ab7f
commit bf7d7342d5
27 changed files with 418 additions and 576 deletions
+3 -3
View File
@@ -12,7 +12,7 @@ import {
return promise to fetch the OBS annotations we need to load. Omit anything return promise to fetch the OBS annotations we need to load. Omit anything
we don't need. we don't need.
*/ */
function obsAnnotationFetchAndLoad(dispatch, schema, universe) { function obsAnnotationFetchAndLoad(dispatch, schema) {
const obsAnnotations = schema?.schema?.annotations?.obs ?? {}; const obsAnnotations = schema?.schema?.annotations?.obs ?? {};
const columns = obsAnnotations.columns ?? []; const columns = obsAnnotations.columns ?? [];
const index = obsAnnotations.index ?? false; const index = obsAnnotations.index ?? false;
@@ -42,7 +42,7 @@ function obsAnnotationFetchAndLoad(dispatch, schema, universe) {
/* /*
return promise fetching VAR annotations we need to load. Only index is currently used. return promise fetching VAR annotations we need to load. Only index is currently used.
*/ */
function varAnnotationFetchAndLoad(dispatch, schema, universe) { function varAnnotationFetchAndLoad(dispatch, schema) {
const varAnnotations = schema?.schema?.annotations?.var ?? {}; const varAnnotations = schema?.schema?.annotations?.var ?? {};
const index = varAnnotations.index ?? false; const index = varAnnotations.index ?? false;
const names = index ? [index] : []; const names = index ? [index] : [];
@@ -71,7 +71,7 @@ function varAnnotationFetchAndLoad(dispatch, schema, universe) {
/* /*
return promise fetching layout we need return promise fetching layout we need
*/ */
function layoutFetchAndLoad(dispatch, schema, universe) { function layoutFetchAndLoad(dispatch) {
return Promise.all( return Promise.all(
["layout/obs"] ["layout/obs"]
.map(path => { .map(path => {
@@ -74,7 +74,7 @@ class FilenameDialog extends React.Component {
color: Colors.ORANGE3 color: Colors.ORANGE3
}} }}
> >
{"Name cannot be blank"} Name cannot be blank
</span> </span>
); );
} else if (err === "characters") { } else if (err === "characters") {
@@ -87,7 +87,7 @@ class FilenameDialog extends React.Component {
color: Colors.ORANGE3 color: Colors.ORANGE3
}} }}
> >
{"Only alphanumeric and underscore allowed"} Only alphanumeric and underscore allowed
</span> </span>
); );
} }
@@ -6,11 +6,9 @@ import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core";
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
categoricalSelection: state.categoricalSelection, categoricalSelection: state.categoricalSelection,
annotations: state.annotations, annotations: state.annotations,
universe: state.universe, universe: state.universe
ontology: state.ontology,
ontologyLoading: state.ontology?.loading
})) }))
class AnnoDialog extends React.Component { class AnnoDialog extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {}; this.state = {};
@@ -26,7 +24,6 @@ class AnnoDialog extends React.Component {
errorMessage, errorMessage,
validationError, validationError,
annoSelect, annoSelect,
ontologySelect,
annoInput, annoInput,
handleCancel, handleCancel,
handleSubmit, handleSubmit,
@@ -58,7 +55,6 @@ class AnnoDialog extends React.Component {
</p> </p>
</div> </div>
{annoSelect || null} {annoSelect || null}
{ontologySelect || null}
</div> </div>
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
@@ -76,7 +72,7 @@ class AnnoDialog extends React.Component {
</Button> </Button>
) : null} ) : null}
<Button <Button
{...primaryButtonProps} {...primaryButtonProps} // eslint-disable-line react/jsx-props-no-spreading
onClick={handleSubmit} onClick={handleSubmit}
disabled={!text || validationError} disabled={!text || validationError}
intent="primary" intent="primary"
@@ -1,20 +1,17 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import AnnoDialog from "./annoDialog"; import AnnoDialog from "./annoDialog";
import AnnoInputs from "./annoInputs"; import LabelInput from "./labelInput";
import { labelPrompt, isLabelErroneous } from "./labelUtil";
import { labelErrorMessage, isLabelErroneous } from "./labelUtil";
@connect(state => ({ @connect(state => ({
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
categoricalSelection: state.categoricalSelection, categoricalSelection: state.categoricalSelection,
annotations: state.annotations, annotations: state.annotations,
universe: state.universe, universe: state.universe,
ontology: state.ontology, ontology: state.ontology
ontologyLoading: state.ontology?.loading,
ontologyEnabled: state.ontology?.enabled
})) }))
class Category extends React.Component { class Category extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@@ -22,14 +19,15 @@ class Category extends React.Component {
}; };
} }
disableAddNewLabelMode = () => { disableAddNewLabelMode = e => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch({
type: "annotation: disable add new label mode"
});
this.setState({ this.setState({
newLabelText: "" newLabelText: ""
}); });
dispatch({
type: "annotation: disable add new label mode"
});
if (e) e.preventDefault();
}; };
handleAddNewLabelToCategory = e => { handleAddNewLabelToCategory = e => {
@@ -46,7 +44,7 @@ class Category extends React.Component {
e.preventDefault(); e.preventDefault();
}; };
addLabelAndAssignCells = () => { addLabelAndAssignCells = e => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
const { newLabelText } = this.state; const { newLabelText } = this.state;
@@ -57,18 +55,7 @@ class Category extends React.Component {
newLabelText, newLabelText,
assignSelectedCells: true assignSelectedCells: true
}); });
}; e.preventDefault();
handleCreateArbitraryLabel = newLabelTextNotInOntology => {
const { dispatch, metadataField } = this.props;
this.disableAddNewLabelMode();
dispatch({
type: "annotation: add new label to category",
metadataField,
newLabelText: newLabelTextNotInOntology,
assignSelectedCells: false
});
}; };
labelNameError = name => { labelNameError = name => {
@@ -76,23 +63,18 @@ class Category extends React.Component {
return isLabelErroneous(name, metadataField, ontology, universe.schema); return isLabelErroneous(name, metadataField, ontology, universe.schema);
}; };
labelNameErrorMessage = name => { instruction = label => {
const { metadataField, ontology, universe } = this.props; return labelPrompt(this.labelNameError(label), "New, unique label", ":");
return labelErrorMessage(name, metadataField, ontology, universe.schema);
}; };
/* leaky to have both of these in multiple components */ handleChangeOrSelect = label => {
handleChoice = e => { this.setState({ newLabelText: label });
this.setState({ newLabelText: e.target });
};
handleTextChange = text => {
this.setState({ newLabelText: text });
}; };
render() { render() {
const { newLabelText } = this.state; const { newLabelText } = this.state;
const { metadataField, annotations, ontologyEnabled } = this.props; const { metadataField, annotations, ontology } = this.props;
const ontologyEnabled = ontology?.enabled ?? false;
return ( return (
<> <>
@@ -106,27 +88,26 @@ class Category extends React.Component {
"data-testid": `${metadataField}:submit-label` "data-testid": `${metadataField}:submit-label`
}} }}
title="Add new label to category" title="Add new label to category"
instruction="New, unique label name:" instruction={this.instruction(newLabelText)}
cancelTooltipContent="Close this dialog without adding a label." cancelTooltipContent="Close this dialog without adding a label."
primaryButtonText="Add label" primaryButtonText="Add label"
secondaryButtonText="Add label and assign currently selected cells" secondaryButtonText="Add label and assign currently selected cells"
handleSecondaryButtonSubmit={this.addLabelAndAssignCells} handleSecondaryButtonSubmit={this.addLabelAndAssignCells}
text={newLabelText} text={newLabelText}
validationError={this.labelNameError(newLabelText)} validationError={this.labelNameError(newLabelText)}
errorMessage={this.labelNameErrorMessage(newLabelText)}
handleSubmit={this.handleAddNewLabelToCategory} handleSubmit={this.handleAddNewLabelToCategory}
handleCancel={this.disableAddNewLabelMode} handleCancel={this.disableAddNewLabelMode}
annoInput={ annoInput={
<AnnoInputs <LabelInput
useSuggest={ontologyEnabled} labelSuggestions={ontologyEnabled ? ontology.terms : null}
text={newLabelText} onChange={this.handleChangeOrSelect}
inputProps={{ "data-testid": `${metadataField}:new-label-name` }} onSelect={this.handleChangeOrSelect}
handleCreateArbitraryLabel={this.handleCreateArbitraryLabel} inputProps={{
handleItemChange={this.handleSuggestActiveItemChange} "data-testid": `${metadataField}:new-label-name`,
handleChoice={this.handleChoice} leftIcon: "tag",
handleTextChange={this.handleTextChange} intent: "none",
isTextInvalid={this.labelNameError} autoFocus: true
isTextInvalidErrorMessage={this.labelNameErrorMessage} }}
/> />
} }
/> />
@@ -1,131 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import AnnoDialog from "./annoDialog";
import OntologySelect from "./ontologySelect";
import { labelErrorMessage, isLabelErroneous } from "./labelUtil";
@connect(state => ({
colorAccessor: state.colors.colorAccessor,
categoricalSelection: state.categoricalSelection,
annotations: state.annotations,
universe: state.universe,
ontology: state.ontology,
ontologyLoading: state.ontology?.loading,
ontologyEnabled: state.ontology?.enabled
}))
class Category extends React.Component {
constructor(props) {
super(props);
this.state = {
newLabelText: ""
};
}
disableAddNewLabelFromOntologyMode = () => {
const { dispatch } = this.props;
dispatch({
type: "annotation: disable add new ontology label mode"
});
this.setState({
newLabelText: ""
});
};
handleAddNewLabelToCategory = e => {
const { dispatch, metadataField } = this.props;
const { newLabelText } = this.state;
this.disableAddNewLabelFromOntologyMode();
dispatch({
type: "annotation: add new label to category",
metadataField,
newLabelText,
assignSelectedCells: false
});
e.preventDefault();
};
addLabelAndAssignCells = () => {
const { dispatch, metadataField } = this.props;
const { newLabelText } = this.state;
this.disableAddNewLabelFromOntologyMode();
dispatch({
type: "annotation: add new label to category",
metadataField,
newLabelText,
assignSelectedCells: true
});
};
handleCreateArbitraryLabel = newLabelTextNotInOntology => {
const { dispatch, metadataField } = this.props;
this.disableAddNewLabelFromOntologyMode();
dispatch({
type: "annotation: add new label to category",
metadataField,
newLabelText: newLabelTextNotInOntology,
assignSelectedCells: false
});
};
labelNameError = name => {
const { metadataField, ontology, universe } = this.props;
return isLabelErroneous(name, metadataField, ontology, universe.schema);
};
labelNameErrorMessage = name => {
const { metadataField, ontology, universe } = this.props;
return labelErrorMessage(name, metadataField, ontology, universe.schema);
};
/* leaky to have both of these in multiple components */
handleChoice = e => {
this.setState({ newLabelText: e.target });
};
handleTextChange = text => {
this.setState({ newLabelText: text });
};
render() {
const { newLabelText } = this.state;
const { metadataField, annotations, ontology } = this.props;
return (
<>
<AnnoDialog
isActive={
annotations.isAddingNewLabelFromOntology &&
annotations.categoryAddingNewLabelFromOntology === metadataField
}
title="Add new label to category from existing ontology terms"
instruction="Choose an ontology term to use as a label name:"
cancelTooltipContent="Close this dialog without adding a label."
primaryButtonText="Add label"
secondaryButtonText="Add label and assign currently selected cells"
handleSecondaryButtonSubmit={this.addLabelAndAssignCells}
text={newLabelText}
validationError={this.labelNameError(newLabelText)}
errorMessage={this.labelNameErrorMessage(newLabelText)}
handleSubmit={this.handleAddNewLabelToCategory}
handleCancel={this.disableAddNewLabelFromOntologyMode}
ontologySelect={
<OntologySelect
handleChooseOntologyTermFromDropdown={term => {
this.handleChoice(term);
}}
categoryToDuplicate={newLabelText}
ontology={ontology}
/>
}
annoInput={null}
/>
</>
);
}
}
export default Category;
@@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Colors } from "@blueprintjs/core";
import AnnoDialog from "./annoDialog"; import AnnoDialog from "./annoDialog";
import AnnoInputs from "./annoInputs"; import LabelInput from "./labelInput";
import { labelPrompt } from "./labelUtil";
import { AnnotationsHelpers } from "../../util/stateManager"; import { AnnotationsHelpers } from "../../util/stateManager";
@@ -11,11 +11,9 @@ import { AnnotationsHelpers } from "../../util/stateManager";
categoricalSelection: state.categoricalSelection, categoricalSelection: state.categoricalSelection,
annotations: state.annotations, annotations: state.annotations,
universe: state.universe, universe: state.universe,
ontology: state.ontology, ontology: state.ontology
ontologyLoading: state.ontology?.loading,
ontologyEnabled: state.ontology?.enabled
})) }))
class AnnoDialogEditCategoryName extends React.Component { class AnnoDialogEditCategoryName extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@@ -23,9 +21,9 @@ class AnnoDialogEditCategoryName extends React.Component {
}; };
} }
handleCategoryEditTextChange = txt => { handleChangeOrSelect = name => {
this.setState({ this.setState({
newCategoryText: txt newCategoryText: name
}); });
}; };
@@ -61,50 +59,19 @@ class AnnoDialogEditCategoryName extends React.Component {
e.preventDefault(); e.preventDefault();
}; };
categoryNameErrorMessage = () => { editedCategoryNameError = name => {
const err = this.editedCategoryNameError();
if (err === false) return null;
const errorMessageMap = {
/* map error code to human readable error message */
"empty-string": "Blank names not allowed",
duplicate: "Category name must be unique",
"trim-spaces": "Leading and trailing spaces not allowed",
"illegal-characters":
"Only alphanumeric and special characters (-_.) allowed",
"multi-space-run": "Multiple consecutive spaces not allowed"
};
const errorMessage = errorMessageMap[err] ?? "error";
return (
<span
style={{
display: "block",
fontStyle: "italic",
fontSize: 12,
marginTop: 5,
color: Colors.ORANGE3
}}
>
{errorMessage}
</span>
);
};
editedCategoryNameError = () => {
const { metadataField, categoricalSelection } = this.props; const { metadataField, categoricalSelection } = this.props;
const { newCategoryText } = this.state;
/* check for syntax errors in category name */ /* check for syntax errors in category name */
const error = AnnotationsHelpers.annotationNameIsErroneous(newCategoryText); const error = AnnotationsHelpers.annotationNameIsErroneous(name);
if (error) { if (error) {
return error; return error;
} }
/* check for duplicative categories */ /* check for duplicative categories */
const allCategoryNames = _.keys(categoricalSelection); const allCategoryNames = _.keys(categoricalSelection);
const categoryNameAlreadyExists = const categoryNameAlreadyExists = allCategoryNames.indexOf(name) > -1;
allCategoryNames.indexOf(newCategoryText) > -1; const sameName = name === metadataField;
const sameName = newCategoryText === metadataField;
if (categoryNameAlreadyExists && !sameName) { if (categoryNameAlreadyExists && !sameName) {
return "duplicate"; return "duplicate";
} }
@@ -113,9 +80,18 @@ class AnnoDialogEditCategoryName extends React.Component {
return false; return false;
}; };
instruction = name => {
return labelPrompt(
this.editedCategoryNameError(name),
"New, unique category name",
":"
);
};
render() { render() {
const { newCategoryText } = this.state; const { newCategoryText } = this.state;
const { metadataField, annotations } = this.props; const { metadataField, annotations, ontology } = this.props;
const ontologyEnabled = ontology?.enabled ?? false;
return ( return (
<> <>
@@ -131,22 +107,26 @@ class AnnoDialogEditCategoryName extends React.Component {
"data-testid": `${metadataField}:submit-category-edit` "data-testid": `${metadataField}:submit-category-edit`
}} }}
title="Edit category name" title="Edit category name"
instruction="New, unique category name:" instruction={this.instruction(newCategoryText)}
cancelTooltipContent="Close this dialog without editing this category." cancelTooltipContent="Close this dialog without editing this category."
primaryButtonText="Edit category name" primaryButtonText="Edit category name"
text={newCategoryText} text={newCategoryText}
validationError={this.editedCategoryNameError(newCategoryText)} validationError={this.editedCategoryNameError(newCategoryText)}
errorMessage={this.categoryNameErrorMessage(newCategoryText)}
handleSubmit={this.handleEditCategory} handleSubmit={this.handleEditCategory}
handleCancel={this.disableEditCategoryMode} handleCancel={this.disableEditCategoryMode}
annoInput={ annoInput={
<AnnoInputs <LabelInput
label={newCategoryText}
labelSuggestions={ontologyEnabled ? ontology.terms : null}
onChange={this.handleChangeOrSelect}
onSelect={this.handleChangeOrSelect}
inputProps={{ inputProps={{
"data-testid": `${metadataField}:edit-category-name-text` "data-testid": `${metadataField}:edit-category-name-text`,
leftIcon: "tag",
intent: "none",
autoFocus: true
}} }}
useSuggest={false} newLabelMessage="New category"
text={newCategoryText}
handleTextChange={this.handleCategoryEditTextChange}
/> />
} }
/> />
@@ -1,48 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { InputGroup } from "@blueprintjs/core";
const VanillaInput = props => {
const { text, handleTextChange, inputProps } = props;
return (
<InputGroup
{...inputProps}
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 { handleTextChange, text, ...restProps } = this.props;
return (
<div>
<VanillaInput
{...restProps}
text={text}
handleTextChange={handleTextChange}
/>
</div>
);
}
}
export default AnnoInputs;
@@ -10,10 +10,9 @@ import {
} from "@blueprintjs/core"; } from "@blueprintjs/core";
@connect(state => ({ @connect(state => ({
annotations: state.annotations, annotations: state.annotations
ontologyEnabled: state.ontology?.enabled
})) }))
class AnnoMenuCategory extends React.Component { class AnnoMenuCategory extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {}; this.state = {};
@@ -27,14 +26,6 @@ class AnnoMenuCategory extends React.Component {
}); });
}; };
activateAddNewOntologyLabelMode = () => {
const { dispatch, metadataField } = this.props;
dispatch({
type: "annotation: activate add new ontology label mode",
data: metadataField
});
};
activateEditCategoryMode = () => { activateEditCategoryMode = () => {
const { dispatch, metadataField } = this.props; const { dispatch, metadataField } = this.props;
@@ -57,9 +48,7 @@ class AnnoMenuCategory extends React.Component {
metadataField, metadataField,
annotations, annotations,
isUserAnno, isUserAnno,
ontologyEnabled,
createText, createText,
createFromOntologyText,
editText, editText,
deleteText deleteText
} = this.props; } = this.props;
@@ -80,15 +69,6 @@ class AnnoMenuCategory extends React.Component {
onClick={this.activateAddNewLabelMode} onClick={this.activateAddNewLabelMode}
text={createText} text={createText}
/> />
{ontologyEnabled ? (
<MenuItem
icon="book"
data-testclass="activateAddNewOntologyLabelMode"
data-testid={`${metadataField}:add-new-ontology-label-mode`}
onClick={this.activateAddNewOntologyLabelMode}
text={createFromOntologyText}
/>
) : null}
<MenuItem <MenuItem
icon="edit" icon="edit"
disabled={annotations.isEditingCategoryName} disabled={annotations.isEditingCategoryName}
@@ -7,11 +7,9 @@ import { Select } from "@blueprintjs/select";
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
categoricalSelection: state.categoricalSelection, categoricalSelection: state.categoricalSelection,
annotations: state.annotations, annotations: state.annotations,
universe: state.universe, universe: state.universe
ontology: state.ontology,
ontologyLoading: state.ontology?.loading
})) }))
class DuplicateCategorySelect extends React.Component { class DuplicateCategorySelect extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {}; this.state = {};
@@ -6,13 +6,15 @@ import * as globals from "../../globals";
import Category from "./category"; import Category from "./category";
import { AnnotationsHelpers, ControlsHelpers } from "../../util/stateManager"; import { AnnotationsHelpers, ControlsHelpers } from "../../util/stateManager";
import AnnoDialog from "./annoDialog"; import AnnoDialog from "./annoDialog";
import AnnoInputs from "./annoInputs";
import AnnoSelect from "./annoSelect"; import AnnoSelect from "./annoSelect";
import LabelInput from "./labelInput";
import { labelPrompt } from "./labelUtil";
@connect(state => ({ @connect(state => ({
writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false, writableCategoriesEnabled: state.config?.parameters?.["annotations"] ?? false,
schema: state.world?.schema, schema: state.world?.schema,
config: state.config config: state.config,
ontology: state.ontology
})) }))
class Categories extends React.Component { class Categories extends React.Component {
constructor(props) { constructor(props) {
@@ -87,46 +89,30 @@ class Categories extends React.Component {
return false; return false;
}; };
categoryNameErrorMessage = name => { handleChange = name => {
const err = this.categoryNameError(name); this.setState({ newCategoryText: name });
if (err === false) return null;
const errorMessageMap = {
/* map error code to human readable error message */
"empty-string": "Blank names not allowed",
duplicate: "Name must be unique",
"trim-spaces": "Leading and trailing spaces not allowed",
"illegal-characters":
"Only alphanumeric and special characters (-_.) allowed",
"multi-space-run": "Multiple consecutive spaces not allowed"
};
const errorMessage = errorMessageMap[err] ?? "error";
return <span>{errorMessage}</span>;
}; };
handleNewCategoryText = txt => { handleSelect = name => {
this.setState({ newCategoryText: txt }); this.setState({ newCategoryText: name });
}; };
handleChoice = e => { instruction = name => {
/* Blueprint Suggest format */ return labelPrompt(
this.setState({ newCategoryText: e.target }); this.categoryNameError(name),
"New, unique category name",
":"
);
}; };
handleSuggestActiveItemChange = () => {};
render() { render() {
const { const {
createAnnoModeActive, createAnnoModeActive,
categoryToDuplicate, categoryToDuplicate,
newCategoryText newCategoryText
} = this.state; } = this.state;
const { const { writableCategoriesEnabled, schema, config, ontology } = this.props;
categoricalSelection, const ontologyEnabled = ontology?.enabled ?? false;
writableCategoriesEnabled,
schema,
config
} = this.props;
/* all names, sorted in display order. Will be rendered in this order */ /* all names, sorted in display order. Will be rendered in this order */
const allCategoryNames = ControlsHelpers.selectableCategoryNames( const allCategoryNames = ControlsHelpers.selectableCategoryNames(
@@ -143,22 +129,26 @@ class Categories extends React.Component {
<AnnoDialog <AnnoDialog
isActive={createAnnoModeActive} isActive={createAnnoModeActive}
title="Create new category" title="Create new category"
instruction="New, unique category name:" instruction={this.instruction(newCategoryText)}
cancelTooltipContent="Close this dialog without creating a category." cancelTooltipContent="Close this dialog without creating a category."
primaryButtonText="Create new category" primaryButtonText="Create new category"
primaryButtonProps={{ "data-testid": "submit-category" }} primaryButtonProps={{ "data-testid": "submit-category" }}
text={newCategoryText} text={newCategoryText}
validationError={this.categoryNameError(newCategoryText)} validationError={this.categoryNameError(newCategoryText)}
errorMessage={this.categoryNameErrorMessage(newCategoryText)}
handleSubmit={this.handleCreateUserAnno} handleSubmit={this.handleCreateUserAnno}
handleCancel={this.handleDisableAnnoMode} handleCancel={this.handleDisableAnnoMode}
annoInput={ annoInput={
<AnnoInputs <LabelInput
text={newCategoryText} labelSuggestions={ontologyEnabled ? ontology.terms : null}
inputProps={{ "data-testid": "new-category-name" }} onChange={this.handleChange}
handleItemChange={this.handleSuggestActiveItemChange} onSelect={this.handleSelect}
handleChoice={this.handleChoice} inputProps={{
handleTextChange={this.handleNewCategoryText} "data-testid": "new-category-name",
leftIcon: "tag",
intent: "none",
autoFocus: true
}}
newLabelMessage="New category"
/> />
} }
annoSelect={ annoSelect={
+5 -10
View File
@@ -2,12 +2,11 @@ import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { FaChevronRight, FaChevronDown } from "react-icons/fa"; import { FaChevronRight, FaChevronDown } from "react-icons/fa";
import { Button, Tooltip, Icon, Spinner } from "@blueprintjs/core"; import { Button, Tooltip, Icon } from "@blueprintjs/core";
import CategoryFlipperLayout from "./categoryFlipperLayout"; import CategoryFlipperLayout from "./categoryFlipperLayout";
import AnnoMenu from "./annoMenuCategory"; import AnnoMenu from "./annoMenuCategory";
import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName"; import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName";
import AnnoDialogAddLabel from "./annoDialogAddLabel"; import AnnoDialogAddLabel from "./annoDialogAddLabel";
import AnnoDialogAddLabelFromOntology from "./annoDialogAddLabelFromOntology";
import * as globals from "../../globals"; import * as globals from "../../globals";
@@ -15,10 +14,7 @@ import * as globals from "../../globals";
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
categoricalSelection: state.categoricalSelection, categoricalSelection: state.categoricalSelection,
annotations: state.annotations, annotations: state.annotations,
universe: state.universe, universe: state.universe
ontology: state.ontology,
ontologyLoading: state.ontology?.loading,
ontologyEnabled: state.ontology?.enabled
})) }))
class Category extends React.Component { class Category extends React.Component {
constructor(props) { constructor(props) {
@@ -99,10 +95,11 @@ class Category extends React.Component {
} }
} }
renderIsStillLoading(metadataField) { renderIsStillLoading() {
/* /*
We are still loading this category, so render a "busy" signal. We are still loading this category, so render a "busy" signal.
*/ */
const { metadataField } = this.props;
return ( return (
<div <div
style={{ style={{
@@ -156,7 +153,7 @@ class Category extends React.Component {
const isStillLoading = !(categoricalSelection?.[metadataField] ?? false); const isStillLoading = !(categoricalSelection?.[metadataField] ?? false);
if (isStillLoading) { if (isStillLoading) {
return this.renderIsStillLoading(metadataField); return this.renderIsStillLoading();
} }
return ( return (
@@ -220,13 +217,11 @@ class Category extends React.Component {
</div> </div>
{<AnnoDialogEditCategoryName metadataField={metadataField} />} {<AnnoDialogEditCategoryName metadataField={metadataField} />}
{<AnnoDialogAddLabel metadataField={metadataField} />} {<AnnoDialogAddLabel metadataField={metadataField} />}
{<AnnoDialogAddLabelFromOntology metadataField={metadataField} />}
<div> <div>
<AnnoMenu <AnnoMenu
metadataField={metadataField} metadataField={metadataField}
isUserAnno={isUserAnno} isUserAnno={isUserAnno}
createText="Add a new label to this category" createText="Add a new label to this category"
createFromOntologyText="Add a new label to this category using existing ontology terms"
editText="Edit this category's name" editText="Edit this category's name"
deleteText="Delete this category, all associated labels, and remove all cell assignments" deleteText="Delete this category, all associated labels, and remove all cell assignments"
/> />
@@ -0,0 +1,176 @@
import React from "react";
import { InputGroup, MenuItem, Keys } from "@blueprintjs/core";
import { Suggest } from "@blueprintjs/select";
import fuzzysort from "fuzzysort";
export default class LabelInput extends React.PureComponent {
/*
Input widget for text labels, which acts like an InputGroup, but will also
accept a suggestion list (of labels), with sublime-like suggest search.
Properties:
* labelSuggestions -- array of suggested lables. May be array of string or
other objects. If array of objects, specify `labelKey`. If null, suggestion
mode is disabled.
* onSelect -- optional, callback upon selection of item from labelSuggestions.
(label) => void
* onChange -- optional, callback upon change in text input. (label) => void
* label -- component value, for controlled use
* newLabelMessage -- text to display when user enters a label not in labelSuggestions
(not used if suggestion mode disabled)
* inputProps -- will be passed to InputGroup
* popoverProps -- will be passed to <Suggest>
*/
constructor(props) {
super(props);
const { label } = props;
const query = label || "";
const queryResults = this.filterLabels(query);
this.state = {
query,
queryResults
};
}
handleQueryChange = (query, event) => {
// https://github.com/palantir/blueprint/issues/2983
if (!event) return;
const queryResults = this.filterLabels(query);
this.setState({
query,
queryResults
});
const { onChange } = this.props;
if (onChange) onChange(query, event);
};
handleItemSelect = (item, event) => {
/* only report the select if not already reported via onChange() */
const { target } = item;
const { query } = this.state;
const { onSelect } = this.props;
if (target !== query && onSelect) onSelect(target, event);
};
handleKeyDown = e => {
/*
prevent these events from propagating to containing form/dialog
and causing further side effects (eg, closing dialog, submitting
form, etc).
*/
const { keyCode } = e;
if (keyCode === Keys.ENTER || keyCode === Keys.ESCAPE) {
e.preventDefault();
}
if (keyCode === Keys.ESCAPE) {
e.stopPropagation();
}
};
handleChange = e => {
const { onChange } = this.props;
if (onChange) onChange(e.target.value);
};
renderLabelSuggestion = (queryResult, { handleClick, modifiers }) => {
if (queryResult.newLabel) {
const { newLabelMessage } = this.props;
return (
<MenuItem
icon="flag"
active={modifiers.active}
disabled={modifiers.disabled}
key={queryResult.target}
onClick={handleClick}
text={<em>{queryResult.target}</em>}
label={newLabelMessage || "New label"}
/>
);
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
key={queryResult.target}
onClick={handleClick}
text={queryResult.target}
/>
);
};
/* maxinum number of suggestions */
static QueryResultLimit = 100;
filterLabels(query) {
const { labelSuggestions } = this.props;
if (!labelSuggestions) return [];
/* empty query is wildcard */
if (query === "") {
return labelSuggestions.slice(0, LabelInput.QueryResultLimit).map(l => ({
target: l,
score: -10000
}));
}
/* else, do a fuzzy query */
const options = {
limit: LabelInput.QueryResultLimit,
threshold: -10000 // don't return bad results
};
let queryResults = fuzzysort.go(query, labelSuggestions, options);
/* exact match will always be first in list */
if (query !== "" && queryResults[0]?.target !== query)
queryResults = [{ target: query, newLabel: true }, ...queryResults];
return queryResults;
}
render() {
const { props } = this;
const { labelSuggestions, label, autoFocus = true } = props;
const suggestEnabled = !!labelSuggestions && labelSuggestions.length > 0;
if (!suggestEnabled) {
return (
<InputGroup
autoFocus={autoFocus}
{...props.inputProps} // eslint-disable-line react/jsx-props-no-spreading
value={label}
onChange={this.handleChange}
/>
);
}
const popoverProps = {
minimal: true,
...props.popoverProps
};
const inputProps = {
...props.inputProps,
autoFocus: false
};
const { queryResults } = this.state;
return (
<>
<Suggest
fill
inputValueRenderer={i => i.target}
items={queryResults}
itemRenderer={this.renderLabelSuggestion}
onItemSelect={this.handleItemSelect}
query={label}
onQueryChange={this.handleQueryChange}
popoverProps={popoverProps}
inputProps={inputProps}
onKeyDown={this.handleKeyDown}
/>
</>
);
}
}
+32 -26
View File
@@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import { Colors } from "@blueprintjs/core";
import { AnnotationsHelpers } from "../../util/stateManager"; import { AnnotationsHelpers } from "../../util/stateManager";
export function isLabelErroneous(label, metadataField, ontology, schema) { export function isLabelErroneous(label, metadataField, ontology, schema) {
@@ -24,34 +26,38 @@ export function isLabelErroneous(label, metadataField, ontology, schema) {
return false; return false;
} }
export function labelErrorMessage(label, metadataField, ontology, schema) { /* all other errors - map code to human error message */
const err = isLabelErroneous(label, metadataField, ontology, schema); const errorMessageMap = {
"empty-string": "Blank names not allowed",
duplicate: "Name must be unique",
"trim-spaces": "Leading and trailing spaces not allowed",
"illegal-characters":
"Only alphanumeric and special characters (-_.) allowed",
"multi-space-run": "Multiple consecutive spaces not allowed"
};
if (err === "duplicate") { export function labelPrompt(err, prolog, epilog) {
/* duplicate error is special cased because it has special formatting */ let errPrompt = null;
return ( if (err) {
<span> let errMsg = errorMessageMap[err] ?? "error";
<span style={{ fontStyle: "italic" }}>{label}</span> already errMsg = errMsg[0].toLowerCase() + errMsg.slice(1);
exists already exists within{" "} errPrompt = (
<span style={{ fontStyle: "italic" }}>{metadataField}</span>{" "} <span
style={{
marginTop: 7,
color: Colors.ORANGE3
}}
>
{errMsg}
</span> </span>
); );
} }
return (
if (err) { <span>
/* all other errors - map code to human error message */ {prolog}
const errorMessageMap = { {err ? " - " : null}
"empty-string": "Blank names not allowed", {errPrompt}
duplicate: "Name must be unique", {epilog}
"trim-spaces": "Leading and trailing spaces not allowed", </span>
"illegal-characters": );
"Only alphanumeric and special characters (-_.) allowed",
"multi-space-run": "Multiple consecutive spaces not allowed"
};
const errorMessage = errorMessageMap[err] ?? "error";
return <span>{errorMessage}</span>;
}
/* no error, no message generated */
return null;
} }
@@ -1,57 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { Button, MenuItem } from "@blueprintjs/core";
import { Select } 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
});
@connect()
class ChooseOntologySelect extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const {
ontology,
categoryToDuplicate,
handleChooseOntologyTermFromDropdown
} = this.props;
return (
<div>
<Select
items={
ontology?.terms ||
[] /* this is a placeholder, could be a subcomponent to avoid this */
}
filterable
itemListPredicate={filterOntology}
itemRenderer={(d, { handleClick }) => {
return (
<MenuItem onClick={handleClick} key={d.target} text={d.target} />
);
}}
noResults={<MenuItem disabled text="No results." />}
onItemSelect={d => {
handleChooseOntologyTermFromDropdown(d);
}}
>
{/* children become the popover target; render value here */}
<Button
text={categoryToDuplicate || "Choose an Ontology Term"}
rightIcon="double-caret-vertical"
/>
</Select>
</div>
);
}
}
export default ChooseOntologySelect;
+39 -63
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import { connect } from "react-redux"; import { connect } from "react-redux";
import React from "react"; import React from "react";
@@ -9,17 +8,16 @@ import {
Popover, Popover,
Position, Position,
PopoverInteractionKind, PopoverInteractionKind,
Tooltip, Tooltip
Colors
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import Occupancy from "./occupancy"; import Occupancy from "./occupancy";
import * as globals from "../../globals"; import * as globals from "../../globals";
import styles from "./categorical.css"; import styles from "./categorical.css";
import AnnoDialog from "./annoDialog"; import AnnoDialog from "./annoDialog";
import AnnoInputs from "./annoInputs"; import LabelInput from "./labelInput";
import { AnnotationsHelpers } from "../../util/stateManager"; import { AnnotationsHelpers } from "../../util/stateManager";
import { labelErrorMessage, isLabelErroneous } from "./labelUtil"; import { labelPrompt, isLabelErroneous } from "./labelUtil";
@connect(state => ({ @connect(state => ({
categoricalSelection: state.categoricalSelection, categoricalSelection: state.categoricalSelection,
@@ -30,19 +28,13 @@ import { labelErrorMessage, isLabelErroneous } from "./labelUtil";
schema: state.world?.schema, schema: state.world?.schema,
world: state.world, world: state.world,
crossfilter: state.crossfilter, crossfilter: state.crossfilter,
ontology: state.ontology, ontology: state.ontology
ontologyLoading: state.ontology?.loading,
ontologyEnabled: state.ontology?.enabled
})) }))
class CategoryValue extends React.Component { class CategoryValue extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
editedLabelText: String( editedLabelText: this.currentLabel()
props.categoricalSelection[props.metadataField].categoryValues[
props.categoryIndex
]
).valueOf()
}; };
} }
@@ -54,9 +46,7 @@ class CategoryValue extends React.Component {
prevProps.categoryIndex !== categoryIndex prevProps.categoryIndex !== categoryIndex
) { ) {
this.setState({ this.setState({
editedLabelText: String( editedLabelText: this.currentLabel()
categoricalSelection[metadataField].categoryValues[categoryIndex]
).valueOf()
}); });
} }
} }
@@ -98,49 +88,27 @@ class CategoryValue extends React.Component {
e.preventDefault(); e.preventDefault();
}; };
handleCreateArbitraryLabel = editedLabelTextNotInOntology => { handleCreateArbitraryLabel = txt => {
const { dispatch, metadataField, categoryIndex } = this.props; const { dispatch, metadataField, categoryIndex } = this.props;
const label = this.getLabel(); const label = this.getLabel();
this.cancelEditMode(); this.cancelEditMode();
dispatch({ dispatch({
type: "annotation: label edited", type: "annotation: label edited",
metadataField, metadataField,
editedLabel: editedLabelTextNotInOntology, editedLabel: txt,
categoryIndex, categoryIndex,
label label
}); });
}; };
labelNameError = name => { labelNameError = name => {
const { const { metadataField, ontology, schema } = this.props;
metadataField, if (name === this.currentLabel()) return false;
ontology,
schema,
categoricalSelection,
categoryIndex
} = this.props;
const category = categoricalSelection[metadataField];
const displayString = String(
category.categoryValues[categoryIndex]
).valueOf();
if (name === displayString) return false;
return isLabelErroneous(name, metadataField, ontology, schema); return isLabelErroneous(name, metadataField, ontology, schema);
}; };
labelNameErrorMessage = name => { instruction = label => {
const { return labelPrompt(this.labelNameError(label), "New, unique label", ":");
metadataField,
ontology,
schema,
categoricalSelection,
categoryIndex
} = this.props;
const category = categoricalSelection[metadataField];
const displayString = String(
category.categoryValues[categoryIndex]
).valueOf();
if (name === displayString) return null;
return labelErrorMessage(name, metadataField, ontology, schema);
}; };
activateEditLabelMode = () => { activateEditLabelMode = () => {
@@ -154,6 +122,9 @@ class CategoryValue extends React.Component {
cancelEditMode = () => { cancelEditMode = () => {
const { dispatch, metadataField, categoryIndex } = this.props; const { dispatch, metadataField, categoryIndex } = this.props;
this.setState({
editedLabelText: this.currentLabel()
});
dispatch({ dispatch({
type: "annotation: cancel edit label mode", type: "annotation: cancel edit label mode",
metadataField, metadataField,
@@ -255,6 +226,13 @@ class CategoryValue extends React.Component {
return label; return label;
}; };
currentLabel() {
const { categoricalSelection, metadataField, categoryIndex } = this.props;
return String(
categoricalSelection[metadataField].categoryValues[categoryIndex]
).valueOf();
}
isAddCurrentSelectionDisabled(category, value) { isAddCurrentSelectionDisabled(category, value) {
/* /*
disable "add current selection to label", if one of the following is true: disable "add current selection to label", if one of the following is true:
@@ -294,12 +272,13 @@ class CategoryValue extends React.Component {
schema, schema,
isUserAnno, isUserAnno,
annotations, annotations,
ontologyEnabled, ontology,
// flippedProps is potentially brittle, their docs want {...flippedProps} on our div, // flippedProps is potentially brittle, their docs want {...flippedProps} on our div,
// our lint doesn't like jsx spread, we are version pinned to prevent api change on their part // our lint doesn't like jsx spread, we are version pinned to prevent api change on their part
flippedProps, flippedProps,
pointDilation pointDilation
} = this.props; } = this.props;
const ontologyEnabled = ontology?.enabled ?? false;
const { editedLabelText } = this.state; const { editedLabelText } = this.state;
@@ -309,9 +288,7 @@ class CategoryValue extends React.Component {
const selected = category.categoryValueSelected[categoryIndex]; const selected = category.categoryValueSelected[categoryIndex];
const count = category.categoryValueCounts[categoryIndex]; const count = category.categoryValueCounts[categoryIndex];
const value = category.categoryValues[categoryIndex]; const value = category.categoryValues[categoryIndex];
const displayString = String( const displayString = this.currentLabel();
category.categoryValues[categoryIndex]
).valueOf();
/* this is the color scale, so add swatches below */ /* this is the color scale, so add swatches below */
const isColorBy = metadataField === colorAccessor; const isColorBy = metadataField === colorAccessor;
@@ -442,30 +419,26 @@ class CategoryValue extends React.Component {
"data-testid": `${metadataField}:${displayString}:submit-label-edit` "data-testid": `${metadataField}:${displayString}:submit-label-edit`
}} }}
title="Edit label" title="Edit label"
instruction={`New label text must be unique within category ${metadataField}:`} instruction={this.instruction(editedLabelText)}
cancelTooltipContent="Close this dialog without editing label text." cancelTooltipContent="Close this dialog without editing label text."
primaryButtonText="Change label text" primaryButtonText="Change label text"
text={editedLabelText} text={editedLabelText}
categoryToDuplicate={null} categoryToDuplicate={null}
validationError={this.labelNameError(editedLabelText)} validationError={this.labelNameError(editedLabelText)}
errorMessage={this.labelNameErrorMessage(editedLabelText)}
handleSubmit={this.handleEditValue} handleSubmit={this.handleEditValue}
handleCancel={this.cancelEditMode} handleCancel={this.cancelEditMode}
annoInput={ annoInput={
<AnnoInputs <LabelInput
useSuggest={ontologyEnabled} label={editedLabelText}
text={editedLabelText} labelSuggestions={ontologyEnabled ? ontology.terms : null}
onChange={this.handleTextChange}
onSelect={this.handleTextChange}
inputProps={{ inputProps={{
"data-testid": `${metadataField}:${displayString}:edit-label-name` "data-testid": `${metadataField}:${displayString}:edit-label-name`,
leftIcon: "tag",
intent: "none",
autoFocus: true
}} }}
handleCreateArbitraryLabel={
this.handleCreateArbitraryLabel
}
handleItemChange={this.handleSuggestActiveItemChange}
handleChoice={this.handleChoice}
handleTextChange={this.handleTextChange}
isTextInvalid={this.labelNameError}
isTextInvalidErrorMessage={this.labelNameErrorMessage}
/> />
} }
annoSelect={null} annoSelect={null}
@@ -475,7 +448,10 @@ class CategoryValue extends React.Component {
</div> </div>
<span style={{ flexShrink: 0 }}> <span style={{ flexShrink: 0 }}>
{colorAccessor && !isColorBy && !annotations.isEditingLabelName ? ( {colorAccessor && !isColorBy && !annotations.isEditingLabelName ? (
<Occupancy category={category} {...this.props} /> <Occupancy
category={category}
{...this.props} // eslint-disable-line react/jsx-props-no-spreading
/>
) : null} ) : null}
</span> </span>
</div> </div>
+28 -27
View File
@@ -2,11 +2,10 @@
/* rc slider https://www.npmjs.com/package/rc-slider */ /* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react"; import React from "react";
import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import * as globals from "../../globals";
import { Button } from "@blueprintjs/core"; import { Button } from "@blueprintjs/core";
import * as globals from "../../globals";
import HistogramBrush from "../brushableHistogram"; import HistogramBrush from "../brushableHistogram";
@connect(state => ({ @connect(state => ({
@@ -28,7 +27,7 @@ class Continuous extends React.PureComponent {
}; };
}; };
renderIsStillLoading(zebra, key) { static renderIsStillLoading(zebra, key) {
return ( return (
<div <div
key={key} key={key}
@@ -45,7 +44,7 @@ class Continuous extends React.PureComponent {
alignItems: "center" alignItems: "center"
}} }}
> >
<div style={{ minWidth: 30 }}></div> <div style={{ minWidth: 30 }} />
<div style={{ display: "flex", alignSelf: "center" }}> <div style={{ display: "flex", alignSelf: "center" }}>
<span style={{ fontStyle: "italic" }}>{key}</span> <span style={{ fontStyle: "italic" }}>{key}</span>
</div> </div>
@@ -68,7 +67,7 @@ class Continuous extends React.PureComponent {
const obsIndex = schema.annotations.obs.index; const obsIndex = schema.annotations.obs.index;
const allContinuousNames = schema.annotations.obs.columns const allContinuousNames = schema.annotations.obs.columns
.filter(col => col.type === "int32" || col.type === "float32") .filter(col => col.type === "int32" || col.type === "float32")
.filter(col => col.name != obsIndex) .filter(col => col.name !== obsIndex)
.map(col => col.name); .map(col => col.name);
/* initial value for iterator to simulate index, ranges is an object */ /* initial value for iterator to simulate index, ranges is an object */
@@ -80,29 +79,31 @@ class Continuous extends React.PureComponent {
if (!obsAnnotations.hasCol(key)) { if (!obsAnnotations.hasCol(key)) {
// still loading! // still loading!
zebra += 1; zebra += 1;
return this.renderIsStillLoading(zebra, key); return Continuous.renderIsStillLoading(zebra, key);
} else {
// data loaded and available
const summary = obsAnnotations.col(key).summarize();
const nonFiniteExtent =
summary.min === undefined ||
summary.max === undefined ||
Number.isNaN(summary.min) ||
Number.isNaN(summary.max);
if (!summary.categorical && !nonFiniteExtent) {
zebra += 1;
return (
<HistogramBrush
key={key}
field={key}
isObs
zebra={zebra % 2 === 0}
ranges={summary}
handleColorAction={this.handleColorAction(key)}
/>
);
}
} }
// data loaded and available
const summary = obsAnnotations.col(key).summarize();
const nonFiniteExtent =
summary.min === undefined ||
summary.max === undefined ||
Number.isNaN(summary.min) ||
Number.isNaN(summary.max);
if (!summary.categorical && !nonFiniteExtent) {
zebra += 1;
return (
<HistogramBrush
key={key}
field={key}
isObs
zebra={zebra % 2 === 0}
ranges={summary}
handleColorAction={this.handleColorAction(key)}
/>
);
}
return null;
})} })}
</div> </div>
); );
+1 -1
View File
@@ -398,7 +398,7 @@ class Graph extends React.Component {
// don't return "change" of state unless we are really changing it! // don't return "change" of state unless we are really changing it!
const { toolSVG } = this.state; const { toolSVG } = this.state;
if (toolSVG === undefined) return {}; if (toolSVG === undefined) return {};
else return { toolSVG: undefined }; return { toolSVG: undefined };
} }
let handleStart; let handleStart;
+8 -2
View File
@@ -283,8 +283,14 @@ class MenuBar extends React.Component {
return ( return (
<ButtonGroup style={{ marginRight: 10 }}> <ButtonGroup style={{ marginRight: 10 }}>
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} /> <CellSetButton
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} /> {...this.props} // eslint-disable-line react/jsx-props-no-spreading
eitherCellSetOneOrTwo={1}
/>
<CellSetButton
{...this.props} // eslint-disable-line react/jsx-props-no-spreading
eitherCellSetOneOrTwo={2}
/>
{!differential.diffExp ? ( {!differential.diffExp ? (
<Tooltip <Tooltip
content={tipMessage} content={tipMessage}
+16 -18
View File
@@ -55,46 +55,40 @@ const Annotations = (
} }
/* CATEGORY */ /* CATEGORY */
case "annotation: activate add new label mode": case "annotation: activate add new label mode": {
return { return {
...state, ...state,
isAddingNewLabel: true, isAddingNewLabel: true,
categoryAddingNewLabel: action.data categoryAddingNewLabel: action.data
}; };
case "annotation: activate add new ontology label mode": }
return {
...state,
isAddingNewLabelFromOntology: true,
categoryAddingNewLabelFromOntology: action.data
};
case "annotation: disable add new ontology label mode":
return {
...state,
isAddingNewLabelFromOntology: false,
categoryAddingNewLabelFromOntology: null
};
case "annotation: disable add new label mode": case "annotation: disable add new label mode": {
return { return {
...state, ...state,
isAddingNewLabel: false, isAddingNewLabel: false,
categoryAddingNewLabel: null categoryAddingNewLabel: null
}; };
case "annotation: activate category edit mode": }
case "annotation: activate category edit mode": {
return { return {
...state, ...state,
isEditingCategoryName: true, isEditingCategoryName: true,
categoryBeingEdited: action.data categoryBeingEdited: action.data
}; };
case "annotation: disable category edit mode": }
case "annotation: disable category edit mode": {
return { return {
...state, ...state,
isEditingCategoryName: false, isEditingCategoryName: false,
categoryBeingEdited: null categoryBeingEdited: null
}; };
}
/* LABEL */ /* LABEL */
case "annotation: activate edit label mode": case "annotation: activate edit label mode": {
return { return {
...state, ...state,
isEditingLabelName: true, isEditingLabelName: true,
@@ -103,12 +97,16 @@ const Annotations = (
label: action.categoryIndex label: action.categoryIndex
} }
}; };
case "annotation: cancel edit label mode": }
case "annotation: cancel edit label mode": {
return { return {
...state, ...state,
isEditingLabelName: false, isEditingLabelName: false,
labelEditable: { category: null, label: null } labelEditable: { category: null, label: null }
}; };
}
default: default:
return state; return state;
} }
@@ -1,5 +1,4 @@
import { ControlsHelpers as CH } from "../util/stateManager"; import { ControlsHelpers as CH } from "../util/stateManager";
import * as globals from "../globals";
const CategoricalSelection = ( const CategoricalSelection = (
state, state,
+1 -2
View File
@@ -24,9 +24,8 @@ const CrossfilterReducerBase = (
) => { ) => {
switch (action.type) { switch (action.type) {
case "universe: column load success": { case "universe: column load success": {
const { schema, world, layoutChoice } = nextSharedState; const { world, layoutChoice } = nextSharedState;
const { obsAnnotations, obsLayout } = world; const { obsAnnotations, obsLayout } = world;
const { dim, dataframe } = action;
// ignore var dimension loads as these are not currently selectable // ignore var dimension loads as these are not currently selectable
if (action.dim === "varAnnotations") return state; if (action.dim === "varAnnotations") return state;
+6 -3
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
const Ontology = ( const Ontology = (
state = { state = {
enabled: false, // are ontology terms enabled? enabled: false, // are ontology terms enabled?
@@ -9,10 +8,12 @@ const Ontology = (
action action
) => { ) => {
switch (action.type) { switch (action.type) {
case "configuration load complete": case "configuration load complete": {
/* eslint-disable camelcase */
const enabled = const enabled =
action.config?.parameters?.annotations_cell_ontology_enabled ?? false; action.config?.parameters?.annotations_cell_ontology_enabled ?? false;
const terms = action.config?.parameters?.annotations_cell_ontology_terms; const terms = action.config?.parameters?.annotations_cell_ontology_terms;
/* eslint-enable camelcase */
const termSet = new Set(terms); const termSet = new Set(terms);
return { return {
...state, ...state,
@@ -21,8 +22,10 @@ const Ontology = (
terms, terms,
termSet termSet
}; };
default: }
default: {
return state; return state;
}
} }
}; };
-2
View File
@@ -44,8 +44,6 @@ const skipOnActions = new Set([
/* annotation component action */ /* annotation component action */
"annotation: activate add new label mode", "annotation: activate add new label mode",
"annotation: activate add new ontology label mode",
"annotation: disable add new ontology label mode",
"annotation: disable add new label mode", "annotation: disable add new label mode",
"annotation: activate category edit mode", "annotation: activate category edit mode",
"annotation: disable category edit mode", "annotation: disable category edit mode",
-1
View File
@@ -10,7 +10,6 @@ TL;DR: sort order is:
import isNumber from "is-number"; import isNumber from "is-number";
import * as globals from "../globals"; import * as globals from "../globals";
import { memoize } from "./dataframe/util";
function caseInsensitiveCompare(a, b) { function caseInsensitiveCompare(a, b) {
const textA = String(a).toUpperCase(); const textA = String(a).toUpperCase();
@@ -58,18 +58,18 @@ function topNCategories(colSchema, summary, N) {
); );
const topNindices = new Set(sortIndex.slice(0, N)); const topNindices = new Set(sortIndex.slice(0, N));
const topNCategories = []; const _topNCategories = [];
const topNCounts = []; const topNCounts = [];
for (let i = 0; i < categories.length; i += 1) { for (let i = 0; i < categories.length; i += 1) {
if (topNindices.has(i)) { if (topNindices.has(i)) {
topNCategories.push(categories[i]); _topNCategories.push(categories[i]);
topNCounts.push(counts[i]); topNCounts.push(counts[i]);
} }
} }
return [topNCategories, topNCounts]; return [_topNCategories, topNCounts];
} }
export function selectableCategoryNames(schema, maxCategoryItems, names) { export function selectableCategoryNames(schema, maxCatItems, names) {
/* /*
return all obs annotation names that are categorical AND have a return all obs annotation names that are categorical AND have a
"reasonably" small number of categories AND are not the index column. "reasonably" small number of categories AND are not the index column.
+2 -5
View File
@@ -1,11 +1,8 @@
import _ from "lodash";
import { unassignedCategoryLabel } from "../../globals"; import { unassignedCategoryLabel } from "../../globals";
import { decodeMatrixFBS } from "./matrix"; import { decodeMatrixFBS } from "./matrix";
import * as Dataframe from "../dataframe"; import * as Dataframe from "../dataframe";
import { isFpTypedArray } from "../typeHelpers"; import { isFpTypedArray } from "../typeHelpers";
import { indexEntireSchema } from "./schemaHelpers"; import { indexEntireSchema } from "./schemaHelpers";
import { isCategoricalAnnotation } from "./annotationsHelpers";
import catLabelSort from "../catLabelSort"; import catLabelSort from "../catLabelSort";
/* /*
@@ -96,7 +93,7 @@ export function matrixFBSToDataframe(arrayBuffers) {
const fbs = arrayBuffers.map(ab => decodeMatrixFBS(ab, true)); // leave in place const fbs = arrayBuffers.map(ab => decodeMatrixFBS(ab, true)); // leave in place
/* check that all FBS have same row dimensionality */ /* check that all FBS have same row dimensionality */
const nRows = fbs[0].nRows; const { nRows } = fbs[0];
fbs.forEach(b => { fbs.forEach(b => {
if (b.nRows !== nRows) if (b.nRows !== nRows)
throw new Error("FBS with inconsistent dimensionality"); throw new Error("FBS with inconsistent dimensionality");
@@ -181,7 +178,7 @@ export function addObsAnnotations(universe, df) {
// for all of the new data, reconcile with schema and sort categories. // for all of the new data, reconcile with schema and sort categories.
const dfs = Array.isArray(df) ? df : [df]; const dfs = Array.isArray(df) ? df : [df];
const keys = dfs.map(df => df.colIndex.keys()).flat(); const keys = dfs.map(d => d.colIndex.keys()).flat();
const { schema } = universe; const { schema } = universe;
keys.forEach(k => { keys.forEach(k => {
const colSchema = schema.annotations.obsByName[k]; const colSchema = schema.annotations.obsByName[k];
+1 -1
View File
@@ -292,7 +292,7 @@ export function createObsDimensions(crossfilter, world, XYdimNames) {
for which we have a supported type, *except* for the index column, indicated for which we have a supported type, *except* for the index column, indicated
by schema.annotations.obs.index. by schema.annotations.obs.index.
*/ */
const { schema, obsLayout, obsAnnotations } = world; const { schema, obsLayout } = world;
const indexName = schema.annotations.obs.index; const indexName = schema.annotations.obs.index;
const annoList = schema.annotations.obs.columns.filter( const annoList = schema.annotations.obs.columns.filter(
anno => anno.name !== indexName anno => anno.name !== indexName