mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-30 02:48:11 +08:00
improve type handling for non-string annotation data (#465)
* improve type handling for non-string annotation data * improve clarity of code
This commit is contained in:
committed by
Charlotte Weaver
parent
2b5094665f
commit
138d30909a
@@ -5,34 +5,13 @@ import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import Category from "./category";
|
||||
|
||||
/* Cap the max number of displayed categories */
|
||||
const truncateCategories = options => {
|
||||
const numOptions = _.size(options);
|
||||
if (numOptions <= globals.maxCategoricalOptionsToDisplay) {
|
||||
return options;
|
||||
}
|
||||
return _(options)
|
||||
.map((v, k) => ({ name: k, val: v }))
|
||||
.sortBy("val")
|
||||
.slice(numOptions - globals.maxCategoricalOptionsToDisplay)
|
||||
.transform((r, v) => {
|
||||
r[v.name] = v.val;
|
||||
}, {})
|
||||
.value();
|
||||
};
|
||||
|
||||
@connect(state => ({
|
||||
ranges: _.get(state.controls.world, "summary.obs", null),
|
||||
categorySelectionLimit: _.get(
|
||||
state.config,
|
||||
"parameters.max-category-items",
|
||||
globals.configDefaults.parameters["max-category-items"]
|
||||
)
|
||||
categoricalSelectionState: state.controls.categoricalSelectionState
|
||||
}))
|
||||
class Categories extends React.Component {
|
||||
render() {
|
||||
const { ranges, categorySelectionLimit } = this.props;
|
||||
if (!ranges) return null;
|
||||
const { categoricalSelectionState } = this.props;
|
||||
if (!categoricalSelectionState) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -47,27 +26,9 @@ class Categories extends React.Component {
|
||||
>
|
||||
Categorical Metadata
|
||||
</p>
|
||||
{_.map(ranges, (value, key) => {
|
||||
const isColorField = key.includes("color") || key.includes("Color");
|
||||
const isSelectableCategory =
|
||||
value.options &&
|
||||
!isColorField &&
|
||||
key !== "name" &&
|
||||
value.numOptions < categorySelectionLimit;
|
||||
|
||||
if (isSelectableCategory) {
|
||||
const categoryOptions = truncateCategories(value.options);
|
||||
return (
|
||||
<Category
|
||||
key={key}
|
||||
metadataField={key}
|
||||
values={categoryOptions}
|
||||
isTruncated={categoryOptions !== value.options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
})}
|
||||
{_.map(categoricalSelectionState, (catState, catName) => (
|
||||
<Category key={catName} metadataField={catName} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,29 +2,15 @@ import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
||||
import memoize from "memoize-one";
|
||||
import { Button, Tooltip, Position } from "@blueprintjs/core";
|
||||
import { Button, Tooltip } from "@blueprintjs/core";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import Value from "./value";
|
||||
import alphabeticallySortedValues from "./util";
|
||||
|
||||
const countCategories = (values, optsAsBools) =>
|
||||
_.reduce(
|
||||
values,
|
||||
(r, v, k) => {
|
||||
r.total += 1;
|
||||
if (optsAsBools[k]) {
|
||||
r.on += 1;
|
||||
}
|
||||
return r;
|
||||
},
|
||||
{ total: 0, on: 0 }
|
||||
);
|
||||
|
||||
@connect(state => ({
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap
|
||||
categoricalSelectionState: state.controls.categoricalSelectionState
|
||||
}))
|
||||
class Category extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -33,24 +19,30 @@ class Category extends React.Component {
|
||||
isChecked: true,
|
||||
isExpanded: false
|
||||
};
|
||||
this.countCategories = memoize((values, optsAsBools) =>
|
||||
countCategories(values, optsAsBools)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { categoricalAsBooleansMap, metadataField, values } = this.props;
|
||||
const categoryCount = this.countCategories(
|
||||
values,
|
||||
categoricalAsBooleansMap[metadataField]
|
||||
);
|
||||
if (categoryCount.on === categoryCount.total) {
|
||||
const { categoricalSelectionState, metadataField } = this.props;
|
||||
const cat = categoricalSelectionState[metadataField];
|
||||
const categoryCount = {
|
||||
// total number of options in this category
|
||||
totalOptionCount: cat.numOptions,
|
||||
// number of selected options in this category
|
||||
selectedOptionCount: _.reduce(
|
||||
cat.optionSelected,
|
||||
(res, cond) => (cond ? res + 1 : res),
|
||||
0
|
||||
)
|
||||
};
|
||||
if (categoryCount.selectedOptionCount === categoryCount.totalOptionCount) {
|
||||
/* everything is on, so not indeterminate */
|
||||
this.checkbox.indeterminate = false;
|
||||
} else if (categoryCount.on === 0) {
|
||||
} else if (categoryCount.selectedOptionCount === 0) {
|
||||
/* nothing is on, so no */
|
||||
this.checkbox.indeterminate = false;
|
||||
} else if (categoryCount.on < categoryCount.total) {
|
||||
} else if (
|
||||
categoryCount.selectedOptionCount < categoryCount.totalOptionCount
|
||||
) {
|
||||
/* to be explicit... */
|
||||
this.checkbox.indeterminate = true;
|
||||
}
|
||||
@@ -74,11 +66,10 @@ class Category extends React.Component {
|
||||
}
|
||||
|
||||
toggleNone() {
|
||||
const { dispatch, metadataField, value } = this.props;
|
||||
const { dispatch, metadataField } = this.props;
|
||||
dispatch({
|
||||
type: "categorical metadata filter none of these",
|
||||
metadataField,
|
||||
value
|
||||
metadataField
|
||||
});
|
||||
this.setState({ isChecked: false });
|
||||
}
|
||||
@@ -94,13 +85,15 @@ class Category extends React.Component {
|
||||
}
|
||||
|
||||
renderCategoryItems() {
|
||||
const { values, metadataField } = this.props;
|
||||
return _.map(alphabeticallySortedValues(values), (v, i) => (
|
||||
const { categoricalSelectionState, metadataField } = this.props;
|
||||
|
||||
const cat = categoricalSelectionState[metadataField];
|
||||
const optTuples = alphabeticallySortedValues([...cat.optionIndex]);
|
||||
return _.map(optTuples, (tuple, i) => (
|
||||
<Value
|
||||
key={v}
|
||||
key={tuple[1]}
|
||||
metadataField={metadataField}
|
||||
count={values[v]}
|
||||
value={v}
|
||||
optionIndex={tuple[1]}
|
||||
i={i}
|
||||
/>
|
||||
));
|
||||
@@ -108,12 +101,15 @@ class Category extends React.Component {
|
||||
|
||||
render() {
|
||||
const { isExpanded, isChecked } = this.state;
|
||||
const { metadataField, colorAccessor, isTruncated } = this.props;
|
||||
const {
|
||||
metadataField,
|
||||
colorAccessor,
|
||||
categoricalSelectionState
|
||||
} = this.props;
|
||||
const { isTruncated } = categoricalSelectionState[metadataField];
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
// display: "flex",
|
||||
// alignItems: "baseline",
|
||||
maxWidth: globals.maxControlsWidth
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
// values is [ [optVal, optIdx], ...]
|
||||
// index is range array
|
||||
// return sorted index
|
||||
export default values =>
|
||||
Object.keys(values).sort((a, b) => {
|
||||
const textA = a.toUpperCase();
|
||||
const textB = b.toUpperCase();
|
||||
values.sort((a, b) => {
|
||||
const textA = String(a[0]).toUpperCase();
|
||||
const textB = String(b[0]).toUpperCase();
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||
});
|
||||
|
||||
@@ -4,45 +4,49 @@ import React from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
@connect(state => ({
|
||||
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap,
|
||||
categoricalSelectionState: state.controls.categoricalSelectionState,
|
||||
colorScale: state.controls.colorScale,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
schema: _.get(state.controls.world, "schema", null)
|
||||
}))
|
||||
class CategoryValue extends React.Component {
|
||||
toggleOff() {
|
||||
const { dispatch, metadataField, value } = this.props;
|
||||
const { dispatch, metadataField, optionIndex } = this.props;
|
||||
dispatch({
|
||||
type: "categorical metadata filter deselect",
|
||||
metadataField,
|
||||
value
|
||||
optionIndex
|
||||
});
|
||||
}
|
||||
|
||||
toggleOn() {
|
||||
const { dispatch, metadataField, value } = this.props;
|
||||
const { dispatch, metadataField, optionIndex } = this.props;
|
||||
dispatch({
|
||||
type: "categorical metadata filter select",
|
||||
metadataField,
|
||||
value
|
||||
optionIndex
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
categoricalAsBooleansMap,
|
||||
categoricalSelectionState,
|
||||
metadataField,
|
||||
count,
|
||||
value,
|
||||
optionIndex,
|
||||
colorAccessor,
|
||||
colorScale,
|
||||
i,
|
||||
schema
|
||||
} = this.props;
|
||||
|
||||
if (!categoricalAsBooleansMap) return null;
|
||||
if (!categoricalSelectionState) return null;
|
||||
|
||||
const category = categoricalSelectionState[metadataField];
|
||||
const selected = category.optionSelected[optionIndex];
|
||||
const count = category.optionCount[optionIndex];
|
||||
const value = category.optionValue[optionIndex];
|
||||
const displayString = String(category.optionValue[optionIndex]).valueOf();
|
||||
|
||||
const selected = categoricalAsBooleansMap[metadataField][value];
|
||||
/* this is the color scale, so add swatches below */
|
||||
const c = metadataField === colorAccessor;
|
||||
let categories = null;
|
||||
@@ -78,7 +82,7 @@ class CategoryValue extends React.Component {
|
||||
type="checkbox"
|
||||
/>
|
||||
<span className="bp3-control-indicator" />
|
||||
{value}
|
||||
{displayString}
|
||||
</label>
|
||||
</div>
|
||||
<span>
|
||||
|
||||
Reference in New Issue
Block a user