#130 redux refactor (#208)

* mocks for redux refactor - for discussion

* more API design on redux refactor

* add new reuqired dependencies for build

* change babel target to use modern browser

* remove dead code

* remove dead code - joy plots

* checkpoint on redux refactoring

* checkpoint on redux refactoring

* fix mistaken rebase conflict resolution

* dead code removal; add name to dataframe backmap

* rename dataframe to universe

* update eslint config to more closely match prettier

* lint

* more eslint updates to match prettier

* additional config to make eslint match prettier

* add expression data to Universe/World

* remove obsolete reducers

* lint fixes

* more eslint cleanup

* lint

* lint

* fix but in countAllOnes when dimensions gt 1

* lint; do not display name metadata field

* lint; colors refactor

* lint; colors refactor

* update comments

* first cut at regraph and reset

* enable object-curly-braces consistent mode

* lint, handle regraph with no selection

* fix expression scatterplot bugs

* fix regression legend display

* add expression data cache

* remove console logging

* reset cell color on regraph/reset

* remove obsolete server URLs

* rename UniverseV01 to Universe_REST_API_v01

* add additional comments on the varDataCache

* merge universe reducer into controls reducer; simplify initialization-related actions

* use spread operator

* fix erroneous comment

* convert universe and world state to plain objects, and functionalize supporting code (remove ES6 classes)

* use spread operator

* lint

* improve variable names

* rename obsCrossfilter to crossfilter and obsDimensionMap to dimensionMap

* rename controls2 to controls
This commit is contained in:
Bruce Martin
2018-09-17 20:43:39 -07:00
committed by GitHub
parent 7b00fea44d
commit d4e850d18f
40 changed files with 2008 additions and 1584 deletions
+252 -182
View File
@@ -1,101 +1,149 @@
// jshint esversion: 6
import _ from "lodash";
import memoize from "memoize-one";
import * as globals from "../globals";
import store from "../reducers";
import URI from "urijs";
import _ from "lodash";
import { Universe } from "../util/stateManager";
const requestCells = (query = "") => {
return dispatch => {
dispatch({ type: "request cells started" });
return fetch(`${globals.API.prefix}${globals.API.version}cells${query}`, {
/*
Catch unexpected errors and make sure we don't lose them!
*/
function catchErrorsWrap(fn) {
return (dispatch, getState) => {
fn(dispatch, getState).catch(error => {
console.error(error);
dispatch({ type: "UNEXPECTED ERROR", error });
});
};
}
async function doRequestInitialize() {
const res = await fetch(
`${globals.API.prefix}${globals.API.version}initialize`,
{
method: "get",
headers: new Headers({
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
data => dispatch({ type: "request cells success", data }),
error => dispatch({ type: "request cells error", error })
}
);
return res.json();
}
async function doRequestCells(query) {
const res = await fetch(
`${globals.API.prefix}${globals.API.version}cells${query}`,
{
method: "get",
headers: new Headers({
"Content-Type": "application/json"
})
}
);
return res.json();
}
function doInitialDataLoad(query = "") {
return catchErrorsWrap(async dispatch => {
dispatch({ type: "initial data load start" });
try {
const res = await Promise.all([
doRequestInitialize(),
doRequestCells(query)
]);
const universe = Universe.createUniverseFromRESTv01Response(
res[0],
res[1]
);
};
};
/* SELECT */
const regraph = () => {
return (dispatch, getState) => {
dispatch({ type: "regraph started" });
const state = getState();
const selectedMetadata = {};
_.each(state.controls.categoricalAsBooleansMap, (options, field) => {
let atLeastOneOptionDeselected = false;
_.each(options, (isActive, option) => {
if (!isActive) {
atLeastOneOptionDeselected = true;
}
dispatch({
type: "initial data load complete (universe exists)",
universe
});
} catch (error) {
dispatch({ type: "initial data load error", error });
}
});
}
if (atLeastOneOptionDeselected) {
_.each(options, (isActive, option) => {
if (isActive) {
if (selectedMetadata[field]) {
selectedMetadata[field].push(option);
} else if (!selectedMetadata[field]) {
selectedMetadata[field] = [option];
}
}
});
}
});
// XXX TODO - this is the old code for doing a regraph. Preserving it solely
// until we port to 0.2 API. The new UX for regraph can't be implemented on
// the 0.1 API (doesn't allow for re-layout on arbitrary sets of cells), so just
// punting for now. See ticket #88
//
//
// /* SELECT */
// const regraph = () => {
// return (dispatch, getState) => {
// dispatch({ type: "regraph started" });
//
// const state = getState();
// const selectedMetadata = {};
//
// _.each(state.controls.categoricalAsBooleansMap, (options, field) => {
// let atLeastOneOptionDeselected = false;
//
// _.each(options, (isActive, option) => {
// if (!isActive) {
// atLeastOneOptionDeselected = true;
// }
// });
//
// if (atLeastOneOptionDeselected) {
// _.each(options, (isActive, option) => {
// if (isActive) {
// if (selectedMetadata[field]) {
// selectedMetadata[field].push(option);
// } else if (!selectedMetadata[field]) {
// selectedMetadata[field] = [option];
// }
// }
// });
// }
// });
//
// let uri = new URI();
// uri.setSearch(selectedMetadata);
// console.log(uri.search(), selectedMetadata);
//
// dispatch(requestCells(uri.search())).then(res => {
// if (res.error) {
// dispatch({ type: "regraph error" });
// } else {
// dispatch({ type: "regraph success" });
// }
// });
// };
// };
let uri = new URI();
uri.setSearch(selectedMetadata);
console.log(uri.search(), selectedMetadata);
dispatch(requestCells(uri.search())).then(res => {
if (res.error) {
dispatch({ type: "regraph error" });
} else {
dispatch({ type: "regraph success" });
}
});
};
const regraph = () => (dispatch, getState) => {
const { universe, world, crossfilter } = getState().controls;
dispatch({
type: "set World to current selection",
universe,
world,
crossfilter
});
};
const resetGraph = () => {
return (dispatch, getState) => {
dispatch({ type: "reset graph" });
};
};
const initialize = () => {
return (dispatch, getState) => {
dispatch({ type: "initialize started" });
fetch(`${globals.API.prefix}${globals.API.version}initialize`, {
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 })
);
};
};
const resetGraph = () => (dispatch, getState) =>
dispatch({
type: "reset World to eq Universe",
universe: getState().controls.universe
});
// This code defends against the case where /expression returns a cellname
// never seen before (ie, not returned by /cells). This should not happen
// (see https://github.com/chanzuckerberg/cellxgene-rest-api/issues/34) but
// occasionally does.
//
// XXX TODO - this code is only relevant in v0.1 REST API, and can be retired
// when we port to 0.2.
//
const makeMetadataMap = memoize(metadata => _.keyBy(metadata, "CellName"));
function cleanupExpressionResponse(data) {
const s = store.getState();
const metadata = s.controls.allCellsMetadataMap;
const { universe } = s.controls;
const metadata = makeMetadataMap(universe.obsAnnotations);
let errorFound = false;
data.data.cells = _.filter(data.data.cells, cell => {
if (!errorFound && !metadata[cell.cellname]) {
@@ -110,123 +158,145 @@ function cleanupExpressionResponse(data) {
return data;
}
const requestGeneExpressionCounts = () => {
return (dispatch, getState) => {
dispatch({ type: "get expression started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "get",
headers: new Headers({
accept: "application/json"
})
})
.then(res => res.json())
.then(data => cleanupExpressionResponse(data))
.then(
data => dispatch({ type: "get expression success", data }),
error => dispatch({ type: "get expression error", error })
);
};
};
/*
Fetch [gene, ...] from V0.1 API. Not an action function - just a helper
which implements the new expression data caching.
*/
async function _doRequestExpressionData(dispatch, getState, genes) {
const state = getState();
/* check cache and only fetch data we do not already have */
const { universe } = state.controls;
const genesToFetch = _.filter(genes, g => !universe.varDataCache[g]);
const requestSingleGeneExpressionCountsForColoringPOST = gene => {
return (dispatch, getState) => {
dispatch({ type: "get single gene expression for coloring started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "POST",
body: JSON.stringify({
genelist: [gene]
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(data => cleanupExpressionResponse(data))
.then(
data =>
dispatch({
type: "color by expression",
gene: gene,
data
dispatch({ type: "expression load start" });
let expressionData = {}; // { gene: data }
if (genesToFetch.length) {
try {
const res = await fetch(
`${globals.API.prefix}${globals.API.version}expression`,
{
method: "POST",
body: JSON.stringify({
genelist: genes
}),
error =>
dispatch({
type: "get single gene expression for coloring error",
error
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
}
);
};
};
let data = await res.json();
data = cleanupExpressionResponse(data);
data = Universe.convertExpressionRESTv01ToObject(universe, data);
expressionData = {
...expressionData,
...data
};
} catch (error) {
dispatch({ type: "expression load error", error });
throw error; // rethrow
}
}
const requestGeneExpressionCountsPOST = genes => {
return (dispatch, getState) => {
dispatch({ type: "get expression started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "POST",
body: JSON.stringify({
genelist: genes
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(data => cleanupExpressionResponse(data))
.then(
data => dispatch({ type: "get expression success", data }),
error => dispatch({ type: "get expression error", error })
);
};
};
// add the cached values
_.forEach(genes, g => {
if (expressionData[g] === undefined) {
expressionData[g] = universe.varDataCache[g];
}
});
const requestDifferentialExpression = (celllist1, celllist2, num_genes = 7) => {
return (dispatch, getState) => {
dispatch({ type: "request differential expression started" });
fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, {
method: "POST",
body: JSON.stringify({
celllist1,
celllist2,
num_genes
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
data => {
/* kick off a secondary action to get all expression counts for all cells now that we know what the top expressed are */
dispatch(
requestGeneExpressionCountsPOST(
_.union(
data.data.celllist1.topgenes,
data.data.celllist2.topgenes
) // ["GPM6B", "FEZ1", "TSPAN31", "PCSK1N", "TUBA1A", "GPM6A", "CLU", "FCER1G", "TYROBP", "C1QB", "CD74", "CYBA", "GPX1", "TMSB4X"]
)
);
/* then send the success case action through */
return dispatch({
type: "request differential expression success",
data
});
return dispatch({ type: "expression load success", expressionData });
}
function requestSingleGeneExpressionCountsForColoringPOST(gene) {
return async (dispatch, getState) => {
dispatch({ type: "get single gene expression for coloring started" });
try {
await _doRequestExpressionData(dispatch, getState, [gene]);
dispatch({
type: "color by expression",
gene,
data: {
[gene]: getState().controls.world.varDataCache[gene]
}
});
} catch (error) {
dispatch({
type: "get single gene expression for coloring error",
error
});
}
};
}
const requestGeneExpressionCountsPOST = genes => async (dispatch, getState) => {
dispatch({ type: "get expression started" });
try {
await _doRequestExpressionData(dispatch, getState, genes);
return dispatch({
type: "get expression success",
genes,
data: _.transform(
genes,
(res, gene) => {
res[gene] = getState().controls.world.varDataCache[gene];
},
error =>
dispatch({ type: "request differential expression error", error })
);
};
{}
)
});
} catch (error) {
return dispatch({ type: "get expression error", error });
}
};
const requestDifferentialExpression = (
celllist1,
celllist2,
num_genes = 7
) => dispatch => {
dispatch({ type: "request differential expression started" });
fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, {
method: "POST",
body: JSON.stringify({
celllist1,
celllist2,
num_genes
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
data => {
/*
kick off a secondary action to get all expression counts for all cells
now that we know what the top expressed are
*/
dispatch(
requestGeneExpressionCountsPOST(
_.union(data.data.celllist1.topgenes, data.data.celllist2.topgenes)
)
);
/* then send the success case action through */
return dispatch({
type: "request differential expression success",
data
});
},
error =>
dispatch({
type: "request differential expression error",
error
})
);
};
export default {
initialize,
requestCells,
regraph,
resetGraph,
requestGeneExpressionCounts,
requestGeneExpressionCountsPOST,
requestSingleGeneExpressionCountsForColoringPOST,
requestDifferentialExpression
requestDifferentialExpression,
doInitialDataLoad
};