@sjcrh/proteinpaint-server 2.166.0 → 2.167.2
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/dataset/termdb.test.js +1 -14
- package/package.json +4 -4
- package/routes/termdb.boxplot.js +123 -65
- package/routes/termdb.chat.js +29 -3
- package/routes/termdb.cluster.js +2 -1
- package/routes/termdb.config.js +1 -0
- package/routes/termdb.violin.js +8 -8
- package/src/app.js +211 -157
package/dataset/termdb.test.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "2.167.2",
|
|
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.
|
|
68
|
-
"@sjcrh/proteinpaint-shared": "2.
|
|
69
|
-
"@sjcrh/proteinpaint-types": "2.
|
|
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",
|
package/routes/termdb.boxplot.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 = {};
|
package/routes/termdb.chat.js
CHANGED
|
@@ -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);
|
package/routes/termdb.cluster.js
CHANGED
|
@@ -105,7 +105,8 @@ async function getResult(q, ds) {
|
|
|
105
105
|
}
|
|
106
106
|
async function getNumericDictTermAnnotation(q, ds) {
|
|
107
107
|
const getDataArgs = {
|
|
108
|
-
|
|
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__
|
package/routes/termdb.config.js
CHANGED
|
@@ -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);
|
package/routes/termdb.violin.js
CHANGED
|
@@ -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)) {
|