@sjcrh/proteinpaint-server 2.171.0 → 2.172.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.
@@ -2,7 +2,7 @@ function termdb_test_default() {
2
2
  return {
3
3
  isMds3: true,
4
4
  isSupportedChartOverride: {
5
- runChart: () => true,
5
+ runChart2: () => true,
6
6
  frequencyChart: () => true,
7
7
  report: () => true,
8
8
  summarizeMutationDiagnosis: () => true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.171.0",
3
+ "version": "2.172.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.143.0",
65
- "@sjcrh/proteinpaint-python": "2.171.0-0",
65
+ "@sjcrh/proteinpaint-python": "2.172.0",
66
66
  "@sjcrh/proteinpaint-r": "2.152.1-0",
67
67
  "@sjcrh/proteinpaint-rust": "2.171.0",
68
68
  "@sjcrh/proteinpaint-shared": "2.171.0-0",
69
- "@sjcrh/proteinpaint-types": "2.171.0",
69
+ "@sjcrh/proteinpaint-types": "2.172.0",
70
70
  "@types/express": "^5.0.0",
71
71
  "@types/express-session": "^1.18.1",
72
72
  "better-sqlite3": "^12.4.1",
@@ -0,0 +1,215 @@
1
+ import { runChartPayload } from "#types/checkers";
2
+ const api = {
3
+ endpoint: "termdb/runChart",
4
+ methods: {
5
+ get: {
6
+ ...runChartPayload,
7
+ init
8
+ },
9
+ post: {
10
+ ...runChartPayload,
11
+ init
12
+ }
13
+ }
14
+ };
15
+ async function getRunChart(q, ds) {
16
+ const terms = [];
17
+ let xTermId;
18
+ let yTermId;
19
+ if (q.term && q.term2) {
20
+ const tws = [
21
+ { term: q.term, q: { mode: "continuous" }, $id: q.term.id },
22
+ { term: q.term2, q: { mode: "continuous" }, $id: q.term2.id }
23
+ ];
24
+ terms.push(...tws);
25
+ xTermId = q.term.id;
26
+ yTermId = q.term2.id;
27
+ } else {
28
+ throw new Error("term and term2 must be provided");
29
+ }
30
+ if (!xTermId || !yTermId) {
31
+ throw new Error("Unable to determine term IDs for x and y axes");
32
+ }
33
+ const { getData } = await import("../src/termdb.matrix.js");
34
+ const data = await getData(
35
+ {
36
+ filter: q.filter,
37
+ terms,
38
+ __protected__: q.__protected__
39
+ },
40
+ ds,
41
+ true
42
+ );
43
+ if (data.error) throw new Error(data.error);
44
+ return buildRunChartFromData(q.aggregation, xTermId, yTermId, data);
45
+ }
46
+ function buildRunChartFromData(aggregation, xTermId, yTermId, data) {
47
+ const buckets = {};
48
+ let skippedSamples = 0;
49
+ for (const sampleId in data.samples || {}) {
50
+ const sample = data.samples[sampleId];
51
+ const xRaw = sample?.[xTermId]?.value ?? sample?.[xTermId]?.key;
52
+ const yRaw = sample?.[yTermId]?.value ?? sample?.[yTermId]?.key;
53
+ if (xRaw == null || yRaw == null) {
54
+ skippedSamples++;
55
+ console.log(
56
+ `Skipping sample ${sampleId}: Missing x or y value - xTermId=${xTermId} (value: ${xRaw}), yTermId=${yTermId} (value: ${yRaw})`
57
+ );
58
+ continue;
59
+ }
60
+ if (typeof xRaw !== "number") {
61
+ throw new Error(
62
+ `x value must be a number for sample ${sampleId}: xTermId=${xTermId}, received type ${typeof xRaw}, value: ${xRaw}`
63
+ );
64
+ }
65
+ let year = null;
66
+ let month = null;
67
+ const parts = String(xRaw).split(".");
68
+ year = Number(parts[0]);
69
+ if (parts.length > 1) {
70
+ const decimalPart = parts[1];
71
+ if (decimalPart.length === 2) {
72
+ const monthCandidate = Number(decimalPart);
73
+ if (monthCandidate >= 1 && monthCandidate <= 12) {
74
+ month = monthCandidate;
75
+ } else {
76
+ const frac = xRaw - year;
77
+ month = Math.floor(frac * 12) + 1;
78
+ }
79
+ } else {
80
+ const frac = xRaw - year;
81
+ month = Math.floor(frac * 12) + 1;
82
+ }
83
+ } else {
84
+ month = 1;
85
+ }
86
+ if (year == null || month == null || Number.isNaN(year) || Number.isNaN(month)) {
87
+ throw new Error(
88
+ `Invalid date value for sample ${sampleId}: xTermId=${xTermId}, xRaw=${xRaw}, parsed year=${year}, month=${month}`
89
+ );
90
+ }
91
+ const bucketKey = `${year}-${String(month).padStart(2, "0")}`;
92
+ const x = Number(`${year}.${String(month).padStart(2, "0")}`);
93
+ const date = new Date(year, month - 1, 1);
94
+ const xName = date.toLocaleString("en-US", { month: "long", year: "numeric" });
95
+ if (!buckets[bucketKey]) {
96
+ buckets[bucketKey] = {
97
+ x,
98
+ xName,
99
+ ySum: 0,
100
+ count: 0,
101
+ success: 0,
102
+ total: 0,
103
+ countSum: 0,
104
+ sortKey: year * 100 + month,
105
+ yValues: []
106
+ };
107
+ }
108
+ if (aggregation === "proportion") {
109
+ if (typeof yRaw === "boolean") {
110
+ buckets[bucketKey].success += yRaw ? 1 : 0;
111
+ buckets[bucketKey].total += 1;
112
+ } else if (typeof yRaw === "number") {
113
+ const yn = Number(yRaw);
114
+ if (!Number.isFinite(yn)) {
115
+ throw new Error(
116
+ `Non-finite y value for proportion aggregation in sample ${sampleId}: yTermId=${yTermId}, yRaw=${yRaw}`
117
+ );
118
+ }
119
+ if (yn <= 1 && yn >= 0) {
120
+ buckets[bucketKey].success += yn;
121
+ buckets[bucketKey].total += 1;
122
+ } else {
123
+ buckets[bucketKey].success += yn;
124
+ buckets[bucketKey].total += 1;
125
+ }
126
+ } else if (typeof yRaw === "object" && yRaw != null) {
127
+ const s = Number(yRaw.success ?? yRaw.y ?? yRaw.value ?? NaN);
128
+ const t = Number(yRaw.total ?? yRaw.n ?? 1);
129
+ if (!Number.isFinite(s) || !Number.isFinite(t)) {
130
+ throw new Error(
131
+ `Non-finite success or total value for proportion aggregation in sample ${sampleId}: yTermId=${yTermId}, success=${s}, total=${t}`
132
+ );
133
+ }
134
+ buckets[bucketKey].success += s;
135
+ buckets[bucketKey].total += t;
136
+ } else {
137
+ throw new Error(
138
+ `Invalid y value type for proportion aggregation in sample ${sampleId}: yTermId=${yTermId}, type=${typeof yRaw}, value=${yRaw}`
139
+ );
140
+ }
141
+ } else if (aggregation === "count") {
142
+ const yn = Number(yRaw);
143
+ if (!Number.isFinite(yn)) {
144
+ throw new Error(
145
+ `Non-finite y value for count aggregation in sample ${sampleId}: yTermId=${yTermId}, yRaw=${yRaw}`
146
+ );
147
+ }
148
+ buckets[bucketKey].countSum += yn;
149
+ buckets[bucketKey].count += 1;
150
+ } else {
151
+ const yn = Number(yRaw);
152
+ if (!Number.isFinite(yn)) {
153
+ throw new Error(
154
+ `Non-finite y value for mean aggregation in sample ${sampleId}: yTermId=${yTermId}, yRaw=${yRaw}`
155
+ );
156
+ }
157
+ buckets[bucketKey].ySum += yn;
158
+ buckets[bucketKey].count += 1;
159
+ buckets[bucketKey].yValues.push(yn);
160
+ }
161
+ }
162
+ if (skippedSamples > 0) {
163
+ console.log(`buildRunChartFromData: Skipped ${skippedSamples} sample(s) due to missing x or y values`);
164
+ }
165
+ const points = Object.values(buckets).sort((a, b) => a.sortKey - b.sortKey).map((b) => {
166
+ if (aggregation === "proportion") {
167
+ const total = b.total || 0;
168
+ const succ = b.success || 0;
169
+ const y = total ? Math.round(succ / total * 1e3) / 1e3 : 0;
170
+ return { x: b.x, xName: b.xName, y, sampleCount: total };
171
+ } else if (aggregation === "count") {
172
+ const y = Math.round((b.countSum || 0) * 100) / 100;
173
+ return { x: b.x, xName: b.xName, y, sampleCount: b.count };
174
+ } else {
175
+ const avg = b.count ? Math.round(b.ySum / b.count * 100) / 100 : 0;
176
+ return { x: b.x, xName: b.xName, y: avg, sampleCount: b.count };
177
+ }
178
+ });
179
+ const yValues = points.map((p) => p.y).filter((v) => typeof v === "number" && !Number.isNaN(v));
180
+ const median = yValues.length > 0 ? (() => {
181
+ const sorted = [...yValues].sort((a, b) => a - b);
182
+ const mid = Math.floor(sorted.length / 2);
183
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
184
+ })() : 0;
185
+ return {
186
+ status: "ok",
187
+ series: [
188
+ {
189
+ median,
190
+ points
191
+ }
192
+ ]
193
+ };
194
+ }
195
+ function init({ genomes }) {
196
+ return async (req, res) => {
197
+ try {
198
+ const q = req.query;
199
+ const genome = genomes[q.genome];
200
+ if (!genome) throw new Error("invalid genome name");
201
+ const ds = genome.datasets?.[q.dslabel];
202
+ if (!ds) throw new Error("invalid ds");
203
+ const result = await getRunChart(q, ds);
204
+ res.send(result);
205
+ } catch (e) {
206
+ console.log(e.stack);
207
+ res.send({ error: e.message || e });
208
+ }
209
+ };
210
+ }
211
+ export {
212
+ api,
213
+ buildRunChartFromData,
214
+ getRunChart
215
+ };