select and deselect

This commit is contained in:
Colin Megill
2017-11-02 06:34:50 -07:00
parent 764f082d24
commit b5f068abf9
12 changed files with 134 additions and 90 deletions
+64 -7
View File
@@ -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
}
+2 -1
View File
@@ -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())
}
+7 -25
View File
@@ -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;
+16 -3
View File
@@ -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",
+4 -3
View File
@@ -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; };
};
+6
View File
@@ -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) => {
-1
View File
@@ -17,7 +17,6 @@ const Cells = (state = {
});
case "request cells error":
return Object.assign({}, state, {
cells: null,
loading: false,
error: action.data
});
+3
View File
@@ -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,
})
+29
View File
@@ -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 -3
View File
@@ -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: []} */