@sjcrh/proteinpaint-server 2.176.0 → 2.177.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.
@@ -1,3 +1,36 @@
1
+ const mafFields = [
2
+ {
3
+ id: "tumor_DNA",
4
+ name: "Tumor DNA",
5
+ parent_id: null,
6
+ child_ids: ["tumor_DNA_WGS"],
7
+ isleaf: true,
8
+ type: "float",
9
+ default: true,
10
+ // indicates default maf term (e.g. will be used by default for making maf filters in predefined groupset)
11
+ min: 0,
12
+ max: 1,
13
+ tvs: {
14
+ ranges: [
15
+ {
16
+ start: 0.1,
17
+ startinclusive: true,
18
+ stopunbounded: true
19
+ }
20
+ ]
21
+ }
22
+ }
23
+ ];
24
+ const mafFilter = {
25
+ opts: { joinWith: ["and", "or"] },
26
+ filter: {
27
+ type: "tvslst",
28
+ join: "",
29
+ in: true,
30
+ lst: []
31
+ },
32
+ terms: mafFields
33
+ };
1
34
  function termdb_test_default() {
2
35
  return {
3
36
  isMds3: true,
@@ -285,6 +318,7 @@ function termdb_test_default() {
285
318
  }
286
319
  ]
287
320
  },
321
+ mafFilter,
288
322
  skewerRim: {
289
323
  type: "format",
290
324
  formatKey: "origin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.176.0",
3
+ "version": "2.177.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",
@@ -64,9 +64,9 @@
64
64
  "@sjcrh/augen": "2.143.0",
65
65
  "@sjcrh/proteinpaint-python": "2.174.0",
66
66
  "@sjcrh/proteinpaint-r": "2.152.1-0",
67
- "@sjcrh/proteinpaint-rust": "2.175.0",
68
- "@sjcrh/proteinpaint-shared": "2.175.0",
69
- "@sjcrh/proteinpaint-types": "2.176.0",
67
+ "@sjcrh/proteinpaint-rust": "2.177.0",
68
+ "@sjcrh/proteinpaint-shared": "2.177.0",
69
+ "@sjcrh/proteinpaint-types": "2.177.0",
70
70
  "@types/express": "^5.0.0",
71
71
  "@types/express-session": "^1.18.1",
72
72
  "better-sqlite3": "^12.4.1",
package/routes/grin2.js CHANGED
@@ -431,17 +431,6 @@ function filterAndConvertSnvIndel(sampleName, entry, options) {
431
431
  if (!Number.isInteger(entry.pos)) {
432
432
  return null;
433
433
  }
434
- if (options.minAltAlleleCount !== void 0 && options.minAltAlleleCount > 0) {
435
- if (!entry.altCount || entry.altCount < options.minAltAlleleCount) {
436
- return null;
437
- }
438
- }
439
- if (options.minTotalDepth !== void 0 && options.minTotalDepth > 0) {
440
- const totalDepth = (entry.refCount || 0) + (entry.altCount || 0);
441
- if (totalDepth < options.minTotalDepth) {
442
- return null;
443
- }
444
- }
445
434
  const start = entry.pos;
446
435
  const end = entry.pos;
447
436
  return [sampleName, entry.chr, start, end, dt2lesion[dtsnvindel].lesionTypes[0].lesionType];
@@ -139,8 +139,7 @@ async function run_chat_pipeline(user_prompt, llm, aiRoute, dataset_json, testin
139
139
  user_prompt,
140
140
  llm,
141
141
  aiRoute,
142
- dataset_json,
143
- testing
142
+ dataset_json
144
143
  );
145
144
  let ai_output_json;
146
145
  mayLog("Time taken for classification:", formatElapsedTime(Date.now() - time1));
@@ -302,7 +301,7 @@ async function readJSONFile(file) {
302
301
  const json_file = await fs.promises.readFile(file);
303
302
  return JSON.parse(json_file.toString());
304
303
  }
305
- async function classify_query_by_dataset_type(user_prompt, llm, aiRoute, dataset_json, testing) {
304
+ async function classify_query_by_dataset_type(user_prompt, llm, aiRoute, dataset_json) {
306
305
  const data = await readJSONFile(aiRoute);
307
306
  let contents = data["general"];
308
307
  for (const key of Object.keys(data)) {
@@ -320,11 +319,7 @@ async function classify_query_by_dataset_type(user_prompt, llm, aiRoute, dataset
320
319
  }
321
320
  const template = contents + " training data is as follows:" + training_data + " Question: {" + user_prompt + "} Answer: {answer}";
322
321
  const response = await route_to_appropriate_llm_provider(template, llm);
323
- if (testing) {
324
- return { action: "html", response: JSON.parse(response) };
325
- } else {
326
- return JSON.parse(response);
327
- }
322
+ return JSON.parse(response);
328
323
  }
329
324
  async function extract_DE_search_terms_from_query(prompt, llm, dataset_db_output, dataset_json, ds, testing) {
330
325
  if (dataset_json.hasDE) {
@@ -567,10 +562,14 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
567
562
  const pp_plot_json = { chartType: "summary" };
568
563
  let html = "";
569
564
  if (response_type.html) html = response_type.html;
570
- if (!response_type.term) html += "term type is not present in summary output";
565
+ if (!response_type.term) {
566
+ html += "term type is not present in summary output";
567
+ return { type: "html", html };
568
+ }
571
569
  const term1_validation = validate_term(response_type.term, common_genes, dataset_json, ds);
572
570
  if (term1_validation.html.length > 0) {
573
571
  html += term1_validation.html;
572
+ return { type: "html", html };
574
573
  } else {
575
574
  pp_plot_json.term = term1_validation.term_type;
576
575
  if (term1_validation.category == "float" || term1_validation.category == "integer") {
@@ -582,6 +581,7 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
582
581
  const term2_validation = validate_term(response_type.term2, common_genes, dataset_json, ds);
583
582
  if (term2_validation.html.length > 0) {
584
583
  html += term2_validation.html;
584
+ return { type: "html", html };
585
585
  } else {
586
586
  pp_plot_json.term2 = term2_validation.term_type;
587
587
  if (term2_validation.category == "float" || term2_validation.category == "integer") {
@@ -594,8 +594,15 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
594
594
  const resolved = resolveChildType(pp_plot_json.category, pp_plot_json.category2, llmChildType);
595
595
  if (resolved.error) {
596
596
  html += resolved.error;
597
+ return { type: "html", html };
597
598
  } else {
598
599
  pp_plot_json.childType = resolved.childType;
600
+ if (pp_plot_json.childType == "barchart") {
601
+ pp_plot_json.term.q = { mode: "discrete" };
602
+ if (pp_plot_json.term2) {
603
+ pp_plot_json.term2.q = { mode: "discrete" };
604
+ }
605
+ }
599
606
  if (resolved.bothNumeric && (resolved.childType == "violin" || resolved.childType == "boxplot")) {
600
607
  pp_plot_json.term2.q = { mode: "discrete" };
601
608
  }
@@ -606,15 +613,12 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
606
613
  const validated_filters = validate_filter(response_type.simpleFilter, ds, "");
607
614
  if (validated_filters.html.length > 0) {
608
615
  html += validated_filters.html;
616
+ return { type: "html", html };
609
617
  } else {
610
618
  pp_plot_json.filter = validated_filters.simplefilter;
611
619
  }
612
620
  }
613
- if (html.length > 0) {
614
- return { type: "html", html };
615
- } else {
616
- return { type: "plot", plot: pp_plot_json };
617
- }
621
+ return { type: "plot", plot: pp_plot_json };
618
622
  }
619
623
  async function extract_matrix_search_terms_from_query(prompt, llm, dataset_db_output, dataset_json, genes_list, ds, testing) {
620
624
  const Schema = {
@@ -647,8 +651,6 @@ async function extract_matrix_search_terms_from_query(prompt, llm, dataset_db_ou
647
651
  };
648
652
  const common_genes = extractGenesFromPrompt(prompt, genes_list);
649
653
  const matrix_ds = dataset_json.charts.filter((chart) => chart.type == "Matrix");
650
- console.log("matrix_ds", matrix_ds);
651
- console.log("dataset_json.charts", dataset_json.charts);
652
654
  if (matrix_ds.length == 0) throw "Matrix information is not present in the dataset file.";
653
655
  if (matrix_ds[0].TrainingData.length == 0) throw "No training data is provided for the matrix agent.";
654
656
  const training_data = formatTrainingExamples(matrix_ds[0].TrainingData);
@@ -1,6 +1,5 @@
1
1
  import { termdbCohortSummaryPayload } from "#types/checkers";
2
2
  import { get_ds_tdb } from "#src/termdb.js";
3
- import { mayCopyFromCookie } from "#src/utils.js";
4
3
  import { get_samples } from "#src/termdb.sql.js";
5
4
  import { authApi } from "#src/auth.js";
6
5
  const api = {
@@ -15,7 +14,6 @@ const api = {
15
14
  function init({ genomes }) {
16
15
  return async (req, res) => {
17
16
  const q = req.query;
18
- mayCopyFromCookie(q, req.cookies);
19
17
  try {
20
18
  const genome = genomes[q.genome];
21
19
  if (!genome) throw "invalid genome";
@@ -1,6 +1,5 @@
1
1
  import { termdbCohortsPayload } from "#types/checkers";
2
2
  import { get_ds_tdb } from "#src/termdb.js";
3
- import { mayCopyFromCookie } from "#src/utils.js";
4
3
  const api = {
5
4
  endpoint: "termdb/cohorts",
6
5
  methods: {
@@ -13,7 +12,6 @@ const api = {
13
12
  function init({ genomes }) {
14
13
  return async (req, res) => {
15
14
  const q = req.query;
16
- mayCopyFromCookie(q, req.cookies);
17
15
  try {
18
16
  const genome = genomes[q.genome];
19
17
  if (!genome) throw "invalid genome";
@@ -1,7 +1,6 @@
1
1
  import serverconfig from "#src/serverconfig.js";
2
2
  import { authApi } from "#src/auth.js";
3
3
  import { get_ds_tdb } from "#src/termdb.js";
4
- import { mayCopyFromCookie } from "#src/utils.js";
5
4
  import { TermTypes } from "#shared/terms.js";
6
5
  const api = {
7
6
  endpoint: "termdb/config",
@@ -20,7 +19,6 @@ const api = {
20
19
  function init({ genomes }) {
21
20
  return async (req, res) => {
22
21
  const q = req.query;
23
- mayCopyFromCookie(q, req.cookies);
24
22
  try {
25
23
  const genome = genomes[q.genome];
26
24
  if (!genome) throw "invalid genome";
@@ -79,6 +77,7 @@ function make(q, req, res, ds, genome) {
79
77
  if (ds.cohort.correlationVolcano) c.correlationVolcano = ds.cohort.correlationVolcano;
80
78
  if (ds.cohort.boxplots) c.boxplots = ds.cohort.boxplots;
81
79
  if (tdb.maxGeneVariantGeneSetSize) c.maxGeneVariantGeneSetSize = tdb.maxGeneVariantGeneSetSize;
80
+ if (tdb.maxAnnoTermsPerClientRequest) c.maxAnnoTermsPerClientRequest = tdb.maxAnnoTermsPerClientRequest;
82
81
  addRestrictAncestries(c, tdb);
83
82
  addMatrixplots(c, ds);
84
83
  addMutationSignatureplots(c, ds);
@@ -1,5 +1,31 @@
1
1
  import { runChartPayload } from "#types/checkers";
2
- import { getNumberFromDate } from "#shared/terms.js";
2
+ import { getDateFromNumber, getNumberFromDate } from "#shared/terms.js";
3
+ function decimalYearToYearMonth(xRaw) {
4
+ const date = getDateFromNumber(xRaw);
5
+ const t = date.getTime();
6
+ if (Number.isFinite(t)) {
7
+ const yearNum = date.getFullYear();
8
+ const monthNum = date.getMonth() + 1;
9
+ if (Number.isFinite(yearNum) && Number.isFinite(monthNum)) return { yearNum, monthNum };
10
+ }
11
+ const parts = String(xRaw).split(".");
12
+ const year = Number(parts[0]);
13
+ if (!Number.isFinite(year)) return null;
14
+ let month;
15
+ if (parts.length > 1) {
16
+ const decimalPart = parts[1];
17
+ if (decimalPart.length === 2) {
18
+ const monthCandidate = Number(decimalPart);
19
+ month = monthCandidate >= 1 && monthCandidate <= 12 ? monthCandidate : Math.floor((xRaw - year) * 12) + 1;
20
+ } else {
21
+ month = Math.floor((xRaw - year) * 12) + 1;
22
+ }
23
+ } else {
24
+ month = 1;
25
+ }
26
+ if (!Number.isFinite(month) || month < 1 || month > 12) return null;
27
+ return { yearNum: year, monthNum: month };
28
+ }
3
29
  const api = {
4
30
  endpoint: "termdb/runChart",
5
31
  methods: {
@@ -14,9 +40,16 @@ const api = {
14
40
  }
15
41
  };
16
42
  async function getRunChart(q, ds) {
17
- const terms = [q.xtw, q.ytw];
18
43
  const xTermId = q.xtw["$id"] ?? q.xtw.term?.id;
19
- const yTermId = q.ytw["$id"] ?? q.ytw.term?.id;
44
+ const yTermId = q.ytw ? q.ytw["$id"] ?? q.ytw.term?.id : void 0;
45
+ if (xTermId == null || xTermId === "") {
46
+ throw new Error("runChart requires xtw with $id or term.id");
47
+ }
48
+ const isFrequency = !q.ytw;
49
+ if (!isFrequency && (yTermId == null || yTermId === "")) {
50
+ throw new Error("runChart requires ytw with $id or term.id when ytw is provided");
51
+ }
52
+ const terms = isFrequency ? [q.xtw] : [q.xtw, q.ytw];
20
53
  const { getData } = await import("../src/termdb.matrix.js");
21
54
  const data = await getData(
22
55
  {
@@ -29,7 +62,13 @@ async function getRunChart(q, ds) {
29
62
  );
30
63
  if (data.error) throw new Error(data.error);
31
64
  const shouldPartition = q.xtw?.q?.mode === "discrete";
32
- return buildRunChartFromData(q.aggregation, xTermId, yTermId, data, shouldPartition, xTermId);
65
+ if (isFrequency) {
66
+ return buildFrequencyFromData(xTermId, data, shouldPartition, xTermId, q.showCumulativeFrequency === true);
67
+ }
68
+ if (yTermId == null || yTermId === "") {
69
+ throw new Error("runChart requires ytw with $id or term.id when ytw is provided");
70
+ }
71
+ return buildRunChartFromData(q.aggregation ?? "median", xTermId, yTermId, data, shouldPartition, xTermId);
33
72
  }
34
73
  function buildRunChartFromData(aggregation, xTermId, yTermId, data, shouldPartition, partitionTermId) {
35
74
  const allSamples = data.samples || {};
@@ -56,6 +95,81 @@ function buildRunChartFromData(aggregation, xTermId, yTermId, data, shouldPartit
56
95
  const one = buildOneSeries(aggregation, xTermId, yTermId, data);
57
96
  return { status: "ok", series: [{ ...one }] };
58
97
  }
98
+ function buildFrequencyFromData(xTermId, data, shouldPartition, partitionTermId, showCumulativeFrequency) {
99
+ const allSamples = data.samples || {};
100
+ if (shouldPartition && partitionTermId) {
101
+ const period2Samples = {};
102
+ for (const sampleId in allSamples) {
103
+ const sample = allSamples[sampleId];
104
+ const partitionTerm = sample?.[partitionTermId];
105
+ if (partitionTerm?.key == null) continue;
106
+ const periodKey = partitionTerm.key;
107
+ if (!period2Samples[periodKey]) period2Samples[periodKey] = {};
108
+ period2Samples[periodKey][sampleId] = sample;
109
+ }
110
+ const periodKeys = Object.keys(period2Samples).sort();
111
+ const series = periodKeys.map((seriesId) => {
112
+ const subset = { samples: period2Samples[seriesId] };
113
+ const one2 = buildOneSeriesFrequency(xTermId, subset, showCumulativeFrequency === true);
114
+ return { seriesId, ...one2 };
115
+ });
116
+ return { status: "ok", series };
117
+ }
118
+ const one = buildOneSeriesFrequency(xTermId, data, showCumulativeFrequency === true);
119
+ return { status: "ok", series: [{ ...one }] };
120
+ }
121
+ function buildOneSeriesFrequency(xTermId, data, showCumulativeFrequency = false) {
122
+ if (xTermId == null || xTermId === "") {
123
+ throw new Error("buildOneSeriesFrequency requires a defined xTermId (xtw.$id or xtw.term.id)");
124
+ }
125
+ const buckets = {};
126
+ for (const sampleId in data.samples || {}) {
127
+ const sample = data.samples[sampleId];
128
+ const xRaw = sample?.[xTermId]?.value ?? sample?.[xTermId]?.key;
129
+ if (xRaw == null) continue;
130
+ if (typeof xRaw !== "number") {
131
+ throw new Error(
132
+ `x value must be a number for sample ${sampleId}: xTermId=${xTermId}, received type ${typeof xRaw}, value: ${xRaw}`
133
+ );
134
+ }
135
+ const parsed = decimalYearToYearMonth(xRaw);
136
+ if (!parsed) continue;
137
+ const { yearNum, monthNum } = parsed;
138
+ const bucketKey = `${yearNum}-${String(monthNum).padStart(2, "0")}`;
139
+ const bucketDate = new Date(yearNum, monthNum - 1, 1);
140
+ const xName = bucketDate.toLocaleString("en-US", { month: "long", year: "numeric" });
141
+ if (!buckets[bucketKey]) {
142
+ const midMonthX = getNumberFromDate(new Date(yearNum, monthNum - 1, 15));
143
+ buckets[bucketKey] = {
144
+ x: Math.round(midMonthX * 100) / 100,
145
+ xName,
146
+ count: 0,
147
+ sortKey: yearNum * 100 + monthNum
148
+ };
149
+ }
150
+ buckets[bucketKey].count += 1;
151
+ }
152
+ let points = Object.values(buckets).sort((a, b) => a.sortKey - b.sortKey).map((b) => ({
153
+ x: b.x,
154
+ xName: b.xName,
155
+ y: b.count,
156
+ sampleCount: b.count
157
+ }));
158
+ if (showCumulativeFrequency) {
159
+ let sum = 0;
160
+ points = points.map((p) => {
161
+ sum += p.y;
162
+ return { ...p, y: sum };
163
+ });
164
+ }
165
+ const yValues = points.map((p) => p.y).filter((v) => typeof v === "number" && !Number.isNaN(v));
166
+ const median = yValues.length > 0 ? (() => {
167
+ const sorted = [...yValues].sort((a, b) => a - b);
168
+ const mid = Math.floor(sorted.length / 2);
169
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
170
+ })() : 0;
171
+ return { median, points };
172
+ }
59
173
  function buildOneSeries(aggregation, xTermId, yTermId, data) {
60
174
  const supportedAggregations = ["proportion", "count", "median"];
61
175
  if (!supportedAggregations.includes(aggregation)) {
@@ -76,39 +190,15 @@ function buildOneSeries(aggregation, xTermId, yTermId, data) {
76
190
  `x value must be a number for sample ${sampleId}: xTermId=${xTermId}, received type ${typeof xRaw}, value: ${xRaw}`
77
191
  );
78
192
  }
79
- let year = null;
80
- let month = null;
81
- const parts = String(xRaw).split(".");
82
- year = Number(parts[0]);
83
- if (parts.length > 1) {
84
- const decimalPart = parts[1];
85
- if (decimalPart.length === 2) {
86
- const monthCandidate = Number(decimalPart);
87
- if (monthCandidate >= 1 && monthCandidate <= 12) {
88
- month = monthCandidate;
89
- } else {
90
- const frac = xRaw - year;
91
- month = Math.floor(frac * 12) + 1;
92
- }
93
- } else {
94
- const frac = xRaw - year;
95
- month = Math.floor(frac * 12) + 1;
96
- }
97
- } else {
98
- month = 1;
99
- }
100
- if (year == null || month == null || Number.isNaN(year) || Number.isNaN(month)) {
101
- continue;
102
- }
103
- const yearNum = year;
104
- const monthNum = month;
193
+ const parsed = decimalYearToYearMonth(xRaw);
194
+ if (!parsed) continue;
195
+ const { yearNum, monthNum } = parsed;
105
196
  const bucketKey = `${yearNum}-${String(monthNum).padStart(2, "0")}`;
106
- const x = Number(`${yearNum}.${String(monthNum).padStart(2, "0")}`);
107
- const date = new Date(yearNum, monthNum - 1, 1);
108
- const xName = date.toLocaleString("en-US", { month: "long", year: "numeric" });
197
+ const bucketDate = new Date(yearNum, monthNum - 1, 1);
198
+ const xName = bucketDate.toLocaleString("en-US", { month: "long", year: "numeric" });
109
199
  if (!buckets[bucketKey]) {
110
200
  buckets[bucketKey] = {
111
- x,
201
+ x: getNumberFromDate(new Date(yearNum, monthNum - 1, 15)),
112
202
  xName,
113
203
  count: 0,
114
204
  missingCount: 0,
@@ -214,6 +304,9 @@ function buildOneSeries(aggregation, xTermId, yTermId, data) {
214
304
  })() : 0;
215
305
  return { median, points };
216
306
  }
307
+ function runChartErrorPayload(message) {
308
+ return { error: String(message), series: [] };
309
+ }
217
310
  function init({ genomes }) {
218
311
  return async (req, res) => {
219
312
  try {
@@ -225,12 +318,15 @@ function init({ genomes }) {
225
318
  const result = await getRunChart(q, ds);
226
319
  res.send(result);
227
320
  } catch (e) {
228
- res.send({ error: e.message || e });
321
+ res.send(runChartErrorPayload(e.message || e));
229
322
  }
230
323
  };
231
324
  }
232
325
  export {
233
326
  api,
327
+ buildFrequencyFromData,
234
328
  buildRunChartFromData,
235
- getRunChart
329
+ decimalYearToYearMonth,
330
+ getRunChart,
331
+ runChartErrorPayload
236
332
  };