mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-24 09:38:12 +08:00
select and deselect
This commit is contained in:
+64
-7
@@ -1,23 +1,80 @@
|
||||
import * as globals from "../globals";
|
||||
import URI from "urijs";
|
||||
|
||||
const requestCells = (selectedMetadataField, selectedMetadataValue) => {
|
||||
|
||||
export const requestCells = (categoriesToSubsetBy) => {
|
||||
return (dispatch, getState) => {
|
||||
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",
|
||||
headers: new Headers({
|
||||
'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(
|
||||
data => dispatch({type: "request cells success", data}),
|
||||
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 Graph from "./graph/graph";
|
||||
import * as globals from "../globals";
|
||||
import * as actions from "../actions";
|
||||
import actions from "../actions";
|
||||
|
||||
import SectionHeader from "./framework/sectionHeader";
|
||||
|
||||
@@ -36,6 +36,7 @@ class App extends React.Component {
|
||||
window.addEventListener('popstate', this._onURLChanged);
|
||||
this._onURLChanged();
|
||||
|
||||
this.props.dispatch(actions.initialize())
|
||||
this.props.dispatch(actions.requestCells())
|
||||
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./categorical.css";
|
||||
import SectionHeader from "../framework/sectionHeader"
|
||||
import createCategoryCounts from "./createCategoryCounts";
|
||||
import Value from "./value";
|
||||
import { alphabeticallySortedValues } from "./util";
|
||||
|
||||
const Category = ({metadataField, values, toggleOn, toggleOff}) => (
|
||||
const Category = ({metadataField, values}) => (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
@@ -22,15 +21,15 @@ const Category = ({metadataField, values, toggleOn, toggleOff}) => (
|
||||
fontFamily: globals.accentFont,
|
||||
fontStyle: "italic",
|
||||
marginRight: 20,
|
||||
}}>{metadataField}:</p>
|
||||
}}>
|
||||
{metadataField}:
|
||||
</p>
|
||||
<div>
|
||||
{
|
||||
_.map(alphabeticallySortedValues(values), (v, i) => {
|
||||
return (
|
||||
<Value
|
||||
key={v}
|
||||
toggleOn={toggleOn}
|
||||
toggleOff={toggleOff}
|
||||
metadataField={metadataField}
|
||||
count={values[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 () {
|
||||
if (!this.props.ranges) return null
|
||||
return (
|
||||
@@ -88,8 +70,6 @@ class Categories extends React.Component {
|
||||
) {
|
||||
return (
|
||||
<Category
|
||||
toggleOn={this.toggleOn.bind(this)}
|
||||
toggleOff={this.toggleOff.bind(this)}
|
||||
key={key}
|
||||
metadataField={key}
|
||||
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> 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 React from "react";
|
||||
import * as globals from "../../globals";
|
||||
import actions from "../../actions";
|
||||
|
||||
@connect((state) => {
|
||||
return {
|
||||
@@ -9,11 +9,24 @@ import * as globals from "../../globals";
|
||||
}
|
||||
})
|
||||
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 () {
|
||||
|
||||
/* has this value for this category already been selected? */
|
||||
let selected = false;
|
||||
let fontWeight;
|
||||
|
||||
if (!this.props.selectedMetadata) { /* there is no selected metadata */
|
||||
selected = false;
|
||||
@@ -27,7 +40,7 @@ class CategoryValue extends React.Component {
|
||||
return (
|
||||
<div
|
||||
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={{
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
|
||||
@@ -28,7 +28,7 @@ class Continuous extends React.Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
ctx: null,
|
||||
|
||||
parallelExists: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,9 +36,10 @@ class Continuous extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
if (nextProps.ranges) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.ranges && !this.state.parallelExists) {
|
||||
drawParallelCoordinates(nextProps.metadata, nextProps.ranges)
|
||||
this.setState({parallelExists: true})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ const drawParallelCoordinates = (data, ranges) => {
|
||||
|
||||
const dimensions = createDimensions(ranges);
|
||||
|
||||
console.log('ranges', data)
|
||||
|
||||
const d3_functor = (v) => {
|
||||
return typeof v === "function" ? v : function() { return v; };
|
||||
};
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import uri from "urijs";
|
||||
|
||||
/*
|
||||
https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
|
||||
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
|
||||
*/
|
||||
|
||||
|
||||
const updateURLMiddleware = (store) => {
|
||||
return (next) => {
|
||||
return (action) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ const Cells = (state = {
|
||||
});
|
||||
case "request cells error":
|
||||
return Object.assign({}, state, {
|
||||
cells: null,
|
||||
loading: false,
|
||||
error: action.data
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { combineReducers, createStore, applyMiddleware } from 'redux';
|
||||
import updateURLMiddleware from "../middleware/updateURLMiddleware";
|
||||
import thunk from "redux-thunk";
|
||||
|
||||
import initialize from "./initialize";
|
||||
import cells from "./cells";
|
||||
import selectedMetadata from "./selectedMetadata";
|
||||
|
||||
const Reducer = combineReducers({
|
||||
initialize,
|
||||
cells,
|
||||
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,
|
||||
because if initial load or back button etc, we'll hear about that
|
||||
and manually reconstruct the state from the url, overriding whatever
|
||||
@@ -25,7 +25,7 @@ const selectedMetadata = (
|
||||
// console.log("onURLchange sees: ", state, parsed)
|
||||
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 */
|
||||
/* {} becomes {Location: ["Tumor"]} */
|
||||
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),
|
||||
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 */
|
||||
let updatedField = _.without(state[action.metadataField], action.value);
|
||||
/* if the array now looks like this {Location: []} */
|
||||
|
||||
Reference in New Issue
Block a user