mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 19:48:11 +08:00
select and deselect
This commit is contained in:
+64
-7
@@ -1,23 +1,80 @@
|
|||||||
import * as globals from "../globals";
|
import * as globals from "../globals";
|
||||||
|
import URI from "urijs";
|
||||||
|
|
||||||
|
const requestCells = (selectedMetadataField, selectedMetadataValue) => {
|
||||||
|
|
||||||
export const requestCells = (categoriesToSubsetBy) => {
|
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({type: "request cells started"})
|
dispatch({type: "request cells started"})
|
||||||
fetch(`${globals.API.prefix}${globals.API.version}cells`, {
|
|
||||||
|
let uri = new URI()
|
||||||
|
uri.setSearch(getState().selectedMetadata)
|
||||||
|
if (selectedMetadataField) {
|
||||||
|
uri.addSearch({[selectedMetadataField]: [selectedMetadataValue]})
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`${globals.API.prefix}${globals.API.version}cells${uri.search()}`, {
|
||||||
method: "get",
|
method: "get",
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}),
|
})
|
||||||
// body: JSON.stringify({
|
|
||||||
// // "celllist": ["1001000173.G8", "1001000173.D4"],
|
|
||||||
// "genelist": ["1/2-SBSRNA4", "A1BG", "A1BG-AS1", "A1CF", "A2LD1", "A2M", "A2ML1", "A2MP1", "A4GALT"]
|
|
||||||
// })
|
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(
|
.then(
|
||||||
data => dispatch({type: "request cells success", data}),
|
data => dispatch({type: "request cells success", data}),
|
||||||
error => dispatch({type: "request cells error", error})
|
error => dispatch({type: "request cells error", error})
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SELECT */
|
||||||
|
const attemptCategoricalMetadataSelection = (metadataField, value) => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({type: "categorical metadata filter selected start"})
|
||||||
|
dispatch(requestCells(metadataField, value)).then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
dispatch({type: "categorical metadata filter selected error"})
|
||||||
|
} else {
|
||||||
|
dispatch({type: "categorical metadata filter selected success", metadataField, value})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DESELECT */
|
||||||
|
const attemptCategoricalMetadataDeselection = (metadataField, value) => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({type: "categorical metadata filter deselected start"})
|
||||||
|
dispatch(requestCells(metadataField, value)).then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
dispatch({type: "categorical metadata filter deselected error"})
|
||||||
|
} else {
|
||||||
|
dispatch({type: "categorical metadata filter deselected success", metadataField, value})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialize = () => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({type: "initialize started"})
|
||||||
|
fetch(`${globals.API.prefix}${globals.API.version}cells`, {
|
||||||
|
method: "get",
|
||||||
|
headers: new Headers({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(
|
||||||
|
data => dispatch({type: "initialize success", data}),
|
||||||
|
error => dispatch({type: "initialize error", error})
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initialize,
|
||||||
|
requestCells,
|
||||||
|
attemptCategoricalMetadataSelection,
|
||||||
|
attemptCategoricalMetadataDeselection
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Continuous from "./continuous/continuous";
|
|||||||
import Joy from "./joy/joy";
|
import Joy from "./joy/joy";
|
||||||
import Graph from "./graph/graph";
|
import Graph from "./graph/graph";
|
||||||
import * as globals from "../globals";
|
import * as globals from "../globals";
|
||||||
import * as actions from "../actions";
|
import actions from "../actions";
|
||||||
|
|
||||||
import SectionHeader from "./framework/sectionHeader";
|
import SectionHeader from "./framework/sectionHeader";
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ class App extends React.Component {
|
|||||||
window.addEventListener('popstate', this._onURLChanged);
|
window.addEventListener('popstate', this._onURLChanged);
|
||||||
this._onURLChanged();
|
this._onURLChanged();
|
||||||
|
|
||||||
|
this.props.dispatch(actions.initialize())
|
||||||
this.props.dispatch(actions.requestCells())
|
this.props.dispatch(actions.requestCells())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import { connect } from "react-redux";
|
|||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import styles from "./categorical.css";
|
import styles from "./categorical.css";
|
||||||
import SectionHeader from "../framework/sectionHeader"
|
import SectionHeader from "../framework/sectionHeader"
|
||||||
import createCategoryCounts from "./createCategoryCounts";
|
|
||||||
import Value from "./value";
|
import Value from "./value";
|
||||||
import { alphabeticallySortedValues } from "./util";
|
import { alphabeticallySortedValues } from "./util";
|
||||||
|
|
||||||
const Category = ({metadataField, values, toggleOn, toggleOff}) => (
|
const Category = ({metadataField, values}) => (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "baseline",
|
alignItems: "baseline",
|
||||||
@@ -22,15 +21,15 @@ const Category = ({metadataField, values, toggleOn, toggleOff}) => (
|
|||||||
fontFamily: globals.accentFont,
|
fontFamily: globals.accentFont,
|
||||||
fontStyle: "italic",
|
fontStyle: "italic",
|
||||||
marginRight: 20,
|
marginRight: 20,
|
||||||
}}>{metadataField}:</p>
|
}}>
|
||||||
|
{metadataField}:
|
||||||
|
</p>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
_.map(alphabeticallySortedValues(values), (v, i) => {
|
_.map(alphabeticallySortedValues(values), (v, i) => {
|
||||||
return (
|
return (
|
||||||
<Value
|
<Value
|
||||||
key={v}
|
key={v}
|
||||||
toggleOn={toggleOn}
|
|
||||||
toggleOff={toggleOff}
|
|
||||||
metadataField={metadataField}
|
metadataField={metadataField}
|
||||||
count={values[v]}
|
count={values[v]}
|
||||||
value={v}
|
value={v}
|
||||||
@@ -57,24 +56,7 @@ class Categories extends React.Component {
|
|||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
toggleOn(metadataField, value) {
|
|
||||||
return () => {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: "categorical metadata filter selected",
|
|
||||||
metadataField,
|
|
||||||
value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleOff(metadataField, value) {
|
|
||||||
return () => {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: "categorical metadata filter deselected",
|
|
||||||
metadataField,
|
|
||||||
value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render () {
|
render () {
|
||||||
if (!this.props.ranges) return null
|
if (!this.props.ranges) return null
|
||||||
return (
|
return (
|
||||||
@@ -88,8 +70,6 @@ class Categories extends React.Component {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Category
|
<Category
|
||||||
toggleOn={this.toggleOn.bind(this)}
|
|
||||||
toggleOff={this.toggleOff.bind(this)}
|
|
||||||
key={key}
|
key={key}
|
||||||
metadataField={key}
|
metadataField={key}
|
||||||
values={value.options}/>
|
values={value.options}/>
|
||||||
@@ -118,6 +98,8 @@ export default Categories;
|
|||||||
<p> <button> Field name [total] [selected in current filters] [selected in graph selection] </button> </p>
|
<p> <button> Field name [total] [selected in current filters] [selected in graph selection] </button> </p>
|
||||||
<p> <button> Tumor [400] [40] [4] </button> </p>
|
<p> <button> Tumor [400] [40] [4] </button> </p>
|
||||||
|
|
||||||
|
might be interesting to show them as an 👁 icon, or with a slash through it, to allow for visible or hidden state
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import * as globals from "../../globals.js"
|
|
||||||
|
|
||||||
/*
|
|
||||||
end result:
|
|
||||||
|
|
||||||
counts: {
|
|
||||||
Sample.type: {
|
|
||||||
glioblastoma: 3452
|
|
||||||
},
|
|
||||||
Selection: {
|
|
||||||
foo: 234,
|
|
||||||
bar: 21
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const categories = globals.categories;
|
|
||||||
|
|
||||||
const createCategoryCounts = (cells) => {
|
|
||||||
|
|
||||||
/* instantiate counts obj */
|
|
||||||
const counts = {};
|
|
||||||
|
|
||||||
categories.forEach((category) => {
|
|
||||||
counts[category] = {};
|
|
||||||
})
|
|
||||||
|
|
||||||
cells.forEach((cell) => {
|
|
||||||
categories.forEach((category) => {
|
|
||||||
|
|
||||||
/* if, for the given category (ie., categories.Location), we do not have ie., glioblastoma already, create it. Otherwise increment it. */
|
|
||||||
if (!counts[category][cell[category]]) {
|
|
||||||
counts[category][cell[category]] = 1;
|
|
||||||
} else {
|
|
||||||
counts[category][cell[category]]++
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
return counts;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createCategoryCounts;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
|
import actions from "../../actions";
|
||||||
|
|
||||||
@connect((state) => {
|
@connect((state) => {
|
||||||
return {
|
return {
|
||||||
@@ -9,11 +9,24 @@ import * as globals from "../../globals";
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
class CategoryValue extends React.Component {
|
class CategoryValue extends React.Component {
|
||||||
|
toggleOn() {
|
||||||
|
this.props.dispatch(
|
||||||
|
actions.attemptCategoricalMetadataSelection(
|
||||||
|
this.props.metadataField,
|
||||||
|
this.props.value
|
||||||
|
))
|
||||||
|
}
|
||||||
|
toggleOff() {
|
||||||
|
this.props.dispatch(
|
||||||
|
actions.attemptCategoricalMetadataDeselection(
|
||||||
|
this.props.metadataField,
|
||||||
|
this.props.value
|
||||||
|
))
|
||||||
|
}
|
||||||
render () {
|
render () {
|
||||||
|
|
||||||
/* has this value for this category already been selected? */
|
/* has this value for this category already been selected? */
|
||||||
let selected = false;
|
let selected = false;
|
||||||
let fontWeight;
|
|
||||||
|
|
||||||
if (!this.props.selectedMetadata) { /* there is no selected metadata */
|
if (!this.props.selectedMetadata) { /* there is no selected metadata */
|
||||||
selected = false;
|
selected = false;
|
||||||
@@ -27,7 +40,7 @@ class CategoryValue extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={this.props.i}
|
key={this.props.i}
|
||||||
onClick={selected ? this.props.toggleOff(this.props.metadataField, this.props.value) : this.props.toggleOn(this.props.metadataField, this.props.value)}
|
onClick={selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)}
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class Continuous extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
ctx: null,
|
ctx: null,
|
||||||
|
parallelExists: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +36,10 @@ class Continuous extends React.Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.ranges) {
|
if (nextProps.ranges && !this.state.parallelExists) {
|
||||||
drawParallelCoordinates(nextProps.metadata, nextProps.ranges)
|
drawParallelCoordinates(nextProps.metadata, nextProps.ranges)
|
||||||
|
this.setState({parallelExists: true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ const drawParallelCoordinates = (data, ranges) => {
|
|||||||
|
|
||||||
const dimensions = createDimensions(ranges);
|
const dimensions = createDimensions(ranges);
|
||||||
|
|
||||||
console.log('ranges', data)
|
|
||||||
|
|
||||||
const d3_functor = (v) => {
|
const d3_functor = (v) => {
|
||||||
return typeof v === "function" ? v : function() { return v; };
|
return typeof v === "function" ? v : function() { return v; };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import uri from "urijs";
|
import uri from "urijs";
|
||||||
|
|
||||||
|
/*
|
||||||
|
https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
|
||||||
|
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
const updateURLMiddleware = (store) => {
|
const updateURLMiddleware = (store) => {
|
||||||
return (next) => {
|
return (next) => {
|
||||||
return (action) => {
|
return (action) => {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const Cells = (state = {
|
|||||||
});
|
});
|
||||||
case "request cells error":
|
case "request cells error":
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
cells: null,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
error: action.data
|
error: action.data
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { combineReducers, createStore, applyMiddleware } from 'redux';
|
import { combineReducers, createStore, applyMiddleware } from 'redux';
|
||||||
import updateURLMiddleware from "../middleware/updateURLMiddleware";
|
import updateURLMiddleware from "../middleware/updateURLMiddleware";
|
||||||
import thunk from "redux-thunk";
|
import thunk from "redux-thunk";
|
||||||
|
|
||||||
|
import initialize from "./initialize";
|
||||||
import cells from "./cells";
|
import cells from "./cells";
|
||||||
import selectedMetadata from "./selectedMetadata";
|
import selectedMetadata from "./selectedMetadata";
|
||||||
|
|
||||||
const Reducer = combineReducers({
|
const Reducer = combineReducers({
|
||||||
|
initialize,
|
||||||
cells,
|
cells,
|
||||||
selectedMetadata,
|
selectedMetadata,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const Initialize = (state = {
|
||||||
|
data: null,
|
||||||
|
loading: null,
|
||||||
|
error: null,
|
||||||
|
}, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "initialize started":
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
case "initialize success":
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
data: action.data,
|
||||||
|
});
|
||||||
|
case "initialize error":
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
data: null,
|
||||||
|
loading: false,
|
||||||
|
error: action.data
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Initialize;
|
||||||
@@ -3,7 +3,7 @@ import _ from "lodash";
|
|||||||
|
|
||||||
/************************************************************************
|
/************************************************************************
|
||||||
*************************************************************************
|
*************************************************************************
|
||||||
1. This is all of the state that will be stored in the URL query params.
|
1. This is state that will be stored in the URL query params.
|
||||||
1a. It is reasonably important it be kept in the same place,
|
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
|
because if initial load or back button etc, we'll hear about that
|
||||||
and manually reconstruct the state from the url, overriding whatever
|
and manually reconstruct the state from the url, overriding whatever
|
||||||
@@ -25,7 +25,7 @@ const selectedMetadata = (
|
|||||||
// console.log("onURLchange sees: ", state, parsed)
|
// console.log("onURLchange sees: ", state, parsed)
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
case "categorical metadata filter selected": {
|
case "categorical metadata filter selected success": {
|
||||||
if (!state[action.metadataField]) { /* we don't have the field, which means this is a simple insertion */
|
if (!state[action.metadataField]) { /* we don't have the field, which means this is a simple insertion */
|
||||||
/* {} becomes {Location: ["Tumor"]} */
|
/* {} becomes {Location: ["Tumor"]} */
|
||||||
return Object.assign({}, state, {[action.metadataField]: [action.value]});
|
return Object.assign({}, state, {[action.metadataField]: [action.value]});
|
||||||
@@ -43,7 +43,7 @@ const selectedMetadata = (
|
|||||||
this the category button is selected when it's clicked (it's bold, etc),
|
this the category button is selected when it's clicked (it's bold, etc),
|
||||||
so we can fire a different action to save us some nesting logic here
|
so we can fire a different action to save us some nesting logic here
|
||||||
*/
|
*/
|
||||||
case "categorical metadata filter deselected": {
|
case "categorical metadata filter deselected success": {
|
||||||
/* remove the value the user just selected from the appropriate array */
|
/* remove the value the user just selected from the appropriate array */
|
||||||
let updatedField = _.without(state[action.metadataField], action.value);
|
let updatedField = _.without(state[action.metadataField], action.value);
|
||||||
/* if the array now looks like this {Location: []} */
|
/* if the array now looks like this {Location: []} */
|
||||||
|
|||||||
Reference in New Issue
Block a user