mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-23 04:48:11 +08:00
* 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:
+252
-182
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user