mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-23 11:28:12 +08:00
411 lines
12 KiB
JavaScript
411 lines
12 KiB
JavaScript
// jshint esversion: 6
|
|
import { connect } from "react-redux";
|
|
import React from "react";
|
|
|
|
import {
|
|
Button,
|
|
InputGroup,
|
|
Menu,
|
|
MenuItem,
|
|
Popover,
|
|
Position,
|
|
Icon,
|
|
PopoverInteractionKind
|
|
} from "@blueprintjs/core";
|
|
import Occupancy from "./occupancy";
|
|
import * as globals from "../../globals";
|
|
import styles from "./categorical.css";
|
|
|
|
@connect(state => ({
|
|
categoricalSelection: state.categoricalSelection,
|
|
annotations: state.annotations,
|
|
colorScale: state.colors.scale,
|
|
colorAccessor: state.colors.colorAccessor,
|
|
schema: state.world?.schema,
|
|
world: state.world
|
|
}))
|
|
class CategoryValue extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
editedLabelText: ""
|
|
};
|
|
}
|
|
|
|
handleDeleteValue = () => {
|
|
const {
|
|
dispatch,
|
|
metadataField,
|
|
categoryIndex,
|
|
categoricalSelection
|
|
} = this.props;
|
|
const category = categoricalSelection[metadataField];
|
|
const label = category.categoryValues[categoryIndex];
|
|
dispatch({
|
|
type: "annotation: delete label",
|
|
metadataField,
|
|
label
|
|
});
|
|
};
|
|
|
|
handleAddCurrentSelectionToThisLabel = () => {
|
|
const {
|
|
dispatch,
|
|
metadataField,
|
|
categoryIndex,
|
|
categoricalSelection
|
|
} = this.props;
|
|
const category = categoricalSelection[metadataField];
|
|
const label = category.categoryValues[categoryIndex];
|
|
dispatch({
|
|
type: "annotation: label current cell selection",
|
|
metadataField,
|
|
categoryIndex,
|
|
label
|
|
});
|
|
};
|
|
|
|
handleEditValue = () => {
|
|
const {
|
|
dispatch,
|
|
metadataField,
|
|
categoryIndex,
|
|
categoricalSelection
|
|
} = this.props;
|
|
const { editedLabelText } = this.state;
|
|
const category = categoricalSelection[metadataField];
|
|
const label = category.categoryValues[categoryIndex];
|
|
dispatch({
|
|
type: "annotation: label edited",
|
|
editedLabel: editedLabelText,
|
|
metadataField,
|
|
categoryIndex,
|
|
label
|
|
});
|
|
this.setState({ editedLabelText: "" });
|
|
};
|
|
|
|
activateEditLabelMode = () => {
|
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
dispatch({
|
|
type: "annotation: activate edit label mode",
|
|
metadataField,
|
|
categoryIndex
|
|
});
|
|
};
|
|
|
|
cancelEdit = () => {
|
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
dispatch({
|
|
type: "annotation: cancel edit label mode",
|
|
metadataField,
|
|
categoryIndex
|
|
});
|
|
};
|
|
|
|
toggleOff = () => {
|
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
dispatch({
|
|
type: "categorical metadata filter deselect",
|
|
metadataField,
|
|
categoryIndex
|
|
});
|
|
};
|
|
|
|
shouldComponentUpdate = nextProps => {
|
|
/*
|
|
Checks to see if at least one of the following changed:
|
|
* world state
|
|
* the color accessor (what is currently being colored by)
|
|
* if this catagorical value's selection status has changed
|
|
|
|
If and only if true, update the component
|
|
*/
|
|
const { props } = this;
|
|
const { metadataField, categoryIndex, categoricalSelection } = props;
|
|
const { categoricalSelection: newCategoricalSelection } = nextProps;
|
|
|
|
const valueSelectionChange =
|
|
categoricalSelection[metadataField].categoryValueSelected[
|
|
categoryIndex
|
|
] !==
|
|
newCategoricalSelection[metadataField].categoryValueSelected[
|
|
categoryIndex
|
|
];
|
|
|
|
const worldChange = props.world !== nextProps.world;
|
|
const colorAccessorChange = props.colorAccessor !== nextProps.colorAccessor;
|
|
const annotationsChange = props.annotations !== nextProps.annotations;
|
|
|
|
return (
|
|
valueSelectionChange ||
|
|
worldChange ||
|
|
colorAccessorChange ||
|
|
annotationsChange
|
|
);
|
|
};
|
|
|
|
toggleOn = () => {
|
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
dispatch({
|
|
type: "categorical metadata filter select",
|
|
metadataField,
|
|
categoryIndex
|
|
});
|
|
};
|
|
|
|
handleMouseEnter = () => {
|
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
dispatch({
|
|
type: "category value mouse hover start",
|
|
metadataField,
|
|
categoryIndex
|
|
});
|
|
};
|
|
|
|
handleMouseExit = () => {
|
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
dispatch({
|
|
type: "category value mouse hover end",
|
|
metadataField,
|
|
categoryIndex
|
|
});
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
categoricalSelection,
|
|
metadataField,
|
|
categoryIndex,
|
|
colorAccessor,
|
|
colorScale,
|
|
i,
|
|
schema,
|
|
isUserAnno,
|
|
annotations
|
|
} = this.props;
|
|
|
|
if (!categoricalSelection) return null;
|
|
|
|
const category = categoricalSelection[metadataField];
|
|
const selected = category.categoryValueSelected[categoryIndex];
|
|
const count = category.categoryValueCounts[categoryIndex];
|
|
const value = category.categoryValues[categoryIndex];
|
|
const displayString = String(
|
|
category.categoryValues[categoryIndex]
|
|
).valueOf();
|
|
|
|
/* this is the color scale, so add swatches below */
|
|
const isColorBy = metadataField === colorAccessor;
|
|
let categories = null;
|
|
|
|
if (isColorBy && schema) {
|
|
categories = schema.annotations.obsByName[colorAccessor]?.categories;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={styles.value}
|
|
data-testclass="categorical-row"
|
|
style={{
|
|
padding: "4px 7px",
|
|
display: "flex",
|
|
alignItems: "baseline",
|
|
justifyContent: "space-between",
|
|
marginBottom: "2px",
|
|
borderRadius: "2px"
|
|
}}
|
|
onMouseEnter={this.handleMouseEnter}
|
|
onMouseLeave={this.handleMouseExit}
|
|
>
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
<div
|
|
style={{
|
|
margin: 0,
|
|
padding: 0,
|
|
userSelect: "none",
|
|
width: globals.leftSidebarWidth - 240,
|
|
display: "flex",
|
|
justifyContent: "flex-start"
|
|
}}
|
|
>
|
|
<div style={{ display: "flex" }}>
|
|
<label className="bp3-control bp3-checkbox" style={{ margin: 0 }}>
|
|
<input
|
|
onChange={selected ? this.toggleOff : this.toggleOn}
|
|
data-testclass="categorical-value-select"
|
|
data-testid={`categorical-value-select-${metadataField}-${displayString}`}
|
|
checked={selected}
|
|
type="checkbox"
|
|
/>
|
|
<span
|
|
className="bp3-control-indicator"
|
|
onMouseEnter={this.handleMouseExit}
|
|
onMouseLeave={this.handleMouseEnter}
|
|
/>
|
|
</label>
|
|
<span
|
|
data-testid={`categorical-value-${metadataField}-${displayString}`}
|
|
data-testclass="categorical-value"
|
|
style={{
|
|
wordBreak: "break-all",
|
|
color:
|
|
displayString === globals.unassignedCategoryLabel
|
|
? "#ababab"
|
|
: "black",
|
|
fontStyle:
|
|
displayString === globals.unassignedCategoryLabel
|
|
? "italic"
|
|
: "normal"
|
|
}}
|
|
>
|
|
{annotations.isEditingLabelName &&
|
|
annotations.labelEditable.category === metadataField &&
|
|
annotations.labelEditable.label === categoryIndex
|
|
? null
|
|
: displayString}
|
|
</span>
|
|
</div>
|
|
{isUserAnno &&
|
|
annotations.isEditingLabelName &&
|
|
annotations.labelEditable.category === metadataField &&
|
|
annotations.labelEditable.label === categoryIndex ? (
|
|
<form
|
|
onSubmit={e => {
|
|
e.preventDefault();
|
|
this.handleEditValue();
|
|
}}
|
|
>
|
|
<InputGroup
|
|
style={{ position: "relative", top: -1 }}
|
|
ref={input => {
|
|
this.editableInput = input;
|
|
}}
|
|
small
|
|
onChange={e => {
|
|
this.setState({ editedLabelText: e.target.value });
|
|
}}
|
|
defaultValue={displayString}
|
|
rightElement={
|
|
<Button
|
|
minimal
|
|
style={{ position: "relative", top: -1 }}
|
|
type="button"
|
|
icon="small-tick"
|
|
data-testclass="submitEdit"
|
|
data-testid="submitEdit"
|
|
onClick={this.handleEditValue}
|
|
/>
|
|
}
|
|
/>
|
|
</form>
|
|
) : null}
|
|
{/*
|
|
CANCEL IT, WITH BUTTON, ESCAPE KEY, CLICK OUT, UNDO?
|
|
|
|
<Button
|
|
minimal
|
|
style={{ position: "relative", top: -1 }}
|
|
type="button"
|
|
icon="cross"
|
|
data-testclass="submitEdit"
|
|
data-testid="submitEdit"
|
|
onClick={this.cancelEdit}
|
|
/> */}
|
|
</div>
|
|
<span style={{ flexShrink: 0 }}>
|
|
{colorAccessor && !isColorBy && !annotations.isEditingLabelName ? (
|
|
<Occupancy category={category} {...this.props} />
|
|
) : null}
|
|
</span>
|
|
</div>
|
|
<span>
|
|
<span
|
|
data-testclass="categorical-value-count"
|
|
data-testid={`categorical-value-count-${metadataField}-${displayString}`}
|
|
style={{
|
|
color:
|
|
displayString === globals.unassignedCategoryLabel
|
|
? "#ababab"
|
|
: "black",
|
|
fontStyle:
|
|
displayString === globals.unassignedCategoryLabel
|
|
? "italic"
|
|
: "auto"
|
|
}}
|
|
>
|
|
{count}
|
|
</span>
|
|
|
|
<svg
|
|
display={isColorBy && categories ? "auto" : "none"}
|
|
style={{
|
|
marginLeft: 5,
|
|
width: 11,
|
|
height: 11,
|
|
backgroundColor:
|
|
isColorBy && categories
|
|
? colorScale(categories.indexOf(value))
|
|
: "inherit"
|
|
}}
|
|
/>
|
|
{isUserAnno ? (
|
|
<span
|
|
onMouseEnter={this.handleMouseExit}
|
|
onMouseLeave={this.handleMouseEnter}
|
|
>
|
|
<Popover
|
|
interactionKind={PopoverInteractionKind.HOVER}
|
|
boundary="window"
|
|
position={Position.RIGHT_TOP}
|
|
content={
|
|
<Menu>
|
|
<MenuItem
|
|
icon="plus"
|
|
data-testclass="handleAddCurrentSelectionToThisLabel"
|
|
data-testid={`handleAddCurrentSelectionToThisLabel-${metadataField}`}
|
|
onClick={this.handleAddCurrentSelectionToThisLabel}
|
|
text={`Label currently selected cells as ${displayString}`}
|
|
/>
|
|
{displayString !== globals.unassignedCategoryLabel ? (
|
|
<MenuItem
|
|
icon="edit"
|
|
text="Edit this label's name"
|
|
data-testclass="handleEditValue"
|
|
data-testid={`handleEditValue-${metadataField}`}
|
|
onClick={this.activateEditLabelMode}
|
|
/>
|
|
) : null}
|
|
{displayString !== globals.unassignedCategoryLabel ? (
|
|
<MenuItem
|
|
icon="delete"
|
|
intent="danger"
|
|
data-testclass="handleDeleteValue"
|
|
data-testid={`handleDeleteValue-${metadataField}`}
|
|
onClick={this.handleDeleteValue}
|
|
text="Delete this label, and reassign all cells to type 'unknown'"
|
|
/>
|
|
) : null}
|
|
</Menu>
|
|
}
|
|
>
|
|
<Button
|
|
style={{ marginLeft: 0, position: "relative", top: -1 }}
|
|
data-testclass="seeActions"
|
|
data-testid={`seeActions-${metadataField}`}
|
|
icon="more"
|
|
small
|
|
minimal
|
|
/>
|
|
</Popover>
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default CategoryValue;
|