@sjcrh/proteinpaint-server 2.166.0 → 2.167.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.
@@ -369,20 +369,7 @@ function termdb_test_default() {
369
369
  jsonFile: "files/hg38/TermdbTest/trackLst/facet.json",
370
370
  activeTracks: ["bw 1", "bed 1"]
371
371
  },
372
- chat: {},
373
- alphaGenome: {
374
- default: {
375
- gene: "FLT3",
376
- chromosome: "chr13",
377
- position: 28034105,
378
- reference: "A",
379
- alternate: "AACTCCCATTTGAGATCATACC",
380
- ontologyTerm: "EFO:0000572",
381
- // lymphoblast
382
- outputType: 4
383
- //RNA SEQ
384
- }
385
- }
372
+ chat: {}
386
373
  }
387
374
  };
388
375
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.166.0",
3
+ "version": "2.167.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",
@@ -64,9 +64,9 @@
64
64
  "@sjcrh/augen": "2.143.0",
65
65
  "@sjcrh/proteinpaint-python": "2.164.1",
66
66
  "@sjcrh/proteinpaint-r": "2.152.1-0",
67
- "@sjcrh/proteinpaint-rust": "2.166.0",
68
- "@sjcrh/proteinpaint-shared": "2.166.0",
69
- "@sjcrh/proteinpaint-types": "2.166.0",
67
+ "@sjcrh/proteinpaint-rust": "2.167.0",
68
+ "@sjcrh/proteinpaint-shared": "2.167.0",
69
+ "@sjcrh/proteinpaint-types": "2.167.0",
70
70
  "@types/express": "^5.0.0",
71
71
  "@types/express-session": "^1.18.1",
72
72
  "better-sqlite3": "^12.4.1",
@@ -3,6 +3,7 @@ import { getData } from "../src/termdb.matrix.js";
3
3
  import { boxplot_getvalue } from "../src/utils.js";
4
4
  import { sortPlot2Values } from "./termdb.violin.ts";
5
5
  import { getDescrStats } from "./termdb.descrstats.ts";
6
+ import { run_rust } from "@sjcrh/proteinpaint-rust";
6
7
  const api = {
7
8
  endpoint: "termdb/boxplot",
8
9
  methods: {
@@ -21,78 +22,18 @@ function init({ genomes }) {
21
22
  const q = req.query;
22
23
  try {
23
24
  const genome = genomes[q.genome];
24
- if (!genome) throw "invalid genome name";
25
+ if (!genome) throw new Error("invalid genome name");
25
26
  const ds = genome.datasets?.[q.dslabel];
26
- if (!ds) throw "invalid ds";
27
+ if (!ds) throw new Error("invalid ds");
27
28
  const terms = [q.tw];
28
29
  if (q.overlayTw) terms.push(q.overlayTw);
29
30
  if (q.divideTw) terms.push(q.divideTw);
30
31
  const data = await getData({ filter: q.filter, filter0: q.filter0, terms, __protected__: q.__protected__ }, ds);
31
- if (data.error) throw data.error;
32
- const samples = Object.values(data.samples);
33
- const values = samples.map((s) => s?.[q.tw.$id]?.value).filter((v) => typeof v === "number" && !q.tw.term.values?.[v]?.uncomputable);
34
- const descrStats = getDescrStats(values, q.removeOutliers);
35
- const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
36
- const overlayTerm = q.overlayTw;
37
- const divideTerm = q.divideTw;
38
- const { absMin, absMax, chart2plot2values, uncomputableValues } = parseValues(
39
- q,
32
+ if (data.error) throw new Error(data.error);
33
+ const { absMin, absMax, charts, uncomputableValues, descrStats, outlierMin, outlierMax } = await processData(
40
34
  data,
41
- sampleType,
42
- q.isLogScale,
43
- overlayTerm,
44
- divideTerm
35
+ q
45
36
  );
46
- if (!absMin && absMin !== 0) throw "absMin is undefined [termdb.boxplot init()]";
47
- if (!absMax && absMax !== 0) throw "absMax is undefined [termdb.boxplot init()]";
48
- const charts = {};
49
- let outlierMin = Number.POSITIVE_INFINITY, outlierMax = Number.NEGATIVE_INFINITY;
50
- for (const [chart, plot2values] of chart2plot2values) {
51
- const plots = [];
52
- for (const [key, values2] of sortPlot2Values(data, plot2values, overlayTerm)) {
53
- const sortedValues = values2.sort((a, b) => a - b);
54
- const vs = sortedValues.map((v) => {
55
- const value = { value: v };
56
- return value;
57
- });
58
- if (q.removeOutliers) {
59
- outlierMin = Math.min(outlierMin, descrStats.outlierMin.value);
60
- outlierMax = Math.max(outlierMax, descrStats.outlierMax.value);
61
- }
62
- const boxplot = boxplot_getvalue(vs, q.removeOutliers);
63
- if (!boxplot) throw "boxplot_getvalue failed [termdb.boxplot init()]";
64
- const _plot = {
65
- boxplot,
66
- descrStats
67
- };
68
- if (overlayTerm) {
69
- const _key = overlayTerm?.term?.values?.[key]?.label || key;
70
- const plotLabel = `${_key}, n=${values2.length}`;
71
- const overlayBins = numericBins(overlayTerm, data);
72
- const plot = Object.assign(_plot, {
73
- color: overlayTerm?.term?.values?.[key]?.color || null,
74
- key: _key,
75
- overlayBins: overlayBins.has(key) ? overlayBins.get(key) : null,
76
- seriesId: key
77
- });
78
- plot.boxplot.label = plotLabel;
79
- plots.push(plot);
80
- } else {
81
- const plotLabel = `${sampleType}, n=${values2.length}`;
82
- const plot = Object.assign(_plot, {
83
- key: sampleType
84
- });
85
- plot.boxplot.label = plotLabel;
86
- plots.push(plot);
87
- }
88
- }
89
- if (q.tw.term?.values) setHiddenPlots(q.tw, plots);
90
- if (overlayTerm && overlayTerm.term?.values) setHiddenPlots(overlayTerm, plots);
91
- if (q.orderByMedian == true) {
92
- plots.sort((a, b) => a.boxplot.p50 - b.boxplot.p50);
93
- }
94
- charts[chart] = { chartId: chart, plots };
95
- }
96
37
  const returnData = {
97
38
  absMin: q.removeOutliers ? outlierMin : absMin,
98
39
  absMax: q.removeOutliers ? outlierMax : absMax,
@@ -107,6 +48,94 @@ function init({ genomes }) {
107
48
  }
108
49
  };
109
50
  }
51
+ async function processData(data, q) {
52
+ const samples = Object.values(data.samples);
53
+ const values = samples.map((s) => s?.[q.tw.$id]?.value).filter((v) => typeof v === "number" && !q.tw.term.values?.[v]?.uncomputable);
54
+ const descrStats = getDescrStats(values, q.removeOutliers);
55
+ const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
56
+ const overlayTerm = q.overlayTw;
57
+ const divideTerm = q.divideTw;
58
+ const { absMin, absMax, chart2plot2values, uncomputableValues } = parseValues(
59
+ q,
60
+ data,
61
+ sampleType,
62
+ q.isLogScale,
63
+ overlayTerm,
64
+ divideTerm
65
+ );
66
+ if (!absMin && absMin !== 0) throw new Error("absMin is undefined");
67
+ if (!absMax && absMax !== 0) throw new Error("absMax is undefined");
68
+ const charts = {};
69
+ let outlierMin = Number.POSITIVE_INFINITY, outlierMax = Number.NEGATIVE_INFINITY;
70
+ for (const [chart, plot2values] of chart2plot2values) {
71
+ const plots = [];
72
+ for (const [key, values2] of sortPlot2Values(data, plot2values, overlayTerm)) {
73
+ ;
74
+ [outlierMax, outlierMin] = setPlotData(
75
+ plots,
76
+ values2,
77
+ key,
78
+ sampleType,
79
+ descrStats,
80
+ q,
81
+ data,
82
+ outlierMin,
83
+ outlierMax,
84
+ overlayTerm
85
+ );
86
+ }
87
+ if (q.tw.term?.values) setHiddenPlots(q.tw, plots);
88
+ if (overlayTerm && overlayTerm.term?.values) setHiddenPlots(overlayTerm, plots);
89
+ if (q.orderByMedian == true) {
90
+ plots.sort((a, b) => a.boxplot.p50 - b.boxplot.p50);
91
+ }
92
+ charts[chart] = { chartId: chart, plots };
93
+ }
94
+ if (q.showAssocTests == true && overlayTerm) await getWilcoxonData(charts);
95
+ Object.keys(charts).forEach((c) => charts[c].plots.forEach((p) => delete p.tempValues));
96
+ return { absMin, absMax, charts, uncomputableValues, descrStats, outlierMin, outlierMax };
97
+ }
98
+ function setPlotData(plots, values, key, sampleType, descrStats, q, data, outlierMin, outlierMax, overlayTerm) {
99
+ const sortedValues = values.sort((a, b) => a - b);
100
+ const vs = sortedValues.map((v) => {
101
+ const value = { value: v };
102
+ return value;
103
+ });
104
+ if (q.removeOutliers) {
105
+ outlierMin = Math.min(outlierMin, descrStats.outlierMin.value);
106
+ outlierMax = Math.max(outlierMax, descrStats.outlierMax.value);
107
+ }
108
+ const boxplot = boxplot_getvalue(vs, q.removeOutliers);
109
+ if (!boxplot) throw new Error("boxplot_getvalue failed [termdb.boxplot init()]");
110
+ const _plot = {
111
+ boxplot,
112
+ descrStats,
113
+ //quick fix
114
+ //to delete later
115
+ tempValues: sortedValues
116
+ };
117
+ if (overlayTerm) {
118
+ const _key = overlayTerm?.term?.values?.[key]?.label || key;
119
+ const plotLabel = `${_key}, n=${values.length}`;
120
+ const overlayBins = numericBins(overlayTerm, data);
121
+ const plot = Object.assign(_plot, {
122
+ color: overlayTerm?.term?.values?.[key]?.color || null,
123
+ key: _key,
124
+ overlayBins: overlayBins.has(key) ? overlayBins.get(key) : null,
125
+ seriesId: key
126
+ });
127
+ plot.boxplot.label = plotLabel;
128
+ plots.push(plot);
129
+ } else {
130
+ const plotLabel = `${sampleType}, n=${values.length}`;
131
+ const plot = Object.assign(_plot, {
132
+ key: sampleType
133
+ });
134
+ plot.boxplot.label = plotLabel;
135
+ plots.push(plot);
136
+ }
137
+ return [outlierMax, outlierMin];
138
+ }
110
139
  function setHiddenPlots(term, plots) {
111
140
  for (const v of Object.values(term.term?.values)) {
112
141
  const plot = plots.find((p) => p.key === v.label);
@@ -125,6 +154,35 @@ function setUncomputableValues(values) {
125
154
  return Object.entries(values).map(([label, v]) => ({ label, value: v }));
126
155
  } else return null;
127
156
  }
157
+ async function getWilcoxonData(charts) {
158
+ for (const chart of Object.values(charts)) {
159
+ const numPlots = chart.plots?.length;
160
+ if (numPlots < 2) continue;
161
+ const wilcoxonInput = [];
162
+ for (let i = 0; i < numPlots; i++) {
163
+ const group1_id = chart.plots[i].boxplot.label.replace(/, n=\d+$/, "");
164
+ const group1_values = chart.plots[i].tempValues;
165
+ for (let j = i + 1; j < numPlots; j++) {
166
+ const group2_id = chart.plots[j].boxplot.label.replace(/, n=\d+$/, "");
167
+ const group2_values = chart.plots[j].tempValues;
168
+ wilcoxonInput.push({ group1_id, group1_values, group2_id, group2_values });
169
+ }
170
+ }
171
+ const wilcoxonOutput = JSON.parse(await run_rust("wilcoxon", JSON.stringify(wilcoxonInput)));
172
+ chart.wilcoxon = [];
173
+ for (const test of wilcoxonOutput) {
174
+ if (test.pvalue == null || test.pvalue == "null") {
175
+ chart.wilcoxon.push([{ value: test.group1_id }, { value: test.group2_id }, { html: "NA" }]);
176
+ } else {
177
+ chart.wilcoxon.push([
178
+ { value: test.group1_id },
179
+ { value: test.group2_id },
180
+ { html: test.pvalue.toPrecision(4) }
181
+ ]);
182
+ }
183
+ }
184
+ }
185
+ }
128
186
  function parseValues(q, data, sampleType, isLog, overlayTerm, divideTerm) {
129
187
  const chart2plot2values = /* @__PURE__ */ new Map();
130
188
  const uncomputableValues = {};
@@ -41,7 +41,6 @@ function init({ genomes }) {
41
41
  return;
42
42
  }
43
43
  const serverconfig_ds_entries = serverconfig.genomes.find((genome) => genome.name == q.genome).datasets.find((dslabel) => dslabel.name == ds.label);
44
- console.log("serverconfig_ds_entries:", serverconfig_ds_entries);
45
44
  if (!serverconfig_ds_entries.aifiles) {
46
45
  throw "aifiles are missing for chatbot to work";
47
46
  }
@@ -75,14 +74,41 @@ function init({ genomes }) {
75
74
  const ai_output_data = await run_rust("aichatbot", JSON.stringify(chatbot_input));
76
75
  const time2 = (/* @__PURE__ */ new Date()).valueOf();
77
76
  mayLog("Time taken to run rust AI chatbot:", time2 - time1, "ms");
78
- let ai_output_json = "";
77
+ let ai_output_json;
79
78
  for (const line of ai_output_data.split("\n")) {
80
79
  if (line.startsWith("final_output:") == true) {
81
- ai_output_json = JSON.parse(line.replace("final_output:", ""));
80
+ ai_output_json = JSON.parse(JSON.parse(line.replace("final_output:", "")));
82
81
  } else {
83
82
  mayLog(line);
84
83
  }
85
84
  }
85
+ if (ai_output_json.type == "plot") {
86
+ if (typeof ai_output_json.plot != "object") throw ".plot{} missing when .type=plot";
87
+ if (ai_output_json.plot.simpleFilter) {
88
+ if (!Array.isArray(ai_output_json.plot.simpleFilter)) throw "ai_output_json.plot.simpleFilter is not array";
89
+ const localfilter = { type: "tvslst", in: true, join: "", lst: [] };
90
+ for (const f of ai_output_json.plot.simpleFilter) {
91
+ const term = ds.cohort.termdb.q.termjsonByOneid(f.term);
92
+ if (!term) throw "invalid term id from simpleFilter[].term";
93
+ if (term.type != "categorical") throw "term not categorical";
94
+ let cat;
95
+ for (const ck in term.values) {
96
+ if (ck == f.category) cat = ck;
97
+ else if (term.values[ck].label == f.category) cat = ck;
98
+ }
99
+ if (!cat) throw "invalid category from " + JSON.stringify(f);
100
+ localfilter.lst.push({
101
+ type: "tvs",
102
+ tvs: {
103
+ term,
104
+ values: [{ key: cat }]
105
+ }
106
+ });
107
+ }
108
+ delete ai_output_json.plot.simpleFilter;
109
+ ai_output_json.plot.filter = localfilter;
110
+ }
111
+ }
86
112
  res.send(ai_output_json);
87
113
  } catch (e) {
88
114
  if (e.stack) console.log(e.stack);
@@ -105,7 +105,8 @@ async function getResult(q, ds) {
105
105
  }
106
106
  async function getNumericDictTermAnnotation(q, ds) {
107
107
  const getDataArgs = {
108
- terms: q.terms.map((term) => ({ term, q: { mode: "continuous" } })),
108
+ // TODO: figure out when term is not a termwrapper
109
+ terms: q.terms.map((tw) => tw.term ? tw : { term: tw, q: { mode: "continuous" } }),
109
110
  filter: q.filter,
110
111
  filter0: q.filter0,
111
112
  __protected__: q.__protected__
@@ -77,6 +77,7 @@ function make(q, req, res, ds, genome) {
77
77
  if (tdb.numericTermCollections) c.numericTermCollections = tdb.numericTermCollections;
78
78
  if (ds.assayAvailability) c.assayAvailability = ds.assayAvailability;
79
79
  if (ds.cohort.correlationVolcano) c.correlationVolcano = ds.cohort.correlationVolcano;
80
+ if (ds.cohort.boxplots) c.boxplots = ds.cohort.boxplots;
80
81
  addRestrictAncestries(c, tdb);
81
82
  addScatterplots(c, ds);
82
83
  addMatrixplots(c, ds);
@@ -27,9 +27,9 @@ function init({ genomes }) {
27
27
  let data;
28
28
  try {
29
29
  const g = genomes[q.genome];
30
- if (!g) throw "invalid genome name";
30
+ if (!g) throw new Error("invalid genome name");
31
31
  const ds = g.datasets?.[q.dslabel];
32
- if (!ds) throw "invalid ds";
32
+ if (!ds) throw new Error("invalid ds");
33
33
  data = await getViolin(q, ds);
34
34
  } catch (e) {
35
35
  data = { error: e?.message || e };
@@ -39,10 +39,10 @@ function init({ genomes }) {
39
39
  };
40
40
  }
41
41
  async function getViolin(q, ds) {
42
- if (typeof q.tw?.term != "object" || typeof q.tw?.q != "object") throw "q.tw not of {term,q}";
42
+ if (typeof q.tw?.term != "object" || typeof q.tw?.q != "object") throw new Error("q.tw not of {term,q}");
43
43
  const term = q.tw.term;
44
44
  if (!q.tw.q.mode) q.tw.q.mode = "continuous";
45
- if (!isNumericTerm(term) && term.type !== "survival") throw "term type is not numeric or survival";
45
+ if (!isNumericTerm(term) && term.type !== "survival") throw new Error("term type is not numeric or survival");
46
46
  const terms = [q.tw];
47
47
  if (q.overlayTw) terms.push(q.overlayTw);
48
48
  if (q.divideTw) terms.push(q.divideTw);
@@ -56,14 +56,14 @@ async function getViolin(q, ds) {
56
56
  },
57
57
  ds
58
58
  );
59
- if (!data) throw "getData() returns nothing";
60
- if (data.error) throw data.error;
59
+ if (!data) throw new Error("getData() returns nothing");
60
+ if (data.error) throw new Error(data.error);
61
61
  const samples = Object.values(data.samples);
62
62
  let values = samples.map((s) => s?.[q.tw.$id]?.value).filter((v) => typeof v === "number" && !q.tw.term.values?.[v]?.uncomputable);
63
63
  if (q.unit == "log") values = values.filter((v) => v > 0);
64
64
  const descrStats = getDescrStats(values);
65
65
  const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
66
- if (data.error) throw data.error;
66
+ if (data.error) throw new Error(data.error);
67
67
  if (q.overlayTw && data.refs.byTermId[q.overlayTw.$id]) {
68
68
  data.refs.byTermId[q.overlayTw.$id].orderedLabels = getOrderedLabels(
69
69
  q.overlayTw,
@@ -174,7 +174,7 @@ function setResponse(valuesObject, data, q) {
174
174
  }
175
175
  async function createCanvasImg(q, result, ds) {
176
176
  if (!q.radius) q.radius = 5;
177
- if (q.radius <= 0) throw "q.radius is not a number";
177
+ if (q.radius <= 0) throw new Error("q.radius is not a number");
178
178
  else q.radius = +q.radius;
179
179
  const isH = q.orientation == "horizontal";
180
180
  for (const k of Object.keys(result.charts)) {