@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.85.1",
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": "^5.60.0",
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.24.8",
59
- "typedoc-plugin-missing-exports": "^2.0.1",
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.85.0",
65
+ "@sjcrh/augen": "2.86.0",
65
66
  "@sjcrh/proteinpaint-rust": "2.84.0",
66
- "@sjcrh/proteinpaint-shared": "2.85.0",
67
- "@sjcrh/proteinpaint-types": "2.85.0",
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",
@@ -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
- const divideCategory = divideByTW ? sampleData[divideByTW.$id].value : "default";
72
- const overlayCategory = overlayTW ? sampleData[overlayTW.$id].value : "default";
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 (!divideByCat[divideCategory][overlayCategory])
76
- divideByCat[divideCategory][overlayCategory] = {
77
- samples: [],
78
- color: overlayTW?.term?.values?.[overlayCategory]?.color || "red"
79
- };
80
- divideByCat[divideCategory][overlayCategory].samples.push(samplePath);
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
- return brainImageDict;
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) => {
@@ -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.divideTw)
30
- terms.push(q.divideTw);
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.divideTw;
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
- continue;
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 || absMin === void 0 || sortedValues[0] < absMin)
77
+ if (absMin === null || sortedValues[0] < absMin)
71
78
  absMin = sortedValues[0];
72
- if (absMax === null || absMax === void 0 || sortedValues[sortedValues.length - 1] > absMax)
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
- // plotValueCount: values.length
82
- boxplot: boxplot_getvalue(vs),
83
- min: sortedValues[0],
84
- max: sortedValues[sortedValues.length - 1]
91
+ boxplot,
92
+ descrStats
85
93
  };
86
94
  if (overlayTerm) {
87
- let label = overlayTerm?.term?.values?.[key]?.label || key;
88
- label = `${label}, n=${values.length}`;
89
- if (!maxLabelLgth || label.length > maxLabelLgth)
90
- maxLabelLgth = label.length;
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
- seriesId: key,
93
- color: overlayTerm?.term?.values?.[key]?.color || null
100
+ color: overlayTerm?.term?.values?.[key]?.color || null,
101
+ key: _key,
102
+ seriesId: key
94
103
  });
95
- plot.boxplot.label = label;
104
+ plot.boxplot.label = plotLabel;
96
105
  plots.push(plot);
97
106
  } else {
98
- const label = `${sampleType}, n=${values.length}`;
99
- if (!maxLabelLgth || label.length > maxLabelLgth.length)
100
- maxLabelLgth = label.length;
101
- _plot.boxplot.label = label;
102
- plots.push(_plot);
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
  };