@sjcrh/proteinpaint-server 2.185.0 → 2.187.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.185.0",
3
+ "version": "2.187.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",
@@ -62,11 +62,11 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@sjcrh/augen": "2.181.1",
65
- "@sjcrh/proteinpaint-python": "2.185.0",
65
+ "@sjcrh/proteinpaint-python": "2.187.0",
66
66
  "@sjcrh/proteinpaint-r": "2.181.0",
67
- "@sjcrh/proteinpaint-rust": "2.185.0",
68
- "@sjcrh/proteinpaint-shared": "2.185.0",
69
- "@sjcrh/proteinpaint-types": "2.185.0",
67
+ "@sjcrh/proteinpaint-rust": "2.186.0",
68
+ "@sjcrh/proteinpaint-shared": "2.187.0",
69
+ "@sjcrh/proteinpaint-types": "2.187.0",
70
70
  "@types/express": "^5.0.0",
71
71
  "@types/express-session": "^1.18.1",
72
72
  "better-sqlite3": "^12.4.1",
@@ -31,6 +31,10 @@ function init({ genomes }) {
31
31
  res.send(results);
32
32
  return;
33
33
  }
34
+ if (typeof results === "object" && results.error) {
35
+ res.send({ status: "error", error: results.error });
36
+ return;
37
+ }
34
38
  if (typeof results != "string") throw new Error("gsea result is not string");
35
39
  res.sendFile(results, (err) => {
36
40
  fs.unlink(results, () => {
@@ -77,12 +81,24 @@ async function resolveGseaGenesAndFoldChange({
77
81
  }) {
78
82
  if (q.cacheId) {
79
83
  if (!q.daRequest) throw new Error("daCacheMissing");
80
- const result = await readCacheFileOrRecompute({ daRequest: q.daRequest, genomes });
84
+ const result = await readCacheFileOrRecompute({
85
+ daRequest: q.daRequest,
86
+ genomes
87
+ });
81
88
  if (result.cacheId !== q.cacheId) throw new Error("cacheId does not match daRequest");
82
- return {
83
- genes: result.geneData.map((g) => g.gene_name),
84
- fold_change: result.geneData.map((g) => g.fold_change)
85
- };
89
+ if (result.kind === "DE") {
90
+ return {
91
+ genes: result.geneData.map((g) => g.gene_name),
92
+ fold_change: result.geneData.map((g) => g.fold_change)
93
+ };
94
+ }
95
+ if (result.kind === "DM") {
96
+ return {
97
+ genes: result.promoterData.map((p) => p.gene_name),
98
+ fold_change: result.promoterData.map((p) => p.fold_change)
99
+ };
100
+ }
101
+ throw new Error(`unexpected result kind: ${result.kind}`);
86
102
  }
87
103
  if (!q.genes || !q.fold_change) throw new Error("requires genes and fold_change when cacheId is absent");
88
104
  return { genes: q.genes, fold_change: q.fold_change };
@@ -0,0 +1,107 @@
1
+ import { ProfileForms2ScoresPayload } from "#types/checkers";
2
+ import { getData } from "../src/termdb.matrix.js";
3
+ const api = {
4
+ endpoint: "termdb/profileForms2Scores",
5
+ methods: {
6
+ get: {
7
+ ...ProfileForms2ScoresPayload,
8
+ init
9
+ },
10
+ post: {
11
+ ...ProfileForms2ScoresPayload,
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
+ async function getScores(query, ds) {
31
+ const { activeCohort, clientAuthResult } = query.__protected__;
32
+ const facilityTermId = ds.cohort.termdb.plotConfigByCohort?.[activeCohort]?.profileForms2?.facilityTW?.id;
33
+ if (!facilityTermId) throw `profileForms2.facilityTW.id missing for cohort '${activeCohort}'`;
34
+ const facilityTW = { term: { id: facilityTermId }, q: {} };
35
+ const terms = [facilityTW, ...query.scoreTerms];
36
+ if (query.scScoreTerms) terms.push(...query.scScoreTerms);
37
+ if (!query.filterByUserSites) query.__protected__.ignoredTermIds.push(facilityTermId);
38
+ const cohortAuth = clientAuthResult[activeCohort];
39
+ const isPublic = !cohortAuth?.role || cohortAuth.role === "public";
40
+ const userSites = cohortAuth?.sites;
41
+ const raw = await getData(
42
+ {
43
+ terms,
44
+ filter: query.filter,
45
+ __protected__: query.__protected__
46
+ },
47
+ ds
48
+ );
49
+ if (raw.error) throw raw.error;
50
+ const samples = Object.values(raw.samples);
51
+ let sites = samples.map((s) => {
52
+ const val = s[facilityTW.$id].value;
53
+ let label = facilityTW.term.values?.[val]?.label || val;
54
+ if (label.length > 50) label = label.slice(0, 47) + "...";
55
+ return { value: val, label };
56
+ });
57
+ if (userSites && query.filterByUserSites) sites = sites.filter((s) => userSites.includes(s.value));
58
+ sites.sort((a, b) => a.label.localeCompare(b.label));
59
+ const eligibleSamples = userSites && query.filterByUserSites ? samples.filter((s) => userSites.includes(s[facilityTW.$id].value)) : samples;
60
+ const term2Score = {};
61
+ for (const d of query.scoreTerms) {
62
+ term2Score[d.term.id] = getPercentsDict((sample) => getDict(d.$id, sample), eligibleSamples);
63
+ }
64
+ if (query.scScoreTerms) {
65
+ for (const d of query.scScoreTerms) {
66
+ term2Score[d.term.id] = getSCPercentsDict(d, eligibleSamples);
67
+ }
68
+ }
69
+ return {
70
+ term2Score,
71
+ sites: isPublic ? [] : sites,
72
+ // public never sees site IDs
73
+ n: eligibleSamples.length
74
+ };
75
+ }
76
+ function getDict(key, sample) {
77
+ if (!sample[key]) return null;
78
+ const termData = sample[key].value;
79
+ return JSON.parse(termData);
80
+ }
81
+ function getPercentsDict(getDictFunc, samples) {
82
+ const percentageDict = {};
83
+ for (const sample of samples) {
84
+ const percents = getDictFunc(sample);
85
+ if (!percents) continue;
86
+ for (const key in percents) {
87
+ const value = percents[key];
88
+ if (!percentageDict[key]) percentageDict[key] = 0;
89
+ percentageDict[key] += value;
90
+ }
91
+ }
92
+ return percentageDict;
93
+ }
94
+ function getSCPercentsDict(tw, samples) {
95
+ if (!tw) throw "tw not defined";
96
+ const percentageDict = {};
97
+ for (const sample of samples) {
98
+ const key = sample[tw.$id]?.value;
99
+ if (key == null) continue;
100
+ if (!percentageDict[key]) percentageDict[key] = 0;
101
+ percentageDict[key] += 1;
102
+ }
103
+ return percentageDict;
104
+ }
105
+ export {
106
+ api
107
+ };
@@ -5,7 +5,7 @@ import serverconfig from "../src/serverconfig.js";
5
5
  import { get_header_txt } from "#src/utils.js";
6
6
  import { run_rust } from "@sjcrh/proteinpaint-rust";
7
7
  import { renderVolcano } from "../src/renderVolcano.ts";
8
- import { readCacheFileOrRecompute, resolveDeContext, resolveSampleGroups } from "../src/diffAnalysis.ts";
8
+ import { readCacheFileOrRecompute, resolveDaContext, resolveSampleGroups } from "../src/diffAnalysis.ts";
9
9
  const api = {
10
10
  endpoint: "termdb/DE",
11
11
  methods: {
@@ -24,7 +24,7 @@ function init({ genomes }) {
24
24
  try {
25
25
  const q = req.query;
26
26
  if (q.preAnalysis) {
27
- const { ds, term_results, term_results2 } = await resolveDeContext(q, genomes);
27
+ const { ds, term_results, term_results2 } = await resolveDaContext(q, genomes);
28
28
  const groups = resolveSampleGroups(q, ds, term_results, term_results2);
29
29
  const group1Name = q.samplelst.groups[0].name;
30
30
  const group2Name = q.samplelst.groups[1].name;
@@ -37,20 +37,18 @@ function init({ genomes }) {
37
37
  });
38
38
  return;
39
39
  }
40
- const { cacheId, geneData, sample_size1, sample_size2, method, images, bcv } = await readCacheFileOrRecompute({
41
- daRequest: q,
42
- genomes
43
- });
44
- const rendered = await renderVolcano(geneData, q.volcanoRender);
45
- rendered.cacheId = cacheId;
40
+ const result = await readCacheFileOrRecompute({ daRequest: q, genomes });
41
+ if (result.kind !== "DE") throw new Error("expected DE result from readCacheFileOrRecompute");
42
+ const rendered = await renderVolcano(result.geneData, q.volcanoRender);
43
+ rendered.cacheId = result.cacheId;
46
44
  const output = {
47
45
  data: rendered,
48
- sample_size1,
49
- sample_size2,
50
- method,
51
- images
46
+ sample_size1: result.sample_size1,
47
+ sample_size2: result.sample_size2,
48
+ method: result.method,
49
+ images: result.images
52
50
  };
53
- if (bcv != null) output.bcv = bcv;
51
+ if (result.bcv != null) output.bcv = result.bcv;
54
52
  res.send(output);
55
53
  } catch (e) {
56
54
  res.send({ status: "error", error: e.message || e });
@@ -261,18 +261,24 @@ async function validateNative(q, ds) {
261
261
  const vr = JSON.parse(tmp);
262
262
  if (vr.status !== "success") throw vr.message;
263
263
  if (!vr.samples?.length) throw "HDF5 file has no samples, please check file.";
264
+ const unknownSamples = /* @__PURE__ */ new Set();
264
265
  for (const sn of vr.samples) {
265
266
  const si = ds.cohort.termdb.q.sampleName2id(sn);
266
- if (si == void 0) {
267
+ if (si === void 0) {
267
268
  if (ds.cohort.db) {
268
269
  throw `unknown sample ${sn} from HDF5 ${q.file}`;
269
270
  } else {
271
+ unknownSamples.add(sn);
270
272
  continue;
271
273
  }
272
274
  }
273
275
  q.samples.push(si);
274
276
  }
275
277
  console.log(`${ds.label}: geneExpression HDF5 file validated. Format: ${vr.format}, Samples:`, q.samples.length);
278
+ if (unknownSamples.size) {
279
+ const arr = [...unknownSamples];
280
+ console.log(`unknown samples from geneExpression HDF5 file (${arr.length}): ${arr.join(", ")}`);
281
+ }
276
282
  } catch (error) {
277
283
  throw `${ds.label}: Failed to validate geneExpression HDF5 file: ${error}`;
278
284
  }
@@ -195,23 +195,37 @@ function addNonDictionaryQueries(c, ds, genome) {
195
195
  }
196
196
  if (q.proteome) {
197
197
  q2.proteome = {};
198
- if (q.proteome.overlayTerm) {
199
- q2.proteome.overlayTerm = JSON.parse(JSON.stringify(q.proteome.overlayTerm));
200
- }
201
- if (q.proteome.assays) {
202
- q2.proteome.assays = {};
203
- for (const assay in q.proteome.assays) {
204
- q2.proteome.assays[assay] = {};
205
- if (q.proteome.assays[assay].cohorts) {
206
- q2.proteome.assays[assay].cohorts = {};
207
- for (const cohort in q.proteome.assays[assay].cohorts) {
208
- q2.proteome.assays[assay].cohorts[cohort] = {};
209
- const src = q.proteome.assays[assay].cohorts[cohort];
210
- if ("controlFilter" in src) {
211
- q2.proteome.assays[assay].cohorts[cohort].controlFilter = JSON.parse(JSON.stringify(src.controlFilter));
212
- }
213
- if ("caseFilter" in src) {
214
- q2.proteome.assays[assay].cohorts[cohort].caseFilter = JSON.parse(JSON.stringify(src.caseFilter));
198
+ if (q.proteome.organisms) {
199
+ q2.proteome.organisms = {};
200
+ for (const organism in q.proteome.organisms) {
201
+ q2.proteome.organisms[organism] = {};
202
+ const orgSrc = q.proteome.organisms[organism];
203
+ if (orgSrc.overlayTerm) {
204
+ q2.proteome.organisms[organism].overlayTerm = JSON.parse(JSON.stringify(orgSrc.overlayTerm));
205
+ }
206
+ if (orgSrc.genomeName) {
207
+ q2.proteome.organisms[organism].genomeName = orgSrc.genomeName;
208
+ }
209
+ if (orgSrc.assays) {
210
+ q2.proteome.organisms[organism].assays = {};
211
+ for (const assay in orgSrc.assays) {
212
+ q2.proteome.organisms[organism].assays[assay] = {};
213
+ if (orgSrc.assays[assay].cohorts) {
214
+ q2.proteome.organisms[organism].assays[assay].cohorts = {};
215
+ for (const cohort in orgSrc.assays[assay].cohorts) {
216
+ q2.proteome.organisms[organism].assays[assay].cohorts[cohort] = {};
217
+ const src = orgSrc.assays[assay].cohorts[cohort];
218
+ if ("controlFilter" in src) {
219
+ q2.proteome.organisms[organism].assays[assay].cohorts[cohort].controlFilter = JSON.parse(
220
+ JSON.stringify(src.controlFilter)
221
+ );
222
+ }
223
+ if ("caseFilter" in src) {
224
+ q2.proteome.organisms[organism].assays[assay].cohorts[cohort].caseFilter = JSON.parse(
225
+ JSON.stringify(src.caseFilter)
226
+ );
227
+ }
228
+ }
215
229
  }
216
230
  }
217
231
  }
@@ -305,7 +319,7 @@ function getDsAllowedTermTypes(ds) {
305
319
  if (ds.queries?.geneExpression) typeSet.add(GENE_EXPRESSION);
306
320
  if (ds.queries?.isoformExpression) typeSet.add(ISOFORM_EXPRESSION);
307
321
  if (ds.queries?.metaboliteIntensity) typeSet.add(METABOLITE_INTENSITY);
308
- if (ds.queries?.proteome?.assays) typeSet.add(PROTEOME_ABUNDANCE);
322
+ if (ds.queries?.proteome) typeSet.add(PROTEOME_ABUNDANCE);
309
323
  if (ds.queries?.ssGSEA) typeSet.add(SSGSEA);
310
324
  if (ds.queries?.dnaMethylation) typeSet.add(DNA_METHYLATION);
311
325
  if (ds.queries?.singleCell) {
@@ -1,9 +1,6 @@
1
1
  import { diffMethPayload } from "#types/checkers";
2
- import { getData } from "../src/termdb.matrix.js";
3
- import { run_R } from "@sjcrh/proteinpaint-r";
4
- import { mayLog } from "#src/helpers.ts";
5
- import { formatElapsedTime } from "#shared";
6
2
  import { renderVolcano } from "../src/renderVolcano.ts";
3
+ import { readCacheFileOrRecompute, resolveDaContext, resolveDmSampleGroups } from "../src/diffAnalysis.ts";
7
4
  const api = {
8
5
  endpoint: "termdb/diffMeth",
9
6
  methods: {
@@ -21,173 +18,38 @@ function init({ genomes }) {
21
18
  return async (req, res) => {
22
19
  try {
23
20
  const q = req.query;
24
- const genome = genomes[q.genome];
25
- if (!genome) throw "unknown genome";
26
- const ds = genome.datasets?.[q.dslabel];
27
- if (!ds) throw "unknown ds";
28
- let term_results = [];
29
- if (q.tw) {
30
- term_results = await getData({ filter: q.filter, filter0: q.filter0, terms: [q.tw] }, ds);
31
- if (term_results.error) throw new Error(term_results.error);
32
- }
33
- let term_results2 = [];
34
- if (q.tw2) {
35
- term_results2 = await getData({ filter: q.filter, filter0: q.filter0, terms: [q.tw2] }, ds);
36
- if (term_results2.error) throw new Error(term_results2.error);
37
- }
38
- const results = await run_diffMeth(req.query, ds, term_results, term_results2);
39
- if (!results || !results.data)
40
- throw new Error(
41
- "Differential methylation analysis returned no data. Please verify sample selections and try again."
42
- );
43
- if ("totalRows" in results.data && results.data.totalRows === 0)
21
+ if (q.preAnalysis) {
22
+ const { ds, term_results, term_results2 } = await resolveDaContext(q, genomes);
23
+ const groups = resolveDmSampleGroups(q, ds, term_results, term_results2);
24
+ const group1Name = q.samplelst.groups[0].name;
25
+ const group2Name = q.samplelst.groups[1].name;
26
+ res.send({
27
+ data: {
28
+ [group1Name]: groups.group1names.length,
29
+ [group2Name]: groups.group2names.length,
30
+ ...groups.alerts.length ? { alert: groups.alerts.join(" | ") } : {}
31
+ }
32
+ });
33
+ return;
34
+ }
35
+ const result = await readCacheFileOrRecompute({ daRequest: q, genomes });
36
+ if (result.kind !== "DM") throw new Error("expected DM result from readCacheFileOrRecompute");
37
+ const rendered = await renderVolcano(result.promoterData, q.volcanoRender);
38
+ rendered.cacheId = result.cacheId;
39
+ if (rendered.totalRows === 0)
44
40
  throw new Error("No promoters passed filtering. Try relaxing group criteria or selecting more samples.");
45
- res.send(results);
41
+ const output = {
42
+ data: rendered,
43
+ sample_size1: result.sample_size1,
44
+ sample_size2: result.sample_size2
45
+ };
46
+ res.send(output);
46
47
  } catch (e) {
47
48
  res.send({ status: "error", error: e.message || e });
48
49
  if (e instanceof Error && e.stack) console.log(e);
49
50
  }
50
51
  };
51
52
  }
52
- async function run_diffMeth(param, ds, term_results, term_results2) {
53
- if (param.samplelst?.groups?.length != 2)
54
- throw new Error("Exactly 2 sample groups are required for differential methylation analysis.");
55
- if (param.samplelst.groups[0].values?.length < 1)
56
- throw new Error("Group 1 has no samples. Please select at least one sample.");
57
- if (param.samplelst.groups[1].values?.length < 1)
58
- throw new Error("Group 2 has no samples. Please select at least one sample.");
59
- const q = ds.queries.dnaMethylation?.promoter;
60
- if (!q) throw new Error("This dataset does not have promoter-level methylation data configured.");
61
- if (!q.file) throw new Error("Promoter methylation data file is not configured for this dataset.");
62
- const group1names = [];
63
- const conf1_group1 = [];
64
- const conf2_group1 = [];
65
- for (const s of param.samplelst.groups[0].values) {
66
- if (!Number.isInteger(s.sampleId)) continue;
67
- const n = ds.cohort.termdb.q.id2sampleName(s.sampleId);
68
- if (!n) continue;
69
- if (!q.allSampleSet.has(n)) continue;
70
- if (param.tw && param.tw2) {
71
- if (term_results.samples[s.sampleId] && term_results2.samples[s.sampleId]) {
72
- conf1_group1.push(
73
- param.tw.q.mode == "continuous" ? term_results.samples[s.sampleId][param.tw.$id]["value"] : term_results.samples[s.sampleId][param.tw.$id]["key"]
74
- );
75
- conf2_group1.push(
76
- param.tw2.q.mode == "continuous" ? term_results2.samples[s.sampleId][param.tw2.$id]["value"] : term_results2.samples[s.sampleId][param.tw2.$id]["key"]
77
- );
78
- group1names.push(n);
79
- }
80
- } else if (param.tw && !param.tw2) {
81
- if (term_results.samples[s.sampleId]) {
82
- conf1_group1.push(
83
- param.tw.q.mode == "continuous" ? term_results.samples[s.sampleId][param.tw.$id]["value"] : term_results.samples[s.sampleId][param.tw.$id]["key"]
84
- );
85
- group1names.push(n);
86
- }
87
- } else if (!param.tw && param.tw2) {
88
- if (term_results2.samples[s.sampleId]) {
89
- conf2_group1.push(
90
- param.tw2.q.mode == "continuous" ? term_results2.samples[s.sampleId][param.tw2.$id]["value"] : term_results2.samples[s.sampleId][param.tw2.$id]["key"]
91
- );
92
- group1names.push(n);
93
- }
94
- } else {
95
- group1names.push(n);
96
- }
97
- }
98
- const group2names = [];
99
- const conf1_group2 = [];
100
- const conf2_group2 = [];
101
- for (const s of param.samplelst.groups[1].values) {
102
- if (!Number.isInteger(s.sampleId)) continue;
103
- const n = ds.cohort.termdb.q.id2sampleName(s.sampleId);
104
- if (!n) continue;
105
- if (!q.allSampleSet.has(n)) continue;
106
- if (param.tw && param.tw2) {
107
- if (term_results.samples[s.sampleId] && term_results2.samples[s.sampleId]) {
108
- conf1_group2.push(
109
- param.tw.q.mode == "continuous" ? term_results.samples[s.sampleId][param.tw.$id]["value"] : term_results.samples[s.sampleId][param.tw.$id]["key"]
110
- );
111
- conf2_group2.push(
112
- param.tw2.q.mode == "continuous" ? term_results2.samples[s.sampleId][param.tw2.$id]["value"] : term_results2.samples[s.sampleId][param.tw2.$id]["key"]
113
- );
114
- group2names.push(n);
115
- }
116
- } else if (param.tw && !param.tw2) {
117
- if (term_results.samples[s.sampleId]) {
118
- conf1_group2.push(
119
- param.tw.q.mode == "continuous" ? term_results.samples[s.sampleId][param.tw.$id]["value"] : term_results.samples[s.sampleId][param.tw.$id]["key"]
120
- );
121
- group2names.push(n);
122
- }
123
- } else if (!param.tw && param.tw2) {
124
- if (term_results2.samples[s.sampleId]) {
125
- conf2_group2.push(
126
- param.tw2.q.mode == "continuous" ? term_results2.samples[s.sampleId][param.tw2.$id]["value"] : term_results2.samples[s.sampleId][param.tw2.$id]["key"]
127
- );
128
- group2names.push(n);
129
- }
130
- } else {
131
- group2names.push(n);
132
- }
133
- }
134
- const sample_size1 = group1names.length;
135
- const sample_size2 = group2names.length;
136
- const alerts = validateGroups(sample_size1, sample_size2, group1names, group2names);
137
- if (param.preAnalysis) {
138
- const group1Name = param.samplelst.groups[0].name;
139
- const group2Name = param.samplelst.groups[1].name;
140
- return {
141
- data: {
142
- [group1Name]: sample_size1,
143
- [group2Name]: sample_size2,
144
- ...alerts.length ? { alert: alerts.join(" | ") } : {}
145
- }
146
- };
147
- }
148
- if (alerts.length) throw new Error(alerts.join(" | "));
149
- const diffMethInput = {
150
- case: group2names.join(","),
151
- control: group1names.join(","),
152
- input_file: q.file,
153
- min_samples_per_group: param.min_samples_per_group
154
- };
155
- if (param.tw) {
156
- diffMethInput.conf1 = [...conf1_group2, ...conf1_group1];
157
- diffMethInput.conf1_mode = param.tw.q.mode;
158
- if (new Set(diffMethInput.conf1).size === 1) {
159
- throw new Error("Confounding variable 1 has only one value");
160
- }
161
- }
162
- if (param.tw2) {
163
- diffMethInput.conf2 = [...conf2_group2, ...conf2_group1];
164
- diffMethInput.conf2_mode = param.tw2.q.mode;
165
- if (new Set(diffMethInput.conf2).size === 1) {
166
- throw new Error("Confounding variable 2 has only one value");
167
- }
168
- }
169
- const time1 = Date.now();
170
- const result = JSON.parse(await run_R("diffMeth.R", JSON.stringify(diffMethInput)));
171
- mayLog("Time taken to run diffMeth:", formatElapsedTime(Date.now() - time1));
172
- const rendered = await renderVolcano(result.promoter_data, param.volcanoRender);
173
- const output = {
174
- data: rendered,
175
- sample_size1,
176
- sample_size2
177
- };
178
- return output;
179
- }
180
- function validateGroups(sample_size1, sample_size2, group1names, group2names) {
181
- const alerts = [];
182
- if (sample_size1 < 1) alerts.push("No samples in group 1 have methylation data available.");
183
- if (sample_size2 < 1) alerts.push("No samples in group 2 have methylation data available.");
184
- const commonnames = group1names.filter((x) => group2names.includes(x));
185
- if (commonnames.length)
186
- alerts.push(
187
- `${commonnames.length} sample(s) appear in both groups: ${commonnames.join(", ")}. Please remove duplicates.`
188
- );
189
- return alerts;
190
- }
191
53
  export {
192
54
  api
193
55
  };