@sjcrh/proteinpaint-server 2.135.3-1 → 2.137.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dataset/termdb.test.js +1 -0
- package/package.json +5 -6
- package/routes/filterTermValues.js +2 -1
- package/routes/grin2.js +245 -0
- package/routes/termdb.categories.js +14 -6
- package/routes/termdb.singlecellSamples.js +69 -57
- package/src/app.js +2261 -1956
- package/src/serverconfig.js +17 -1
package/dataset/termdb.test.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.137.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "a genomics visualization tool for exploring a cohort's genotype and phenotype data",
|
|
6
6
|
"main": "src/app.js",
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"babel-loader": "^8.2.2",
|
|
48
48
|
"c8": "^10.1.3",
|
|
49
49
|
"esbuild": "^0.19.12",
|
|
50
|
-
"glob": "^10.4.5",
|
|
51
50
|
"monocart-coverage-reports": "^2.12.1",
|
|
52
51
|
"node-notifier": "^9.0.1",
|
|
53
52
|
"node-watch": "^0.7.1",
|
|
@@ -60,12 +59,12 @@
|
|
|
60
59
|
"typescript": "^5.6.3"
|
|
61
60
|
},
|
|
62
61
|
"dependencies": {
|
|
63
|
-
"@sjcrh/augen": "2.
|
|
62
|
+
"@sjcrh/augen": "2.136.0",
|
|
64
63
|
"@sjcrh/proteinpaint-python": "2.135.2-0",
|
|
65
64
|
"@sjcrh/proteinpaint-r": "2.130.0",
|
|
66
|
-
"@sjcrh/proteinpaint-rust": "2.
|
|
67
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
68
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
65
|
+
"@sjcrh/proteinpaint-rust": "2.136.0",
|
|
66
|
+
"@sjcrh/proteinpaint-shared": "2.136.0",
|
|
67
|
+
"@sjcrh/proteinpaint-types": "2.137.0",
|
|
69
68
|
"@types/express": "^5.0.0",
|
|
70
69
|
"@types/express-session": "^1.18.1",
|
|
71
70
|
"better-sqlite3": "^9.4.1",
|
|
@@ -35,7 +35,8 @@ function getList(samplesPerFilter, filtersData, tw) {
|
|
|
35
35
|
for (const sample of twSamples) {
|
|
36
36
|
data.push(filtersData.samples[sample]);
|
|
37
37
|
}
|
|
38
|
-
const
|
|
38
|
+
const annotations = data.filter((s) => s != void 0).map((sample) => sample[tw.$id]?.value);
|
|
39
|
+
const sampleValues = Array.from(new Set(annotations));
|
|
39
40
|
for (const value of values) {
|
|
40
41
|
value.value = value.label;
|
|
41
42
|
const label = value.label;
|
package/routes/grin2.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { GRIN2Payload } from "#types/checkers";
|
|
2
|
+
import serverconfig from "#src/serverconfig.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
5
|
+
import { mayLog } from "#src/helpers.ts";
|
|
6
|
+
import { get_samples } from "#src/termdb.sql.js";
|
|
7
|
+
import { read_file, file_is_readable } from "#src/utils.js";
|
|
8
|
+
import { dtsnvindel, dtcnv, dtfusionrna } from "#shared/common.js";
|
|
9
|
+
const api = {
|
|
10
|
+
endpoint: "grin2",
|
|
11
|
+
methods: {
|
|
12
|
+
get: {
|
|
13
|
+
...GRIN2Payload,
|
|
14
|
+
init
|
|
15
|
+
},
|
|
16
|
+
post: {
|
|
17
|
+
...GRIN2Payload,
|
|
18
|
+
init
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function init({ genomes }) {
|
|
23
|
+
return async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const request = req.query;
|
|
26
|
+
const g = genomes[request.genome];
|
|
27
|
+
if (!g)
|
|
28
|
+
throw new Error("genome missing");
|
|
29
|
+
const ds = g.datasets?.[request.dslabel];
|
|
30
|
+
if (!ds)
|
|
31
|
+
throw new Error("ds missing");
|
|
32
|
+
if (!ds.queries?.singleSampleMutation)
|
|
33
|
+
throw new Error("singleSampleMutation query missing from dataset");
|
|
34
|
+
const result = await runGrin2(g, ds, request);
|
|
35
|
+
res.json(result);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error("[GRIN2] Error stack:", e.stack);
|
|
38
|
+
const errorResponse = {
|
|
39
|
+
status: "error",
|
|
40
|
+
error: e.message || String(e)
|
|
41
|
+
};
|
|
42
|
+
res.status(500).send(errorResponse);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function runGrin2(g, ds, request) {
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
mayLog("[GRIN2] Getting samples from cohort filter...");
|
|
49
|
+
const samples = await get_samples(
|
|
50
|
+
request.filter,
|
|
51
|
+
ds,
|
|
52
|
+
true
|
|
53
|
+
// must set to true to return sample name to be able to access file. FIXME this can let names revealed to grin2 client, may need to apply access control
|
|
54
|
+
);
|
|
55
|
+
const cohortTime = Date.now() - startTime;
|
|
56
|
+
mayLog(`[GRIN2] Retrieved ${samples.length.toLocaleString()} samples in ${Math.round(cohortTime / 1e3)} seconds`);
|
|
57
|
+
if (samples.length === 0) {
|
|
58
|
+
throw new Error("No samples found matching the provided filter criteria");
|
|
59
|
+
}
|
|
60
|
+
mayLog("[GRIN2] Processing sample data...");
|
|
61
|
+
const processingStartTime = Date.now();
|
|
62
|
+
const { lesionData, processingSummary } = await processSampleData(samples, ds, request);
|
|
63
|
+
const processingTime = Date.now() - processingStartTime;
|
|
64
|
+
const processingTimeToPrint = Math.round(processingTime / 1e3);
|
|
65
|
+
mayLog(`[GRIN2] Data processing took ${processingTimeToPrint} seconds`);
|
|
66
|
+
mayLog(
|
|
67
|
+
`[GRIN2] Processing summary: ${processingSummary?.successfulSamples ?? 0}/${processingSummary?.totalSamples ?? samples.length} samples processed successfully`
|
|
68
|
+
);
|
|
69
|
+
if (processingSummary && processingSummary.failedSamples > 0) {
|
|
70
|
+
mayLog(`[GRIN2] Warning: ${processingSummary.failedSamples} samples failed to process`);
|
|
71
|
+
}
|
|
72
|
+
if (lesionData.length === 0) {
|
|
73
|
+
throw new Error("No lesions found after processing all samples. Check filter criteria and input data.");
|
|
74
|
+
}
|
|
75
|
+
const pyInput = {
|
|
76
|
+
genedb: path.join(serverconfig.tpmasterdir, g.genedb.dbfile),
|
|
77
|
+
chromosomelist: {},
|
|
78
|
+
lesion: JSON.stringify(lesionData)
|
|
79
|
+
};
|
|
80
|
+
for (const c in g.majorchr) {
|
|
81
|
+
if (ds.queries.singleSampleMutation.discoPlot?.skipChrM) {
|
|
82
|
+
if (c.toLowerCase() == "chrm")
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
pyInput.chromosomelist[c] = g.majorchr[c];
|
|
86
|
+
}
|
|
87
|
+
mayLog(`[GRIN2] Prepared ${lesionData.length.toLocaleString()} lesions for analysis`);
|
|
88
|
+
const grin2AnalysisStart = Date.now();
|
|
89
|
+
const pyResult = await run_python("grin2PpWrapper.py", JSON.stringify(pyInput));
|
|
90
|
+
if (pyResult.stderr?.trim()) {
|
|
91
|
+
mayLog(`[GRIN2] Python stderr: ${pyResult.stderr}`);
|
|
92
|
+
if (pyResult.stderr.includes("ERROR:")) {
|
|
93
|
+
throw new Error(`Python script error: ${pyResult.stderr}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const grin2AnalysisTime = Date.now() - grin2AnalysisStart;
|
|
97
|
+
const grin2AnalysisTimeToPrint = Math.round(grin2AnalysisTime / 1e3);
|
|
98
|
+
mayLog(`[GRIN2] Python processing took ${grin2AnalysisTimeToPrint} seconds`);
|
|
99
|
+
const resultData = JSON.parse(pyResult.stdout);
|
|
100
|
+
if (!resultData?.png?.[0]) {
|
|
101
|
+
throw new Error("Invalid Python output: missing PNG data");
|
|
102
|
+
}
|
|
103
|
+
const totalTime = Math.round((Date.now() - startTime) / 1e3);
|
|
104
|
+
const response = {
|
|
105
|
+
status: "success",
|
|
106
|
+
pngImg: resultData.png[0],
|
|
107
|
+
topGeneTable: resultData.topGeneTable,
|
|
108
|
+
totalGenes: resultData.totalGenes,
|
|
109
|
+
showingTop: resultData.showingTop,
|
|
110
|
+
timing: {
|
|
111
|
+
processingTime: processingTimeToPrint,
|
|
112
|
+
grin2Time: grin2AnalysisTimeToPrint,
|
|
113
|
+
totalTime
|
|
114
|
+
},
|
|
115
|
+
processingSummary
|
|
116
|
+
};
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
async function processSampleData(samples, ds, request) {
|
|
120
|
+
const lesions = [];
|
|
121
|
+
let lesionId = 1;
|
|
122
|
+
const processingSummary = {
|
|
123
|
+
totalSamples: samples.length,
|
|
124
|
+
successfulSamples: 0,
|
|
125
|
+
failedSamples: 0,
|
|
126
|
+
failedFiles: []
|
|
127
|
+
};
|
|
128
|
+
mayLog(`[GRIN2] Processing JSON files for ${samples.length.toLocaleString()} samples`);
|
|
129
|
+
for (const sample of samples) {
|
|
130
|
+
try {
|
|
131
|
+
const filepath = path.join(serverconfig.tpmasterdir, ds.queries.singleSampleMutation.folder, sample.name);
|
|
132
|
+
await file_is_readable(filepath);
|
|
133
|
+
const mlst = JSON.parse(await read_file(filepath));
|
|
134
|
+
const sampleLesions = await processSampleMlst(sample.name, mlst, lesionId, request);
|
|
135
|
+
lesions.push(...sampleLesions);
|
|
136
|
+
lesionId += sampleLesions.length;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
mayLog(
|
|
139
|
+
`[GRIN2] Error processing sample ${sample.name}: ${typeof error === "object" && error !== null && "message" in error ? error.message : String(error)}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
mayLog(`[GRIN2] Total lesions processed: ${lesions.length.toLocaleString()}`);
|
|
144
|
+
return {
|
|
145
|
+
lesionData: lesions,
|
|
146
|
+
processingSummary
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function processSampleMlst(sampleName, mlst, startId, request) {
|
|
150
|
+
const lesions = [];
|
|
151
|
+
for (const m of mlst) {
|
|
152
|
+
switch (m.dt) {
|
|
153
|
+
case dtsnvindel: {
|
|
154
|
+
if (!request.snvindelOptions)
|
|
155
|
+
break;
|
|
156
|
+
const snvIndelLesion = filterAndConvertSnvIndel(sampleName, m, request.snvindelOptions);
|
|
157
|
+
if (snvIndelLesion)
|
|
158
|
+
lesions.push(snvIndelLesion);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case dtcnv: {
|
|
162
|
+
if (!request.cnvOptions)
|
|
163
|
+
break;
|
|
164
|
+
const cnvLesion = filterAndConvertCnv(sampleName, m, request.cnvOptions);
|
|
165
|
+
if (cnvLesion)
|
|
166
|
+
lesions.push(cnvLesion);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case dtfusionrna: {
|
|
170
|
+
if (!request.fusionOptions)
|
|
171
|
+
break;
|
|
172
|
+
const fusionLesion = filterAndConvertFusion(sampleName, m, request.fusionOptions);
|
|
173
|
+
if (fusionLesion)
|
|
174
|
+
lesions.push(fusionLesion);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
mayLog(`[GRIN2] Unknown data type "${m.dt}" in sample ${sampleName}, skipping entry`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return lesions;
|
|
183
|
+
}
|
|
184
|
+
function filterAndConvertSnvIndel(sampleName, entry, options) {
|
|
185
|
+
const opts = {
|
|
186
|
+
minTotalDepth: options?.minTotalDepth ?? 10,
|
|
187
|
+
minAltAlleleCount: options?.minAltAlleleCount ?? 2,
|
|
188
|
+
consequences: options?.consequences ?? [],
|
|
189
|
+
hyperMutator: options?.hyperMutator ?? 1e3
|
|
190
|
+
};
|
|
191
|
+
if (opts.consequences.length > 0 && entry.consequence && !opts.consequences.includes(entry.consequence)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
sampleName,
|
|
196
|
+
normalizeChromosome(entry.chromosome || entry.chr),
|
|
197
|
+
String(entry.start || entry.position),
|
|
198
|
+
String(entry.end || entry.position),
|
|
199
|
+
"mutation"
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
function filterAndConvertCnv(sampleName, entry, options) {
|
|
203
|
+
const opts = {
|
|
204
|
+
lossThreshold: options?.lossThreshold ?? -0.4,
|
|
205
|
+
gainThreshold: options?.gainThreshold ?? 0.3,
|
|
206
|
+
maxSegLength: options?.maxSegLength ?? 0,
|
|
207
|
+
minSegLength: options?.minSegLength ?? 0,
|
|
208
|
+
hyperMutator: options?.hyperMutator ?? 500
|
|
209
|
+
};
|
|
210
|
+
const isGain = entry.log2Ratio >= opts.gainThreshold;
|
|
211
|
+
const isLoss = entry.log2Ratio <= opts.lossThreshold;
|
|
212
|
+
if (!isGain && !isLoss)
|
|
213
|
+
return null;
|
|
214
|
+
const lesionType = entry.log2Ratio >= opts.gainThreshold ? "gain" : "loss";
|
|
215
|
+
return [
|
|
216
|
+
sampleName,
|
|
217
|
+
normalizeChromosome(entry.chromosome || entry.chr),
|
|
218
|
+
String(entry.start || entry.begin),
|
|
219
|
+
String(entry.end || entry.stop),
|
|
220
|
+
lesionType
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
function filterAndConvertFusion(sampleName, entry, options) {
|
|
224
|
+
const opts = {
|
|
225
|
+
fusionTypes: options?.fusionTypes ?? ["gene-gene", "gene-intergenic", "readthrough"],
|
|
226
|
+
minConfidence: options?.minConfidence ?? 0.7
|
|
227
|
+
};
|
|
228
|
+
if (entry.fusionType && !opts.fusionTypes.includes(entry.fusionType))
|
|
229
|
+
return null;
|
|
230
|
+
if (entry.confidence && entry.confidence < opts.minConfidence)
|
|
231
|
+
return null;
|
|
232
|
+
return [
|
|
233
|
+
sampleName,
|
|
234
|
+
normalizeChromosome(entry.chromosome || entry.chr),
|
|
235
|
+
String(entry.start || entry.position),
|
|
236
|
+
String(entry.end || entry.position),
|
|
237
|
+
"fusion"
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
function normalizeChromosome(chrom) {
|
|
241
|
+
return chrom.startsWith("chr") ? chrom : `chr${chrom}`;
|
|
242
|
+
}
|
|
243
|
+
export {
|
|
244
|
+
api
|
|
245
|
+
};
|
|
@@ -51,12 +51,20 @@ async function trigger_getcategories(q, res, tdb, ds, genome) {
|
|
|
51
51
|
const data = await getData(arg, ds, genome);
|
|
52
52
|
if (data.error)
|
|
53
53
|
throw data.error;
|
|
54
|
+
const [lst, orderedLabels] = getCategories(data, q, ds, $id);
|
|
55
|
+
res.send({
|
|
56
|
+
lst,
|
|
57
|
+
orderedLabels
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function getCategories(data, q, ds, $id) {
|
|
54
61
|
const lst = [];
|
|
55
62
|
if (q.tw.term.type == "geneVariant" && q.tw.q.type != "predefined-groupset" && q.tw.q.type != "custom-groupset") {
|
|
56
63
|
const samples = data.samples;
|
|
57
64
|
const dtClassMap = /* @__PURE__ */ new Map();
|
|
58
65
|
if (ds.assayAvailability?.byDt) {
|
|
59
|
-
for (const [dtType,
|
|
66
|
+
for (const [dtType, _dtValue] of Object.entries(ds.assayAvailability.byDt)) {
|
|
67
|
+
const dtValue = _dtValue;
|
|
60
68
|
if (dtValue.byOrigin) {
|
|
61
69
|
dtClassMap.set(parseInt(dtType), { byOrigin: { germline: {}, somatic: {} } });
|
|
62
70
|
}
|
|
@@ -65,6 +73,8 @@ async function trigger_getcategories(q, res, tdb, ds, genome) {
|
|
|
65
73
|
const sampleCountedFor = /* @__PURE__ */ new Set();
|
|
66
74
|
for (const sampleData of Object.values(samples)) {
|
|
67
75
|
const key = $id;
|
|
76
|
+
if (!Object.keys(sampleData).includes(key))
|
|
77
|
+
continue;
|
|
68
78
|
const values = sampleData[key].values;
|
|
69
79
|
sampleCountedFor.clear();
|
|
70
80
|
for (const value of values) {
|
|
@@ -126,11 +136,9 @@ async function trigger_getcategories(q, res, tdb, ds, genome) {
|
|
|
126
136
|
if (orderedLabels.length) {
|
|
127
137
|
lst.sort((a, b) => orderedLabels.indexOf(a.label) - orderedLabels.indexOf(b.label));
|
|
128
138
|
}
|
|
129
|
-
|
|
130
|
-
lst,
|
|
131
|
-
orderedLabels
|
|
132
|
-
});
|
|
139
|
+
return [lst, orderedLabels];
|
|
133
140
|
}
|
|
134
141
|
export {
|
|
135
|
-
api
|
|
142
|
+
api,
|
|
143
|
+
getCategories
|
|
136
144
|
};
|
|
@@ -51,9 +51,14 @@ async function validate_query_singleCell(ds, genome) {
|
|
|
51
51
|
const q = ds.queries.singleCell;
|
|
52
52
|
if (!q)
|
|
53
53
|
return;
|
|
54
|
-
if (typeof q.samples
|
|
54
|
+
if (typeof q.samples != "object")
|
|
55
|
+
throw "singleCell.samples{} not object";
|
|
56
|
+
if (typeof q.samples.get == "function") {
|
|
57
|
+
} else {
|
|
55
58
|
validateSamplesNative(q.samples, q.data, ds);
|
|
56
59
|
}
|
|
60
|
+
if (typeof q.data != "object")
|
|
61
|
+
throw "singleCell.data{} not object";
|
|
57
62
|
if (q.data.src == "gdcapi") {
|
|
58
63
|
gdc_validate_query_singleCell_data(ds, genome);
|
|
59
64
|
} else if (q.data.src == "native") {
|
|
@@ -62,6 +67,8 @@ async function validate_query_singleCell(ds, genome) {
|
|
|
62
67
|
throw "unknown singleCell.data.src";
|
|
63
68
|
}
|
|
64
69
|
if (q.geneExpression) {
|
|
70
|
+
if (typeof q.geneExpression != "object")
|
|
71
|
+
throw "singleCell.geneExpression not object";
|
|
65
72
|
if (q.geneExpression.src == "native") {
|
|
66
73
|
validateGeneExpressionNative(q.geneExpression);
|
|
67
74
|
} else if (q.geneExpression.src == "gdcapi") {
|
|
@@ -71,9 +78,13 @@ async function validate_query_singleCell(ds, genome) {
|
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
80
|
if (q.DEgenes) {
|
|
81
|
+
if (typeof q.DEgenes != "object")
|
|
82
|
+
throw "singleCell.DEgenes not object";
|
|
74
83
|
validate_query_singleCell_DEgenes(ds);
|
|
75
84
|
}
|
|
76
85
|
if (q.images) {
|
|
86
|
+
if (typeof q.images != "object")
|
|
87
|
+
throw "singleCell.images not object";
|
|
77
88
|
validateImages(q.images);
|
|
78
89
|
}
|
|
79
90
|
}
|
|
@@ -127,68 +138,69 @@ function validateDataNative(D, ds) {
|
|
|
127
138
|
if (nameSet.has(plot.name))
|
|
128
139
|
throw "duplicate plot.name";
|
|
129
140
|
nameSet.add(plot.name);
|
|
141
|
+
if (!plot.folder)
|
|
142
|
+
throw "plot.folder missing";
|
|
130
143
|
}
|
|
144
|
+
const file2Lines = {};
|
|
131
145
|
D.get = async (q) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const expCells = [];
|
|
154
|
-
const noExpCells = [];
|
|
155
|
-
for (let i = 1; i < lines.length; i++) {
|
|
156
|
-
const l = lines[i].split(" ");
|
|
157
|
-
const cellId = lines.length > 3 ? l[0] : void 0, x = Number(l[plot.coordsColumns.x]), y = Number(l[plot.coordsColumns.y]);
|
|
158
|
-
const category = l[colorColumn?.index] || "";
|
|
159
|
-
if (!cellId)
|
|
160
|
-
throw "cell id missing";
|
|
161
|
-
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
162
|
-
throw "x/y not number";
|
|
163
|
-
const cell = { cellId, x, y, category };
|
|
164
|
-
if (geneExpMap) {
|
|
165
|
-
if (geneExpMap[cellId] !== void 0) {
|
|
166
|
-
cell.geneExp = geneExpMap[cellId];
|
|
167
|
-
expCells.push(cell);
|
|
168
|
-
} else {
|
|
169
|
-
noExpCells.push(cell);
|
|
170
|
-
}
|
|
171
|
-
} else
|
|
172
|
-
noExpCells.push(cell);
|
|
146
|
+
const plots = [];
|
|
147
|
+
let geneExpMap;
|
|
148
|
+
if (ds.queries.singleCell.geneExpression && q.gene) {
|
|
149
|
+
geneExpMap = await ds.queries.singleCell.geneExpression.get({ sample: q.sample, gene: q.gene });
|
|
150
|
+
}
|
|
151
|
+
for (const plot of D.plots) {
|
|
152
|
+
if (!q.plots.includes(plot.name))
|
|
153
|
+
continue;
|
|
154
|
+
const tsvfile = path.join(serverconfig.tpmasterdir, plot.folder, (q.sample.eID || q.sample.sID) + plot.fileSuffix);
|
|
155
|
+
if (!file2Lines[tsvfile]) {
|
|
156
|
+
await file_is_readable(tsvfile);
|
|
157
|
+
const text = await read_file(tsvfile);
|
|
158
|
+
const lines = text.trim().split("\n");
|
|
159
|
+
let first = true;
|
|
160
|
+
const lines2 = [];
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
if (first) {
|
|
163
|
+
first = false;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
lines2.push(line.split(" "));
|
|
173
167
|
}
|
|
174
|
-
|
|
175
|
-
name: plot.name,
|
|
176
|
-
expCells,
|
|
177
|
-
noExpCells,
|
|
178
|
-
colorColumns: plot.colorColumns.map((c) => c.name),
|
|
179
|
-
colorBy: colorColumn?.name,
|
|
180
|
-
colorMap: colorColumn?.colorMap
|
|
181
|
-
});
|
|
168
|
+
file2Lines[tsvfile] = lines2;
|
|
182
169
|
}
|
|
183
|
-
|
|
184
|
-
|
|
170
|
+
const colorColumn = plot.colorColumns.find((c) => c.name == q.colorBy?.[plot.name]) || plot.colorColumns[0];
|
|
171
|
+
const expCells = [];
|
|
172
|
+
const noExpCells = [];
|
|
173
|
+
for (const l of file2Lines[tsvfile]) {
|
|
174
|
+
const cellId = l[0], x = Number(l[plot.coordsColumns.x]), y = Number(l[plot.coordsColumns.y]);
|
|
175
|
+
const category = l[colorColumn?.index] || "";
|
|
176
|
+
if (!cellId)
|
|
177
|
+
throw "cell id missing";
|
|
178
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
179
|
+
throw "x/y not number";
|
|
180
|
+
const cell = { cellId, x, y, category };
|
|
181
|
+
if (geneExpMap) {
|
|
182
|
+
if (geneExpMap[cellId] !== void 0) {
|
|
183
|
+
cell.geneExp = geneExpMap[cellId];
|
|
184
|
+
expCells.push(cell);
|
|
185
|
+
} else {
|
|
186
|
+
noExpCells.push(cell);
|
|
187
|
+
}
|
|
188
|
+
} else
|
|
189
|
+
noExpCells.push(cell);
|
|
185
190
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
plots.push({
|
|
192
|
+
name: plot.name,
|
|
193
|
+
expCells,
|
|
194
|
+
noExpCells,
|
|
195
|
+
colorColumns: plot.colorColumns.map((c) => c.name),
|
|
196
|
+
colorBy: colorColumn?.name,
|
|
197
|
+
colorMap: colorColumn?.colorMap
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (plots.length == 0) {
|
|
201
|
+
return { nodata: true };
|
|
191
202
|
}
|
|
203
|
+
return { plots };
|
|
192
204
|
};
|
|
193
205
|
}
|
|
194
206
|
function validateGeneExpressionNative(G) {
|