wire up geneset reducer (#2082)

* first cut at GET /genesets route

* update existing tests to match code changes

* more GET /genesets and initial tests

* add missing test fixture

* geneset validation accepts OTA format

* genesets route: better error handling, more tests

* lint

* genesets reducer and initial load

* fix lint

* add autosave support for genesets

* remove debug logging

* fix typo

* fix another typo

* update smoke test config for genesets

* smoke test fixes

* more fiddling with smoke tests
This commit is contained in:
Bruce Martin
2021-03-02 12:12:58 -08:00
committed by GitHub
parent b3aadf6632
commit b00496198d
8 changed files with 1082 additions and 19 deletions
+524
View File
@@ -0,0 +1,524 @@
import genesetsReducer from "../../src/reducers/genesets";
describe("initial reducer state", () => {
test("some other action", () => {
expect(genesetsReducer(undefined, { type: "foo" })).toMatchObject({
initialized: false,
lastTid: undefined,
genesets: new Map(),
});
});
});
describe("geneset: initial load", () => {
test("missing JSON response", () => {
expect(() =>
genesetsReducer(undefined, {
type: "geneset: initial load",
})
).toThrow("missing or malformed JSON response");
});
test("empty geneset", () => {
expect(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
})
).toMatchObject({
initialized: true,
lastTid: 0,
genesets: new Map(),
});
});
test("non-empty geneset", () => {
expect(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 99,
genesets: [
{
geneset_name: "G1",
genes: [{ gene_symbol: "F5" }],
},
{
geneset_name: "G2",
geneset_description: "G2 desc",
genes: [{ gene_symbol: "F6" }],
},
{
geneset_name: "G3",
geneset_description: "G3 desc",
genes: [{ gene_symbol: "F7", gene_description: "gene desc" }],
},
],
},
})
).toMatchObject({
initialized: true,
lastTid: 99,
genesets: new Map([
[
"G1",
{
genesetName: "G1",
genesetDescription: "",
genes: new Map([["F5", { geneSymbol: "F5", geneDescription: "" }]]),
},
],
[
"G2",
{
genesetName: "G2",
genesetDescription: "G2 desc",
genes: new Map([["F6", { geneSymbol: "F6", geneDescription: "" }]]),
},
],
[
"G3",
{
genesetName: "G3",
genesetDescription: "G3 desc",
genes: new Map([
["F7", { geneSymbol: "F7", geneDescription: "gene desc" }],
]),
},
],
]),
});
});
});
describe("geneset: create", () => {
const initialState = genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
});
test("simple create", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "a geneset",
genesetDescription: "",
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"a geneset",
{
genesetName: "a geneset",
genesetDescription: "",
genes: new Map(),
},
],
]),
});
});
test("error - duplicate name", () => {
expect(() => {
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
genesetDescription: "foo",
}),
{
type: "geneset: create",
genesetName: "foo",
genesetDescription: "bar",
}
);
}).toThrow("name already defined");
});
test("error - missing required action values", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: create",
genesetDescription: "foo",
});
}).toThrow();
expect(() => {
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
});
}).toThrow("name or description unspecified");
});
});
describe("geneset: delete", () => {
const initialState = genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
});
test("simple delete", () => {
expect(
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
genesetDescription: "foo",
}),
{
type: "geneset: delete",
genesetName: "foo",
}
)
).toMatchObject({
initialized: true,
lastTid: 0,
genesets: new Map(),
});
});
test("error - missing name", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: delete",
genesetName: "foo",
});
}).toThrow("name does not exist");
});
});
describe("geneset: update", () => {
const initialState = genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
});
test("simple update", () => {
expect(
genesetsReducer(
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo1",
genesetDescription: "foo1",
}),
{
type: "geneset: create",
genesetName: "foo2",
genesetDescription: "foo2",
}
),
{
type: "geneset: update",
genesetName: "foo1",
update: {
genesetName: "bar",
genesetDescription: "bar",
},
}
)
).toMatchObject({
initialized: true,
lastTid: 0,
genesets: new Map([
[
"bar",
{ genesetName: "bar", genesetDescription: "bar", genes: new Map() },
],
[
"foo2",
{ genesetName: "foo2", genesetDescription: "foo2", genes: new Map() },
],
]),
});
});
test("error - unknown name", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: update",
genesetName: "foo",
update: {
genesetName: "foo",
genesetDescription: "bar",
},
});
}).toThrow("name unspecified or does not exist");
});
test("error - duplicate name", () => {
expect(() => {
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
genesetDescription: "foo",
}),
{
type: "geneset: update",
genesetName: "foo",
update: {
genesetName: "foo",
genesetDescription: "foo",
},
}
);
}).toThrow("update specified existing name");
});
});
describe("geneset: add genes", () => {
const initialState = genesetsReducer(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
}),
{
type: "geneset: create",
genesetName: "test",
genesetDescription: "",
}
);
test("add a gene", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: add genes",
genesetName: "test",
genes: [{ geneSymbol: "F5" }],
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map([["F5", { geneSymbol: "F5", geneDescription: "" }]]),
},
],
]),
});
expect(
genesetsReducer(initialState, {
type: "geneset: add genes",
genesetName: "test",
genes: [
{ geneSymbol: "F5", geneDescription: "desc" },
{ geneSymbol: "SET1", geneDescription: "" },
],
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map([
["F5", { geneSymbol: "F5", geneDescription: "desc" }],
["SET1", { geneSymbol: "SET1", geneDescription: "" }],
]),
},
],
]),
});
});
test("no such geneset error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: add genes",
genesetName: "mumble",
genes: [],
});
}).toThrow("geneset name does not exist");
});
});
describe("geneset: delete genes", () => {
const initialState = genesetsReducer(
genesetsReducer(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
}),
{
type: "geneset: create",
genesetName: "test",
genesetDescription: "",
}
),
{
type: "geneset: add genes",
genesetName: "test",
genes: [{ geneSymbol: "F5" }],
}
);
test("simple", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: delete genes",
genesetName: "test",
geneSymbols: ["F5"],
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map(),
},
],
]),
});
});
test("no such geneset error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: delete genes",
genesetName: "mumble",
geneSymbols: [],
});
}).toThrow("name does not exist");
});
});
describe("geneset: set gene description", () => {
const initialState = genesetsReducer(
genesetsReducer(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
}),
{
type: "geneset: create",
genesetName: "test",
genesetDescription: "",
}
),
{
type: "geneset: add genes",
genesetName: "test",
genes: [{ geneSymbol: "F5" }],
}
);
test("simple set", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: set gene description",
genesetName: "test",
update: {
geneSymbol: "F5",
geneDescription: "mumble",
},
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map([
["F5", { geneSymbol: "F5", geneDescription: "mumble" }],
]),
},
],
]),
});
});
test("no such geneset error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: set gene description",
genesetName: "does not exist",
update: {
geneSymbol: "F5",
geneDescription: "mumble",
},
});
}).toThrow("geneset name does not exist");
});
test("no such gene error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: set gene description",
genesetName: "test",
update: {
geneSymbol: "NO SUCH GENE",
geneDescription: "mumble",
},
});
}).toThrow("no such gene");
});
});
describe("geneset: set tid", () => {
test("simple set", () => {
expect(
genesetsReducer(undefined, {
type: "geneset: set tid",
tid: 1,
})
).toMatchObject({ lastTid: 1 });
});
test("not a number error", () => {
expect(() => {
genesetsReducer(
{ lastTid: 1 },
{
type: "geneset: set tid",
tid: "0",
}
);
}).toThrow("must be a positive integer");
});
test("decrement error", () => {
expect(() => {
genesetsReducer(
{ lastTid: 1 },
{
type: "geneset: set tid",
tid: 0,
}
);
}).toThrow("may not be decremented");
});
});
+93
View File
@@ -387,3 +387,96 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
});
}
};
export const saveGenesetsAction = () => async (dispatch, getState) => {
const state = getState();
// bail if gene sets not available, or in readonly mode.
const { config } = state;
const genesetsAreAvailable =
config?.parameters?.["annotations_genesets"] ?? false;
const genesetsReadonly =
config?.parameters?.["annotations_genesets_readonly"] ?? true;
if (!genesetsAreAvailable || genesetsReadonly) {
// our non-save was completed!
return dispatch({
type: "autosave: genesets complete",
lastSavedGenesets: state.genesets,
});
}
const { lastTid, genesets: lastGenesets } = state.genesets;
/* Create the JSON OTA data structure */
const tid = (lastTid ?? 0) + 1;
const genesets = [];
for (const [name, gs] of lastGenesets) {
// const genes = Array.from(gs.genes.values());
const genes = [];
for (const g of gs.genes.values()) {
genes.push({
gene_symbol: g.geneSymbol,
gene_description: g.geneDescription,
});
}
genesets.push({
geneset_name: name,
geneset_description: gs.genesetDescription,
genes,
});
}
const ota = {
tid,
genesets,
};
/* Save to server */
try {
const {
dataCollectionNameIsReadOnly,
dataCollectionName,
} = state.annotations;
const queryString =
!dataCollectionNameIsReadOnly && !!dataCollectionName
? `?annotation-collection-name=${encodeURIComponent(
dataCollectionName
)}`
: "";
const res = await fetch(
`${globals.API.prefix}${globals.API.version}genesets${queryString}`,
{
method: "PUT",
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
body: JSON.stringify(ota),
credentials: "include",
}
);
if (!res.ok) {
return dispatch({
type: "autosave: genesets error",
message: `HTTP error ${res.status} - ${res.statusText}`,
res,
});
}
return Promise.all([
dispatch({
type: "autosave: genesets complete",
lastSavedGenesets: genesets,
}),
dispatch({
type: "geneset: set tid",
tid,
}),
]);
} catch (error) {
return dispatch({
type: "autosave: genesets error",
message: error.toString(),
error,
});
}
};
+24
View File
@@ -52,6 +52,27 @@ async function userInfoFetch(dispatch) {
});
}
async function genesetsFetch(dispatch, config) {
/* request genesets ONLY if the backend supports the feature */
const defaultResponse = {
genesets: [],
tid: 0,
};
if (config?.parameters?.["annotations_genesets"] ?? false) {
fetchJson("genesets").then((response) => {
dispatch({
type: "geneset: initial load",
data: response ?? defaultResponse,
});
});
} else {
dispatch({
type: "geneset: initial load",
data: defaultResponse,
});
}
}
function prefetchEmbeddings(annoMatrix) {
/*
prefetch requests for all embeddings
@@ -76,6 +97,8 @@ const doInitialDataLoad = () =>
userInfoFetch(dispatch),
]);
genesetsFetch(dispatch, config);
const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema);
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
@@ -242,6 +265,7 @@ export default {
annotationRenameLabelInCategory: annoActions.annotationRenameLabelInCategory,
annotationLabelCurrentSelection: annoActions.annotationLabelCurrentSelection,
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
saveGenesetsAction: annoActions.saveGenesetsAction,
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
layoutChoiceAction: embActions.layoutChoiceAction,
setCellSetFromSelection: selnActions.setCellSetFromSelection,
+39 -10
View File
@@ -5,11 +5,18 @@ import FilenameDialog from "./filenameDialog";
@connect((state) => ({
annotations: state.annotations,
saveInProgress: state.autosave?.saveInProgress ?? false,
obsAnnotationSaveInProgress:
state.autosave?.obsAnnotationSaveInProgress ?? false,
genesetSaveInProgress: state.autosave?.genesetSaveInProgress ?? false,
error: state.autosave?.error,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
writableGenesetsEnabled: !(
state.config?.parameters?.["annotations_genesets_readonly"] ?? true
),
annoMatrix: state.annoMatrix,
genesets: state.genesets,
lastSavedAnnoMatrix: state.autosave?.lastSavedAnnoMatrix,
lastSavedGenesets: state.autosave?.lastSavedGenesets,
}))
class Autosave extends React.Component {
constructor(props) {
@@ -20,11 +27,11 @@ class Autosave extends React.Component {
}
componentDidMount() {
const { writableCategoriesEnabled } = this.props;
const { writableCategoriesEnabled, writableGenesetsEnabled } = this.props;
let { timer } = this.state;
if (timer) clearInterval(timer);
if (writableCategoriesEnabled) {
if (writableCategoriesEnabled || writableGenesetsEnabled) {
timer = setInterval(this.tick, 2500);
} else {
timer = null;
@@ -38,18 +45,40 @@ class Autosave extends React.Component {
}
tick = () => {
const { dispatch, saveInProgress } = this.props;
if (this.needToSave() && !saveInProgress) {
const {
dispatch,
obsAnnotationSaveInProgress,
genesetSaveInProgress,
} = this.props;
if (!obsAnnotationSaveInProgress && this.needToSaveObsAnnotations()) {
dispatch(actions.saveObsAnnotationsAction());
}
if (!genesetSaveInProgress && this.needToSaveGenesets()) {
dispatch(actions.saveGenesetsAction());
}
};
needToSave = () => {
/* return true if we need to save, false if we don't */
needToSaveObsAnnotations = () => {
/* return true if we need to save obs cell labels, false if we don't */
const { annoMatrix, lastSavedAnnoMatrix } = this.props;
return actions.needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix);
};
needToSaveGenesets = () => {
/* return true if we need to save gene ses, false if we do not */
const { genesets, lastSavedGenesets } = this.props;
return genesets.initialized && genesets !== lastSavedGenesets;
};
needToSave() {
return this.needToSaveGenesets() || this.needToSaveObsAnnotations();
}
saveInProgress() {
const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props;
return obsAnnotationSaveInProgress || genesetSaveInProgress;
}
statusMessage() {
const { error } = this.props;
if (error) {
@@ -61,12 +90,12 @@ class Autosave extends React.Component {
render() {
const {
writableCategoriesEnabled,
saveInProgress,
writableGenesetsEnabled,
lastSavedAnnoMatrix,
} = this.props;
const initialDataLoadComplete = lastSavedAnnoMatrix;
if (!writableCategoriesEnabled) return null;
if (!writableCategoriesEnabled && !writableGenesetsEnabled) return null;
return (
<div
@@ -74,7 +103,7 @@ class Autosave extends React.Component {
data-testclass={
!initialDataLoadComplete
? "autosave-init"
: this.needToSave() || saveInProgress
: this.saveInProgress() || this.needToSave()
? "autosave-incomplete"
: "autosave-complete"
}
+48 -7
View File
@@ -1,17 +1,25 @@
const Autosave = (
state = {
saveInProgress: false,
error: false,
// cell labels
obsAnnotationSaveInProgress: false,
lastSavedAnnoMatrix: null,
// gene sets
genesetSaveInProgress: false,
lastSavedGenesets: null,
// error state
error: false,
},
action
action,
nextSharedState
) => {
switch (action.type) {
case "annoMatrix: init complete": {
return {
...state,
error: false,
saveInProgress: false,
obsAnnotationSaveInProgress: false,
lastSavedAnnoMatrix: action.annoMatrix,
};
}
@@ -19,7 +27,7 @@ const Autosave = (
case "writable obs annotations - save started": {
return {
...state,
saveInProgress: true,
obsAnnotationSaveInProgress: true,
};
}
@@ -27,7 +35,7 @@ const Autosave = (
return {
...state,
error: action.message,
saveInProgress: false,
obsAnnotationSaveInProgress: false,
};
}
@@ -35,12 +43,45 @@ const Autosave = (
const { lastSavedAnnoMatrix } = action;
return {
...state,
saveInProgress: false,
obsAnnotationSaveInProgress: false,
error: false,
lastSavedAnnoMatrix,
};
}
case "geneset: initial load": {
return {
...state,
genesetSaveInProgress: false,
lastSavedGenesets: nextSharedState.genesets,
};
}
case "autosave: genesets started": {
return {
...state,
genesetSaveInProgress: true,
};
}
case "autosave: genesets error": {
return {
...state,
genesetSaveInProgress: false,
error: action.message,
};
}
case "autosave: genesets complete": {
const { lastSavedGenesets } = action;
return {
...state,
genesetSaveInProgess: false,
error: false,
lastSavedGenesets,
};
}
default:
return { ...state };
}
+349
View File
@@ -0,0 +1,349 @@
/**
* Gene set state. Geneset UI state is in a different reducer.
*
* geneset reducer state is a Map object, where:
* key: the geneset name, a string.
* val: the geneset defined as an object ("geneset object")
*
* A geneset object is:
* {
* genesetName: <string> # same as the map key
* genesetDescription: <string>
* genes: Map<<string>, {
* geneSymbol: <string>, # same as the map key
* geneDescription: <string>
* }>
* }
*
* Geneset and genes Map order is significant, and will be preserved across
* CRUD operations on either.
*
* This reducer does light error checking, but not as much as the backend
* routes. Do not rely on it to enforce geneset integrity - eg, no duplicate
* genes in a geneset.
*/
const GeneSets = (
state = {
initialized: false,
lastTid: undefined,
genesets: new Map(),
},
action
) => {
switch (action.type) {
/**
* Initial, load-time bootstrap.
* {
* type: "geneset: initial load"
* data: JSON response
* }
*/
case "geneset: initial load": {
const { data } = action;
if (
!data ||
typeof data.tid !== "number" ||
!Array.isArray(data.genesets)
)
throw new Error("missing or malformed JSON response");
const lastTid = data.tid;
const genesetsData = data.genesets;
const genesets = new Map();
for (const gsData of genesetsData) {
const genes = new Map();
for (const gene of gsData.genes) {
genes.set(gene.gene_symbol, {
geneSymbol: gene.gene_symbol,
geneDescription: gene?.["gene_description"] ?? "",
});
}
const gs = {
genesetName: gsData.geneset_name,
genesetDescription: gsData?.["geneset_description"] ?? "",
genes,
};
genesets.set(gsData.geneset_name, gs);
}
return {
initialized: true,
lastTid,
genesets,
};
}
/**
* Creates a new & empty geneset with the given name and description.
* {
* type: "geneset: create",
* genesetName: string, // gene set name
* genesetDescription: string, // geneset description
* }
*
*/
case "geneset: create": {
const { genesetName, genesetDescription } = action;
if (
typeof genesetName !== "string" ||
!genesetName ||
genesetDescription === undefined
)
throw new Error("geneset: create -- name or description unspecified.");
if (state.genesets.has(genesetName))
throw new Error("geneset: create -- name already defined.");
const genesets = new Map(state.genesets); // clone
genesets.set(genesetName, {
genesetName,
genesetDescription,
genes: new Map(),
});
return {
...state,
genesets,
};
}
/**
* Deletes the named geneset, if it exists. Throws if it does not.
* {
* type: "geneset: delete",
* genesetName: string
* }
*/
case "geneset: delete": {
const { genesetName } = action;
if (!state.genesets.has(genesetName))
throw new Error("geneset: delete -- geneset name does not exist.");
const genesets = new Map(state.genesets); // clone
genesets.delete(genesetName);
return {
...state,
genesets,
};
}
/**
* Update the named geneset with a new name and description. Preserves the existing
* order of the geneset, even when the genesetName changes.
* {
* type: "geneset: update",
* genesetName: string, current name of geneset to be updated
* update: {
* genesetName: string, new name
* genesetDescription: string, new description
* }
* }
*
* For example, if you want to update JUST the description:
* dispatch({
* action: "geneset: update",
* genesetName: "foo",
* update: { genesetName: "foo", genesetDescription: "a new description"}
* })
*/
case "geneset: update": {
const { genesetName, update } = action;
if (
typeof genesetName !== "string" ||
!genesetName ||
!state.genesets.has(genesetName)
)
throw new Error(
"geneset: update -- geneset name unspecified or does not exist."
);
if (state.genesets.has(update.genesetName))
throw new Error("geneset: update -- update specified existing name.");
const prevGs = state.genesets.get(genesetName);
const newGs = {
...update,
genes: prevGs.genes,
}; // clone
// clone the map, preserving current insert order, but mapping name->newName.
const genesets = new Map();
for (const [name, gs] of state.genesets) {
if (name === genesetName) genesets.set(newGs.genesetName, newGs);
else genesets.set(name, gs);
}
return {
...state,
genesets,
};
}
/**
* Adds genes to the geneset. They are appended to the END of the geneset, in the
* order provided. Duplicates or genes already in the geneset, will be ignored.
* {
* type: "geneset: add genes"
* genesetName: <string>, // gene set name
* genes: Array<{
* geneSymbol: <string>,
* geneDescription: <string>
* }>
* }
*
* Example:
* dispatch({
* type: "add genes",
* genesetName: "foo",
* genes: [ { geneSymbol: "FOXP", geneDescription: "test" }]
* });
*/
case "geneset: add genes": {
const { genesetName, genes } = action;
if (!state.genesets.has(genesetName))
throw new Error("geneset: add genes -- geneset name does not exist.");
// clone
const genesets = new Map(state.genesets);
const gs = {
...genesets.get(genesetName),
genes: new Map(genesets.get(genesetName).genes),
};
genesets.set(genesetName, gs);
// add
const newGenes = gs.genes;
for (const gene of genes) {
const { geneSymbol } = gene;
const geneDescription = gene?.geneDescription ?? "";
// ignore genes already present
if (!newGenes.has(geneSymbol))
newGenes.set(geneSymbol, {
geneSymbol,
geneDescription,
});
}
return {
...state,
genesets,
};
}
/**
* Delete genes from the named geneset. Will throw if the genesetName does
* not exist. Will ignore geneSymbols that do not exist.
* {
* type: "geneset: delete genes",
* genesetName: <string>, // the geneset from which to delete genes
* geneSymbols: [<string>, ...], // the gene symbols to delete.
* }
*
* Example:
* dispatch({
* type: "geneset: delete genes",
* genesetName: "a geneset name",
* geneSymbols: ["F5"]
* })
*/
case "geneset: delete genes": {
const { genesetName, geneSymbols } = action;
if (!state.genesets.has(genesetName))
throw new Error(
"geneset: delete genes -- geneset name does not exist."
);
// clone
const genesets = new Map(state.genesets);
const gs = {
...genesets.get(genesetName),
genes: new Map(genesets.get(genesetName).genes),
};
genesets.set(genesetName, gs);
// delete
const { genes } = gs;
for (const geneSymbol of geneSymbols) {
genes.delete(geneSymbol);
}
return {
...state,
genesets,
};
}
/**
* Set/update the description of the gene. NOTE that this does not allow the name
* of the gene to change - only "geneset: add" and "geneset: delete" can change
* the genes in a geneset. Use this to update a gene description AFTER you add it
* to the geneset.
* {
* type: "geneset: set gene description",
* genesetName: <string>, // the geneset to update
* update: {
* geneSymbol: <string>, // the gene to update, MUST exist already in the geneset
* geneDescription: <string>
* }
* }
*
* Example:
* dispatch({
* type: "geneset: set gene description",
* genesetName: "my fav geneset",
* update: {
* geneSymbol: "F5",
* geneDescription: "tada, moar description"
* }
* })
*/
case "geneset: set gene description": {
const { genesetName, update } = action;
if (!state.genesets.has(genesetName))
throw new Error(
"geneset: set gene description -- geneset name does not exist."
);
// clone
const genesets = new Map(state.genesets);
const gs = {
...genesets.get(genesetName),
genes: new Map(genesets.get(genesetName).genes),
};
genesets.set(genesetName, gs);
const { geneSymbol, geneDescription } = update;
const gene = gs.genes.get(geneSymbol);
if (!gene)
throw new Error("geneset: set gene description -- no such gene");
gs.genes.set(geneSymbol, {
geneSymbol,
geneDescription,
});
return {
...state,
genesets,
};
}
/**
* Used by autosave to update the server synchronization TID
*/
case "geneset: set tid": {
const { tid } = action;
if (!Number.isInteger(tid) || tid < 0)
throw new Error("TID must be a positive integer number");
if (state.lastTid !== undefined && tid < state.lastTid)
throw new Error("TID may not be decremented.");
return {
...state,
lastTid: tid,
};
}
default:
return state;
}
};
export default GeneSets;
+3
View File
@@ -15,6 +15,7 @@ import differential from "./differential";
import layoutChoice from "./layoutChoice";
import controls from "./controls";
import annotations from "./annotations";
import genesets from "./genesets";
import autosave from "./autosave";
import ontology from "./ontology";
import centroidLabels from "./centroidLabels";
@@ -31,6 +32,7 @@ const Reducer = undoable(
["obsCrossfilter", obsCrossfilter],
["ontology", ontology],
["annotations", annotations],
["genesets", genesets],
["layoutChoice", layoutChoice],
["categoricalSelection", categoricalSelection],
["continuousSelection", continuousSelection],
@@ -55,6 +57,7 @@ const Reducer = undoable(
"differential",
"layoutChoice",
"centroidLabels",
"genesets",
"annotations",
],
undoableConfig
@@ -108,7 +108,7 @@ class AnnotationsLocalFile(Annotations):
def read_genesets(self, data_adaptor, context=None):
if has_request_context():
if not current_app.auth.is_user_authenticated():
return ([], None)
return ({}, self.last_geneset_tid)
fname = self._get_genesets_filename(data_adaptor)
genesets = {}
@@ -165,7 +165,7 @@ class AnnotationsLocalFile(Annotations):
output_file = self.label_output_file or self.genesets_output_file
if output_file:
return os.path.dirname(self.path.abspath(output_file))
return os.path.dirname(os.path.abspath(output_file))
return os.getcwd()