@sjcrh/proteinpaint-server 2.180.1 → 2.181.1

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.
@@ -0,0 +1,509 @@
1
+ import { violinBoxPayload } from "#types/checkers";
2
+ import { scaleLinear, scaleLog } from "d3";
3
+ import { run_rust } from "@sjcrh/proteinpaint-rust";
4
+ import { getData } from "../src/termdb.matrix.js";
5
+ import { createCanvas } from "canvas";
6
+ import { getOrderedLabels } from "../src/termdb.barchart.js";
7
+ import { getDescrStats, getStdDev, getMean } from "./termdb.descrstats.ts";
8
+ import { isNumericTerm } from "#shared/terms.js";
9
+ import { boxplot_getvalue } from "../src/utils.js";
10
+ import { run_R } from "@sjcrh/proteinpaint-r";
11
+ import { roundValueAuto } from "#shared/roundValue.js";
12
+ const api = {
13
+ endpoint: "termdb/violinBox",
14
+ methods: {
15
+ get: {
16
+ ...violinBoxPayload,
17
+ init
18
+ },
19
+ post: {
20
+ ...violinBoxPayload,
21
+ init
22
+ }
23
+ }
24
+ };
25
+ function init({ genomes }) {
26
+ return async (req, res) => {
27
+ const q = req.query;
28
+ let result;
29
+ try {
30
+ const g = genomes[q.genome];
31
+ if (!g) throw new Error("invalid genome name");
32
+ const ds = g.datasets?.[q.dslabel];
33
+ if (!ds) throw new Error("invalid ds");
34
+ if (typeof q.tw?.term != "object" || typeof q.tw?.q != "object") throw new Error("q.tw not of {term,q}");
35
+ const term = q.tw.term;
36
+ if (!isNumericTerm(term) && term.type !== "survival") throw new Error("term type is not numeric or survival");
37
+ const arg = {
38
+ terms: [q.tw],
39
+ filter: q.filter,
40
+ filter0: q.filter0,
41
+ currentGeneNames: q.currentGeneNames,
42
+ __protected__: q.__protected__,
43
+ __abortSignal: q.__abortSignal
44
+ };
45
+ if (q.overlayTw) arg.terms.push(q.overlayTw);
46
+ if (q.divideTw) arg.terms.push(q.divideTw);
47
+ const data = await getData(arg, ds);
48
+ if (!data) throw new Error("getData() returns nothing");
49
+ if (data.error) throw new Error(data.error);
50
+ if (q.plotType === "violin") {
51
+ result = await getViolin(q, data, ds);
52
+ } else if (q.plotType === "box") {
53
+ result = await getBoxPlot(q, data);
54
+ } else {
55
+ throw new Error("invalid plotType");
56
+ }
57
+ } catch (e) {
58
+ result = { error: e?.message || e };
59
+ if (e instanceof Error && e.stack) console.log(e);
60
+ }
61
+ res.send(result);
62
+ };
63
+ }
64
+ async function getViolin(q, data, ds) {
65
+ const samples = Object.values(data.samples);
66
+ let values = samples.map((s) => s?.[q.tw.$id]?.value).filter((v) => typeof v === "number" && !q.tw.term.values?.[v]?.uncomputable);
67
+ if (q.isLogScale) values = values.filter((v) => v > 0);
68
+ const descrStats = getDescrStats(values);
69
+ const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
70
+ if (data.error) throw new Error(data.error);
71
+ if (q.overlayTw && data.refs.byTermId[q.overlayTw.$id]) {
72
+ data.refs.byTermId[q.overlayTw.$id].orderedLabels = getOrderedLabels(
73
+ q.overlayTw,
74
+ data.refs.byTermId[q.overlayTw.$id]?.bins,
75
+ void 0,
76
+ q.overlayTw.q
77
+ );
78
+ }
79
+ if (q.scale) setScaleData(q, data, q.tw);
80
+ const valuesObject = divideValues(q, data, sampleType);
81
+ const result = setViolinResponse(valuesObject, data, q);
82
+ if (q.overlayTw) await getViolinWilcoxonData(result);
83
+ await createCanvasImg(q, result, ds);
84
+ result["descrStats"] = descrStats;
85
+ return result;
86
+ }
87
+ function setScaleData(q, data, tw) {
88
+ if (!q.scale) return;
89
+ const scale = Number(q.scale);
90
+ for (const val of Object.values(data.samples)) {
91
+ if (!tw.$id || !val[tw.$id]) continue;
92
+ if (tw.term.values?.[val[tw.$id]?.value]?.uncomputable) continue;
93
+ val[tw.$id].key = val[tw.$id].key / scale;
94
+ val[tw.$id].value = val[tw.$id].value / scale;
95
+ }
96
+ }
97
+ function divideValues(q, data, sampleType) {
98
+ const overlayTerm = q.overlayTw;
99
+ const divideTerm = q.divideTw;
100
+ const useLog = q.isLogScale;
101
+ const { absMax, absMin, chart2plot2values, uncomputableValues } = parseValues(
102
+ q,
103
+ data,
104
+ sampleType,
105
+ useLog == true ? true : false,
106
+ // avoid tsc err
107
+ overlayTerm,
108
+ divideTerm
109
+ );
110
+ return {
111
+ chart2plot2values,
112
+ min: absMin,
113
+ max: absMax,
114
+ uncomputableValues: sortObj(uncomputableValues)
115
+ };
116
+ }
117
+ function sortObj(object) {
118
+ return Object.fromEntries(Object.entries(object).sort(([, a], [, b]) => a - b));
119
+ }
120
+ function sortPlot2Values(data, plot2values, overlayTerm) {
121
+ const orderedLabels = overlayTerm?.$id ? data.refs.byTermId[overlayTerm.$id]?.keyOrder : void 0;
122
+ plot2values = new Map(
123
+ [...plot2values].sort(
124
+ orderedLabels ? (a, b) => orderedLabels.indexOf(a[0]) - orderedLabels.indexOf(b[0]) : overlayTerm?.term?.type === "categorical" ? (a, b) => b[1].length - a[1].length : overlayTerm?.term?.type === "condition" ? (a, b) => Number(a[0]) - Number(b[0]) : (a, b) => a.toString().replace(/[^a-zA-Z0-9<]/g, "").localeCompare(b.toString().replace(/[^a-zA-Z0-9<]/g, ""), void 0, { numeric: true })
125
+ )
126
+ );
127
+ return plot2values;
128
+ }
129
+ function setViolinResponse(valuesObject, data, q) {
130
+ const charts = {};
131
+ const overlayTerm = q.overlayTw;
132
+ const divideTw = q.divideTw;
133
+ for (const [chart, plot2values] of valuesObject.chart2plot2values) {
134
+ const plots = [];
135
+ for (const [plot, values] of sortPlot2Values(data, plot2values, overlayTerm)) {
136
+ plots.push({
137
+ label: overlayTerm?.term?.values?.[plot]?.label || plot,
138
+ // avoid strange tsc err
139
+ values,
140
+ seriesId: plot,
141
+ chartId: chart,
142
+ //quick fix to get list samples working
143
+ plotValueCount: values?.length,
144
+ color: overlayTerm?.term?.values?.[plot]?.color || ""
145
+ });
146
+ }
147
+ charts[chart] = { chartId: chart, plots };
148
+ }
149
+ const bins = {
150
+ term1: numericBins(q.tw, data)
151
+ };
152
+ if (overlayTerm) bins.term2 = numericBins(overlayTerm, data);
153
+ if (divideTw) bins.term0 = numericBins(divideTw, data);
154
+ const result = {
155
+ min: valuesObject.min,
156
+ max: valuesObject.max,
157
+ bins,
158
+ charts,
159
+ uncomputableValues: Object.keys(valuesObject.uncomputableValues).length > 0 ? valuesObject.uncomputableValues : null
160
+ };
161
+ return result;
162
+ }
163
+ async function createCanvasImg(q, result, ds) {
164
+ if (!q.radius) q.radius = 5;
165
+ if (q.radius <= 0) throw new Error("q.radius is not a number");
166
+ else q.radius = +q.radius;
167
+ const isH = q.orientation == "horizontal";
168
+ for (const k of Object.keys(result.charts)) {
169
+ const chart = result.charts[k];
170
+ const plot2Values = {};
171
+ for (const plot of chart.plots) plot2Values[plot.label] = plot.values;
172
+ const useLog = q.isLogScale;
173
+ const logBase = ds.cohort.termdb.logscaleBase2 ? 2 : 10;
174
+ const densities = await getDensities(plot2Values, useLog, logBase);
175
+ let axisScale;
176
+ if (useLog) {
177
+ axisScale = scaleLog().base(ds.cohort.termdb.logscaleBase2 ? 2 : 10).domain([result.min, result.max]).range(isH ? [0, q.svgw] : [q.svgw, 0]);
178
+ } else {
179
+ axisScale = scaleLinear().domain([result.min, result.max]).range(isH ? [0, q.svgw] : [q.svgw, 0]);
180
+ }
181
+ const [width, height] = isH ? [q.svgw * q.devicePixelRatio, q.radius * q.devicePixelRatio] : [q.radius * q.devicePixelRatio, q.svgw * q.devicePixelRatio];
182
+ for (const plot of chart.plots) {
183
+ plot.density = densities[plot.label];
184
+ const canvas = createCanvas(width, height);
185
+ const ctx = canvas.getContext("2d");
186
+ if (q.devicePixelRatio != 1) ctx.scale(q.devicePixelRatio, q.devicePixelRatio);
187
+ ctx.strokeStyle = "black";
188
+ ctx.lineWidth = 1;
189
+ if (q.datasymbol === "rug") {
190
+ ctx.globalAlpha = 0.8;
191
+ plot.values.forEach((i) => {
192
+ const s = axisScale(i);
193
+ ctx.beginPath();
194
+ if (isH) {
195
+ ctx.moveTo(s, 0);
196
+ ctx.lineTo(s, q.radius);
197
+ } else {
198
+ ctx.moveTo(0, s);
199
+ ctx.lineTo(q.radius, s);
200
+ }
201
+ ctx.stroke();
202
+ });
203
+ } else if (q.datasymbol === "bean") {
204
+ ctx.globalAlpha = 0.6;
205
+ ctx.fillStyle = "#ffe6e6";
206
+ plot.values.forEach((i) => {
207
+ const s = axisScale(i);
208
+ ctx.beginPath();
209
+ if (isH) ctx.arc(s, q.radius / 2, q.radius / 2, 0, 2 * Math.PI);
210
+ else ctx.arc(q.radius / 2, s, q.radius / 2, 0, 2 * Math.PI);
211
+ ctx.fill();
212
+ ctx.stroke();
213
+ });
214
+ }
215
+ plot.src = canvas.toDataURL();
216
+ plot.summaryStats = getDescrStats(plot.values);
217
+ }
218
+ }
219
+ }
220
+ async function getViolinWilcoxonData(result) {
221
+ for (const k of Object.keys(result.charts)) {
222
+ const chart = result.charts[k];
223
+ const numPlots = chart.plots.length;
224
+ if (numPlots < 2) continue;
225
+ const wilcoxInput = [];
226
+ for (let i = 0; i < numPlots; i++) {
227
+ const group1_id = chart.plots[i].label;
228
+ const group1_values = chart.plots[i].values;
229
+ for (let j = i + 1; j < numPlots; j++) {
230
+ const group2_id = chart.plots[j].label;
231
+ const group2_values = chart.plots[j].values;
232
+ wilcoxInput.push({ group1_id, group1_values, group2_id, group2_values });
233
+ }
234
+ }
235
+ const wilcoxOutput = JSON.parse(await run_rust("wilcoxon", JSON.stringify(wilcoxInput)));
236
+ chart.pvalues = [];
237
+ for (const test of wilcoxOutput) {
238
+ if (test.pvalue == null || test.pvalue == "null") {
239
+ chart.pvalues.push([{ value: test.group1_id }, { value: test.group2_id }, { html: "NA" }]);
240
+ } else {
241
+ chart.pvalues.push([{ value: test.group1_id }, { value: test.group2_id }, { html: test.pvalue.toPrecision(4) }]);
242
+ }
243
+ }
244
+ }
245
+ }
246
+ async function getDensity(values) {
247
+ const result = await getDensities({ plot: values });
248
+ return result.plot;
249
+ }
250
+ async function getDensities(plot2Values, useLog = false, logBase = 10) {
251
+ let transformedPlot2Values = {};
252
+ if (useLog) {
253
+ for (const plot in plot2Values) {
254
+ transformedPlot2Values[plot] = plot2Values[plot].filter((v) => v > 0).map((v) => Math.log(v) / Math.log(logBase));
255
+ }
256
+ } else {
257
+ transformedPlot2Values = plot2Values;
258
+ }
259
+ const plot2Density = JSON.parse(
260
+ await run_R("density.R", JSON.stringify({ plot2Values: transformedPlot2Values }))
261
+ );
262
+ const densities = {};
263
+ for (const plot in plot2Density) {
264
+ const result = plot2Density[plot];
265
+ const bins = [];
266
+ let densityMin = Infinity;
267
+ let densityMax = -Infinity;
268
+ let xMin = Infinity;
269
+ let xMax = -Infinity;
270
+ for (const [i, x] of Object.entries(result.x)) {
271
+ const density2 = result.y[i];
272
+ const x0 = useLog ? Math.pow(logBase, x) : x;
273
+ xMin = Math.min(xMin, x0);
274
+ xMax = Math.max(xMax, x0);
275
+ densityMin = Math.min(densityMin, density2);
276
+ densityMax = Math.max(densityMax, density2);
277
+ bins.push({ x0, density: density2 });
278
+ }
279
+ bins.unshift({ x0: xMin, density: densityMin });
280
+ bins.push({ x0: xMax, density: densityMin });
281
+ const density = { bins, densityMin, densityMax, minvalue: xMin, maxvalue: xMax };
282
+ densities[plot] = density;
283
+ }
284
+ return densities;
285
+ }
286
+ async function getBoxPlot(q, data) {
287
+ const { absMin, absMax, bins, charts, uncomputableValues, descrStats, outlierMin, outlierMax } = await processBoxPlotData(data, q);
288
+ const returnData = {
289
+ absMin: q.removeOutliers ? outlierMin : absMin,
290
+ absMax: q.removeOutliers ? outlierMax : absMax,
291
+ bins,
292
+ charts,
293
+ uncomputableValues: setUncomputableValues(uncomputableValues),
294
+ descrStats
295
+ };
296
+ return returnData;
297
+ }
298
+ async function processBoxPlotData(data, q) {
299
+ const samples = Object.values(data.samples);
300
+ const values = samples.map((s) => s?.[q.tw.$id]?.value).filter((v) => typeof v === "number" && !q.tw.term.values?.[v]?.uncomputable);
301
+ const descrStats = getDescrStats(values, q.removeOutliers);
302
+ const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
303
+ const overlayTw = q.overlayTw;
304
+ const divideTw = q.divideTw;
305
+ const { absMin, absMax, chart2plot2values, uncomputableValues } = parseValues(
306
+ q,
307
+ data,
308
+ sampleType,
309
+ q.isLogScale == true ? true : false,
310
+ // avoid tsc err
311
+ overlayTw,
312
+ divideTw
313
+ );
314
+ if (!absMin && absMin !== 0) throw new Error("absMin is undefined");
315
+ if (!absMax && absMax !== 0) throw new Error("absMax is undefined");
316
+ const charts = {};
317
+ let outlierMin = Number.POSITIVE_INFINITY, outlierMax = Number.NEGATIVE_INFINITY;
318
+ for (const [chart, plot2values] of chart2plot2values) {
319
+ const plots = [];
320
+ for (const [key, values2] of sortPlot2Values(data, plot2values, overlayTw)) {
321
+ ;
322
+ [outlierMax, outlierMin] = setPlotData(
323
+ plots,
324
+ values2,
325
+ key,
326
+ sampleType,
327
+ descrStats,
328
+ q,
329
+ outlierMin,
330
+ outlierMax,
331
+ overlayTw
332
+ );
333
+ }
334
+ if (q.tw.term?.values) setHiddenPlots(q.tw, plots);
335
+ if (overlayTw && overlayTw.term?.values) setHiddenPlots(overlayTw, plots);
336
+ if (divideTw && divideTw.term?.values) setHiddenPlots(divideTw, plots);
337
+ if (q.orderByMedian == true) {
338
+ plots.sort((a, b) => a.boxplot.p50 - b.boxplot.p50);
339
+ }
340
+ const sampleCount = plots.reduce((total, p) => {
341
+ if (p.hidden) return total;
342
+ return total + p.descrStats.total.value;
343
+ }, 0);
344
+ charts[chart] = { chartId: chart, plots, sampleCount };
345
+ }
346
+ const bins = {
347
+ term1: numericBins(q.tw, data)
348
+ };
349
+ if (overlayTw) bins.term2 = numericBins(overlayTw, data);
350
+ if (divideTw) bins.term0 = numericBins(divideTw, data);
351
+ if (q.showAssocTests && overlayTw) await getBoxPlotWilcoxonData(charts);
352
+ Object.keys(charts).forEach((c) => charts[c].plots.forEach((p) => delete p.tempValues));
353
+ return { absMin, absMax, bins, charts, uncomputableValues, descrStats, outlierMin, outlierMax };
354
+ }
355
+ function setPlotData(plots, values, key, sampleType, descrStats, q, outlierMin, outlierMax, overlayTw) {
356
+ const sortedValues = values.sort((a, b) => a - b);
357
+ const vs = sortedValues.map((v) => {
358
+ const value = { value: v };
359
+ return value;
360
+ });
361
+ if (q.removeOutliers) {
362
+ outlierMin = Math.min(outlierMin, descrStats.outlierMin.value);
363
+ outlierMax = Math.max(outlierMax, descrStats.outlierMax.value);
364
+ }
365
+ const boxplot = boxplot_getvalue(vs, q.removeOutliers);
366
+ if (!boxplot) throw new Error("boxplot_getvalue failed [termdb.violinBox init()]");
367
+ const plot = {
368
+ boxplot,
369
+ descrStats: setIndividualBoxPlotStats(boxplot, sortedValues),
370
+ //quick fix
371
+ //to delete later
372
+ tempValues: sortedValues
373
+ };
374
+ if (overlayTw) {
375
+ const _key = overlayTw?.term?.values?.[key]?.label || key;
376
+ plot.color = overlayTw?.term?.values?.[key]?.color || null;
377
+ plot.key = _key;
378
+ plot.seriesId = key;
379
+ plot.boxplot.label = `${_key}, n=${values.length}`;
380
+ } else {
381
+ plot.key = sampleType;
382
+ plot.boxplot.label = `${sampleType}, n=${values.length}`;
383
+ }
384
+ plots.push(plot);
385
+ return [outlierMax, outlierMin];
386
+ }
387
+ function setIndividualBoxPlotStats(boxplot, values) {
388
+ const stats = {
389
+ total: { key: "total", label: "Total", value: values.length },
390
+ min: { key: "min", label: "Minimum", value: values[0] },
391
+ p25: { key: "p25", label: "1st quartile", value: boxplot.p25 },
392
+ median: { key: "median", label: "Median", value: boxplot.p50 },
393
+ p75: { key: "p75", label: "3rd quartile", value: boxplot.p75 },
394
+ mean: { key: "mean", label: "Mean", value: getMean(values) },
395
+ max: { key: "max", label: "Maximum", value: values[values.length - 1] },
396
+ stdDev: { key: "stdDev", label: "Standard deviation", value: getStdDev(values) }
397
+ };
398
+ for (const key of Object.keys(stats)) {
399
+ stats[key].value = roundValueAuto(stats[key].value);
400
+ }
401
+ return stats;
402
+ }
403
+ function setHiddenPlots(term, plots) {
404
+ for (const v of Object.values(term.term?.values)) {
405
+ const plot = plots.find((p) => p.key === v.label);
406
+ if (plot) plot.isHidden = v?.uncomputable;
407
+ }
408
+ if (term.q?.hiddenValues) {
409
+ for (const key of Object.keys(term.q.hiddenValues)) {
410
+ const plot = plots.find((p) => p.key === key);
411
+ if (plot) plot.isHidden = true;
412
+ }
413
+ }
414
+ return plots;
415
+ }
416
+ function setUncomputableValues(values) {
417
+ if (Object.entries(values)?.length) {
418
+ return Object.entries(values).map(([label, v]) => ({ label, value: v }));
419
+ } else return null;
420
+ }
421
+ async function getBoxPlotWilcoxonData(charts) {
422
+ for (const chart of Object.values(charts)) {
423
+ const numPlots = chart.plots?.length;
424
+ if (numPlots < 2) continue;
425
+ const wilcoxonInput = [];
426
+ for (let i = 0; i < numPlots; i++) {
427
+ const group1_id = chart.plots[i].boxplot.label.replace(/, n=\d+$/, "");
428
+ const group1_values = chart.plots[i].tempValues;
429
+ for (let j = i + 1; j < numPlots; j++) {
430
+ const group2_id = chart.plots[j].boxplot.label.replace(/, n=\d+$/, "");
431
+ const group2_values = chart.plots[j].tempValues;
432
+ wilcoxonInput.push({ group1_id, group1_values, group2_id, group2_values });
433
+ }
434
+ }
435
+ const wilcoxonOutput = JSON.parse(await run_rust("wilcoxon", JSON.stringify(wilcoxonInput)));
436
+ chart.wilcoxon = [];
437
+ for (const test of wilcoxonOutput) {
438
+ if (test.pvalue == null || test.pvalue == "null") {
439
+ chart.wilcoxon.push([{ value: test.group1_id }, { value: test.group2_id }, { html: "NA" }]);
440
+ } else {
441
+ chart.wilcoxon.push([
442
+ { value: test.group1_id },
443
+ { value: test.group2_id },
444
+ { html: test.pvalue.toPrecision(4) }
445
+ ]);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ function parseValues(q, data, sampleType, isLog, overlayTw, divideTw) {
451
+ const chart2plot2values = /* @__PURE__ */ new Map();
452
+ const uncomputableValues = {};
453
+ let absMin = Infinity, absMax = -Infinity;
454
+ for (const val of Object.values(data.samples)) {
455
+ const value = val[q.tw.$id];
456
+ if (!Number.isFinite(value?.value)) continue;
457
+ if (q.tw.term.values?.[value.value]?.uncomputable) {
458
+ const label = q.tw.term.values[value.value].label;
459
+ uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
460
+ continue;
461
+ }
462
+ if (isLog && value.value <= 0) continue;
463
+ let chart = "";
464
+ let plot = sampleType;
465
+ if (divideTw) {
466
+ if (!val[divideTw?.$id]) continue;
467
+ const value0 = val[divideTw.$id];
468
+ if (divideTw.term?.values?.[value0.key]?.uncomputable) {
469
+ const label = divideTw.term.values[value0?.key]?.label;
470
+ uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
471
+ }
472
+ chart = value0.key;
473
+ }
474
+ if (overlayTw) {
475
+ if (!val[overlayTw?.$id]) continue;
476
+ const value2 = val[overlayTw.$id];
477
+ if (overlayTw.term?.values?.[value2.key]?.uncomputable) {
478
+ const label = overlayTw.term.values[value2?.key]?.label;
479
+ uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
480
+ }
481
+ plot = value2.key;
482
+ }
483
+ if (!chart2plot2values.has(chart)) chart2plot2values.set(chart, /* @__PURE__ */ new Map());
484
+ const plot2values = chart2plot2values.get(chart);
485
+ if (!plot2values.has(plot)) plot2values.set(plot, []);
486
+ const values = plot2values.get(plot);
487
+ values.push(value.value);
488
+ if (value.value < absMin) absMin = value.value;
489
+ if (value.value > absMax) absMax = value.value;
490
+ }
491
+ return { absMax, absMin, chart2plot2values, uncomputableValues };
492
+ }
493
+ function numericBins(tw, data) {
494
+ const bins = {};
495
+ if (!isNumericTerm(tw?.term)) return bins;
496
+ for (const bin of data.refs.byTermId[tw?.$id]?.bins || []) {
497
+ bins[bin.label] = bin;
498
+ }
499
+ return bins;
500
+ }
501
+ export {
502
+ api,
503
+ getDensities,
504
+ getDensity,
505
+ getViolinWilcoxonData,
506
+ numericBins,
507
+ parseValues,
508
+ sortPlot2Values
509
+ };