mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-02 23:28:12 +08:00
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:
@@ -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={
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user