@sjcrh/proteinpaint-server 2.85.1 → 2.86.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 +8 -7
- package/routes/brainImaging.js +46 -21
- package/routes/termdb.boxplot.js +82 -27
- package/src/app.js +313 -202
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.86.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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@babel/register": "^7.14.5",
|
|
45
45
|
"@types/node": "^20.11.24",
|
|
46
46
|
"@types/tough-cookie": "^4.0.5",
|
|
47
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
47
|
+
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
|
48
48
|
"babel-loader": "^8.2.2",
|
|
49
49
|
"esbuild": "^0.19.12",
|
|
50
50
|
"glob": "^10.4.5",
|
|
@@ -55,16 +55,17 @@
|
|
|
55
55
|
"ts-node": "^10.9.1",
|
|
56
56
|
"ts-patch": "^3.0.2",
|
|
57
57
|
"tsx": "^4.7.1",
|
|
58
|
-
"typedoc": "^0.
|
|
59
|
-
"typedoc-plugin-missing-exports": "^
|
|
58
|
+
"typedoc": "^0.26.11",
|
|
59
|
+
"typedoc-plugin-missing-exports": "^3.0.0",
|
|
60
|
+
"typedoc-plugin-replace-text": "^4.0.0",
|
|
60
61
|
"typescript": "^5.6.3",
|
|
61
62
|
"typia": "^4.1.14"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
|
-
"@sjcrh/augen": "2.
|
|
65
|
+
"@sjcrh/augen": "2.86.0",
|
|
65
66
|
"@sjcrh/proteinpaint-rust": "2.84.0",
|
|
66
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
67
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
67
|
+
"@sjcrh/proteinpaint-shared": "2.86.0",
|
|
68
|
+
"@sjcrh/proteinpaint-types": "2.86.0",
|
|
68
69
|
"better-sqlite3": "^9.4.1",
|
|
69
70
|
"body-parser": "^1.15.2",
|
|
70
71
|
"canvas": "~2.11.2",
|
package/routes/brainImaging.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
1
|
import path from "path";
|
|
3
2
|
import serverconfig from "#src/serverconfig.js";
|
|
4
3
|
import { brainImagingPayload } from "#types";
|
|
5
4
|
import { spawn } from "child_process";
|
|
6
5
|
import { getData } from "../src/termdb.matrix.js";
|
|
6
|
+
import { isNumericTerm } from "@sjcrh/proteinpaint-shared/terms.js";
|
|
7
7
|
const api = {
|
|
8
8
|
endpoint: "brainImaging",
|
|
9
9
|
methods: {
|
|
@@ -38,8 +38,8 @@ function init({ genomes }) {
|
|
|
38
38
|
plane = "T";
|
|
39
39
|
index = query.t;
|
|
40
40
|
}
|
|
41
|
-
const brainImage = await getBrainImage(query, genomes, plane, index);
|
|
42
|
-
res.send({ brainImage, plane });
|
|
41
|
+
const [brainImage, legend] = await getBrainImage(query, genomes, plane, index);
|
|
42
|
+
res.send({ brainImage, plane, legend });
|
|
43
43
|
} catch (e) {
|
|
44
44
|
console.log(e);
|
|
45
45
|
res.status(404).send("Sample brain image not found");
|
|
@@ -53,7 +53,6 @@ async function getBrainImage(query, genomes, plane, index) {
|
|
|
53
53
|
if (q[key].referenceFile && q[key].samples) {
|
|
54
54
|
const refFile = path.join(serverconfig.tpmasterdir, q[key].referenceFile);
|
|
55
55
|
const dirPath = path.join(serverconfig.tpmasterdir, q[key].samples);
|
|
56
|
-
const files = fs.readdirSync(dirPath).filter((file) => file.endsWith(".nii") && fs.statSync(path.join(dirPath, file)).isFile());
|
|
57
56
|
const terms = [];
|
|
58
57
|
const divideByTW = query.divideByTW;
|
|
59
58
|
const overlayTW = query.overlayTW;
|
|
@@ -68,16 +67,34 @@ async function getBrainImage(query, genomes, plane, index) {
|
|
|
68
67
|
const sampleId = ds.sampleName2Id.get(sampleName);
|
|
69
68
|
const sampleData = data.samples[sampleId];
|
|
70
69
|
const samplePath = path.join(dirPath, sampleName) + ".nii";
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
let divideCategory = "default";
|
|
71
|
+
let overlayCategory = "default";
|
|
72
|
+
if (divideByTW) {
|
|
73
|
+
const value = sampleData[divideByTW.$id];
|
|
74
|
+
if (value)
|
|
75
|
+
divideCategory = divideByTW.term.values?.[value.key]?.label || value.key;
|
|
76
|
+
}
|
|
77
|
+
if (overlayTW) {
|
|
78
|
+
const value = sampleData[overlayTW.$id];
|
|
79
|
+
if (value)
|
|
80
|
+
overlayCategory = overlayTW.term.values?.[value.key]?.label || value.key;
|
|
81
|
+
}
|
|
73
82
|
if (!divideByCat[divideCategory])
|
|
74
83
|
divideByCat[divideCategory] = {};
|
|
75
|
-
if (!
|
|
76
|
-
divideByCat[divideCategory][overlayCategory]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
if (!query.legendFilter?.includes(overlayCategory)) {
|
|
85
|
+
if (!divideByCat[divideCategory][overlayCategory]) {
|
|
86
|
+
let color = overlayTW?.term?.values?.[overlayCategory]?.color || "red";
|
|
87
|
+
if (overlayTW && isNumericTerm(overlayTW.term)) {
|
|
88
|
+
const bins = data.refs.byTermId[overlayTW.$id].bins;
|
|
89
|
+
color = bins.find((b) => b.label == overlayCategory).color;
|
|
90
|
+
}
|
|
91
|
+
divideByCat[divideCategory][overlayCategory] = {
|
|
92
|
+
samples: [],
|
|
93
|
+
color
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
divideByCat[divideCategory][overlayCategory].samples.push(samplePath);
|
|
97
|
+
}
|
|
81
98
|
}
|
|
82
99
|
const lengths = [];
|
|
83
100
|
for (const dcategory in divideByCat)
|
|
@@ -87,25 +104,33 @@ async function getBrainImage(query, genomes, plane, index) {
|
|
|
87
104
|
}
|
|
88
105
|
const maxLength = Math.max(...lengths);
|
|
89
106
|
const brainImageDict = {};
|
|
107
|
+
const legend = {};
|
|
90
108
|
for (const dcategory in divideByCat) {
|
|
91
109
|
let catNum = 0;
|
|
92
110
|
const filesByCat = divideByCat[dcategory];
|
|
93
|
-
for (const category in filesByCat)
|
|
111
|
+
for (const category in filesByCat) {
|
|
112
|
+
if (filesByCat[category].samples.length < 1)
|
|
113
|
+
continue;
|
|
94
114
|
catNum += filesByCat[category].samples.length;
|
|
115
|
+
if (!legend[category])
|
|
116
|
+
legend[category] = { color: filesByCat[category].color, maxLength };
|
|
117
|
+
}
|
|
95
118
|
const url = await generateBrainImage(refFile, plane, index, maxLength, JSON.stringify(filesByCat));
|
|
96
119
|
brainImageDict[dcategory] = { url, catNum };
|
|
97
120
|
}
|
|
98
|
-
|
|
121
|
+
if (query.legendFilter) {
|
|
122
|
+
for (const cat of query.legendFilter) {
|
|
123
|
+
legend[cat] = {
|
|
124
|
+
color: "white",
|
|
125
|
+
maxLength,
|
|
126
|
+
crossedOut: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [brainImageDict, legend];
|
|
99
131
|
} else {
|
|
100
132
|
throw "no reference or sample files";
|
|
101
133
|
}
|
|
102
|
-
function getFilesByCat(tw) {
|
|
103
|
-
const filesByCat = {};
|
|
104
|
-
for (const [key2, value] of Object.entries(tw.term.values)) {
|
|
105
|
-
filesByCat[key2] = { samples: [], color: value.color || "red" };
|
|
106
|
-
}
|
|
107
|
-
return filesByCat;
|
|
108
|
-
}
|
|
109
134
|
}
|
|
110
135
|
async function generateBrainImage(refFile, plane, index, maxLength, filesJson) {
|
|
111
136
|
return new Promise((resolve, reject) => {
|
package/routes/termdb.boxplot.js
CHANGED
|
@@ -2,6 +2,7 @@ import { boxplotPayload } from "#types";
|
|
|
2
2
|
import { getData } from "../src/termdb.matrix.js";
|
|
3
3
|
import { boxplot_getvalue } from "../src/utils.js";
|
|
4
4
|
import { sortKey2values } from "../src/termdb.violin.js";
|
|
5
|
+
import { roundValue } from "#shared/roundValue.js";
|
|
5
6
|
const api = {
|
|
6
7
|
endpoint: "termdb/boxplot",
|
|
7
8
|
methods: {
|
|
@@ -26,8 +27,8 @@ function init({ genomes }) {
|
|
|
26
27
|
if (!ds)
|
|
27
28
|
throw "invalid ds";
|
|
28
29
|
const terms = [q.tw];
|
|
29
|
-
if (q.
|
|
30
|
-
terms.push(q.
|
|
30
|
+
if (q.overlayTw)
|
|
31
|
+
terms.push(q.overlayTw);
|
|
31
32
|
const data = await getData(
|
|
32
33
|
{
|
|
33
34
|
filter: q.filter,
|
|
@@ -41,19 +42,25 @@ function init({ genomes }) {
|
|
|
41
42
|
throw data.error;
|
|
42
43
|
const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
|
|
43
44
|
const key2values = /* @__PURE__ */ new Map();
|
|
44
|
-
const overlayTerm = q.
|
|
45
|
+
const overlayTerm = q.overlayTw;
|
|
46
|
+
const uncomputableValues = {};
|
|
45
47
|
for (const val of Object.values(data.samples)) {
|
|
46
48
|
const value = val[q.tw.$id];
|
|
47
49
|
if (!Number.isFinite(value?.value))
|
|
48
50
|
continue;
|
|
49
|
-
if (q.tw.term.values?.[value.value]?.uncomputable)
|
|
51
|
+
if (q.tw.term.values?.[value.value]?.uncomputable) {
|
|
52
|
+
const label = q.tw.term.values[value.value].label;
|
|
53
|
+
uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
|
|
50
54
|
continue;
|
|
55
|
+
}
|
|
51
56
|
if (overlayTerm) {
|
|
52
57
|
if (!val[overlayTerm?.$id])
|
|
53
58
|
continue;
|
|
54
59
|
const value2 = val[overlayTerm.$id];
|
|
55
|
-
if (overlayTerm.term?.values?.[value2.key]?.uncomputable)
|
|
56
|
-
|
|
60
|
+
if (overlayTerm.term?.values?.[value2.key]?.uncomputable) {
|
|
61
|
+
const label = overlayTerm.term.values[value2?.key]?.label;
|
|
62
|
+
uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
|
|
63
|
+
}
|
|
57
64
|
if (!key2values.has(value2.key))
|
|
58
65
|
key2values.set(value2.key, []);
|
|
59
66
|
key2values.get(value2.key).push(value.value);
|
|
@@ -64,49 +71,61 @@ function init({ genomes }) {
|
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
const plots = [];
|
|
67
|
-
let absMin, absMax, maxLabelLgth;
|
|
74
|
+
let absMin = null, absMax = null, maxLabelLgth = null;
|
|
68
75
|
for (const [key, values] of sortKey2values(data, key2values, overlayTerm)) {
|
|
69
76
|
const sortedValues = values.sort((a, b) => a - b);
|
|
70
|
-
if (absMin === null ||
|
|
77
|
+
if (absMin === null || sortedValues[0] < absMin)
|
|
71
78
|
absMin = sortedValues[0];
|
|
72
|
-
if (absMax === null ||
|
|
79
|
+
if (absMax === null || sortedValues[sortedValues.length - 1] > absMax)
|
|
73
80
|
absMax = sortedValues[sortedValues.length - 1];
|
|
74
81
|
const vs = sortedValues.map((v) => {
|
|
75
82
|
const value = { value: v };
|
|
76
83
|
return value;
|
|
77
84
|
});
|
|
85
|
+
const boxplot = boxplot_getvalue(vs);
|
|
86
|
+
if (!boxplot)
|
|
87
|
+
throw "boxplot_getvalue failed [termdb.boxplot init()]";
|
|
88
|
+
const descrStats = setDescrStats(boxplot, sortedValues);
|
|
78
89
|
const _plot = {
|
|
79
|
-
// label,
|
|
80
90
|
// values,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
min: sortedValues[0],
|
|
84
|
-
max: sortedValues[sortedValues.length - 1]
|
|
91
|
+
boxplot,
|
|
92
|
+
descrStats
|
|
85
93
|
};
|
|
86
94
|
if (overlayTerm) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
maxLabelLgth =
|
|
95
|
+
const _key = overlayTerm?.term?.values?.[key]?.label || key;
|
|
96
|
+
const plotLabel = `${_key}, n=${values.length}`;
|
|
97
|
+
if (maxLabelLgth === null || plotLabel.length > maxLabelLgth)
|
|
98
|
+
maxLabelLgth = plotLabel.length;
|
|
91
99
|
const plot = Object.assign(_plot, {
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
color: overlayTerm?.term?.values?.[key]?.color || null,
|
|
101
|
+
key: _key,
|
|
102
|
+
seriesId: key
|
|
94
103
|
});
|
|
95
|
-
plot.boxplot.label =
|
|
104
|
+
plot.boxplot.label = plotLabel;
|
|
96
105
|
plots.push(plot);
|
|
97
106
|
} else {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
maxLabelLgth =
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const plotLabel = `${sampleType}, n=${values.length}`;
|
|
108
|
+
if (maxLabelLgth === null || plotLabel.length > maxLabelLgth)
|
|
109
|
+
maxLabelLgth = plotLabel.length;
|
|
110
|
+
const plot = Object.assign(_plot, {
|
|
111
|
+
key: sampleType
|
|
112
|
+
});
|
|
113
|
+
plot.boxplot.label = plotLabel;
|
|
114
|
+
plots.push(plot);
|
|
103
115
|
}
|
|
104
116
|
}
|
|
117
|
+
if (absMin == null || absMax == null || maxLabelLgth == null)
|
|
118
|
+
throw "absMin, absMax, or maxLabelLgth is null [termdb.boxplot init()]";
|
|
119
|
+
if (q.tw.term?.values)
|
|
120
|
+
setUncomputablePlots(q.tw, plots);
|
|
121
|
+
if (overlayTerm && overlayTerm.term?.values)
|
|
122
|
+
setUncomputablePlots(overlayTerm, plots);
|
|
105
123
|
const returnData = {
|
|
106
124
|
absMin,
|
|
107
125
|
absMax,
|
|
108
126
|
maxLabelLgth,
|
|
109
|
-
plots
|
|
127
|
+
plots,
|
|
128
|
+
uncomputableValues: setUncomputableValues(uncomputableValues)
|
|
110
129
|
};
|
|
111
130
|
res.send(returnData);
|
|
112
131
|
} catch (e) {
|
|
@@ -116,6 +135,42 @@ function init({ genomes }) {
|
|
|
116
135
|
}
|
|
117
136
|
};
|
|
118
137
|
}
|
|
138
|
+
function setUncomputablePlots(term, plots) {
|
|
139
|
+
for (const v of Object.values(term.term.values)) {
|
|
140
|
+
const plot = plots.find((p) => p.key === v.label);
|
|
141
|
+
if (plot)
|
|
142
|
+
plot.uncomputable = v.uncomputable;
|
|
143
|
+
}
|
|
144
|
+
return plots;
|
|
145
|
+
}
|
|
146
|
+
function setDescrStats(boxplot, sortedValues) {
|
|
147
|
+
const mean = sortedValues.reduce((s2, i) => s2 + i, 0) / sortedValues.length;
|
|
148
|
+
let s = 0;
|
|
149
|
+
for (const v of sortedValues) {
|
|
150
|
+
s += Math.pow(v - mean, 2);
|
|
151
|
+
}
|
|
152
|
+
const sd = Math.sqrt(s / (sortedValues.length - 1));
|
|
153
|
+
const squareDiffs = sortedValues.map((x) => (x - mean) ** 2).reduce((a, b) => a + b, 0);
|
|
154
|
+
const variance = squareDiffs / (sortedValues.length - 1);
|
|
155
|
+
return [
|
|
156
|
+
{ id: "total", label: "Total", value: sortedValues.length },
|
|
157
|
+
{ id: "min", label: "Minimum", value: roundValue(sortedValues[0], 2) },
|
|
158
|
+
{ id: "p25", label: "1st quartile", value: roundValue(boxplot.p25, 2) },
|
|
159
|
+
{ id: "median", label: "Median", value: roundValue(boxplot.p50, 2) },
|
|
160
|
+
{ id: "mean", label: "Mean", value: roundValue(mean, 2) },
|
|
161
|
+
{ id: "p75", label: "3rd quartile", value: roundValue(boxplot.p75, 2) },
|
|
162
|
+
{ id: "max", label: "Maximum", value: roundValue(sortedValues[sortedValues.length - 1], 2) },
|
|
163
|
+
{ id: "sd", label: "Standard deviation", value: isNaN(sd) ? null : roundValue(sd, 2) },
|
|
164
|
+
{ id: "variance", label: "Variance", value: roundValue(variance, 2) },
|
|
165
|
+
{ id: "iqr", label: "Inter-quartile range", value: roundValue(boxplot.iqr, 2) }
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
function setUncomputableValues(values) {
|
|
169
|
+
if (Object.entries(values)?.length) {
|
|
170
|
+
return Object.entries(values).map(([label, v]) => ({ label, value: v }));
|
|
171
|
+
} else
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
119
174
|
export {
|
|
120
175
|
api
|
|
121
176
|
};
|