From b5f068abf9cd16beda6b5f788f6aa852e036077d Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Thu, 2 Nov 2017 06:34:50 -0700 Subject: [PATCH] select and deselect --- src/actions/index.js | 71 +++++++++++++++++-- src/components/app.js | 3 +- src/components/categorical/categorical.js | 32 ++------- .../categorical/createCategoryCounts.js | 45 ------------ src/components/categorical/value.js | 19 ++++- src/components/continuous/continuous.js | 7 +- .../continuous/drawParallelCoordinates.js | 2 - src/middleware/updateURLMiddleware.js | 6 ++ src/reducers/cells.js | 1 - src/reducers/index.js | 3 + src/reducers/initialize.js | 29 ++++++++ src/reducers/selectedMetadata.js | 6 +- 12 files changed, 134 insertions(+), 90 deletions(-) delete mode 100644 src/components/categorical/createCategoryCounts.js create mode 100644 src/reducers/initialize.js diff --git a/src/actions/index.js b/src/actions/index.js index b15e0401..e2b5b1e3 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -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 +} diff --git a/src/components/app.js b/src/components/app.js index 2585c349..c18cd873 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -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()) } diff --git a/src/components/categorical/categorical.js b/src/components/categorical/categorical.js index 1fdc964a..d7918068 100644 --- a/src/components/categorical/categorical.js +++ b/src/components/categorical/categorical.js @@ -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}) => (
( fontFamily: globals.accentFont, fontStyle: "italic", marginRight: 20, - }}>{metadataField}:

+ }}> + {metadataField}: +

{ _.map(alphabeticallySortedValues(values), (v, i) => { 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 ( @@ -118,6 +98,8 @@ export default Categories;

+ might be interesting to show them as an 👁 icon, or with a slash through it, to allow for visible or hidden state + */ diff --git a/src/components/categorical/createCategoryCounts.js b/src/components/categorical/createCategoryCounts.js deleted file mode 100644 index e70c049a..00000000 --- a/src/components/categorical/createCategoryCounts.js +++ /dev/null @@ -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; diff --git a/src/components/categorical/value.js b/src/components/categorical/value.js index 3cd0cc52..2b3f75e6 100644 --- a/src/components/categorical/value.js +++ b/src/components/categorical/value.js @@ -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 (
{ const dimensions = createDimensions(ranges); - console.log('ranges', data) - const d3_functor = (v) => { return typeof v === "function" ? v : function() { return v; }; }; diff --git a/src/middleware/updateURLMiddleware.js b/src/middleware/updateURLMiddleware.js index 432b79ce..f6982749 100644 --- a/src/middleware/updateURLMiddleware.js +++ b/src/middleware/updateURLMiddleware.js @@ -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) => { diff --git a/src/reducers/cells.js b/src/reducers/cells.js index de5c6d5f..ce85a66c 100644 --- a/src/reducers/cells.js +++ b/src/reducers/cells.js @@ -17,7 +17,6 @@ const Cells = (state = { }); case "request cells error": return Object.assign({}, state, { - cells: null, loading: false, error: action.data }); diff --git a/src/reducers/index.js b/src/reducers/index.js index 743761b4..b844e299 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -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, }) diff --git a/src/reducers/initialize.js b/src/reducers/initialize.js new file mode 100644 index 00000000..97370757 --- /dev/null +++ b/src/reducers/initialize.js @@ -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; diff --git a/src/reducers/selectedMetadata.js b/src/reducers/selectedMetadata.js index 51af0505..b165fd1e 100644 --- a/src/reducers/selectedMetadata.js +++ b/src/reducers/selectedMetadata.js @@ -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: []} */