@sjcrh/proteinpaint-server 2.180.1 → 2.181.1
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 +7 -1
- package/package.json +7 -7
- package/routes/profile.polar2.js +101 -0
- package/routes/termdb.cluster.js +111 -3
- package/routes/termdb.config.js +13 -5
- package/routes/termdb.diffMeth.js +23 -12
- package/routes/termdb.dmr.js +52 -18
- package/routes/termdb.isoformAvailability.js +35 -0
- package/routes/termdb.profileFormScores.js +15 -4
- package/routes/termdb.profileScores.js +9 -1
- package/routes/termdb.sampleScatter.js +14 -2
- package/routes/termdb.violinBox.js +509 -0
- package/src/app.js +1512 -1158
- package/routes/termdb.boxplot.js +0 -260
- package/routes/termdb.violin.js +0 -285
package/dataset/termdb.test.js
CHANGED
|
@@ -60,7 +60,9 @@ function termdb_test_default() {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
]
|
|
63
|
-
}
|
|
63
|
+
},
|
|
64
|
+
/** Test by commenting out cohort.termdb.cohort.termdb.selectCohort */
|
|
65
|
+
html: "<div>Demo HTML Tab Content</div>"
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
},
|
|
@@ -383,6 +385,10 @@ function termdb_test_default() {
|
|
|
383
385
|
file: "files/hg38/TermdbTest/rnaseq/TermdbTest.fpkm.matrix.new.h5",
|
|
384
386
|
unit: "FPKM"
|
|
385
387
|
},
|
|
388
|
+
isoformExpression: {
|
|
389
|
+
file: "files/hg38/TermdbTest/rnaseq/TermdbTest.isoform.tpm.h5",
|
|
390
|
+
unit: "TPM"
|
|
391
|
+
},
|
|
386
392
|
ssGSEA: {
|
|
387
393
|
file: "files/hg38/TermdbTest/rnaseq/TermdbTest.ssgsea.h5"
|
|
388
394
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.181.1",
|
|
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",
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
"typescript": "^5.6.3"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@sjcrh/augen": "2.
|
|
65
|
-
"@sjcrh/proteinpaint-python": "2.
|
|
66
|
-
"@sjcrh/proteinpaint-r": "2.
|
|
67
|
-
"@sjcrh/proteinpaint-rust": "2.
|
|
68
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
69
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
64
|
+
"@sjcrh/augen": "2.181.1",
|
|
65
|
+
"@sjcrh/proteinpaint-python": "2.181.0",
|
|
66
|
+
"@sjcrh/proteinpaint-r": "2.181.0",
|
|
67
|
+
"@sjcrh/proteinpaint-rust": "2.181.0",
|
|
68
|
+
"@sjcrh/proteinpaint-shared": "2.181.0",
|
|
69
|
+
"@sjcrh/proteinpaint-types": "2.181.0",
|
|
70
70
|
"@types/express": "^5.0.0",
|
|
71
71
|
"@types/express-session": "^1.18.1",
|
|
72
72
|
"better-sqlite3": "^12.4.1",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ProfileScoresPayload } from "#types/checkers";
|
|
2
|
+
import { getData } from "../src/termdb.matrix.js";
|
|
3
|
+
const api = {
|
|
4
|
+
endpoint: "termdb/profilePolar2Scores",
|
|
5
|
+
methods: {
|
|
6
|
+
get: {
|
|
7
|
+
...ProfileScoresPayload,
|
|
8
|
+
init
|
|
9
|
+
},
|
|
10
|
+
post: {
|
|
11
|
+
...ProfileScoresPayload,
|
|
12
|
+
init
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function init({ genomes }) {
|
|
17
|
+
return async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const g = genomes[req.query.genome];
|
|
20
|
+
if (!g) throw "invalid genome name";
|
|
21
|
+
const ds = g.datasets?.[req.query.dslabel];
|
|
22
|
+
const result = await getScores(req.query, ds);
|
|
23
|
+
res.send(result);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.log(e);
|
|
26
|
+
res.send({ status: "error", error: e.message || e });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function derivePrefix(query) {
|
|
31
|
+
const firstScoreId = query.scoreTerms?.[0]?.score?.term?.id;
|
|
32
|
+
if (firstScoreId?.startsWith("F")) return "F";
|
|
33
|
+
if (firstScoreId?.startsWith("A")) return "A";
|
|
34
|
+
for (const entry of query.filter?.lst || []) {
|
|
35
|
+
const id = entry.tvs?.term?.id;
|
|
36
|
+
if (id?.startsWith("F")) return "F";
|
|
37
|
+
if (id?.startsWith("A")) return "A";
|
|
38
|
+
}
|
|
39
|
+
throw "cannot determine cohort prefix from scoreTerms or filter term IDs";
|
|
40
|
+
}
|
|
41
|
+
async function getScores(query, ds) {
|
|
42
|
+
const { activeCohort, clientAuthResult } = query.__protected__;
|
|
43
|
+
const prefix = derivePrefix(query);
|
|
44
|
+
const facilityTermId = `${prefix}UNIT`;
|
|
45
|
+
const facilityTW = { term: { id: facilityTermId }, q: {} };
|
|
46
|
+
const terms = [facilityTW];
|
|
47
|
+
for (const t of query.scoreTerms) {
|
|
48
|
+
terms.push(t.score);
|
|
49
|
+
if (t.maxScore?.term) terms.push(t.maxScore);
|
|
50
|
+
}
|
|
51
|
+
if (!query.filterByUserSites) {
|
|
52
|
+
query.__protected__.ignoredTermIds.push(facilityTermId);
|
|
53
|
+
}
|
|
54
|
+
const cohortAuth = clientAuthResult[activeCohort];
|
|
55
|
+
const isPublic = !cohortAuth?.role || cohortAuth.role === "public";
|
|
56
|
+
const userSites = cohortAuth?.sites;
|
|
57
|
+
const raw = await getData(
|
|
58
|
+
{
|
|
59
|
+
terms,
|
|
60
|
+
filter: query.filter,
|
|
61
|
+
__protected__: query.__protected__
|
|
62
|
+
},
|
|
63
|
+
ds
|
|
64
|
+
);
|
|
65
|
+
if (raw.error) throw raw.error;
|
|
66
|
+
const sampleList = Object.values(raw.samples);
|
|
67
|
+
let sites = sampleList.map((s) => {
|
|
68
|
+
const val = s[facilityTW.$id].value;
|
|
69
|
+
let label = facilityTW.term.values?.[val]?.label || val;
|
|
70
|
+
if (label.length > 50) label = label.slice(0, 47) + "...";
|
|
71
|
+
return { value: val, label };
|
|
72
|
+
});
|
|
73
|
+
if (userSites && query.filterByUserSites) {
|
|
74
|
+
sites = sites.filter((s) => userSites.includes(s.value));
|
|
75
|
+
}
|
|
76
|
+
sites.sort((a, b) => a.label.localeCompare(b.label));
|
|
77
|
+
const samples = Object.values(raw.samples);
|
|
78
|
+
const eligibleSamples = userSites && query.filterByUserSites ? samples.filter((s) => userSites.includes(s[facilityTW.$id].value)) : samples;
|
|
79
|
+
const term2Score = {};
|
|
80
|
+
for (const d of query.scoreTerms) {
|
|
81
|
+
const score = computeMedianPercentage(d, eligibleSamples);
|
|
82
|
+
if (score !== null) term2Score[d.score.term.id] = score;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
term2Score,
|
|
86
|
+
// Public users see only aggregated scores — do not expose site IDs or names
|
|
87
|
+
sites: isPublic ? [] : sites,
|
|
88
|
+
n: eligibleSamples.length
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function computeMedianPercentage(d, samples) {
|
|
92
|
+
const percentages = samples.filter((s) => s[d.score.$id]?.value).map((s) => s[d.score.$id].value / (s[d.maxScore.$id]?.value || d.maxScore) * 100);
|
|
93
|
+
if (percentages.length === 0) return null;
|
|
94
|
+
percentages.sort((a, b) => a - b);
|
|
95
|
+
const mid = Math.floor(percentages.length / 2);
|
|
96
|
+
const median = percentages.length % 2 !== 0 ? percentages[mid] : (percentages[mid - 1] + percentages[mid]) / 2;
|
|
97
|
+
return Math.round(median);
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
api
|
|
101
|
+
};
|
package/routes/termdb.cluster.js
CHANGED
|
@@ -7,7 +7,7 @@ import serverconfig from "#src/serverconfig.js";
|
|
|
7
7
|
import { gdc_validate_query_geneExpression } from "#src/mds3.gdc.js";
|
|
8
8
|
import { mayLimitSamples } from "#src/mds3.filter.js";
|
|
9
9
|
import { clusterMethodLst, distanceMethodLst } from "#shared/clustering.js";
|
|
10
|
-
import { TermTypes, PROTEOME_ABUNDANCE } from "#shared/terms.js";
|
|
10
|
+
import { TermTypes, ISOFORM_EXPRESSION, PROTEOME_ABUNDANCE } from "#shared/terms.js";
|
|
11
11
|
import { termType2label } from "#shared/terms.js";
|
|
12
12
|
import { formatElapsedTime } from "#shared/time.js";
|
|
13
13
|
const api = {
|
|
@@ -34,7 +34,7 @@ function init({ genomes }) {
|
|
|
34
34
|
if (!ds) throw "invalid dataset name";
|
|
35
35
|
if (ds.label === "GDC" && !ds.__gdc?.doneCaching)
|
|
36
36
|
throw "The server has not finished caching the case IDs: try again in about 2 minutes.";
|
|
37
|
-
if ([TermTypes.GENE_EXPRESSION, TermTypes.METABOLITE_INTENSITY].includes(q.dataType)) {
|
|
37
|
+
if ([TermTypes.GENE_EXPRESSION, ISOFORM_EXPRESSION, TermTypes.METABOLITE_INTENSITY].includes(q.dataType)) {
|
|
38
38
|
if (!ds.queries?.[q.dataType]) throw `no ${q.dataType} data on this dataset`;
|
|
39
39
|
if (!q.terms) throw `missing gene list`;
|
|
40
40
|
if (!Array.isArray(q.terms)) throw `gene list is not an array`;
|
|
@@ -81,7 +81,7 @@ async function getResult(q, ds) {
|
|
|
81
81
|
for (const [term, obj] of term2sample2value) {
|
|
82
82
|
if (Object.keys(obj).length === 0) {
|
|
83
83
|
const tw = q.terms.find((t2) => t2.$id == term);
|
|
84
|
-
const termName = !tw ? term : tw.term.type == "geneExpression" ? tw.term.gene : tw.term.name;
|
|
84
|
+
const termName = !tw ? term : tw.term.type == "geneExpression" ? tw.term.gene : tw.term.type == "isoformExpression" ? tw.term.isoform : tw.term.name;
|
|
85
85
|
noValueTerms.push(termName);
|
|
86
86
|
term2sample2value.delete(term);
|
|
87
87
|
delete byTermId[term];
|
|
@@ -309,7 +309,115 @@ async function validateNative(q, ds) {
|
|
|
309
309
|
return { term2sample2value, byTermId, bySampleId };
|
|
310
310
|
};
|
|
311
311
|
}
|
|
312
|
+
async function validateQueryIsoformExpression(ds, _genome) {
|
|
313
|
+
const q = ds.queries.isoformExpression;
|
|
314
|
+
if (!q) return;
|
|
315
|
+
q.geneExpression2bins = {};
|
|
316
|
+
if (typeof q.get == "function") return;
|
|
317
|
+
if (q.file) {
|
|
318
|
+
await validateNativeIsoform(q, ds);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
throw "isoformExpression requires either .get() or .file";
|
|
322
|
+
}
|
|
323
|
+
async function validateNativeIsoform(q, ds) {
|
|
324
|
+
q.file = path.join(serverconfig.tpmasterdir, q.file);
|
|
325
|
+
q.samples = [];
|
|
326
|
+
try {
|
|
327
|
+
await utils.file_is_readable(q.file);
|
|
328
|
+
const tmp = await run_rust("readH5", JSON.stringify({ hdf5_file: q.file, validate: true, include_items: true }));
|
|
329
|
+
const vr = JSON.parse(tmp);
|
|
330
|
+
if (vr.status !== "success") throw vr.message;
|
|
331
|
+
if (!vr.samples?.length) throw "HDF5 file has no samples, please check file.";
|
|
332
|
+
for (const sn of vr.samples) {
|
|
333
|
+
const si = ds.cohort.termdb.q.sampleName2id(sn);
|
|
334
|
+
if (si == void 0) {
|
|
335
|
+
if (ds.cohort.db) {
|
|
336
|
+
throw `unknown sample ${sn} from HDF5 ${q.file}`;
|
|
337
|
+
} else {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
q.samples.push(si);
|
|
342
|
+
}
|
|
343
|
+
q.availableItems = vr.items || [];
|
|
344
|
+
console.log(
|
|
345
|
+
`${ds.label}: isoformExpression HDF5 file validated. Format: ${vr.format}, Samples:`,
|
|
346
|
+
q.samples.length,
|
|
347
|
+
"Items:",
|
|
348
|
+
q.availableItems.length
|
|
349
|
+
);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw `${ds.label}: Failed to validate isoformExpression HDF5 file: ${error}`;
|
|
352
|
+
}
|
|
353
|
+
q.get = async (param) => {
|
|
354
|
+
const limitSamples = await mayLimitSamples(param, q.samples, ds);
|
|
355
|
+
if (limitSamples?.size == 0) {
|
|
356
|
+
return { term2sample2value: /* @__PURE__ */ new Map(), byTermId: {}, bySampleId: {} };
|
|
357
|
+
}
|
|
358
|
+
const bySampleId = {};
|
|
359
|
+
const samples = q.samples || [];
|
|
360
|
+
if (limitSamples) {
|
|
361
|
+
for (const sid of limitSamples) {
|
|
362
|
+
if (ds.cohort?.termdb?.q?.id2sampleRefs) {
|
|
363
|
+
bySampleId[sid] = ds.cohort.termdb.q.id2sampleRefs(sid);
|
|
364
|
+
} else {
|
|
365
|
+
bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
for (const sid of samples) {
|
|
370
|
+
if (ds.cohort?.termdb?.q?.id2sampleRefs) {
|
|
371
|
+
bySampleId[sid] = ds.cohort.termdb.q.id2sampleRefs(sid);
|
|
372
|
+
} else {
|
|
373
|
+
bySampleId[sid] = { label: ds.cohort.termdb.q.id2sampleName(sid) };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const term2sample2value = /* @__PURE__ */ new Map();
|
|
378
|
+
const byTermId = {};
|
|
379
|
+
const isoformIds = [];
|
|
380
|
+
for (const tw of param.terms) {
|
|
381
|
+
if (tw.term.isoform) {
|
|
382
|
+
isoformIds.push(tw.term.isoform);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (isoformIds.length === 0) {
|
|
386
|
+
console.log("No isoforms to query");
|
|
387
|
+
return { term2sample2value, byTermId };
|
|
388
|
+
}
|
|
389
|
+
const time1 = Date.now();
|
|
390
|
+
const isoformData = JSON.parse(await queryHDF5(q.file, isoformIds, null));
|
|
391
|
+
console.log("Time taken to run isoform query:", formatElapsedTime(Date.now() - time1));
|
|
392
|
+
const isoformsData = isoformData.query_output || {};
|
|
393
|
+
if (!isoformsData) throw "No expression data returned from HDF5 query";
|
|
394
|
+
for (const tw of param.terms) {
|
|
395
|
+
if (!tw.term.isoform) continue;
|
|
396
|
+
const isoformResult = isoformsData[tw.term.isoform];
|
|
397
|
+
if (!isoformResult) {
|
|
398
|
+
console.warn(`No data found for isoform ${tw.term.isoform} in the response`);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const samplesData = isoformResult.samples || {};
|
|
402
|
+
const s2v = {};
|
|
403
|
+
for (const sampleName in samplesData) {
|
|
404
|
+
const sampleId = ds.cohort.termdb.q.sampleName2id(sampleName);
|
|
405
|
+
if (!sampleId) continue;
|
|
406
|
+
if (limitSamples && !limitSamples.has(sampleId)) continue;
|
|
407
|
+
s2v[sampleId] = samplesData[sampleName];
|
|
408
|
+
}
|
|
409
|
+
if (Object.keys(s2v).length) {
|
|
410
|
+
term2sample2value.set(tw.$id, s2v);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (term2sample2value.size == 0) {
|
|
414
|
+
throw "No data available for the input " + param.terms?.map((tw) => tw.term.isoform).join(", ");
|
|
415
|
+
}
|
|
416
|
+
return { term2sample2value, byTermId, bySampleId };
|
|
417
|
+
};
|
|
418
|
+
}
|
|
312
419
|
export {
|
|
313
420
|
api,
|
|
421
|
+
validateQueryIsoformExpression,
|
|
314
422
|
validate_query_geneExpression
|
|
315
423
|
};
|
package/routes/termdb.config.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TermTypeGroups,
|
|
6
6
|
SINGLECELL_CELLTYPE,
|
|
7
7
|
GENE_EXPRESSION,
|
|
8
|
+
ISOFORM_EXPRESSION,
|
|
8
9
|
METABOLITE_INTENSITY,
|
|
9
10
|
PROTEOME_ABUNDANCE,
|
|
10
11
|
SINGLECELL_GENE_EXPRESSION,
|
|
@@ -162,7 +163,8 @@ function addNonDictionaryQueries(c, ds, genome) {
|
|
|
162
163
|
"cnvCutoffsByGene",
|
|
163
164
|
"absoluteValueRenderMax",
|
|
164
165
|
"gainColor",
|
|
165
|
-
"lossColor"
|
|
166
|
+
"lossColor",
|
|
167
|
+
"requiresHardcodeCnvOnlyFlag"
|
|
166
168
|
]) {
|
|
167
169
|
if (k in q.cnv) q2.cnv[k] = q.cnv[k];
|
|
168
170
|
}
|
|
@@ -187,6 +189,9 @@ function addNonDictionaryQueries(c, ds, genome) {
|
|
|
187
189
|
if (q.geneExpression) {
|
|
188
190
|
q2.geneExpression = { unit: q.geneExpression.unit };
|
|
189
191
|
}
|
|
192
|
+
if (q.isoformExpression) {
|
|
193
|
+
q2.isoformExpression = { unit: q.isoformExpression.unit };
|
|
194
|
+
}
|
|
190
195
|
if (q.proteome) {
|
|
191
196
|
q2.proteome = {};
|
|
192
197
|
if (q.proteome.assays) {
|
|
@@ -197,10 +202,12 @@ function addNonDictionaryQueries(c, ds, genome) {
|
|
|
197
202
|
q2.proteome.assays[assay].cohorts = {};
|
|
198
203
|
for (const cohort in q.proteome.assays[assay].cohorts) {
|
|
199
204
|
q2.proteome.assays[assay].cohorts[cohort] = {};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
const src = q.proteome.assays[assay].cohorts[cohort];
|
|
206
|
+
if ("filter" in src) {
|
|
207
|
+
q2.proteome.assays[assay].cohorts[cohort].filter = JSON.parse(JSON.stringify(src.filter));
|
|
208
|
+
}
|
|
209
|
+
if ("overlayTerm" in src) {
|
|
210
|
+
q2.proteome.assays[assay].cohorts[cohort].overlayTerm = JSON.parse(JSON.stringify(src.overlayTerm));
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
}
|
|
@@ -292,6 +299,7 @@ function getAllowedTermTypes(ds) {
|
|
|
292
299
|
for (const t of ds.cohort.termdb.allowedTermTypes) typeSet.add(t);
|
|
293
300
|
}
|
|
294
301
|
if (ds.queries?.geneExpression) typeSet.add(GENE_EXPRESSION);
|
|
302
|
+
if (ds.queries?.isoformExpression) typeSet.add(ISOFORM_EXPRESSION);
|
|
295
303
|
if (ds.queries?.metaboliteIntensity) typeSet.add(METABOLITE_INTENSITY);
|
|
296
304
|
if (ds.queries?.proteome?.assays) typeSet.add(PROTEOME_ABUNDANCE);
|
|
297
305
|
if (ds.queries?.ssGSEA) typeSet.add(SSGSEA);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { diffMethPayload } from "#types/checkers";
|
|
2
2
|
import { getData } from "../src/termdb.matrix.js";
|
|
3
|
-
import { get_ds_tdb } from "../src/termdb.js";
|
|
4
3
|
import { run_R } from "@sjcrh/proteinpaint-r";
|
|
5
4
|
import { mayLog } from "#src/helpers.ts";
|
|
6
5
|
import { formatElapsedTime } from "#shared";
|
|
@@ -22,8 +21,9 @@ function init({ genomes }) {
|
|
|
22
21
|
try {
|
|
23
22
|
const q = req.query;
|
|
24
23
|
const genome = genomes[q.genome];
|
|
25
|
-
if (!genome) throw
|
|
26
|
-
const
|
|
24
|
+
if (!genome) throw "unknown genome";
|
|
25
|
+
const ds = genome.datasets?.[q.dslabel];
|
|
26
|
+
if (!ds) throw "unknown ds";
|
|
27
27
|
let term_results = [];
|
|
28
28
|
if (q.tw) {
|
|
29
29
|
term_results = await getData({ filter: q.filter, filter0: q.filter0, terms: [q.tw] }, ds);
|
|
@@ -35,7 +35,12 @@ function init({ genomes }) {
|
|
|
35
35
|
if (term_results2.error) throw new Error(term_results2.error);
|
|
36
36
|
}
|
|
37
37
|
const results = await run_diffMeth(req.query, ds, term_results, term_results2);
|
|
38
|
-
if (!results || !results.data)
|
|
38
|
+
if (!results || !results.data)
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Differential methylation analysis returned no data. Please verify sample selections and try again."
|
|
41
|
+
);
|
|
42
|
+
if (Array.isArray(results.data) && !results.data.length)
|
|
43
|
+
throw new Error("No promoters passed filtering. Try relaxing group criteria or selecting more samples.");
|
|
39
44
|
res.send(results);
|
|
40
45
|
} catch (e) {
|
|
41
46
|
res.send({ status: "error", error: e.message || e });
|
|
@@ -44,12 +49,15 @@ function init({ genomes }) {
|
|
|
44
49
|
};
|
|
45
50
|
}
|
|
46
51
|
async function run_diffMeth(param, ds, term_results, term_results2) {
|
|
47
|
-
if (param.samplelst?.groups?.length != 2)
|
|
48
|
-
|
|
49
|
-
if (param.samplelst.groups[
|
|
52
|
+
if (param.samplelst?.groups?.length != 2)
|
|
53
|
+
throw new Error("Exactly 2 sample groups are required for differential methylation analysis.");
|
|
54
|
+
if (param.samplelst.groups[0].values?.length < 1)
|
|
55
|
+
throw new Error("Group 1 has no samples. Please select at least one sample.");
|
|
56
|
+
if (param.samplelst.groups[1].values?.length < 1)
|
|
57
|
+
throw new Error("Group 2 has no samples. Please select at least one sample.");
|
|
50
58
|
const q = ds.queries.dnaMethylation?.promoter;
|
|
51
|
-
if (!q) throw new Error("
|
|
52
|
-
if (!q.file) throw new Error("
|
|
59
|
+
if (!q) throw new Error("This dataset does not have promoter-level methylation data configured.");
|
|
60
|
+
if (!q.file) throw new Error("Promoter methylation data file is not configured for this dataset.");
|
|
53
61
|
const group1names = [];
|
|
54
62
|
const conf1_group1 = [];
|
|
55
63
|
const conf2_group1 = [];
|
|
@@ -169,10 +177,13 @@ async function run_diffMeth(param, ds, term_results, term_results2) {
|
|
|
169
177
|
}
|
|
170
178
|
function validateGroups(sample_size1, sample_size2, group1names, group2names) {
|
|
171
179
|
const alerts = [];
|
|
172
|
-
if (sample_size1 < 1) alerts.push("
|
|
173
|
-
if (sample_size2 < 1) alerts.push("
|
|
180
|
+
if (sample_size1 < 1) alerts.push("No samples in group 1 have methylation data available.");
|
|
181
|
+
if (sample_size2 < 1) alerts.push("No samples in group 2 have methylation data available.");
|
|
174
182
|
const commonnames = group1names.filter((x) => group2names.includes(x));
|
|
175
|
-
if (commonnames.length)
|
|
183
|
+
if (commonnames.length)
|
|
184
|
+
alerts.push(
|
|
185
|
+
`${commonnames.length} sample(s) appear in both groups: ${commonnames.join(", ")}. Please remove duplicates.`
|
|
186
|
+
);
|
|
176
187
|
return alerts;
|
|
177
188
|
}
|
|
178
189
|
export {
|
package/routes/termdb.dmr.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { TermdbDmrPayload } from "#types/checkers";
|
|
2
|
-
import {
|
|
2
|
+
import { run_rust } from "@sjcrh/proteinpaint-rust";
|
|
3
|
+
import { run_R } from "@sjcrh/proteinpaint-r";
|
|
3
4
|
import { invalidcoord } from "#shared/common.js";
|
|
4
5
|
import { mayLog } from "#src/helpers.ts";
|
|
6
|
+
import serverconfig from "#src/serverconfig.js";
|
|
5
7
|
import { formatElapsedTime } from "#shared";
|
|
6
8
|
const api = {
|
|
7
9
|
endpoint: "termdb/dmr",
|
|
@@ -21,32 +23,64 @@ function init({ genomes }) {
|
|
|
21
23
|
try {
|
|
22
24
|
const q = req.query;
|
|
23
25
|
const genome = genomes[q.genome];
|
|
24
|
-
if (!genome) throw
|
|
26
|
+
if (!genome) throw "unknown genome";
|
|
25
27
|
const ds = genome.datasets?.[q.dslabel];
|
|
26
|
-
if (!ds) throw
|
|
27
|
-
if (!ds.queries?.dnaMethylation) throw new Error("
|
|
28
|
-
if (!Array.isArray(q.group1) || q.group1.length == 0)
|
|
29
|
-
|
|
30
|
-
if (
|
|
28
|
+
if (!ds) throw "unknown ds";
|
|
29
|
+
if (!ds.queries?.dnaMethylation) throw new Error("This dataset does not support DNA methylation analysis.");
|
|
30
|
+
if (!Array.isArray(q.group1) || q.group1.length == 0)
|
|
31
|
+
throw new Error("Group 1 has no samples. Please select at least one sample.");
|
|
32
|
+
if (!Array.isArray(q.group2) || q.group2.length == 0)
|
|
33
|
+
throw new Error("Group 2 has no samples. Please select at least one sample.");
|
|
34
|
+
if (invalidcoord(genome, q.chr, q.start, q.stop))
|
|
35
|
+
throw new Error(`Invalid genomic coordinates: ${q.chr}:${q.start}-${q.stop}`);
|
|
36
|
+
const SERVER_MAX_REGION_BP = 1e7;
|
|
37
|
+
const span = q.stop - q.start;
|
|
38
|
+
if (span > SERVER_MAX_REGION_BP)
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Region too large (${(span / 1e6).toFixed(1)} Mb). Server maximum is ${SERVER_MAX_REGION_BP / 1e6} Mb.`
|
|
41
|
+
);
|
|
31
42
|
const group1 = q.group1.map((s) => s.sample).filter(Boolean);
|
|
32
43
|
const group2 = q.group2.map((s) => s.sample).filter(Boolean);
|
|
33
|
-
if (group1.length < 3)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
if (group1.length < 3)
|
|
45
|
+
throw new Error(`Group 1 needs at least 3 samples with methylation data, got ${group1.length}.`);
|
|
46
|
+
if (group2.length < 3)
|
|
47
|
+
throw new Error(`Group 2 needs at least 3 samples with methylation data, got ${group2.length}.`);
|
|
48
|
+
const useR = q.backend === "r";
|
|
49
|
+
const dmrInput = {
|
|
50
|
+
probe_h5_file: ds.queries.dnaMethylation.file,
|
|
51
|
+
cachedir: serverconfig.cachedir,
|
|
52
|
+
genome: q.genome,
|
|
37
53
|
chr: q.chr,
|
|
38
54
|
start: q.start,
|
|
39
55
|
stop: q.stop,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
case: group2.join(","),
|
|
57
|
+
control: group1.join(","),
|
|
58
|
+
fdr_cutoff: q.fdr_cutoff,
|
|
59
|
+
lambda: q.lambda,
|
|
60
|
+
C: q.C,
|
|
61
|
+
blockWidth: q.blockWidth,
|
|
62
|
+
devicePixelRatio: q.devicePixelRatio,
|
|
63
|
+
maxLoessRegion: q.maxLoessRegion,
|
|
64
|
+
colors: q.colors
|
|
44
65
|
};
|
|
45
66
|
const time1 = Date.now();
|
|
46
|
-
const result = JSON.parse(await
|
|
47
|
-
mayLog(
|
|
67
|
+
const result = useR ? JSON.parse(await run_R("dmrcate_full.R", JSON.stringify(dmrInput))) : JSON.parse(await run_rust("dmrcate", JSON.stringify(dmrInput)));
|
|
68
|
+
mayLog(`DMR analysis (${useR ? "R" : "Rust"}) time:`, formatElapsedTime(Date.now() - time1));
|
|
48
69
|
if (result.error) throw new Error(result.error);
|
|
49
|
-
|
|
70
|
+
if (result.diagnostic?.probes) {
|
|
71
|
+
const p = result.diagnostic.probes;
|
|
72
|
+
mayLog(
|
|
73
|
+
`${useR ? "R" : "Rust"} probes logFC:`,
|
|
74
|
+
p.logFC,
|
|
75
|
+
"fdr:",
|
|
76
|
+
p.fdr?.map((f) => f.toExponential(4))
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
res.send({
|
|
80
|
+
status: "ok",
|
|
81
|
+
dmrs: result.dmrs,
|
|
82
|
+
diagnostic: result.diagnostic
|
|
83
|
+
});
|
|
50
84
|
} catch (e) {
|
|
51
85
|
const msg = e instanceof Error ? e.message : String(e);
|
|
52
86
|
res.send({ error: msg });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TermdbIsoformAvailabilityPayload } from "#types/checkers";
|
|
2
|
+
const api = {
|
|
3
|
+
endpoint: "termdb/isoformAvailability",
|
|
4
|
+
methods: {
|
|
5
|
+
get: {
|
|
6
|
+
...TermdbIsoformAvailabilityPayload,
|
|
7
|
+
init
|
|
8
|
+
},
|
|
9
|
+
post: {
|
|
10
|
+
...TermdbIsoformAvailabilityPayload,
|
|
11
|
+
init
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function init({ genomes }) {
|
|
16
|
+
return (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const q = req.query;
|
|
19
|
+
const genome = genomes[q.genome];
|
|
20
|
+
if (!genome) throw "invalid genome";
|
|
21
|
+
const ds = genome.datasets?.[q.dslabel];
|
|
22
|
+
if (!ds) throw "invalid dslabel";
|
|
23
|
+
const isoQ = ds.queries?.isoformExpression;
|
|
24
|
+
if (!isoQ) throw "isoformExpression not configured for this dataset";
|
|
25
|
+
const itemSet = new Set(isoQ.availableItems || []);
|
|
26
|
+
const available = (q.isoforms || []).filter((id) => itemSet.has(id));
|
|
27
|
+
res.send({ available });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
res.send({ error: e.message || e });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
api
|
|
35
|
+
};
|
|
@@ -42,10 +42,21 @@ async function getScoresDict(query, ds) {
|
|
|
42
42
|
const percents = getSCPercentsDict(d, data.samples);
|
|
43
43
|
term2Score[d.term.id] = percents;
|
|
44
44
|
}
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const { clientAuthResult, activeCohort } = query.__protected__;
|
|
46
|
+
const isPublic = !clientAuthResult[activeCohort]?.role || clientAuthResult[activeCohort].role === "public";
|
|
47
|
+
let hospital;
|
|
48
|
+
if (!isPublic && data.sampleData) {
|
|
49
|
+
const facilityValue = data.sampleData[query.facilityTW.$id];
|
|
50
|
+
const termValue = query.facilityTW.term.values[facilityValue?.value];
|
|
51
|
+
hospital = termValue?.label || termValue?.key;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
term2Score,
|
|
55
|
+
// Do not expose individual site IDs, names, or hospital to public-role users
|
|
56
|
+
sites: isPublic ? [] : data.sites,
|
|
57
|
+
hospital,
|
|
58
|
+
n: data.sampleData ? 1 : data.samples.length
|
|
59
|
+
};
|
|
49
60
|
}
|
|
50
61
|
function getDict(key, sample) {
|
|
51
62
|
if (!sample[key]) return null;
|
|
@@ -81,7 +81,15 @@ async function getScores(query, ds) {
|
|
|
81
81
|
for (const d of query.scoreTerms) {
|
|
82
82
|
term2Score[d.score.term.id] = getPercentage(d, data.samples, data.sampleData);
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
const { clientAuthResult, activeCohort } = query.__protected__;
|
|
85
|
+
const isPublic = !clientAuthResult[activeCohort]?.role || clientAuthResult[activeCohort].role === "public";
|
|
86
|
+
return {
|
|
87
|
+
term2Score,
|
|
88
|
+
// Do not expose individual site IDs or names to public-role users
|
|
89
|
+
sites: isPublic ? [] : data.sites,
|
|
90
|
+
site: isPublic ? void 0 : data.site,
|
|
91
|
+
n: data.sampleData ? 1 : data.samples.length
|
|
92
|
+
};
|
|
85
93
|
}
|
|
86
94
|
function getPercentage(d, samples, sampleData) {
|
|
87
95
|
if (!d) return null;
|
|
@@ -131,9 +131,21 @@ async function getSingleCellScatter(req, res, ds) {
|
|
|
131
131
|
const data = await ds.queries.singleCell.data.get(arg);
|
|
132
132
|
const plot = data.plots[0];
|
|
133
133
|
const cells = [...plot.expCells, ...plot.noExpCells];
|
|
134
|
+
const groups = tw.q?.customset?.groups;
|
|
135
|
+
const cat2GrpName = /* @__PURE__ */ new Map();
|
|
136
|
+
if (groups) {
|
|
137
|
+
for (const group of groups) {
|
|
138
|
+
for (const value of Object.values(group.values)) {
|
|
139
|
+
cat2GrpName.set(value.key, group.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
134
143
|
const samples = cells.map((cell) => {
|
|
144
|
+
let category = cell.category;
|
|
145
|
+
const groupName = cat2GrpName.get(category);
|
|
146
|
+
if (groupName !== void 0) category = groupName;
|
|
135
147
|
const hidden = {
|
|
136
|
-
category: tw?.q?.hiddenValues ?
|
|
148
|
+
category: tw?.q?.hiddenValues ? category in tw.q.hiddenValues : false
|
|
137
149
|
};
|
|
138
150
|
return {
|
|
139
151
|
sample: cell.cellId,
|
|
@@ -141,7 +153,7 @@ async function getSingleCellScatter(req, res, ds) {
|
|
|
141
153
|
x: cell.x,
|
|
142
154
|
y: cell.y,
|
|
143
155
|
z: 0,
|
|
144
|
-
category
|
|
156
|
+
category,
|
|
145
157
|
shape: "Ref",
|
|
146
158
|
hidden,
|
|
147
159
|
geneExp: cell.geneExp
|