import { connect } from "react-redux"; import React from "react"; import { Button, Menu, MenuItem, Popover, Position, PopoverInteractionKind, Tooltip } from "@blueprintjs/core"; import Occupancy from "./occupancy"; import * as globals from "../../globals"; import styles from "./categorical.css"; import AnnoDialog from "./annoDialog"; import LabelInput from "./labelInput"; import { AnnotationsHelpers } from "../../util/stateManager"; import { labelPrompt, isLabelErroneous } from "./labelUtil"; @connect(state => ({ categoricalSelection: state.categoricalSelection, annotations: state.annotations, colorScale: state.colors.scale, colorAccessor: state.colors.colorAccessor, pointDilation: state.pointDilation, schema: state.world?.schema, world: state.world, crossfilter: state.crossfilter, ontology: state.ontology })) class CategoryValue extends React.Component { constructor(props) { super(props); this.state = { editedLabelText: this.currentLabel() }; } componentDidUpdate(prevProps) { const { categoricalSelection, metadataField, categoryIndex } = this.props; if ( prevProps.categoricalSelection !== categoricalSelection || prevProps.metadataField !== metadataField || prevProps.categoryIndex !== categoryIndex ) { this.setState({ editedLabelText: this.currentLabel() }); } } handleDeleteValue = () => { const { dispatch, metadataField } = this.props; const label = this.getLabel(); dispatch({ type: "annotation: delete label", metadataField, label }); }; handleAddCurrentSelectionToThisLabel = () => { const { dispatch, metadataField, categoryIndex } = this.props; const label = this.getLabel(); dispatch({ type: "annotation: label current cell selection", metadataField, categoryIndex, label }); }; handleEditValue = e => { const { dispatch, metadataField, categoryIndex } = this.props; const { editedLabelText } = this.state; const label = this.getLabel(); this.cancelEditMode(); dispatch({ type: "annotation: label edited", editedLabel: editedLabelText, metadataField, categoryIndex, label }); e.preventDefault(); }; handleCreateArbitraryLabel = txt => { const { dispatch, metadataField, categoryIndex } = this.props; const label = this.getLabel(); this.cancelEditMode(); dispatch({ type: "annotation: label edited", metadataField, editedLabel: txt, categoryIndex, label }); }; labelNameError = name => { const { metadataField, ontology, schema } = this.props; if (name === this.currentLabel()) return false; return isLabelErroneous(name, metadataField, ontology, schema); }; instruction = label => { return labelPrompt(this.labelNameError(label), "New, unique label", ":"); }; activateEditLabelMode = () => { const { dispatch, metadataField, categoryIndex } = this.props; dispatch({ type: "annotation: activate edit label mode", metadataField, categoryIndex }); }; cancelEditMode = () => { const { dispatch, metadataField, categoryIndex } = this.props; this.setState({ editedLabelText: this.currentLabel() }); 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, nextState) => { /* 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 * the crossfilter (ie, global selection state) If and only if true, update the component */ const { props, state } = 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; const crossfilterChange = props.isUserAnno && props.crossfilter !== nextProps.crossfilter; const editingLabel = state.editedLabelText !== nextState.editedLabelText; const dilationChange = props.pointDilation !== nextProps.pointDilation; return ( valueSelectionChange || worldChange || colorAccessorChange || annotationsChange || crossfilterChange || editingLabel || dilationChange ); }; 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 }); }; handleTextChange = text => { this.setState({ editedLabelText: text }); }; handleChoice = e => { /* Blueprint Suggest format */ this.setState({ editedLabelText: e.target }); }; getLabel = () => { const { metadataField, categoryIndex, categoricalSelection } = this.props; const category = categoricalSelection[metadataField]; const label = category.categoryValues[categoryIndex]; return label; }; currentLabel() { const { categoricalSelection, metadataField, categoryIndex } = this.props; return String( categoricalSelection[metadataField].categoryValues[categoryIndex] ).valueOf(); } isAddCurrentSelectionDisabled(category, value) { /* disable "add current selection to label", if one of the following is true: 1. no cells are selected 2. all currently selected cells already have this label, on this category */ const { crossfilter, world } = this.props; // 1. no cells selected? if (crossfilter.countSelected() === 0) { return true; } // 2. all selected cells already have the label const mask = crossfilter.allSelectedMask(); if ( AnnotationsHelpers.allHaveLabelByMask( world.obsAnnotations, category, value, mask ) ) { return true; } // else, don't disable return false; } render() { const { categoricalSelection, metadataField, categoryIndex, colorAccessor, colorScale, i, schema, isUserAnno, annotations, ontology, // 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 flippedProps, pointDilation } = this.props; const ontologyEnabled = ontology?.enabled ?? false; const { editedLabelText } = this.state; 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 = this.currentLabel(); /* 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; } let truncatedString = null; if ( colorAccessor && !isColorBy && displayString.length > globals.categoryLabelDisplayStringShortLength ) { truncatedString = `${displayString.slice( 0, globals.categoryLabelDisplayStringShortLength / 2 )}…${displayString.slice( -globals.categoryLabelDisplayStringShortLength / 2 )}`; } else if ( displayString.length > globals.categoryLabelDisplayStringLongLength ) { truncatedString = `${displayString.slice( 0, globals.categoryLabelDisplayStringLongLength / 2 )}…${displayString.slice( -globals.categoryLabelDisplayStringLongLength / 2 )}`; } const editModeActive = isUserAnno && annotations.labelEditable.category === metadataField && annotations.isEditingLabelName && annotations.labelEditable.label === categoryIndex; return (
{truncatedString || displayString} {editModeActive ? (
} annoSelect={null} />
) : null}
{colorAccessor && !isColorBy && !annotations.isEditingLabelName ? ( ) : null}
{count} {isUserAnno ? ( Re-label currently selected cells as {` ${displayString}`} } disabled={this.isAddCurrentSelectionDisabled( metadataField, value )} /> {displayString !== globals.unassignedCategoryLabel ? ( ) : null} {displayString !== globals.unassignedCategoryLabel ? ( ) : null} } >
); } } export default CategoryValue;