@sjcrh/proteinpaint-server 2.208.0 → 2.209.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/genome/hg38.base.js +22 -0
- package/package.json +6 -6
- package/routes/termdb.diffMeth.js +178 -5
- package/src/app.js +2925 -865
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.209.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",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@sjcrh/augen": "2.204.0",
|
|
60
|
-
"@sjcrh/proteinpaint-python": "2.
|
|
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.209.0",
|
|
64
|
+
"@sjcrh/proteinpaint-types": "2.209.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-16T15:51:09Z"
|
|
125
125
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { DMR_SCAN_ELEMENT_TYPE } from "#types";
|
|
2
|
+
import { runDmrBatch } from "#src/routes/termdb.dmrBatch.ts";
|
|
3
|
+
import { dmrScanToRows, summarizeProfile, coarsenProfile } from "#src/utils/dmrScanRows.ts";
|
|
4
|
+
import { resolveGroupNames, matchedSamplelst, eligibleMethylationSamples } from "#src/utils/methylationMatrix.ts";
|
|
1
5
|
import { mayLog } from "#src/helpers.ts";
|
|
2
6
|
import { run_R } from "@sjcrh/proteinpaint-r";
|
|
3
7
|
import { formatElapsedTime } from "#shared";
|
|
4
8
|
import { renderVolcano } from "../src/renderVolcano.ts";
|
|
9
|
+
import { renderManhattanPoints } from "../src/renderManhattan.ts";
|
|
10
|
+
import { HYPER_COLOR, HYPO_COLOR } from "#shared/dmrColors.js";
|
|
5
11
|
import { cacheOrRecompute } from "#src/utils/cacheOrRecompute.ts";
|
|
6
12
|
import {
|
|
7
13
|
buildGroupValues,
|
|
@@ -30,12 +36,37 @@ function init({ genomes }) {
|
|
|
30
36
|
const rendered = await renderVolcano(result.promoterRows, q.volcanoRender);
|
|
31
37
|
rendered.cacheId = cacheId;
|
|
32
38
|
if (rendered.totalRows === 0)
|
|
33
|
-
throw new Error(
|
|
39
|
+
throw new Error(
|
|
40
|
+
!result.scan ? "No promoters passed filtering. Try relaxing group criteria or selecting more samples." : result.scan.kept && result.scan.backgroundCorrection ? (
|
|
41
|
+
/* rows omit unscored DMRs under the correction, so none plotted does not mean
|
|
42
|
+
none met the floor */
|
|
43
|
+
`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.`
|
|
44
|
+
) : "The scan called no DMRs that met the CpG floor."
|
|
45
|
+
);
|
|
34
46
|
const output = {
|
|
35
47
|
data: rendered,
|
|
36
48
|
sample_size1: result.sample_size1,
|
|
37
49
|
sample_size2: result.sample_size2
|
|
38
50
|
};
|
|
51
|
+
if (result.scan) {
|
|
52
|
+
output.scan = result.scan;
|
|
53
|
+
if (result.scan.binMethylation) {
|
|
54
|
+
output.scan.profileSummary = summarizeProfile(result.scan.binMethylation);
|
|
55
|
+
output.scan.profile = await renderMethylationProfile(
|
|
56
|
+
result.scan,
|
|
57
|
+
genomes[q.genome],
|
|
58
|
+
q.volcanoRender?.devicePixelRatio,
|
|
59
|
+
// display width only; the summary rows above stay on the native 100 kb bin
|
|
60
|
+
q.scan?.profileBinBp
|
|
61
|
+
);
|
|
62
|
+
delete output.scan.binMethylation;
|
|
63
|
+
}
|
|
64
|
+
output.scan.manhattan = await renderScanManhattan(
|
|
65
|
+
result.promoterRows,
|
|
66
|
+
genomes[q.genome],
|
|
67
|
+
q.volcanoRender?.devicePixelRatio
|
|
68
|
+
);
|
|
69
|
+
}
|
|
39
70
|
res.send(output);
|
|
40
71
|
} catch (e) {
|
|
41
72
|
res.status(e.status || 500).send({ status: "error", error: e.message || e, code: e.code });
|
|
@@ -95,6 +126,7 @@ function dmKeyInputs(req, imputeMissing) {
|
|
|
95
126
|
};
|
|
96
127
|
}
|
|
97
128
|
async function getDmCacheResult(req, genomes) {
|
|
129
|
+
if (req.element_type === DMR_SCAN_ELEMENT_TYPE) return getDmrScanAsDm(req, genomes);
|
|
98
130
|
const imputeMissing = genomes?.[req.genome]?.datasets?.[req.dslabel]?.queries?.dnaMethylation?.platform != "wgbs";
|
|
99
131
|
const { result, cacheId } = await cacheOrRecompute({
|
|
100
132
|
computeArgument: dmKeyInputs(req, imputeMissing),
|
|
@@ -106,6 +138,146 @@ async function getDmCacheResult(req, genomes) {
|
|
|
106
138
|
});
|
|
107
139
|
return { result, cacheId };
|
|
108
140
|
}
|
|
141
|
+
function scanChromosomes(req, genome) {
|
|
142
|
+
let chromosomes = req.scan?.chromosome ? [req.scan.chromosome] : genome.majorchrorder.filter((c) => c != "chrM" && c != "chrMT");
|
|
143
|
+
if (req.exclude_sex_chr) chromosomes = chromosomes.filter((c) => !/^chr[XY]$/i.test(c));
|
|
144
|
+
if (!chromosomes.length) throw new Error("No chromosomes left to scan.");
|
|
145
|
+
return chromosomes;
|
|
146
|
+
}
|
|
147
|
+
async function getDmrScanAsDm(req, genomes) {
|
|
148
|
+
if (req.tw || req.tw2) throw new Error("Confounding factors are not supported by the DMR scan.");
|
|
149
|
+
const genome = genomes[req.genome];
|
|
150
|
+
if (!genome) throw new Error("unknown genome");
|
|
151
|
+
const ds = genome.datasets?.[req.dslabel];
|
|
152
|
+
if (!ds) throw new Error("unknown dataset");
|
|
153
|
+
const groups = req.samplelst?.groups;
|
|
154
|
+
if (groups?.length != 2)
|
|
155
|
+
throw new Error("Exactly 2 sample groups are required for differential methylation analysis.");
|
|
156
|
+
const chromosomes = scanChromosomes(req, genome);
|
|
157
|
+
const { payload, cacheId } = await runDmrBatch(
|
|
158
|
+
{
|
|
159
|
+
genome: req.genome,
|
|
160
|
+
dslabel: req.dslabel,
|
|
161
|
+
group1: groups[0].values,
|
|
162
|
+
group2: groups[1].values,
|
|
163
|
+
scanChromosomes: chromosomes,
|
|
164
|
+
backgroundCorrection: !!req.scan?.backgroundCorrection,
|
|
165
|
+
// the genome-wide profile: the metric the methylome literature compares cohorts with
|
|
166
|
+
binMethylation: true,
|
|
167
|
+
lambda: positiveOrUndefined(req.scan?.lambda),
|
|
168
|
+
C: positiveOrUndefined(req.scan?.C),
|
|
169
|
+
fdr_cutoff: positiveOrUndefined(req.scan?.fdrCutoff)
|
|
170
|
+
},
|
|
171
|
+
genomes
|
|
172
|
+
);
|
|
173
|
+
const eligible = eligibleMethylationSamples(ds, void 0);
|
|
174
|
+
const { group1, group2 } = await resolveGroupNames(groups[0].values, groups[1].values, eligible, ds);
|
|
175
|
+
const { rows, scan } = dmrScanToRows(payload, {
|
|
176
|
+
chromosomes,
|
|
177
|
+
minCpgs: req.scan?.minCpgs,
|
|
178
|
+
backgroundCorrection: !!req.scan?.backgroundCorrection
|
|
179
|
+
});
|
|
180
|
+
scan.matchedSamplelst = await matchedSamplelst(req.samplelst, eligible, ds);
|
|
181
|
+
scan.cacheId = cacheId;
|
|
182
|
+
return {
|
|
183
|
+
result: { promoterRows: rows, sample_size1: group1.length, sample_size2: group2.length, scan },
|
|
184
|
+
cacheId
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function positiveOrUndefined(v) {
|
|
188
|
+
const n = Number(v);
|
|
189
|
+
return v != null && Number.isFinite(n) && n > 0 ? n : void 0;
|
|
190
|
+
}
|
|
191
|
+
const SCAN_INTERACTIVE_PER_SIDE = 1e3;
|
|
192
|
+
async function renderScanManhattan(rows, genome, devicePixelRatio) {
|
|
193
|
+
const chrSizes = {};
|
|
194
|
+
for (const c of genome.majorchrorder) if (c != "chrM" && c != "chrMT") chrSizes[c] = genome.majorchr[c];
|
|
195
|
+
const points = rows.map((r) => {
|
|
196
|
+
const p = r.original_p_value;
|
|
197
|
+
const mag = p > 0 ? -Math.log10(p) : Infinity;
|
|
198
|
+
return {
|
|
199
|
+
chrom: r.chr,
|
|
200
|
+
pos: r.start,
|
|
201
|
+
y: r.delta_beta < 0 ? -mag : mag,
|
|
202
|
+
color: r.delta_beta < 0 ? HYPO_COLOR : HYPER_COLOR,
|
|
203
|
+
start: r.start,
|
|
204
|
+
stop: r.stop,
|
|
205
|
+
delta_beta: r.delta_beta,
|
|
206
|
+
fold_change: r.fold_change,
|
|
207
|
+
p,
|
|
208
|
+
no_cpgs: r.no_cpgs,
|
|
209
|
+
gene_name: r.gene_name,
|
|
210
|
+
...r.excess != null ? { excess: r.excess } : {}
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
const plotWidth = 1e3;
|
|
214
|
+
const plotHeight = 300;
|
|
215
|
+
const { png, plot_data } = await renderManhattanPoints({
|
|
216
|
+
points,
|
|
217
|
+
chrSizes,
|
|
218
|
+
plotWidth,
|
|
219
|
+
plotHeight,
|
|
220
|
+
devicePixelRatio: devicePixelRatio || 1,
|
|
221
|
+
pngDotRadius: 2,
|
|
222
|
+
// the GRIN2 defaults: raise the cap only when more than 5 dots would sit on it
|
|
223
|
+
maxCappedPoints: 5,
|
|
224
|
+
hardCap: 200,
|
|
225
|
+
binSize: 10,
|
|
226
|
+
interactive: SCAN_INTERACTIVE_PER_SIDE,
|
|
227
|
+
signed: true,
|
|
228
|
+
// open circles, as the volcano above draws the same DMRs
|
|
229
|
+
hollow: true
|
|
230
|
+
});
|
|
231
|
+
return { png, plotData: plot_data, interactive: SCAN_INTERACTIVE_PER_SIDE, plotWidth, plotHeight };
|
|
232
|
+
}
|
|
233
|
+
function renderMethylationProfile(scan, genome, devicePixelRatio, profileBinBp) {
|
|
234
|
+
if (!scan.binMethylation?.bins.length) return void 0;
|
|
235
|
+
const bm = coarsenProfile(scan.binMethylation, profileBinBp);
|
|
236
|
+
const chrSizes = {};
|
|
237
|
+
for (const c of genome.majorchrorder) if (c != "chrM" && c != "chrMT") chrSizes[c] = genome.majorchr[c];
|
|
238
|
+
const points = bm.bins.map((b) => {
|
|
239
|
+
const d = b.case - b.control;
|
|
240
|
+
return {
|
|
241
|
+
chrom: b.chr,
|
|
242
|
+
pos: b.start,
|
|
243
|
+
y: d,
|
|
244
|
+
color: d < 0 ? HYPO_COLOR : HYPER_COLOR,
|
|
245
|
+
control: b.control,
|
|
246
|
+
case: b.case,
|
|
247
|
+
n_probes: b.n_probes
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
const plotWidth = 1e3;
|
|
251
|
+
const plotHeight = 160;
|
|
252
|
+
const dotRadius = 1;
|
|
253
|
+
return renderManhattanPoints({
|
|
254
|
+
points,
|
|
255
|
+
chrSizes,
|
|
256
|
+
plotWidth,
|
|
257
|
+
plotHeight,
|
|
258
|
+
devicePixelRatio: devicePixelRatio || 1,
|
|
259
|
+
pngDotRadius: dotRadius,
|
|
260
|
+
maxCappedPoints: 5,
|
|
261
|
+
hardCap: 200,
|
|
262
|
+
binSize: 10,
|
|
263
|
+
/* The bins that moved most, each way. Ranking is |y| = |Δβ| here rather than evidence: on
|
|
264
|
+
this figure the effect size IS the result, and the bins a reader wants to identify are the
|
|
265
|
+
excursions away from the zero line. */
|
|
266
|
+
interactive: SCAN_INTERACTIVE_PER_SIDE,
|
|
267
|
+
signed: true,
|
|
268
|
+
capping: false
|
|
269
|
+
}).then(({ png, plot_data }) => ({
|
|
270
|
+
png,
|
|
271
|
+
plotData: plot_data,
|
|
272
|
+
binBp: bm.binBp,
|
|
273
|
+
plotWidth,
|
|
274
|
+
plotHeight,
|
|
275
|
+
dotRadius,
|
|
276
|
+
bins: bm.bins.length,
|
|
277
|
+
// what the top-N-per-side rule actually left live, which at a coarse width is every bin
|
|
278
|
+
interactive: plot_data.points.length
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
109
281
|
async function runDmFresh(param, ds, term_results, term_results2, imputeMissing) {
|
|
110
282
|
const groups = await resolveDmSampleGroups(param, ds, term_results, term_results2);
|
|
111
283
|
if (groups.alerts.length) throw new Error(groups.alerts.join(" | "));
|
|
@@ -150,10 +322,10 @@ async function resolveDmSampleGroups(param, ds, term_results, term_results2) {
|
|
|
150
322
|
throw new Error("Group 1 has no samples. Please select at least one sample.");
|
|
151
323
|
if (param.samplelst.groups[1].values?.length < 1)
|
|
152
324
|
throw new Error("Group 2 has no samples. Please select at least one sample.");
|
|
153
|
-
const
|
|
325
|
+
const allSampleSet = eligibleMethylationSamples(ds, param.element_type);
|
|
154
326
|
const g1 = await buildGroupValues(
|
|
155
327
|
param.samplelst.groups[0].values,
|
|
156
|
-
|
|
328
|
+
allSampleSet,
|
|
157
329
|
ds,
|
|
158
330
|
param.tw,
|
|
159
331
|
param.tw2,
|
|
@@ -162,7 +334,7 @@ async function resolveDmSampleGroups(param, ds, term_results, term_results2) {
|
|
|
162
334
|
);
|
|
163
335
|
const g2 = await buildGroupValues(
|
|
164
336
|
param.samplelst.groups[1].values,
|
|
165
|
-
|
|
337
|
+
allSampleSet,
|
|
166
338
|
ds,
|
|
167
339
|
param.tw,
|
|
168
340
|
param.tw2,
|
|
@@ -192,5 +364,6 @@ export {
|
|
|
192
364
|
init,
|
|
193
365
|
listElementTypes,
|
|
194
366
|
resolveDmSampleGroups,
|
|
195
|
-
resolveElementQuery
|
|
367
|
+
resolveElementQuery,
|
|
368
|
+
scanChromosomes
|
|
196
369
|
};
|