@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.
@@ -3,9 +3,10 @@ const mafFields = [
3
3
  id: "tumor_DNA",
4
4
  name: "Tumor DNA",
5
5
  parent_id: null,
6
- child_ids: ["tumor_DNA_WGS"],
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,
@@ -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.208.0",
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.204.0",
60
- "@sjcrh/proteinpaint-python": "2.208.0",
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.208.0",
63
- "@sjcrh/proteinpaint-shared": "2.208.0",
64
- "@sjcrh/proteinpaint-types": "2.208.0",
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-13T18:11:20Z"
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("No promoters passed filtering. Try relaxing group criteria or selecting more samples.");
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 { q } = resolveElementQuery(ds, param.element_type);
333
+ const allSampleSet = eligibleMethylationSamples(ds, param.element_type);
154
334
  const g1 = await buildGroupValues(
155
335
  param.samplelst.groups[0].values,
156
- q.allSampleSet,
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
- q.allSampleSet,
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
  };