mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-02 15:58:11 +08:00
categorical metadata selectable
This commit is contained in:
@@ -8,38 +8,49 @@ import SectionHeader from "../framework/sectionHeader"
|
|||||||
import createCategoryCounts from "./createCategoryCounts";
|
import createCategoryCounts from "./createCategoryCounts";
|
||||||
import { alphabeticallySortedValues } from "./util";
|
import { alphabeticallySortedValues } from "./util";
|
||||||
|
|
||||||
|
@connect((state) => {
|
||||||
|
return {
|
||||||
|
selectedMetadata: state.selectedMetadata
|
||||||
|
}
|
||||||
|
})
|
||||||
class Button extends React.Component {
|
class Button extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
|
|
||||||
|
/* has this value for this category already been selected? */
|
||||||
|
let fontWeight;
|
||||||
|
if (!this.props.selectedMetadata) { /* there is no selected metadata */
|
||||||
|
fontWeight = 400;
|
||||||
|
} else if (
|
||||||
|
this.props.selectedMetadata[this.props.metadataField] && /* the key {Location: []} is present */
|
||||||
|
this.props.selectedMetadata[this.props.metadataField].indexOf(this.props.value) > -1 /* "Tumor" exists in {Location: ["Tumor"]} */
|
||||||
|
) {
|
||||||
|
fontWeight = 700
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={this.props.i}
|
key={this.props.i}
|
||||||
onClick={this.props.handleClick(this.props.category, this.props.value)}
|
onClick={this.props.handleClick(this.props.metadataField, this.props.value)}
|
||||||
style={{
|
style={{
|
||||||
border: "none",
|
|
||||||
background: "none",
|
|
||||||
color: "black",
|
|
||||||
marginRight: 3,
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
fontWeight,
|
||||||
}}>
|
}}>
|
||||||
{this.props.value}
|
<div style={{
|
||||||
<span
|
width: 200,
|
||||||
style={{
|
flexShrink: 0,
|
||||||
fontFamily: globals.accentFont,
|
|
||||||
fontStyle: "italic",
|
|
||||||
color: globals.darkGrey,
|
|
||||||
fontSize: globals.tiniestFontSize,
|
|
||||||
marginLeft: 4,
|
|
||||||
position: "relative",
|
|
||||||
top: -1,
|
|
||||||
}}>
|
}}>
|
||||||
|
{this.props.value}
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
{this.props.count}
|
{this.props.count}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Category = ({category, values, handleClick}) => (
|
const Category = ({metadataField, values, handleClick}) => (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "baseline",
|
alignItems: "baseline",
|
||||||
@@ -52,7 +63,7 @@ const Category = ({category, values, handleClick}) => (
|
|||||||
fontFamily: globals.accentFont,
|
fontFamily: globals.accentFont,
|
||||||
fontStyle: "italic",
|
fontStyle: "italic",
|
||||||
marginRight: 20,
|
marginRight: 20,
|
||||||
}}>{category}:</p>
|
}}>{metadataField}:</p>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
_.map(alphabeticallySortedValues(values), (v, i) => {
|
_.map(alphabeticallySortedValues(values), (v, i) => {
|
||||||
@@ -60,7 +71,7 @@ const Category = ({category, values, handleClick}) => (
|
|||||||
<Button
|
<Button
|
||||||
key={v}
|
key={v}
|
||||||
handleClick={handleClick}
|
handleClick={handleClick}
|
||||||
category={category}
|
metadataField={metadataField}
|
||||||
count={values[v]}
|
count={values[v]}
|
||||||
value={v}
|
value={v}
|
||||||
i={i} />
|
i={i} />
|
||||||
@@ -73,17 +84,10 @@ const Category = ({category, values, handleClick}) => (
|
|||||||
|
|
||||||
@connect((state) => {
|
@connect((state) => {
|
||||||
|
|
||||||
let ranges = null;
|
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null;
|
||||||
|
|
||||||
if (
|
|
||||||
state.cells.cells &&
|
|
||||||
state.cells.cells.data.ranges
|
|
||||||
) {
|
|
||||||
ranges = state.cells.cells.data.ranges;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ranges: ranges
|
ranges
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
class Categories extends React.Component {
|
class Categories extends React.Component {
|
||||||
@@ -93,9 +97,9 @@ class Categories extends React.Component {
|
|||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
handleClick(category, value) {
|
handleClick(metadataField, value) {
|
||||||
return () => {
|
return () => {
|
||||||
this.props.dispatch({type: "category changed", data: {category, value}})
|
this.props.dispatch({type: "categorical metadata filter selected", metadataField, value})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
@@ -113,7 +117,7 @@ class Categories extends React.Component {
|
|||||||
<Category
|
<Category
|
||||||
handleClick={this.handleClick.bind(this)}
|
handleClick={this.handleClick.bind(this)}
|
||||||
key={key}
|
key={key}
|
||||||
category={key}
|
metadataField={key}
|
||||||
values={value.options}/>
|
values={value.options}/>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import uri from "urijs";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
/************************************************************************
|
||||||
|
*************************************************************************
|
||||||
|
1. This is all of the state that will be stored in the URL query params.
|
||||||
|
1a. It is reasonably important it be kept in the same place,
|
||||||
|
because if initial load or back button etc, we'll hear about that
|
||||||
|
and manually reconstruct the state from the url, overriding whatever
|
||||||
|
we had. We could of course listen for the "url changed" type in another
|
||||||
|
reducer, if this gets messy.
|
||||||
|
1b. The data structures used here are constrained as well, as urijs
|
||||||
|
needs to be able to parse them, if arrays: https://hackernoon.com/redux-patterns-add-edit-remove-objects-in-an-array-6ee70cab2456
|
||||||
|
*************************************************************************
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
const selectedMetadata = (
|
||||||
|
state = {},
|
||||||
|
action
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "url changed": {
|
||||||
|
const {url} = action;
|
||||||
|
const parsed = uri.parseQuery(window.location.search);
|
||||||
|
// console.log("onURLchange sees: ", state, parsed)
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
case "categorical metadata filter selected": {
|
||||||
|
if (!state[action.metadataField]) { /* we don't have the field, which means this is a simple insertion */
|
||||||
|
/* {} becomes {Location: ["Tumor"]} */
|
||||||
|
return Object.assign({}, state, {[action.metadataField]: [action.value]});
|
||||||
|
} else { /* we do have the field, so we need to push to a copy of the array */
|
||||||
|
const updatedField = state[action.metadataField].slice()
|
||||||
|
updatedField.push(action.value)
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
/* {Location: ["Tumor"]} becomes {Location: ["Tumor", "Periphery"]} */
|
||||||
|
[action.metadataField]: updatedField
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "categorical metadata filter deselected": {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default selectedMetadata;
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import uri from "urijs";
|
|
||||||
|
|
||||||
/************************************************************************
|
|
||||||
*************************************************************************
|
|
||||||
1. This is all of the state that will be stored in the URL query params.
|
|
||||||
1a. It is reasonably important it be kept in the same place,
|
|
||||||
because if initial load or back button etc, we'll hear about that
|
|
||||||
and manually reconstruct the state from the url, overriding whatever
|
|
||||||
we had. We could of course listen for the "url changed" type in another
|
|
||||||
reducer, if this gets messy.
|
|
||||||
1b. The data structures used here are constrained as well, as urijs
|
|
||||||
needs to be able to parse them
|
|
||||||
*************************************************************************
|
|
||||||
************************************************************************/
|
|
||||||
|
|
||||||
const url = (
|
|
||||||
state = {
|
|
||||||
selectedMetadata: {}
|
|
||||||
},
|
|
||||||
action
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "url changed": {
|
|
||||||
const {url} = action;
|
|
||||||
|
|
||||||
// const parsed = uri.parseQuery(window.location.search);
|
|
||||||
|
|
||||||
// console.log("onURLchange sees: ", state, parsed)
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
case "category changed": {
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default url;
|
|
||||||
Reference in New Issue
Block a user