@sjcrh/proteinpaint-server 2.83.0 → 2.84.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.83.0",
3
+ "version": "2.84.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",
@@ -43,7 +43,7 @@
43
43
  "@babel/preset-env": "^7.9.6",
44
44
  "@babel/preset-typescript": "^7.21.4",
45
45
  "@babel/register": "^7.14.5",
46
- "@sjcrh/proteinpaint-types": "2.83.0",
46
+ "@sjcrh/proteinpaint-types": "2.84.0",
47
47
  "@types/node": "^20.11.24",
48
48
  "@types/tough-cookie": "^4.0.5",
49
49
  "@typescript-eslint/eslint-plugin": "^5.60.0",
@@ -58,12 +58,13 @@
58
58
  "ts-patch": "^3.0.2",
59
59
  "tsx": "^4.7.1",
60
60
  "typedoc": "^0.24.8",
61
- "typescript": "^5.0.3",
61
+ "typedoc-plugin-missing-exports": "^2.0.1",
62
+ "typescript": "^5.6.3",
62
63
  "typia": "^4.1.14"
63
64
  },
64
65
  "dependencies": {
65
66
  "@sjcrh/augen": "2.46.0",
66
- "@sjcrh/proteinpaint-rust": "2.81.5",
67
+ "@sjcrh/proteinpaint-rust": "2.84.0",
67
68
  "@sjcrh/proteinpaint-shared": "2.83.0",
68
69
  "better-sqlite3": "^9.4.1",
69
70
  "body-parser": "^1.15.2",
@@ -85,8 +86,7 @@
85
86
  "node-fetch": "^2.6.1",
86
87
  "partjson": "^0.58.2",
87
88
  "tiny-async-pool": "^1.2.0",
88
- "tough-cookie": "^4.1.4",
89
- "typedoc-plugin-missing-exports": "^2.0.1"
89
+ "tough-cookie": "^4.1.4"
90
90
  },
91
91
  "repository": {
92
92
  "type": "git",
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import serverconfig from "#src/serverconfig.js";
4
4
  import { spawn } from "child_process";
5
+ import { getData } from "../src/termdb.matrix.js";
5
6
  const api = {
6
7
  endpoint: "brainImaging",
7
8
  methods: {
@@ -13,6 +14,15 @@ const api = {
13
14
  response: {
14
15
  typeId: "GetBrainImagingResponse"
15
16
  }
17
+ },
18
+ post: {
19
+ init,
20
+ request: {
21
+ typeId: "GetBrainImagingRequest"
22
+ },
23
+ response: {
24
+ typeId: "GetBrainImagingResponse"
25
+ }
16
26
  }
17
27
  }
18
28
  };
@@ -26,15 +36,26 @@ function init({ genomes }) {
26
36
  const ds = g.datasets[query.dslabel];
27
37
  if (!ds)
28
38
  throw "invalid dataset name";
29
- const brainImage = await getBrainImage(query, genomes);
30
- res.send({ brainImage });
39
+ let plane, index;
40
+ if (query.l) {
41
+ plane = "L";
42
+ index = query.l;
43
+ } else if (query.f) {
44
+ plane = "F";
45
+ index = query.f;
46
+ } else {
47
+ plane = "T";
48
+ index = query.t;
49
+ }
50
+ const brainImage = await getBrainImage(query, genomes, plane, index);
51
+ res.send({ brainImage, plane });
31
52
  } catch (e) {
32
53
  console.log(e);
33
54
  res.status(404).send("Sample brain image not found");
34
55
  }
35
56
  };
36
57
  }
37
- async function getBrainImage(query, genomes) {
58
+ async function getBrainImage(query, genomes, plane, index) {
38
59
  const ds = genomes[query.genome].datasets[query.dslabel];
39
60
  const q = ds.queries.NIdata;
40
61
  const key = query.refKey;
@@ -42,58 +63,89 @@ async function getBrainImage(query, genomes) {
42
63
  const refFile = path.join(serverconfig.tpmasterdir, q[key].referenceFile);
43
64
  const dirPath = path.join(serverconfig.tpmasterdir, q[key].samples);
44
65
  const files = fs.readdirSync(dirPath).filter((file) => file.endsWith(".nii") && fs.statSync(path.join(dirPath, file)).isFile());
45
- if (query.samplesOnly) {
46
- const sampleNames = files.map((name) => name.split(".nii")[0]);
47
- if (q[key].sampleColumns) {
48
- const samples = {};
49
- for (const s of sampleNames) {
50
- const annoForOneS = { sample: s };
51
- const sid = ds.cohort.termdb.q.sampleName2id(s);
52
- for (const term of q[key].sampleColumns) {
53
- const v = ds.cohort.termdb.q.getSample2value(term.termid, sid);
54
- if (v[0]) {
55
- annoForOneS[term.termid] = v[0].value;
56
- }
57
- }
58
- samples[s] = annoForOneS;
59
- }
60
- return Object.values(samples);
66
+ const terms = [];
67
+ const divideByTW = query.divideByTW;
68
+ const overlayTW = query.overlayTW;
69
+ if (divideByTW)
70
+ terms.push(divideByTW);
71
+ if (overlayTW)
72
+ terms.push(overlayTW);
73
+ const selectedSampleNames = query.selectedSampleFileNames.map((s) => s.split(".nii")[0]);
74
+ const data = await getData({ terms }, ds, q.genome);
75
+ const divideByCat = {};
76
+ for (const sampleName of selectedSampleNames) {
77
+ const sampleId = ds.sampleName2Id.get(sampleName);
78
+ const sampleData = data.samples[sampleId];
79
+ const samplePath = path.join(dirPath, sampleName) + ".nii";
80
+ const divideCategory = divideByTW ? sampleData[divideByTW.$id].value : "default";
81
+ const overlayCategory = overlayTW ? sampleData[overlayTW.$id].value : "default";
82
+ if (!divideByCat[divideCategory])
83
+ divideByCat[divideCategory] = {};
84
+ if (!divideByCat[divideCategory][overlayCategory])
85
+ divideByCat[divideCategory][overlayCategory] = {
86
+ samples: [],
87
+ color: overlayTW?.term?.values?.[overlayCategory]?.color || "red"
88
+ };
89
+ divideByCat[divideCategory][overlayCategory].samples.push(samplePath);
90
+ }
91
+ const lengths = [];
92
+ for (const dcategory in divideByCat)
93
+ for (const category in divideByCat[dcategory]) {
94
+ const samples = divideByCat[dcategory][category].samples;
95
+ lengths.push(samples.length);
61
96
  }
62
- return sampleNames;
97
+ const maxLength = Math.max(...lengths);
98
+ const brainImageDict = {};
99
+ for (const dcategory in divideByCat) {
100
+ let catNum = 0;
101
+ const filesByCat = divideByCat[dcategory];
102
+ for (const category in filesByCat)
103
+ catNum += filesByCat[category].samples.length;
104
+ const url = await generateBrainImage(refFile, plane, index, maxLength, JSON.stringify(filesByCat));
105
+ brainImageDict[dcategory] = { url, catNum };
63
106
  }
64
- return new Promise((resolve, reject) => {
65
- const filePaths = query.selectedSampleFileNames.map((file) => path.join(dirPath, file));
66
- const cmd = [
67
- `${serverconfig.binpath}/../python/src/plotBrainImaging.py`,
68
- refFile,
69
- query.l,
70
- query.f,
71
- query.t,
72
- ...filePaths
73
- ];
74
- const ps = spawn(serverconfig.python, cmd);
75
- const imgData = [];
76
- ps.stdout.on("data", (data) => {
77
- imgData.push(data);
78
- });
79
- ps.stderr.on("data", (data) => {
80
- console.error(`stderr: ${data}`);
81
- reject(new Error(`Python script filed: ${data}`));
82
- });
83
- ps.on("close", (code) => {
84
- if (code === 0) {
85
- const imageBuffer = Buffer.concat(imgData);
86
- const base64Data = imageBuffer.toString("base64");
87
- const imgUrl = `data:image/png;base64,${base64Data}`;
88
- resolve(imgUrl);
89
- } else {
90
- reject(new Error(`Python script exited with code ${code}`));
91
- }
92
- });
93
- });
107
+ return brainImageDict;
94
108
  } else {
95
109
  throw "no reference or sample files";
96
110
  }
111
+ function getFilesByCat(tw) {
112
+ const filesByCat = {};
113
+ for (const [key2, value] of Object.entries(tw.term.values)) {
114
+ filesByCat[key2] = { samples: [], color: value.color || "red" };
115
+ }
116
+ return filesByCat;
117
+ }
118
+ }
119
+ async function generateBrainImage(refFile, plane, index, maxLength, filesJson) {
120
+ return new Promise((resolve, reject) => {
121
+ const cmd = [
122
+ `${serverconfig.binpath}/../python/src/plotBrainImaging.py`,
123
+ refFile,
124
+ plane,
125
+ index,
126
+ maxLength,
127
+ filesJson
128
+ ];
129
+ const ps = spawn(serverconfig.python, cmd);
130
+ const imgData = [];
131
+ ps.stdout.on("data", (data) => {
132
+ imgData.push(data);
133
+ });
134
+ ps.stderr.on("data", (data) => {
135
+ console.error(`stderr: ${data}`);
136
+ reject(new Error(`Python script filed: ${data}`));
137
+ });
138
+ ps.on("close", (code) => {
139
+ if (code === 0) {
140
+ const imageBuffer = Buffer.concat(imgData);
141
+ const base64Data = imageBuffer.toString("base64");
142
+ const imgUrl = `data:image/png;base64,${base64Data}`;
143
+ resolve(imgUrl);
144
+ } else {
145
+ reject(new Error(`Python script exited with code ${code}`));
146
+ }
147
+ });
148
+ });
97
149
  }
98
150
  export {
99
151
  api
@@ -0,0 +1,120 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import serverconfig from "#src/serverconfig.js";
4
+ import { spawn } from "child_process";
5
+ const api = {
6
+ endpoint: "brainImagingSamples",
7
+ methods: {
8
+ get: {
9
+ init,
10
+ request: {
11
+ typeId: "GetBrainImagingSamplesRequest"
12
+ },
13
+ response: {
14
+ typeId: "GetBrainImagingSamplesResponse"
15
+ }
16
+ }
17
+ }
18
+ };
19
+ function init({ genomes }) {
20
+ return async (req, res) => {
21
+ try {
22
+ const query = req.query;
23
+ const g = genomes[query.genome];
24
+ if (!g)
25
+ throw "invalid genome name";
26
+ const ds = g.datasets[query.dslabel];
27
+ if (!ds)
28
+ throw "invalid dataset name";
29
+ const samples = await getBrainImageSamples(query, genomes);
30
+ res.send({ samples });
31
+ } catch (e) {
32
+ console.log(e);
33
+ res.status(404).send("Sample brain image not found");
34
+ }
35
+ };
36
+ }
37
+ async function getBrainImageSamples(query, genomes) {
38
+ const ds = genomes[query.genome].datasets[query.dslabel];
39
+ const q = ds.queries.NIdata;
40
+ const key = query.refKey;
41
+ if (q[key].referenceFile && q[key].samples) {
42
+ const dirPath = path.join(serverconfig.tpmasterdir, q[key].samples);
43
+ const files = fs.readdirSync(dirPath).filter((file) => file.endsWith(".nii") && fs.statSync(path.join(dirPath, file)).isFile());
44
+ const sampleNames = files.map((name) => name.split(".nii")[0]);
45
+ if (q[key].sampleColumns) {
46
+ const samples = {};
47
+ for (const s of sampleNames) {
48
+ const annoForOneS = { sample: s };
49
+ const sid = ds.cohort.termdb.q.sampleName2id(s);
50
+ for (const term of q[key].sampleColumns) {
51
+ const v = ds.cohort.termdb.q.getSample2value(term.termid, sid);
52
+ if (v[0]) {
53
+ annoForOneS[term.termid] = v[0].value;
54
+ }
55
+ }
56
+ samples[s] = annoForOneS;
57
+ }
58
+ return Object.values(samples);
59
+ }
60
+ return sampleNames.map((name) => ({ sample: name }));
61
+ } else {
62
+ throw "no reference or sample files";
63
+ }
64
+ }
65
+ async function validate_query_NIdata(ds, genome) {
66
+ const q = ds.queries.NIdata;
67
+ if (!q || !serverconfig.features?.showBrainImaging)
68
+ return;
69
+ for (const key in q) {
70
+ if (q[key].referenceFile && q[key].samples) {
71
+ q[key].get = async (sampleName, l, f, t) => {
72
+ const refFile = path.join(serverconfig.tpmasterdir, q[key].referenceFile);
73
+ const sampleFile = path.join(serverconfig.tpmasterdir, q[key].samples, sampleName);
74
+ try {
75
+ await fs.promises.stat(sampleFile);
76
+ } catch (e) {
77
+ if (e.code == "EACCES")
78
+ throw "cannot read file, permission denied";
79
+ if (e.code == "ENOENT")
80
+ throw "no data for this sample";
81
+ throw "failed to load data";
82
+ }
83
+ return new Promise((resolve, reject) => {
84
+ const ps = spawn(serverconfig.python, [
85
+ `${serverconfig.binpath}/utils/plotBrainImaging.py`,
86
+ refFile,
87
+ sampleFile,
88
+ l,
89
+ f,
90
+ t
91
+ ]);
92
+ const imgData = [];
93
+ ps.stdout.on("data", (data) => {
94
+ imgData.push(data);
95
+ });
96
+ ps.stderr.on("data", (data) => {
97
+ console.error(`stderr: ${data}`);
98
+ reject(new Error(`Python script filed: ${data}`));
99
+ });
100
+ ps.on("close", (code) => {
101
+ if (code === 0) {
102
+ const imageBuffer = Buffer.concat(imgData);
103
+ const base64Data = imageBuffer.toString("base64");
104
+ const imgUrl = `data:image/png;base64,${base64Data}`;
105
+ resolve(imgUrl);
106
+ } else {
107
+ reject(new Error(`Python script exited with code ${code}`));
108
+ }
109
+ });
110
+ });
111
+ };
112
+ } else {
113
+ throw "no reference or sample files";
114
+ }
115
+ }
116
+ }
117
+ export {
118
+ api,
119
+ validate_query_NIdata
120
+ };
@@ -43,14 +43,18 @@ function init({ genomes }) {
43
43
  const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
44
44
  const key2values = /* @__PURE__ */ new Map();
45
45
  const overlayTerm = q.divideTw;
46
- for (const [key, val] of Object.entries(data.samples)) {
46
+ for (const val of Object.values(data.samples)) {
47
47
  const value = val[q.tw.$id];
48
48
  if (!Number.isFinite(value?.value))
49
49
  continue;
50
+ if (q.tw.term.values?.[value.value]?.uncomputable)
51
+ continue;
50
52
  if (overlayTerm) {
51
53
  if (!val[overlayTerm?.$id])
52
54
  continue;
53
55
  const value2 = val[overlayTerm.$id];
56
+ if (overlayTerm.term?.values?.[value2.key]?.uncomputable)
57
+ continue;
54
58
  if (!key2values.has(value2.key))
55
59
  key2values.set(value2.key, []);
56
60
  key2values.get(value2.key).push(value.value);
@@ -64,57 +68,52 @@ function init({ genomes }) {
64
68
  let absMin, absMax, maxLabelLgth;
65
69
  for (const [key, values] of sortKey2values(data, key2values, overlayTerm)) {
66
70
  const sortedValues = values.sort((a, b) => a - b);
67
- if (!absMin || sortedValues[0] < absMin)
71
+ if (absMin === null || absMin === void 0 || sortedValues[0] < absMin)
68
72
  absMin = sortedValues[0];
69
- if (!absMax || sortedValues[sortedValues.length - 1] > absMax)
73
+ if (absMax === null || absMax === void 0 || sortedValues[sortedValues.length - 1] > absMax)
70
74
  absMax = sortedValues[sortedValues.length - 1];
71
75
  const vs = sortedValues.map((v) => {
72
76
  const value = { value: v };
73
77
  return value;
74
78
  });
79
+ const _plot = {
80
+ // label,
81
+ // values,
82
+ // plotValueCount: values.length
83
+ boxplot: boxplot_getvalue(vs),
84
+ min: sortedValues[0],
85
+ max: sortedValues[sortedValues.length - 1]
86
+ };
75
87
  if (overlayTerm) {
76
88
  let label = overlayTerm?.term?.values?.[key]?.label || key;
77
89
  label = `${label}, n=${values.length}`;
78
- if (!maxLabelLgth || label.length > maxLabelLgth.length)
90
+ if (!maxLabelLgth || label.length > maxLabelLgth)
79
91
  maxLabelLgth = label.length;
80
- const plot = {
81
- // label,
82
- // values,
92
+ const plot = Object.assign(_plot, {
83
93
  seriesId: key,
84
- color: overlayTerm?.term?.values?.[key]?.color || null,
85
- boxplot: boxplot_getvalue(vs),
86
- //Need sd and mean?
87
- // plotValueCount: values.length,
88
- min: sortedValues[0],
89
- max: sortedValues[sortedValues.length - 1]
90
- };
94
+ color: overlayTerm?.term?.values?.[key]?.color || null
95
+ });
91
96
  plot.boxplot.label = label;
92
97
  plots.push(plot);
93
98
  } else {
94
99
  const label = `${sampleType}, n=${values.length}`;
95
100
  if (!maxLabelLgth || label.length > maxLabelLgth.length)
96
101
  maxLabelLgth = label.length;
97
- const plot = {
98
- // label,
99
- // values,
100
- // plotValueCount: values.length,
101
- boxplot: boxplot_getvalue(vs),
102
- min: sortedValues[0],
103
- max: sortedValues[sortedValues.length - 1]
104
- };
105
- plot.boxplot.label = label;
106
- plots.push(plot);
102
+ _plot.boxplot.label = label;
103
+ plots.push(_plot);
107
104
  }
108
105
  }
109
- data.plots = plots;
110
- data.absMin = absMin;
111
- data.absMax = absMax;
112
- data.maxLabelLgth = maxLabelLgth;
113
- res.send(data);
106
+ const returnData = {
107
+ absMin,
108
+ absMax,
109
+ maxLabelLgth,
110
+ plots
111
+ };
112
+ res.send(returnData);
114
113
  } catch (e) {
115
114
  res.send({ error: e?.message || e });
116
115
  if (e instanceof Error && e.stack)
117
- console.log(e);
116
+ console.error(e);
118
117
  }
119
118
  };
120
119
  }