mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-29 07:08:11 +08:00
Merge branch 'main' into colinmegill/geneset-prototype
This commit is contained in:
@@ -390,41 +390,51 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
|
||||
|
||||
export const saveGenesetsAction = () => async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { config, genesets, annotations } = state;
|
||||
|
||||
// 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!
|
||||
dispatch({
|
||||
return dispatch({
|
||||
type: "autosave: genesets complete",
|
||||
lastSavedGenesets: genesets,
|
||||
lastSavedGenesets: state.genesets,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
JSON data structure is an array of arrays, where the first
|
||||
element is gene set name, remainder are the genes. Eg,
|
||||
const { lastTid, genesets: lastGenesets } = state.genesets;
|
||||
|
||||
{
|
||||
"genesets": [
|
||||
[ "gs1", ["TNFRSF4","SUMO3","BRWD1"]],
|
||||
[ "gs2", ["DSCR3", "BRWD1", "BACE2", "SIK1", "C21orf33", "ICOSLG", "SUMO3"]]
|
||||
]
|
||||
/* Create the JSON OTA data structure */
|
||||
const tid = (lastTid ?? 0) + 1;
|
||||
const genesets = [];
|
||||
for (const [name, gs] of lastGenesets) {
|
||||
const genes = [];
|
||||
for (const g of gs.genes.values()) {
|
||||
genes.push({
|
||||
gene_symbol: g.geneSymbol,
|
||||
gene_description: g.geneDescription,
|
||||
});
|
||||
}
|
||||
|
||||
Order of gene sets and genes is significant
|
||||
*/
|
||||
const gsArr = [];
|
||||
for (const [gsName, gsGenes] of genesets.genesets) {
|
||||
gsArr.push([gsName, Array.from(gsGenes)]);
|
||||
genesets.push({
|
||||
geneset_name: name,
|
||||
geneset_description: gs.genesetDescription,
|
||||
genes,
|
||||
});
|
||||
}
|
||||
const ota = {
|
||||
tid,
|
||||
genesets,
|
||||
};
|
||||
|
||||
/* Save to server */
|
||||
try {
|
||||
const { dataCollectionNameIsReadOnly, dataCollectionName } = annotations;
|
||||
const {
|
||||
dataCollectionNameIsReadOnly,
|
||||
dataCollectionName,
|
||||
} = state.annotations;
|
||||
const queryString =
|
||||
!dataCollectionNameIsReadOnly && !!dataCollectionName
|
||||
? `?annotation-collection-name=${encodeURIComponent(
|
||||
@@ -440,26 +450,29 @@ export const saveGenesetsAction = () => async (dispatch, getState) => {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
genesets: gsArr,
|
||||
}),
|
||||
body: JSON.stringify(ota),
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
if (res.ok) {
|
||||
dispatch({
|
||||
type: "autosave: genesets complete",
|
||||
lastSavedGenesets: genesets,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
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) {
|
||||
dispatch({
|
||||
return dispatch({
|
||||
type: "autosave: genesets error",
|
||||
message: error.toString(),
|
||||
error,
|
||||
|
||||
@@ -53,18 +53,22 @@ 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) => {
|
||||
const genesets = response?.genesets ?? {};
|
||||
dispatch({
|
||||
type: "geneset: initial load",
|
||||
init: genesets,
|
||||
data: response ?? defaultResponse,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: "geneset: initial load",
|
||||
init: [],
|
||||
data: defaultResponse,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import FilenameDialog from "./filenameDialog";
|
||||
error: state.autosave?.error,
|
||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||
writableGenesetsEnabled: !(
|
||||
state.config?.parameters?.annotations_genesets_readonly ?? true
|
||||
state.config?.parameters?.["annotations_genesets_readonly"] ?? true
|
||||
),
|
||||
annoMatrix: state.annoMatrix,
|
||||
genesets: state.genesets,
|
||||
@@ -27,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;
|
||||
@@ -58,11 +58,6 @@ class Autosave extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
saveInProgress() {
|
||||
const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props;
|
||||
return obsAnnotationSaveInProgress || genesetSaveInProgress;
|
||||
}
|
||||
|
||||
needToSaveObsAnnotations = () => {
|
||||
/* return true if we need to save obs cell labels, false if we don't */
|
||||
const { annoMatrix, lastSavedAnnoMatrix } = this.props;
|
||||
@@ -79,6 +74,11 @@ class Autosave extends React.Component {
|
||||
return this.needToSaveGenesets() || this.needToSaveObsAnnotations();
|
||||
}
|
||||
|
||||
saveInProgress() {
|
||||
const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props;
|
||||
return obsAnnotationSaveInProgress || genesetSaveInProgress;
|
||||
}
|
||||
|
||||
statusMessage() {
|
||||
const { error } = this.props;
|
||||
if (error) {
|
||||
@@ -88,10 +88,14 @@ class Autosave extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { writableCategoriesEnabled, lastSavedAnnoMatrix } = this.props;
|
||||
const {
|
||||
writableCategoriesEnabled,
|
||||
writableGenesetsEnabled,
|
||||
lastSavedAnnoMatrix,
|
||||
} = this.props;
|
||||
const initialDataLoadComplete = lastSavedAnnoMatrix;
|
||||
|
||||
if (!writableCategoriesEnabled) return null;
|
||||
if (!writableCategoriesEnabled && !writableGenesetsEnabled) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -174,7 +174,7 @@ class CategoryValue extends React.Component {
|
||||
Checks to see if at least one of the following changed:
|
||||
* world state
|
||||
* the color accessor (what is currently being colored by)
|
||||
* if this catagorical value's selection status has changed
|
||||
* if this categorical value's selection status has changed
|
||||
* the crossfilter (ie, global selection state)
|
||||
|
||||
If and only if true, update the component
|
||||
@@ -201,6 +201,13 @@ class CategoryValue extends React.Component {
|
||||
const newCount = newCategorySummary.categoryValueCounts[newCategoryIndex];
|
||||
const countChanged = count !== newCount;
|
||||
|
||||
// If the user edits an annotation that is currently colored-by, colors may be re-assigned.
|
||||
// This test is conservative - it may cause re-rendering of entire category (all labels)
|
||||
// if any one changes, but only for the currently colored-by category.
|
||||
const colorMightHaveChanged =
|
||||
nextProps.colorAccessor === nextProps.metadataField &&
|
||||
props.categorySummary !== nextProps.categorySummary;
|
||||
|
||||
return (
|
||||
labelChanged ||
|
||||
valueSelectionChange ||
|
||||
@@ -208,7 +215,8 @@ class CategoryValue extends React.Component {
|
||||
annotationsChange ||
|
||||
editingLabel ||
|
||||
dilationChange ||
|
||||
countChanged
|
||||
countChanged ||
|
||||
colorMightHaveChanged
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function drawPointsRegl(regl) {
|
||||
uniform float nPoints;
|
||||
uniform float minViewportDimension;
|
||||
|
||||
varying vec4 fragColor;
|
||||
varying lowp vec4 fragColor;
|
||||
|
||||
const float zBottom = 0.99;
|
||||
const float zMiddle = 0.;
|
||||
@@ -27,23 +27,23 @@ export default function drawPointsRegl(regl) {
|
||||
${glPointSize}
|
||||
|
||||
void main() {
|
||||
bool isNaN, isSelected, isHighlight;
|
||||
getFlags(flag, isNaN, isSelected, isHighlight);
|
||||
bool isBackground, isSelected, isHighlight;
|
||||
getFlags(flag, isBackground, isSelected, isHighlight);
|
||||
|
||||
float size = pointSize(nPoints, minViewportDimension, isSelected, isHighlight);
|
||||
gl_PointSize = size * pow(distance, 0.5);
|
||||
|
||||
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle);
|
||||
float z = isBackground ? zBottom : (isHighlight ? zTop : zMiddle);
|
||||
vec3 xy = projView * vec3(position, 1.);
|
||||
gl_Position = vec4(xy.xy, z, 1.);
|
||||
|
||||
float alpha = isNaN ? 0.9 : 1.0;
|
||||
float alpha = isBackground ? 0.9 : 1.0;
|
||||
fragColor = vec4(color, alpha);
|
||||
}`,
|
||||
|
||||
frag: `
|
||||
precision mediump float;
|
||||
varying vec4 fragColor;
|
||||
varying lowp vec4 fragColor;
|
||||
void main() {
|
||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
||||
discard;
|
||||
@@ -67,5 +67,15 @@ export default function drawPointsRegl(regl) {
|
||||
count: regl.prop("count"),
|
||||
|
||||
primitive: "points",
|
||||
|
||||
blend: {
|
||||
enable: true,
|
||||
func: {
|
||||
srcRGB: "src alpha",
|
||||
srcAlpha: 1,
|
||||
dstRGB: 0,
|
||||
dstAlpha: "zero",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ import CentroidLabels from "./overlays/centroidLabels";
|
||||
import actions from "../../actions";
|
||||
import renderThrottle from "../../util/renderThrottle";
|
||||
|
||||
import {
|
||||
flagBackground,
|
||||
flagSelected,
|
||||
flagHighlight,
|
||||
} from "../../util/glHelpers";
|
||||
|
||||
/*
|
||||
Simple 2D transforms control all point painting. There are three:
|
||||
* model - convert from underlying per-point coordinate to a layout.
|
||||
@@ -62,10 +68,6 @@ function createModelTF() {
|
||||
return m;
|
||||
}
|
||||
|
||||
const flagSelected = 1;
|
||||
const flagNaN = 2;
|
||||
const flagHighlight = 4;
|
||||
|
||||
@connect((state) => ({
|
||||
annoMatrix: state.annoMatrix,
|
||||
crossfilter: state.obsCrossfilter,
|
||||
@@ -159,8 +161,9 @@ class Graph extends React.Component {
|
||||
const flags = new Float32Array(nObs);
|
||||
if (colorByData) {
|
||||
for (let i = 0, len = flags.length; i < len; i += 1) {
|
||||
if (!Number.isFinite(colorByData[i])) {
|
||||
flags[i] = flagNaN;
|
||||
const val = colorByData[i];
|
||||
if (typeof val === "number" && !Number.isFinite(val)) {
|
||||
flags[i] = flagBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function drawPointsRegl(regl) {
|
||||
uniform float nPoints;
|
||||
uniform float minViewportDimension;
|
||||
|
||||
varying vec4 fragColor;
|
||||
varying lowp vec4 fragColor;
|
||||
|
||||
const float zBottom = 0.99;
|
||||
const float zMiddle = 0.;
|
||||
@@ -26,22 +26,22 @@ export default function drawPointsRegl(regl) {
|
||||
${glPointSize}
|
||||
|
||||
void main() {
|
||||
bool isNaN, isSelected, isHighlight;
|
||||
getFlags(flag, isNaN, isSelected, isHighlight);
|
||||
bool isBackground, isSelected, isHighlight;
|
||||
getFlags(flag, isBackground, isSelected, isHighlight);
|
||||
|
||||
gl_PointSize = pointSize(nPoints, minViewportDimension, isSelected, isHighlight);
|
||||
|
||||
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle);
|
||||
float z = isBackground ? zBottom : (isHighlight ? zTop : zMiddle);
|
||||
vec3 xy = projection * vec3(position, 1.);
|
||||
gl_Position = vec4(xy.xy, z, 1.);
|
||||
|
||||
float alpha = isNaN ? 0.9 : 1.0;
|
||||
float alpha = isBackground ? 0.9 : 1.0;
|
||||
fragColor = vec4(color, alpha);
|
||||
}`,
|
||||
|
||||
frag: `
|
||||
precision mediump float;
|
||||
varying vec4 fragColor;
|
||||
varying lowp vec4 fragColor;
|
||||
void main() {
|
||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
||||
discard;
|
||||
@@ -64,5 +64,15 @@ export default function drawPointsRegl(regl) {
|
||||
count: regl.prop("count"),
|
||||
|
||||
primitive: "points",
|
||||
|
||||
blend: {
|
||||
enable: true,
|
||||
func: {
|
||||
srcRGB: "src alpha",
|
||||
srcAlpha: 1,
|
||||
dstRGB: 0,
|
||||
dstAlpha: "zero",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
createColorQuery,
|
||||
} from "../../util/stateManager/colorHelpers";
|
||||
import renderThrottle from "../../util/renderThrottle";
|
||||
|
||||
const flagSelected = 1;
|
||||
const flagNaN = 2;
|
||||
const flagHighlight = 4;
|
||||
import {
|
||||
flagBackground,
|
||||
flagSelected,
|
||||
flagHighlight,
|
||||
} from "../../util/glHelpers";
|
||||
|
||||
function createProjectionTF(viewportWidth, viewportHeight) {
|
||||
/*
|
||||
@@ -135,8 +136,9 @@ class Scatterplot extends React.PureComponent {
|
||||
const flags = new Float32Array(nObs);
|
||||
if (colorByData) {
|
||||
for (let i = 0, len = flags.length; i < len; i += 1) {
|
||||
if (!Number.isFinite(colorByData[i])) {
|
||||
flags[i] = flagNaN;
|
||||
const val = colorByData[i];
|
||||
if (typeof val === "number" && !Number.isFinite(val)) {
|
||||
flags[i] = flagBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+266
-85
@@ -2,14 +2,30 @@
|
||||
* Gene set state. Geneset UI state is in a different reducer.
|
||||
*
|
||||
* geneset reducer state is a Map object, where:
|
||||
* key: the gene set name, a string.
|
||||
* val: the genes in the gene set, which is a Set of string
|
||||
* key: the geneset name, a string.
|
||||
* val: the geneset defined as an object ("geneset object")
|
||||
*
|
||||
* Ie, Map<string, Set<string>>
|
||||
* 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
|
||||
@@ -19,39 +35,73 @@ const GeneSets = (
|
||||
* Initial, load-time bootstrap.
|
||||
* {
|
||||
* type: "geneset: initial load"
|
||||
* init: Array<Tuple<string, Array<string>>>
|
||||
* data: JSON response
|
||||
* }
|
||||
*/
|
||||
case "geneset: initial load": {
|
||||
const { init } = action;
|
||||
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 gs of init) {
|
||||
const genes = new Set();
|
||||
for (const gene of gs[1]) {
|
||||
genes.add(gene);
|
||||
|
||||
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"] ?? "",
|
||||
});
|
||||
}
|
||||
genesets.set(gs[0], genes);
|
||||
const gs = {
|
||||
genesetName: gsData.geneset_name,
|
||||
genesetDescription: gsData?.["geneset_description"] ?? "",
|
||||
genes,
|
||||
};
|
||||
genesets.set(gsData.geneset_name, gs);
|
||||
}
|
||||
|
||||
return {
|
||||
genesets,
|
||||
initialized: true,
|
||||
lastTid,
|
||||
genesets,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new & empty geneset with the given name and description.
|
||||
* {
|
||||
* type: "geneset: create",
|
||||
* name: string, // gene set name
|
||||
* genes: Set<string> || Array<string> || undefined
|
||||
* genesetName: string, // gene set name
|
||||
* genesetDescription: string, // geneset description
|
||||
* }
|
||||
*
|
||||
*/
|
||||
case "geneset: create": {
|
||||
const { name, genes } = action;
|
||||
if (state.genesets.has(name))
|
||||
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(name, new Set(genes || []));
|
||||
genesets.set(genesetName, {
|
||||
genesetName,
|
||||
genesetDescription,
|
||||
genes: new Map(),
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
genesets,
|
||||
@@ -59,18 +109,19 @@ const GeneSets = (
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the named geneset, if it exists. Throws if it does not.
|
||||
* {
|
||||
* type: "geneset: delete",
|
||||
* name: string
|
||||
* genesetName: string
|
||||
* }
|
||||
*/
|
||||
case "geneset: delete": {
|
||||
const { name } = action;
|
||||
if (!state.genesets.has(name))
|
||||
throw new Error("geneset: delete -- name does not exist.");
|
||||
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(name);
|
||||
genesets.delete(genesetName);
|
||||
return {
|
||||
...state,
|
||||
genesets,
|
||||
@@ -78,81 +129,142 @@ const 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: add genes"
|
||||
* name: string, // gene set name
|
||||
* genes: Set<string> || Array<string> // genes to add
|
||||
* type: "geneset: update",
|
||||
* genesetName: string, current name of geneset to be updated
|
||||
* update: {
|
||||
* genesetName: string, new name
|
||||
* genesetDescription: string, new description
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
case "geneset: add genes": {
|
||||
const { name, genes } = action;
|
||||
if (!state.genesets.has(name))
|
||||
throw new Error("geneset: add genes -- name does not exist.");
|
||||
|
||||
const genesets = new Map(state.genesets); // clone
|
||||
const newGenes = new Set(state.genesets.get(name)); // clone
|
||||
for (const gene of genes) {
|
||||
newGenes.add(gene);
|
||||
}
|
||||
genesets.set(name, newGenes);
|
||||
return {
|
||||
...state,
|
||||
genesets,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {
|
||||
* type: "geneset: del genes"
|
||||
* name: string, // gene set name
|
||||
* genes: Set<string> || Array<string> // genes to delete
|
||||
* }
|
||||
*/
|
||||
case "geneset: del genes": {
|
||||
const { name, genes } = action;
|
||||
|
||||
if (!state.genesets.has(name))
|
||||
throw new Error("geneset: add genes -- name does not exist.");
|
||||
|
||||
const genesets = new Map(state.genesets); // clone
|
||||
const newGenes = new Set(state.genesets.get(name)); // clone
|
||||
for (const gene of genes) {
|
||||
newGenes.delete(gene);
|
||||
}
|
||||
genesets.set(name, newGenes);
|
||||
return {
|
||||
...state,
|
||||
genesets,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the gene set, preserving its order in the geneset collection.
|
||||
*
|
||||
* {
|
||||
* type: "geneset: rename",
|
||||
* name: string, // gene set current (previous) name
|
||||
* newName: string, // the new gene set name
|
||||
* }
|
||||
* 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: rename": {
|
||||
const { name, newName } = action;
|
||||
if (!state.genesets.has(name))
|
||||
throw new Error("geneset: rename -- name does not exist.");
|
||||
case "geneset: update": {
|
||||
const { genesetName, update } = action;
|
||||
if (
|
||||
typeof newName !== "string" ||
|
||||
!newName.length ||
|
||||
state.genesets.has(newName)
|
||||
typeof genesetName !== "string" ||
|
||||
!genesetName ||
|
||||
!state.genesets.has(genesetName)
|
||||
)
|
||||
throw new Error(
|
||||
"geneset: rename -- new name must be unique, non-null string."
|
||||
"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 [gsName, gsGenes] of state.genesets) {
|
||||
if (gsName === name) genesets.set(newName, gsGenes);
|
||||
else genesets.set(gsName, gsGenes);
|
||||
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,
|
||||
@@ -160,6 +272,75 @@ const 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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import thunk from "redux-thunk";
|
||||
import cascadeReducers from "./cascade";
|
||||
import undoable from "./undoable";
|
||||
import config from "./config";
|
||||
import userInfo from "./userInfo";
|
||||
import userInfo from "./userinfo";
|
||||
import annoMatrix from "./annoMatrix";
|
||||
import obsCrossfilter from "./obsCrossfilter";
|
||||
import categoricalSelection from "./categoricalSelection";
|
||||
@@ -59,6 +59,7 @@ const Reducer = undoable(
|
||||
"differential",
|
||||
"layoutChoice",
|
||||
"centroidLabels",
|
||||
"genesets",
|
||||
"annotations",
|
||||
],
|
||||
undoableConfig
|
||||
|
||||
@@ -8,18 +8,24 @@ PointFlags:
|
||||
|
||||
We want a bitmask-like flag structure, but due to webgl limitations
|
||||
must emulate it with floats.
|
||||
|
||||
Supported flags are:
|
||||
|
||||
selected: the point is currently selected
|
||||
highlight: the point is currently highlighted
|
||||
background: the point is background information
|
||||
*/
|
||||
|
||||
// for JS
|
||||
export const flagSelected = 1;
|
||||
export const flagNaN = 2;
|
||||
export const flagBackground = 2;
|
||||
export const flagHighlight = 4;
|
||||
|
||||
// for GLSL
|
||||
export const glPointFlags = `
|
||||
|
||||
const float flagSelected = 1.;
|
||||
const float flagNaN = 2.;
|
||||
const float flagBackground = 2.;
|
||||
const float flagHighlight = 4.;
|
||||
|
||||
bool isLowBitSet(float f) {
|
||||
@@ -32,12 +38,12 @@ export const glPointFlags = `
|
||||
}
|
||||
|
||||
void getFlags(in float flag,
|
||||
out bool isNaN,
|
||||
out bool isBackground,
|
||||
out bool isSelected,
|
||||
out bool isHighlight) {
|
||||
isSelected = isLowBitSet(flag);
|
||||
flag = shiftRightOne(flag);
|
||||
isNaN = isLowBitSet(flag);
|
||||
isBackground = isLowBitSet(flag);
|
||||
flag = shiftRightOne(flag);
|
||||
isHighlight = isLowBitSet(flag);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ create colors scale and RGB array and return as object. Parameters:
|
||||
* userColors - optional user color table
|
||||
Returns:
|
||||
{
|
||||
scale: color scale
|
||||
rgb: cell to color mapping
|
||||
scale: function, mapping label index to color scale
|
||||
rgb: cell label to color mapping
|
||||
}
|
||||
*/
|
||||
function _createColorTable(
|
||||
@@ -70,7 +70,7 @@ function _createColorTable(
|
||||
case "color by categorical metadata": {
|
||||
const data = colorByData.col(colorByAccessor).asArray();
|
||||
if (userColors && colorByAccessor in userColors) {
|
||||
return createUserColors(data, colorByAccessor, userColors);
|
||||
return createUserColors(data, colorByAccessor, schema, userColors);
|
||||
}
|
||||
return createColorsByCategoricalMetadata(data, colorByAccessor, schema);
|
||||
}
|
||||
@@ -91,42 +91,41 @@ function _createColorTable(
|
||||
}
|
||||
export const createColorTable = memoize(_createColorTable);
|
||||
|
||||
/**
|
||||
* Create two category label-indexed objects:
|
||||
* - colors: maps label to RGB triplet for that label (used by graph, etc)
|
||||
* - scale: function which given label returns d3 color scale for label
|
||||
* Order doesn't matter - everything is keyed by label value.
|
||||
*/
|
||||
export function loadUserColorConfig(userColors) {
|
||||
const convertedUserColors = {};
|
||||
Object.keys(userColors).forEach((category) => {
|
||||
// We cannot iterate over keys without sorting
|
||||
// because we handle categorical values in alphabetical order __ignoring case__
|
||||
// while Object.keys() _usually_ is ordered alphabetically where all upper characters are less than lowercase (A, B, C, a, b, c)
|
||||
const [colors, scaleMap] = Object.keys(userColors[category])
|
||||
.sort((a, b) => {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
if (a === b) return 0;
|
||||
if (a > b) return 1;
|
||||
return -1;
|
||||
})
|
||||
.reduce(
|
||||
(acc, label) => {
|
||||
const color = parseRGB(userColors[category][label]);
|
||||
acc[0][label] = color;
|
||||
acc[1][label] = d3.rgb(
|
||||
255 * color[0],
|
||||
255 * color[1],
|
||||
255 * color[2]
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[{}, {}]
|
||||
);
|
||||
const [colors, scaleMap] = Object.keys(userColors[category]).reduce(
|
||||
(acc, label) => {
|
||||
const color = parseRGB(userColors[category][label]);
|
||||
acc[0][label] = color;
|
||||
acc[1][label] = d3.rgb(255 * color[0], 255 * color[1], 255 * color[2]);
|
||||
return acc;
|
||||
},
|
||||
[{}, {}]
|
||||
);
|
||||
const scale = (label) => scaleMap[label];
|
||||
convertedUserColors[category] = { colors, scale };
|
||||
});
|
||||
return convertedUserColors;
|
||||
}
|
||||
|
||||
function _createUserColors(data, colorAccessor, userColors) {
|
||||
const { colors, scale } = userColors[colorAccessor];
|
||||
function _createUserColors(data, colorAccessor, schema, userColors) {
|
||||
const { colors, scale: scaleByLabel } = userColors[colorAccessor];
|
||||
const rgb = createRgbArray(data, colors);
|
||||
|
||||
// color scale function param is INDEX (offset) into schema categories. It is NOT label value.
|
||||
// See createColorsByCategoricalMetadata() for another example.
|
||||
const { categories } = schema.annotations.obsByName[colorAccessor];
|
||||
const categoryMap = new Map();
|
||||
categories.forEach((label, idx) => categoryMap.set(idx, label));
|
||||
const scale = (idx) => scaleByLabel(categoryMap.get(idx));
|
||||
|
||||
return { rgb, scale };
|
||||
}
|
||||
const createUserColors = memoize(_createUserColors);
|
||||
|
||||
Reference in New Issue
Block a user