@sjcrh/proteinpaint-server 2.208.0 → 2.210.1-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 +2 -1
- package/genome/hg38.base.js +22 -0
- package/package.json +7 -7
- package/routes/termdb.diffMeth.js +186 -5
- package/src/app.js +3181 -1015
- package/src/serverconfig.js +7 -0
package/dataset/termdb.test.js
CHANGED
|
@@ -3,9 +3,10 @@ const mafFields = [
|
|
|
3
3
|
id: "tumor_DNA",
|
|
4
4
|
name: "Tumor DNA",
|
|
5
5
|
parent_id: null,
|
|
6
|
-
|
|
6
|
+
mafFormatKey: "tumor_DNA_WGS",
|
|
7
7
|
isleaf: true,
|
|
8
8
|
type: "float",
|
|
9
|
+
mafFilterMode: "maf",
|
|
9
10
|
default: true,
|
|
10
11
|
// indicates default maf term (e.g. will be used by default for making maf filters in predefined groupset)
|
|
11
12
|
min: 0,
|
package/genome/hg38.base.js
CHANGED
|
@@ -69,6 +69,28 @@ function getHg38() {
|
|
|
69
69
|
stackspace: 1,
|
|
70
70
|
vpad: 4
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
/* ENCODE cCRE registry, coloured by class in SCREEN's own colours. Declared here rather
|
|
74
|
+
than in a dataset so any hg38 block can turn it on from the Tracks menu; like
|
|
75
|
+
RepeatMasker it is available, not default-on, because __isgene is what
|
|
76
|
+
first_genetrack_tolist adds automatically. The differential-methylation region view
|
|
77
|
+
switches it on explicitly, where a cCRE is at a scale it can actually be drawn at.
|
|
78
|
+
Built by utils/dnaMeth/build_ccre_track.py -- the shipped registry carries the class in
|
|
79
|
+
column 6, where bedj cannot see it. */
|
|
80
|
+
type: "bedj",
|
|
81
|
+
name: "ENCODE cCREs",
|
|
82
|
+
file: "anno/encodeCCREtrack.hg38.gz",
|
|
83
|
+
stackheight: 12,
|
|
84
|
+
stackspace: 1,
|
|
85
|
+
vpad: 3,
|
|
86
|
+
categories: {
|
|
87
|
+
PLS: { color: "#FF0000", label: "Promoter-like" },
|
|
88
|
+
pELS: { color: "#FFA700", label: "Proximal enhancer-like" },
|
|
89
|
+
dELS: { color: "#FFCD00", label: "Distal enhancer-like" },
|
|
90
|
+
"CTCF-only": { color: "#00B0F0", label: "CTCF-only" },
|
|
91
|
+
"DNase-H3K4me3": { color: "#FFAAAA", label: "DNase-H3K4me3" }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
72
94
|
{
|
|
73
95
|
type: "bedj",
|
|
74
96
|
name: "RepeatMasker",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.210.1-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",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"typescript": "^5.6.3"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@sjcrh/augen": "2.
|
|
60
|
-
"@sjcrh/proteinpaint-python": "2.
|
|
59
|
+
"@sjcrh/augen": "2.210.0",
|
|
60
|
+
"@sjcrh/proteinpaint-python": "2.209.0",
|
|
61
61
|
"@sjcrh/proteinpaint-r": "2.207.1",
|
|
62
|
-
"@sjcrh/proteinpaint-rust": "2.
|
|
63
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
64
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
62
|
+
"@sjcrh/proteinpaint-rust": "2.209.0",
|
|
63
|
+
"@sjcrh/proteinpaint-shared": "2.210.0",
|
|
64
|
+
"@sjcrh/proteinpaint-types": "2.210.1-0",
|
|
65
65
|
"@types/express": "^5.0.0",
|
|
66
66
|
"@types/express-session": "^1.18.1",
|
|
67
67
|
"better-sqlite3": "^12.4.1",
|
|
@@ -121,5 +121,5 @@
|
|
|
121
121
|
]
|
|
122
122
|
]
|
|
123
123
|
},
|
|
124
|
-
"_buildTime": "2026-09-
|
|
124
|
+
"_buildTime": "2026-09-21T19:04:04Z"
|
|
125
125
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { DMR_SCAN_ELEMENT_TYPE } from "#types";
|
|
2
|
+
import { runDmrBatch } from "#src/routes/termdb.dmrBatch.ts";
|
|
3
|
+
import { dmrScanToRows, summarizeProfile, coarsenProfile, dmrBedjLines } from "#src/utils/dmrScanRows.ts";
|
|
4
|
+
import { writeBedjFile } from "#src/utils/bedjCache.ts";
|
|
5
|
+
import { resolveGroupNames, matchedSamplelst, eligibleMethylationSamples } from "#src/utils/methylationMatrix.ts";
|
|
1
6
|
import { mayLog } from "#src/helpers.ts";
|
|
2
7
|
import { run_R } from "@sjcrh/proteinpaint-r";
|
|
3
8
|
import { formatElapsedTime } from "#shared";
|
|
4
9
|
import { renderVolcano } from "../src/renderVolcano.ts";
|
|
10
|
+
import { renderManhattanPoints } from "../src/renderManhattan.ts";
|
|
11
|
+
import { HYPER_COLOR, HYPO_COLOR } from "#shared/dmrColors.js";
|
|
5
12
|
import { cacheOrRecompute } from "#src/utils/cacheOrRecompute.ts";
|
|
6
13
|
import {
|
|
7
14
|
buildGroupValues,
|
|
@@ -30,12 +37,37 @@ function init({ genomes }) {
|
|
|
30
37
|
const rendered = await renderVolcano(result.promoterRows, q.volcanoRender);
|
|
31
38
|
rendered.cacheId = cacheId;
|
|
32
39
|
if (rendered.totalRows === 0)
|
|
33
|
-
throw new Error(
|
|
40
|
+
throw new Error(
|
|
41
|
+
!result.scan ? "No promoters passed filtering. Try relaxing group criteria or selecting more samples." : result.scan.kept && result.scan.backgroundCorrection ? (
|
|
42
|
+
/* rows omit unscored DMRs under the correction, so none plotted does not mean
|
|
43
|
+
none met the floor */
|
|
44
|
+
`None of the ${result.scan.kept.toLocaleString()} DMRs meeting the CpG floor could be scored against matched background. Turn off the background correction to see them.`
|
|
45
|
+
) : "The scan called no DMRs that met the CpG floor."
|
|
46
|
+
);
|
|
34
47
|
const output = {
|
|
35
48
|
data: rendered,
|
|
36
49
|
sample_size1: result.sample_size1,
|
|
37
50
|
sample_size2: result.sample_size2
|
|
38
51
|
};
|
|
52
|
+
if (result.scan) {
|
|
53
|
+
output.scan = result.scan;
|
|
54
|
+
if (result.scan.binMethylation) {
|
|
55
|
+
output.scan.profileSummary = summarizeProfile(result.scan.binMethylation);
|
|
56
|
+
output.scan.profile = await renderMethylationProfile(
|
|
57
|
+
result.scan,
|
|
58
|
+
genomes[q.genome],
|
|
59
|
+
q.volcanoRender?.devicePixelRatio,
|
|
60
|
+
// display width only; the summary rows above stay on the native 100 kb bin
|
|
61
|
+
q.scan?.profileBinBp
|
|
62
|
+
);
|
|
63
|
+
delete output.scan.binMethylation;
|
|
64
|
+
}
|
|
65
|
+
output.scan.manhattan = await renderScanManhattan(
|
|
66
|
+
result.promoterRows,
|
|
67
|
+
genomes[q.genome],
|
|
68
|
+
q.volcanoRender?.devicePixelRatio
|
|
69
|
+
);
|
|
70
|
+
}
|
|
39
71
|
res.send(output);
|
|
40
72
|
} catch (e) {
|
|
41
73
|
res.status(e.status || 500).send({ status: "error", error: e.message || e, code: e.code });
|
|
@@ -95,6 +127,7 @@ function dmKeyInputs(req, imputeMissing) {
|
|
|
95
127
|
};
|
|
96
128
|
}
|
|
97
129
|
async function getDmCacheResult(req, genomes) {
|
|
130
|
+
if (req.element_type === DMR_SCAN_ELEMENT_TYPE) return getDmrScanAsDm(req, genomes);
|
|
98
131
|
const imputeMissing = genomes?.[req.genome]?.datasets?.[req.dslabel]?.queries?.dnaMethylation?.platform != "wgbs";
|
|
99
132
|
const { result, cacheId } = await cacheOrRecompute({
|
|
100
133
|
computeArgument: dmKeyInputs(req, imputeMissing),
|
|
@@ -106,6 +139,153 @@ async function getDmCacheResult(req, genomes) {
|
|
|
106
139
|
});
|
|
107
140
|
return { result, cacheId };
|
|
108
141
|
}
|
|
142
|
+
function scanChromosomes(req, genome) {
|
|
143
|
+
let chromosomes = req.scan?.chromosome ? [req.scan.chromosome] : genome.majorchrorder.filter((c) => c != "chrM" && c != "chrMT");
|
|
144
|
+
if (req.exclude_sex_chr) chromosomes = chromosomes.filter((c) => !/^chr[XY]$/i.test(c));
|
|
145
|
+
if (!chromosomes.length) throw new Error("No chromosomes left to scan.");
|
|
146
|
+
return chromosomes;
|
|
147
|
+
}
|
|
148
|
+
async function getDmrScanAsDm(req, genomes) {
|
|
149
|
+
if (req.tw || req.tw2) throw new Error("Confounding factors are not supported by the DMR scan.");
|
|
150
|
+
const genome = genomes[req.genome];
|
|
151
|
+
if (!genome) throw new Error("unknown genome");
|
|
152
|
+
const ds = genome.datasets?.[req.dslabel];
|
|
153
|
+
if (!ds) throw new Error("unknown dataset");
|
|
154
|
+
const groups = req.samplelst?.groups;
|
|
155
|
+
if (groups?.length != 2)
|
|
156
|
+
throw new Error("Exactly 2 sample groups are required for differential methylation analysis.");
|
|
157
|
+
const chromosomes = scanChromosomes(req, genome);
|
|
158
|
+
const { payload, cacheId } = await runDmrBatch(
|
|
159
|
+
{
|
|
160
|
+
genome: req.genome,
|
|
161
|
+
dslabel: req.dslabel,
|
|
162
|
+
group1: groups[0].values,
|
|
163
|
+
group2: groups[1].values,
|
|
164
|
+
scanChromosomes: chromosomes,
|
|
165
|
+
backgroundCorrection: !!req.scan?.backgroundCorrection,
|
|
166
|
+
// the genome-wide profile: the metric the methylome literature compares cohorts with
|
|
167
|
+
binMethylation: true,
|
|
168
|
+
lambda: positiveOrUndefined(req.scan?.lambda),
|
|
169
|
+
C: positiveOrUndefined(req.scan?.C),
|
|
170
|
+
fdr_cutoff: positiveOrUndefined(req.scan?.fdrCutoff)
|
|
171
|
+
},
|
|
172
|
+
genomes
|
|
173
|
+
);
|
|
174
|
+
const eligible = eligibleMethylationSamples(ds, void 0);
|
|
175
|
+
const { group1, group2 } = await resolveGroupNames(groups[0].values, groups[1].values, eligible, ds);
|
|
176
|
+
const { rows, scan } = dmrScanToRows(payload, {
|
|
177
|
+
chromosomes,
|
|
178
|
+
minCpgs: req.scan?.minCpgs,
|
|
179
|
+
backgroundCorrection: !!req.scan?.backgroundCorrection
|
|
180
|
+
});
|
|
181
|
+
scan.matchedSamplelst = await matchedSamplelst(req.samplelst, eligible, ds);
|
|
182
|
+
scan.cacheId = cacheId;
|
|
183
|
+
try {
|
|
184
|
+
scan.bedjFile = await writeBedjFile(`dmr-${cacheId}-${scan.minCpgs}.gz`, {
|
|
185
|
+
text: dmrBedjLines(payload, scan.minCpgs)
|
|
186
|
+
});
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.error("DMR scan: bedj cache write failed, browser track unavailable:", e?.message || e);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
result: { promoterRows: rows, sample_size1: group1.length, sample_size2: group2.length, scan },
|
|
192
|
+
cacheId
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function positiveOrUndefined(v) {
|
|
196
|
+
const n = Number(v);
|
|
197
|
+
return v != null && Number.isFinite(n) && n > 0 ? n : void 0;
|
|
198
|
+
}
|
|
199
|
+
const SCAN_INTERACTIVE_PER_SIDE = 1e3;
|
|
200
|
+
async function renderScanManhattan(rows, genome, devicePixelRatio) {
|
|
201
|
+
const chrSizes = {};
|
|
202
|
+
for (const c of genome.majorchrorder) if (c != "chrM" && c != "chrMT") chrSizes[c] = genome.majorchr[c];
|
|
203
|
+
const points = rows.map((r) => {
|
|
204
|
+
const p = r.original_p_value;
|
|
205
|
+
const mag = p > 0 ? -Math.log10(p) : Infinity;
|
|
206
|
+
return {
|
|
207
|
+
chrom: r.chr,
|
|
208
|
+
pos: r.start,
|
|
209
|
+
y: r.delta_beta < 0 ? -mag : mag,
|
|
210
|
+
color: r.delta_beta < 0 ? HYPO_COLOR : HYPER_COLOR,
|
|
211
|
+
start: r.start,
|
|
212
|
+
stop: r.stop,
|
|
213
|
+
delta_beta: r.delta_beta,
|
|
214
|
+
fold_change: r.fold_change,
|
|
215
|
+
p,
|
|
216
|
+
no_cpgs: r.no_cpgs,
|
|
217
|
+
gene_name: r.gene_name,
|
|
218
|
+
...r.excess != null ? { excess: r.excess } : {}
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const plotWidth = 1e3;
|
|
222
|
+
const plotHeight = 300;
|
|
223
|
+
const { png, plot_data } = await renderManhattanPoints({
|
|
224
|
+
points,
|
|
225
|
+
chrSizes,
|
|
226
|
+
plotWidth,
|
|
227
|
+
plotHeight,
|
|
228
|
+
devicePixelRatio: devicePixelRatio || 1,
|
|
229
|
+
pngDotRadius: 2,
|
|
230
|
+
// the GRIN2 defaults: raise the cap only when more than 5 dots would sit on it
|
|
231
|
+
maxCappedPoints: 5,
|
|
232
|
+
hardCap: 200,
|
|
233
|
+
binSize: 10,
|
|
234
|
+
interactive: SCAN_INTERACTIVE_PER_SIDE,
|
|
235
|
+
signed: true,
|
|
236
|
+
// open circles, as the volcano above draws the same DMRs
|
|
237
|
+
hollow: true
|
|
238
|
+
});
|
|
239
|
+
return { png, plotData: plot_data, interactive: SCAN_INTERACTIVE_PER_SIDE, plotWidth, plotHeight };
|
|
240
|
+
}
|
|
241
|
+
function renderMethylationProfile(scan, genome, devicePixelRatio, profileBinBp) {
|
|
242
|
+
if (!scan.binMethylation?.bins.length) return void 0;
|
|
243
|
+
const bm = coarsenProfile(scan.binMethylation, profileBinBp);
|
|
244
|
+
const chrSizes = {};
|
|
245
|
+
for (const c of genome.majorchrorder) if (c != "chrM" && c != "chrMT") chrSizes[c] = genome.majorchr[c];
|
|
246
|
+
const points = bm.bins.map((b) => {
|
|
247
|
+
const d = b.case - b.control;
|
|
248
|
+
return {
|
|
249
|
+
chrom: b.chr,
|
|
250
|
+
pos: b.start,
|
|
251
|
+
y: d,
|
|
252
|
+
color: d < 0 ? HYPO_COLOR : HYPER_COLOR,
|
|
253
|
+
control: b.control,
|
|
254
|
+
case: b.case,
|
|
255
|
+
n_probes: b.n_probes
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
const plotWidth = 1e3;
|
|
259
|
+
const plotHeight = 160;
|
|
260
|
+
const dotRadius = 1;
|
|
261
|
+
return renderManhattanPoints({
|
|
262
|
+
points,
|
|
263
|
+
chrSizes,
|
|
264
|
+
plotWidth,
|
|
265
|
+
plotHeight,
|
|
266
|
+
devicePixelRatio: devicePixelRatio || 1,
|
|
267
|
+
pngDotRadius: dotRadius,
|
|
268
|
+
maxCappedPoints: 5,
|
|
269
|
+
hardCap: 200,
|
|
270
|
+
binSize: 10,
|
|
271
|
+
/* The bins that moved most, each way. Ranking is |y| = |Δβ| here rather than evidence: on
|
|
272
|
+
this figure the effect size IS the result, and the bins a reader wants to identify are the
|
|
273
|
+
excursions away from the zero line. */
|
|
274
|
+
interactive: SCAN_INTERACTIVE_PER_SIDE,
|
|
275
|
+
signed: true,
|
|
276
|
+
capping: false
|
|
277
|
+
}).then(({ png, plot_data }) => ({
|
|
278
|
+
png,
|
|
279
|
+
plotData: plot_data,
|
|
280
|
+
binBp: bm.binBp,
|
|
281
|
+
plotWidth,
|
|
282
|
+
plotHeight,
|
|
283
|
+
dotRadius,
|
|
284
|
+
bins: bm.bins.length,
|
|
285
|
+
// what the top-N-per-side rule actually left live, which at a coarse width is every bin
|
|
286
|
+
interactive: plot_data.points.length
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
109
289
|
async function runDmFresh(param, ds, term_results, term_results2, imputeMissing) {
|
|
110
290
|
const groups = await resolveDmSampleGroups(param, ds, term_results, term_results2);
|
|
111
291
|
if (groups.alerts.length) throw new Error(groups.alerts.join(" | "));
|
|
@@ -150,10 +330,10 @@ async function resolveDmSampleGroups(param, ds, term_results, term_results2) {
|
|
|
150
330
|
throw new Error("Group 1 has no samples. Please select at least one sample.");
|
|
151
331
|
if (param.samplelst.groups[1].values?.length < 1)
|
|
152
332
|
throw new Error("Group 2 has no samples. Please select at least one sample.");
|
|
153
|
-
const
|
|
333
|
+
const allSampleSet = eligibleMethylationSamples(ds, param.element_type);
|
|
154
334
|
const g1 = await buildGroupValues(
|
|
155
335
|
param.samplelst.groups[0].values,
|
|
156
|
-
|
|
336
|
+
allSampleSet,
|
|
157
337
|
ds,
|
|
158
338
|
param.tw,
|
|
159
339
|
param.tw2,
|
|
@@ -162,7 +342,7 @@ async function resolveDmSampleGroups(param, ds, term_results, term_results2) {
|
|
|
162
342
|
);
|
|
163
343
|
const g2 = await buildGroupValues(
|
|
164
344
|
param.samplelst.groups[1].values,
|
|
165
|
-
|
|
345
|
+
allSampleSet,
|
|
166
346
|
ds,
|
|
167
347
|
param.tw,
|
|
168
348
|
param.tw2,
|
|
@@ -192,5 +372,6 @@ export {
|
|
|
192
372
|
init,
|
|
193
373
|
listElementTypes,
|
|
194
374
|
resolveDmSampleGroups,
|
|
195
|
-
resolveElementQuery
|
|
375
|
+
resolveElementQuery,
|
|
376
|
+
scanChromosomes
|
|
196
377
|
};
|