@sjcrh/proteinpaint-server 2.96.2-0 → 2.98.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.96.2-0",
3
+ "version": "2.98.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",
@@ -42,7 +42,6 @@
42
42
  "@babel/preset-env": "^7.9.6",
43
43
  "@babel/preset-typescript": "^7.21.4",
44
44
  "@babel/register": "^7.14.5",
45
- "@sjcrh/proteinpaint-types": "2.96.2-0",
46
45
  "@types/node": "^20.11.24",
47
46
  "@types/tough-cookie": "^4.0.5",
48
47
  "@typescript-eslint/eslint-plugin": "^8.13.0",
@@ -61,7 +60,8 @@
61
60
  "dependencies": {
62
61
  "@sjcrh/augen": "2.87.0",
63
62
  "@sjcrh/proteinpaint-rust": "2.84.0",
64
- "@sjcrh/proteinpaint-shared": "2.96.0",
63
+ "@sjcrh/proteinpaint-shared": "2.98.0",
64
+ "@sjcrh/proteinpaint-types": "2.98.0",
65
65
  "better-sqlite3": "^9.4.1",
66
66
  "body-parser": "^1.15.2",
67
67
  "canvas": "~2.11.2",
@@ -1,7 +1,7 @@
1
1
  import { boxplotPayload } from "#types/checkers";
2
2
  import { getData } from "../src/termdb.matrix.js";
3
3
  import { boxplot_getvalue } from "../src/utils.js";
4
- import { sortKey2values } from "../src/termdb.violin.js";
4
+ import { sortKey2values } from "./termdb.violin.ts";
5
5
  import { roundValueAuto } from "#shared/roundValue.js";
6
6
  const api = {
7
7
  endpoint: "termdb/boxplot",
@@ -41,43 +41,18 @@ function init({ genomes }) {
41
41
  if (data.error)
42
42
  throw data.error;
43
43
  const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
44
- const key2values = /* @__PURE__ */ new Map();
45
44
  const overlayTerm = q.overlayTw;
46
- const uncomputableValues = {};
47
- for (const val of Object.values(data.samples)) {
48
- const value = val[q.tw.$id];
49
- if (!Number.isFinite(value?.value))
50
- continue;
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;
54
- continue;
55
- }
56
- if (overlayTerm) {
57
- if (!val[overlayTerm?.$id])
58
- continue;
59
- const value2 = val[overlayTerm.$id];
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
- }
64
- if (!key2values.has(value2.key))
65
- key2values.set(value2.key, []);
66
- key2values.get(value2.key).push(value.value);
67
- } else {
68
- if (!key2values.has(sampleType))
69
- key2values.set(sampleType, []);
70
- key2values.get(sampleType).push(value.value);
71
- }
72
- }
45
+ const isLog = false;
46
+ const { absMin, absMax, key2values, uncomputableValues } = parseValues(
47
+ q,
48
+ data,
49
+ sampleType,
50
+ isLog,
51
+ overlayTerm
52
+ );
73
53
  const plots = [];
74
- let absMin = null, absMax = null;
75
54
  for (const [key, values] of sortKey2values(data, key2values, overlayTerm)) {
76
55
  const sortedValues = values.sort((a, b) => a - b);
77
- if (absMin === null || sortedValues[0] < absMin)
78
- absMin = sortedValues[0];
79
- if (absMax === null || sortedValues[sortedValues.length - 1] > absMax)
80
- absMax = sortedValues[sortedValues.length - 1];
81
56
  const vs = sortedValues.map((v) => {
82
57
  const value = { value: v };
83
58
  return value;
@@ -179,10 +154,50 @@ function setUncomputableValues(values) {
179
154
  } else
180
155
  return null;
181
156
  }
157
+ function parseValues(q, data, sampleType, isLog, overlayTerm) {
158
+ const key2values = /* @__PURE__ */ new Map();
159
+ const uncomputableValues = {};
160
+ let absMin = null, absMax = null;
161
+ for (const val of Object.values(data.samples)) {
162
+ const value = val[q.tw.$id];
163
+ if (!Number.isFinite(value?.value))
164
+ continue;
165
+ if (q.tw.term.values?.[value.value]?.uncomputable) {
166
+ const label = q.tw.term.values[value.value].label;
167
+ uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
168
+ continue;
169
+ }
170
+ if (isLog && value.value <= 0)
171
+ continue;
172
+ if (overlayTerm) {
173
+ if (!val[overlayTerm?.$id])
174
+ continue;
175
+ const value2 = val[overlayTerm.$id];
176
+ if (overlayTerm.term?.values?.[value2.key]?.uncomputable) {
177
+ const label = overlayTerm.term.values[value2?.key]?.label;
178
+ uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
179
+ }
180
+ if (!key2values.has(value2.key))
181
+ key2values.set(value2.key, []);
182
+ key2values.get(value2.key).push(value.value);
183
+ } else {
184
+ if (!key2values.has(sampleType))
185
+ key2values.set(sampleType, []);
186
+ key2values.get(sampleType).push(value.value);
187
+ }
188
+ if (absMin === null || value.value < absMin)
189
+ absMin = value.value;
190
+ if (absMax === null || value.value > absMax)
191
+ absMax = value.value;
192
+ }
193
+ return { absMax, absMin, key2values, uncomputableValues };
194
+ }
182
195
  function numericBins(overlayTerm, data) {
183
196
  const overlayBins = data.refs.byTermId[overlayTerm?.$id]?.bins ?? [];
184
197
  return new Map(overlayBins.map((bin) => [bin.label, bin]));
185
198
  }
186
199
  export {
187
- api
200
+ api,
201
+ numericBins,
202
+ parseValues
188
203
  };
@@ -1,5 +1,13 @@
1
1
  import { violinPayload } from "#types/checkers";
2
- import { trigger_getViolinPlotData } from "#src/termdb.violin.js";
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 summaryStats from "#shared/descriptive.stats.js";
8
+ import { isNumericTerm } from "#shared/terms.js";
9
+ import { getBinsDensity } from "#shared/violin.bins.js";
10
+ import { numericBins, parseValues } from "./termdb.boxplot.ts";
3
11
  const api = {
4
12
  endpoint: "termdb/violin",
5
13
  methods: {
@@ -24,7 +32,7 @@ function init({ genomes }) {
24
32
  const ds = g.datasets?.[q.dslabel];
25
33
  if (!ds)
26
34
  throw "invalid ds";
27
- data = await trigger_getViolinPlotData(q, null, ds, g);
35
+ data = await trigger_getViolinPlotData(q, ds, g);
28
36
  } catch (e) {
29
37
  data = { error: e?.message || e };
30
38
  if (e instanceof Error && e.stack)
@@ -33,6 +41,196 @@ function init({ genomes }) {
33
41
  res.send(data);
34
42
  };
35
43
  }
44
+ async function trigger_getViolinPlotData(q, ds, genome) {
45
+ if (typeof q.tw?.term != "object" || typeof q.tw?.q != "object")
46
+ throw "q.tw not of {term,q}";
47
+ const term = q.tw.term;
48
+ if (!q.tw.q.mode)
49
+ q.tw.q.mode = "continuous";
50
+ if (!isNumericTerm(term) && term.type !== "survival")
51
+ throw "term type is not numeric or survival";
52
+ const terms = [q.tw];
53
+ if (q.divideTw)
54
+ terms.push(q.divideTw);
55
+ const data = await getData(
56
+ { terms, filter: q.filter, filter0: q.filter0, currentGeneNames: q.currentGeneNames },
57
+ ds,
58
+ genome
59
+ );
60
+ const sampleType = `All ${data.sampleType?.plural_name || "samples"}`;
61
+ if (data.error)
62
+ throw data.error;
63
+ if (q.divideTw && data.refs.byTermId[q.divideTw.$id]) {
64
+ data.refs.byTermId[q.divideTw.$id].orderedLabels = getOrderedLabels(
65
+ q.divideTw,
66
+ data.refs.byTermId[q.divideTw.$id]?.bins,
67
+ void 0,
68
+ q.divideTw.q
69
+ );
70
+ }
71
+ if (q.scale)
72
+ setScaleData(q, data, q.tw);
73
+ const valuesObject = divideValues(q, data, sampleType);
74
+ const result = setResponse(valuesObject, data, q, sampleType);
75
+ await getWilcoxonData(q.divideTw, result);
76
+ createCanvasImg(q, result, ds);
77
+ return result;
78
+ }
79
+ async function getWilcoxonData(divideTw, result) {
80
+ if (!divideTw)
81
+ return;
82
+ const numPlots = result.plots.length;
83
+ if (numPlots < 2)
84
+ return;
85
+ const wilcoxInput = [];
86
+ for (let i = 0; i < numPlots; i++) {
87
+ const group1_id = result.plots[i].label;
88
+ const group1_values = result.plots[i].values;
89
+ for (let j = i + 1; j < numPlots; j++) {
90
+ const group2_id = result.plots[j].label;
91
+ const group2_values = result.plots[j].values;
92
+ wilcoxInput.push({ group1_id, group1_values, group2_id, group2_values });
93
+ }
94
+ }
95
+ const wilcoxOutput = JSON.parse(await run_rust("wilcoxon", JSON.stringify(wilcoxInput)));
96
+ for (const test of wilcoxOutput) {
97
+ if (test.pvalue == null || test.pvalue == "null") {
98
+ result.pvalues.push([{ value: test.group1_id }, { value: test.group2_id }, { html: "NA" }]);
99
+ } else {
100
+ result.pvalues.push([{ value: test.group1_id }, { value: test.group2_id }, { html: test.pvalue.toPrecision(4) }]);
101
+ }
102
+ }
103
+ }
104
+ function setScaleData(q, data, tw) {
105
+ if (!q.scale)
106
+ return;
107
+ const scale = Number(q.scale);
108
+ for (const val of Object.values(data.samples)) {
109
+ if (!tw.$id || !val[tw.$id])
110
+ continue;
111
+ if (tw.term.values?.[val[tw.$id]?.value]?.uncomputable)
112
+ continue;
113
+ val[tw.$id].key = val[tw.$id].key / scale;
114
+ val[tw.$id].value = val[tw.$id].value / scale;
115
+ }
116
+ }
117
+ function divideValues(q, data, sampleType) {
118
+ const overlayTerm = q.divideTw;
119
+ const useLog = q.unit == "log";
120
+ const { absMax, absMin, key2values, uncomputableValues } = parseValues(q, data, sampleType, useLog, overlayTerm);
121
+ return {
122
+ key2values,
123
+ min: absMin,
124
+ max: absMax,
125
+ uncomputableValueObj: sortObj(uncomputableValues)
126
+ };
127
+ }
128
+ function sortObj(object) {
129
+ return Object.fromEntries(Object.entries(object).sort(([, a], [, b]) => a - b));
130
+ }
131
+ function sortKey2values(data, key2values, overlayTerm) {
132
+ const orderedLabels = overlayTerm?.$id ? data.refs.byTermId[overlayTerm.$id]?.keyOrder : void 0;
133
+ key2values = new Map(
134
+ [...key2values].sort(
135
+ 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 })
136
+ )
137
+ );
138
+ return key2values;
139
+ }
140
+ function setResponse(valuesObject, data, q, sampleType) {
141
+ const overlayTerm = q.divideTw;
142
+ const plots = [];
143
+ for (const [key, values] of sortKey2values(data, valuesObject.key2values, overlayTerm)) {
144
+ if (overlayTerm) {
145
+ plots.push({
146
+ label: overlayTerm?.term?.values?.[key]?.label || key,
147
+ values,
148
+ seriesId: key,
149
+ plotValueCount: values?.length,
150
+ color: overlayTerm?.term?.values?.[key]?.color || null,
151
+ divideTwBins: isNumericTerm(overlayTerm.term) ? numericBins(overlayTerm, data) : null,
152
+ uncomputableValueObj: Object.keys(valuesObject.uncomputableValueObj).length > 0 ? valuesObject.uncomputableValueObj : null
153
+ });
154
+ } else {
155
+ const plot = {
156
+ label: sampleType,
157
+ values,
158
+ plotValueCount: values.length
159
+ };
160
+ plots.push(plot);
161
+ }
162
+ }
163
+ const result = {
164
+ min: valuesObject.min,
165
+ max: valuesObject.max,
166
+ plots,
167
+ pvalues: [],
168
+ uncomputableValueObj: Object.keys(valuesObject.uncomputableValueObj).length > 0 ? valuesObject.uncomputableValueObj : null
169
+ };
170
+ return result;
171
+ }
172
+ function createCanvasImg(q, result, ds) {
173
+ if (!q.radius)
174
+ q.radius = 5;
175
+ if (q.radius <= 0)
176
+ throw "q.radius is not a number";
177
+ else
178
+ q.radius = +q.radius;
179
+ if (!q.strokeWidth)
180
+ q.strokeWidth = 0.2;
181
+ const refSize = q.radius * 4;
182
+ let axisScale;
183
+ const useLog = q.unit == "log";
184
+ if (useLog) {
185
+ axisScale = scaleLog().base(ds.cohort.termdb.logscaleBase2 ? 2 : 10).domain([result.min, result.max]).range(q.orientation === "horizontal" ? [0, q.svgw] : [q.svgw, 0]);
186
+ } else {
187
+ axisScale = scaleLinear().domain([result.min, result.max]).range(q.orientation === "horizontal" ? [0, q.svgw] : [q.svgw, 0]);
188
+ }
189
+ const [width, height] = q.orientation == "horizontal" ? [q.svgw * q.devicePixelRatio, refSize * q.devicePixelRatio] : [refSize * q.devicePixelRatio, q.svgw * q.devicePixelRatio];
190
+ const scaledRadius = q.radius / q.devicePixelRatio;
191
+ const arcEndAngle = scaledRadius * Math.PI;
192
+ for (const plot of result.plots) {
193
+ const canvas = createCanvas(width, height);
194
+ const ctx = canvas.getContext("2d");
195
+ ctx.strokeStyle = "rgba(0,0,0,0.8)";
196
+ ctx.lineWidth = q.strokeWidth / q.devicePixelRatio;
197
+ ctx.globalAlpha = 0.5;
198
+ ctx.fillStyle = "#ffe6e6";
199
+ if (q.devicePixelRatio != 1) {
200
+ ctx.scale(q.devicePixelRatio, q.devicePixelRatio);
201
+ }
202
+ if (q.datasymbol === "rug")
203
+ plot.values.forEach((i) => {
204
+ ctx.beginPath();
205
+ if (q.orientation == "horizontal") {
206
+ ctx.moveTo(+axisScale(i), 0);
207
+ ctx.lineTo(+axisScale(i), scaledRadius * 2);
208
+ } else {
209
+ ctx.moveTo(0, +axisScale(i));
210
+ ctx.lineTo(scaledRadius * 2, +axisScale(i));
211
+ }
212
+ ctx.stroke();
213
+ });
214
+ else if (q.datasymbol === "bean")
215
+ plot.values.forEach((i) => {
216
+ ctx.beginPath();
217
+ if (q.orientation === "horizontal")
218
+ ctx.arc(+axisScale(i), q.radius, scaledRadius, 0, arcEndAngle);
219
+ else
220
+ ctx.arc(q.radius, +axisScale(i), scaledRadius, 0, arcEndAngle);
221
+ ctx.fill();
222
+ ctx.stroke();
223
+ });
224
+ plot.src = canvas.toDataURL();
225
+ const isKDE = q.isKDE;
226
+ plot.density = getBinsDensity(axisScale, plot, isKDE, q.ticks);
227
+ plot.summaryStats = summaryStats(plot.values);
228
+ delete plot.values;
229
+ }
230
+ }
36
231
  export {
37
- api
232
+ api,
233
+ getWilcoxonData,
234
+ sortKey2values,
235
+ trigger_getViolinPlotData
38
236
  };