Gene sets UI, right sidebar refactor (#2097)

* prototyping

* render histos on open gene set

* prototyping

* render histos on open gene set

* factor out add genes to own component

* remove unused import

* mock reducer

* color by geneset stub

* menus and buttons

* geneset dialogue stub

* remove heatmap mock

* componetize histogram

* reenable add genes

* re-add isuserdefined

* test data

* remove have fetched

* add isExpanded state to gene, and pass to histogram

* expand button

* toggleable

* mini

* bump number of genes to 50

* don't clear diffexp on subset

* move create category to top

* render diffexp as geneset

* geneset show mean expression

* gene set reducer

* add geneset UI reducer

* wire e2e gene set loading prototype

* fix sniffing bug

* fix typo

* add gene modals

* client/src/actions/

* add autosave

* rename data-dir cli param

* add geneset, add gene, delete set

* prototype: remove csv upload placeholder

* handle delete gene from set

* prepopulate geneset with genes from modal

* add geneset: rename action

* icons, language consistency

* chevron after

* handle empty string case on genes for create geneset

* edit geneset

* fix language on create

* copy correction

* add popper2
upgrade react popper
upgrade react popper
adding popover2 package

* truncate uses tooltip2

* gene set button text typo

* remove logging

* moving server over

* remove test imports

* don't try to destructure map, use array.from

* fix add gene map datastructure error

* Revert "fix add gene map datastructure error"

This reverts commit b0eed45952.

* name --> genesetName, genes --> geneSymbols

* add gene to geneset, temporary format

* handle empty case, clear form input

* lint -- genesets wasn't passed via props

* userinfo

* move genes string to object conversion to action

* remove tmp gene description

* emptystring default for description

* remove empty string

* remove top level package json

* remove package lock as well

* remove flag for feature toggle

* remove comments in geneset

* comment cleanup

* remove comment

* revert diffexp genes to 10

* color by gene set

* disable color by gene set

* Gene menus are now inline, remove dead prototype code

* remove todo, magic number to variable

* remove jshint in rightsidebar

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>

* remove unused geneset validation code

* tmp format pending geneset description

* move magic number into variable

* reorganize genesetsUI reducer pending tests

* rewire edit given new action name

* add basic validation and feedback for geneset name uniqueness

* mv annoDialog

* mv label, repair paths

* Update client/src/components/brushableHistogram/header.js

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>

* add imports for icon in histo

* update jest snapshots given blueprint/tooltip2 usage of index -1

* ensure no empty paragraph

* intent from blueprint

* remove remainder of jshint references

* do not push undo when autosave fires

* fix autosave bugs

* remove todos

* clamp to util

* scient to util

* revert clearing diffexp

* rename value to be more specific stacked bar

* clean up logging and commetns

* remove gene entry tests pending rewrite

* tab index -1

* update jest snapshot, blueprint tooltip 2

* caret margin

* snapshot update

* ensure histogram is centered

* add geneset actions to config

* comment maybeScientific

* comment clamp

* comment ui reducer

* remove prototype code

* remove error log

* remove references to bl.ocks

* componetize parseBulkGeneString

* catch case where geneset rename same name

* genesetui reducer tests

* add geneset ui to index reducer config

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
This commit is contained in:
Colin Megill
2021-03-24 16:33:26 -04:00
committed by GitHub
co-authored by Severiano Badajoz bkmartinjr
parent 5335c39184
commit e6e358ddc8
66 changed files with 1926 additions and 905 deletions
@@ -1,84 +0,0 @@
import React from "react";
import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core";
class AnnoDialog extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
const {
isActive,
text,
title,
instruction,
cancelTooltipContent,
errorMessage,
validationError,
annoSelect,
annoInput,
handleCancel,
handleSubmit,
primaryButtonText,
secondaryButtonText,
handleSecondaryButtonSubmit,
primaryButtonProps,
} = this.props;
return (
<Dialog icon="tag" title={title} isOpen={isActive} onClose={handleCancel}>
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<div className={Classes.DIALOG_BODY}>
<div style={{ marginBottom: 20 }}>
<p>{instruction}</p>
{annoInput || null}
<p
style={{
marginTop: 7,
visibility: validationError ? "visible" : "hidden",
color: Colors.ORANGE3,
}}
>
{errorMessage}
</p>
</div>
{annoSelect || null}
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Tooltip content={cancelTooltipContent}>
<Button onClick={handleCancel}>Cancel</Button>
</Tooltip>
{handleSecondaryButtonSubmit && secondaryButtonText ? (
<Button
onClick={handleSecondaryButtonSubmit}
disabled={!text || validationError}
intent="none"
type="button"
>
{secondaryButtonText}
</Button>
) : null}
<Button
{...primaryButtonProps} // eslint-disable-line react/jsx-props-no-spreading -- Spreading props allows for modularity
onClick={handleSubmit}
disabled={!text || validationError}
intent="primary"
type="submit"
>
{primaryButtonText}
</Button>
</div>
</div>
</form>
</Dialog>
);
}
}
export default AnnoDialog;
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import AnnoDialog from "../annoDialog";
import LabelInput from "../labelInput";
import AnnoDialog from "../../annoDialog";
import LabelInput from "../../labelInput";
import { labelPrompt, isLabelErroneous } from "../labelUtil";
import actions from "../../../actions";
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import AnnoDialog from "../annoDialog";
import LabelInput from "../labelInput";
import AnnoDialog from "../../annoDialog";
import LabelInput from "../../labelInput";
import { labelPrompt } from "../labelUtil";
import { AnnotationsHelpers } from "../../../util/stateManager";
@@ -9,7 +9,9 @@ import {
Tooltip,
Icon,
PopoverInteractionKind,
Intent,
} from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../../globals";
import actions from "../../../actions";
@@ -89,8 +91,8 @@ class AnnoMenuCategory extends React.PureComponent {
text={editText}
/>
<MenuItem
icon="delete"
intent="danger"
icon={IconNames.TRASH}
intent={Intent.DANGER}
data-testclass="handleDeleteCategory"
data-testid={`${metadataField}:delete-category`}
onClick={this.handleDeleteCategory}
+31 -30
View File
@@ -1,13 +1,12 @@
// jshint esversion: 6
import React from "react";
import { AnchorButton, Tooltip, Position } from "@blueprintjs/core";
import { connect } from "react-redux";
import * as globals from "../../globals";
import Category from "./category";
import { AnnotationsHelpers, ControlsHelpers } from "../../util/stateManager";
import AnnoDialog from "./annoDialog";
import AnnoDialog from "../annoDialog";
import AnnoSelect from "./annoSelect";
import LabelInput from "./labelInput";
import LabelInput from "../labelInput";
import { labelPrompt } from "./labelUtil";
import actions from "../../actions";
@@ -182,6 +181,35 @@ class Categories extends React.Component {
}
/>
{writableCategoriesEnabled ? (
<div style={{ marginBottom: 10 }}>
<Tooltip
content={
userInfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
position={Position.RIGHT}
boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<AnchorButton
type="button"
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
disabled={!userInfo.is_authenticated}
>
Create new <strong>category</strong>
</AnchorButton>
</Tooltip>
</div>
) : null}
{/* READ ONLY CATEGORICAL FIELDS */}
{/* this is duplicative but flat, could be abstracted */}
{allCategoryNames.map((catName) =>
@@ -209,33 +237,6 @@ class Categories extends React.Component {
/>
) : null
)}
{writableCategoriesEnabled ? (
<Tooltip
content={
userInfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
position={Position.RIGHT}
boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<AnchorButton
type="button"
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
disabled={!userInfo.is_authenticated}
>
Create new category
</AnchorButton>
</Tooltip>
) : null}
</div>
);
}
@@ -1,178 +0,0 @@
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>
*/
/* maxinum number of suggestions */
static QueryResultLimit = 100;
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}
/>
);
};
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 --- Allows for modularity
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}
/>
</>
);
}
}
@@ -14,8 +14,8 @@ import {
} from "@blueprintjs/core";
import * as globals from "../../../globals";
import styles from "../categorical.css";
import AnnoDialog from "../annoDialog";
import LabelInput from "../labelInput";
import AnnoDialog from "../../annoDialog";
import LabelInput from "../../labelInput";
import Truncate from "../../util/truncate";
import { AnnotationsHelpers } from "../../../util/stateManager";
@@ -25,8 +25,8 @@ import MiniHistogram from "../../miniHistogram";
import MiniStackedBar from "../../miniStackedBar";
import { CategoryCrossfilterContext } from "../categoryContext";
const VALUE_HEIGHT = 11;
const CHART_WIDTH = 100;
const STACKED_BAR_HEIGHT = 11;
const STACKED_BAR_WIDTH = 100;
/* this is defined outside of the class so we can use it in connect() */
function _currentLabelAsString(ownProps) {
@@ -410,7 +410,7 @@ class CategoryValue extends React.Component {
label,
colorTable,
schema,
CHART_WIDTH
STACKED_BAR_WIDTH
) ?? {};
if (!domainValues || !scale || !domain || !occupancy) {
@@ -426,8 +426,8 @@ class CategoryValue extends React.Component {
domain,
occupancy,
}}
height={VALUE_HEIGHT}
width={CHART_WIDTH}
height={STACKED_BAR_HEIGHT}
width={STACKED_BAR_WIDTH}
/>
);
};
@@ -460,8 +460,8 @@ class CategoryValue extends React.Component {
colorAccessor,
colorData,
label,
CHART_WIDTH,
VALUE_HEIGHT
STACKED_BAR_WIDTH,
STACKED_BAR_HEIGHT
) ?? {}; // if createHistogramBins returns empty object assign null to deconstructed
if (!xScale || !yScale || !bins) return null;
@@ -476,8 +476,8 @@ class CategoryValue extends React.Component {
}}
obsOrVarContinuousFieldDisplayName={colorAccessor}
domainLabel={label}
height={VALUE_HEIGHT}
width={CHART_WIDTH}
height={STACKED_BAR_HEIGHT}
width={STACKED_BAR_WIDTH}
/>
);
};
@@ -534,7 +534,7 @@ class CategoryValue extends React.Component {
colorAccessor && !isColorBy
? globals.leftSidebarWidth -
otherElementsWidth -
CHART_WIDTH -
STACKED_BAR_WIDTH -
CHART_MARGIN
: globals.leftSidebarWidth - otherElementsWidth;
@@ -677,8 +677,8 @@ class CategoryValue extends React.Component {
display={isColorBy && categoryValueIndices ? "auto" : "none"}
style={{
marginLeft: 5,
width: VALUE_HEIGHT,
height: VALUE_HEIGHT,
width: 15,
height: 15,
backgroundColor:
isColorBy && categoryValueIndices
? colorScale(categoryValueIndices.get(label))
@@ -739,7 +739,7 @@ class CategoryValue extends React.Component {
) : null}
{displayString !== globals.unassignedCategoryLabel ? (
<MenuItem
icon="delete"
icon="trash"
intent="danger"
data-testclass="handleDeleteValue"
data-testid={`${metadataField}:${displayString}:delete-label`}
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import { connect } from "react-redux";
import * as d3 from "d3";