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 * 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
}
+2 -1
View File
@@ -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())
} }
+7 -25
View File
@@ -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;
+16 -3
View File
@@ -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",
+4 -3
View File
@@ -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; };
}; };
+6
View File
@@ -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) => {
-1
View File
@@ -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
}); });
+3
View File
@@ -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,
}) })
+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, 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: []} */