@sjcrh/proteinpaint-server 2.191.6 → 2.193.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/dataset/termdb.test.js +3 -0
- package/genome/hg38.base.js +8 -0
- package/package.json +5 -5
- package/routes/termdb.proteome.js +24 -3
- package/routes/termdb.violinBox.js +1 -1
- package/src/app.js +6579 -5381
- package/src/serverconfig.js +0 -2
- package/routes/gdc.grin2.list.js +0 -238
- package/routes/gdc.grin2.run.js +0 -127
- package/routes/termdb.descrstats.js +0 -115
- package/routes/termdb.sampleScatter.js +0 -434
- package/routes/termdb.singleCellPlots.js +0 -159
- package/routes/termdb.singlecellSamples.js +0 -312
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
import { getData } from "#src/termdb.matrix.js";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import serverconfig from "#src/serverconfig.js";
|
|
4
|
-
import { schemeCategory20, getColors, mclass, dt2label, morigin, isNumericTerm } from "#shared";
|
|
5
|
-
import { authApi } from "#src/auth.js";
|
|
6
|
-
import { run_R } from "@sjcrh/proteinpaint-r";
|
|
7
|
-
import { read_file } from "#src/utils.js";
|
|
8
|
-
import { getDescrStats } from "./termdb.descrstats.ts";
|
|
9
|
-
const refColor = "#F5F5DC";
|
|
10
|
-
function init({ genomes }) {
|
|
11
|
-
return async function(req, res) {
|
|
12
|
-
try {
|
|
13
|
-
const q = req.query;
|
|
14
|
-
if (!q.genome || !q.dslabel) {
|
|
15
|
-
throw new Error("Genome and dataset label are required for termdb/sampleScatter request.");
|
|
16
|
-
}
|
|
17
|
-
const g = genomes[q.genome];
|
|
18
|
-
const ds = g.datasets[q.dslabel];
|
|
19
|
-
let refSamples = [], cohortSamples;
|
|
20
|
-
const terms = [];
|
|
21
|
-
if (q.colorTW) terms.push(q.colorTW);
|
|
22
|
-
if (q.shapeTW) terms.push(q.shapeTW);
|
|
23
|
-
if (q.divideByTW) terms.push(q.divideByTW);
|
|
24
|
-
if (q.scaleDotTW) terms.push(q.scaleDotTW);
|
|
25
|
-
if (q.coordTWs) for (const tw of q.coordTWs) terms.push(tw);
|
|
26
|
-
const data = await getData(
|
|
27
|
-
{ filter: q.filter, filter0: q.filter0, terms, __protected__: q.__protected__, __abortSignal: q.__abortSignal },
|
|
28
|
-
ds,
|
|
29
|
-
true
|
|
30
|
-
// FIXME 3rd arg hardcoded to true
|
|
31
|
-
);
|
|
32
|
-
if (data.error) throw new Error(data.error);
|
|
33
|
-
let result;
|
|
34
|
-
if (q.coordTWs && q.coordTWs.length > 0) {
|
|
35
|
-
const tmp = await getSampleCoordinatesByTerms(req, q, ds, data);
|
|
36
|
-
cohortSamples = tmp[0];
|
|
37
|
-
} else {
|
|
38
|
-
if (!q.plotName) throw new Error("Neither plot name or coordinates where provided");
|
|
39
|
-
if (typeof ds.cohort?.scatterplots?.get == "function") {
|
|
40
|
-
const allowed = ds.cohort.scatterplots.get(q.__protected__?.clientAuthResult);
|
|
41
|
-
if (!allowed?.find((i) => i.name == q.plotName)) throw new Error("No permission to display plot");
|
|
42
|
-
}
|
|
43
|
-
if (!Array.isArray(ds.cohort?.scatterplots?.plots)) throw new Error("not supported");
|
|
44
|
-
const plot = ds.cohort.scatterplots.plots.find((p) => p.name == q.plotName);
|
|
45
|
-
if (!plot) throw new Error(`plot not found with plotName ${q.plotName}`);
|
|
46
|
-
const tmp = await getSamples(ds, plot);
|
|
47
|
-
refSamples = tmp[0];
|
|
48
|
-
cohortSamples = tmp[1];
|
|
49
|
-
if (q.colorColumn) {
|
|
50
|
-
let categories = new Set(refSamples.map((s) => s.category));
|
|
51
|
-
categories = Array.from(categories);
|
|
52
|
-
const colorMap = {};
|
|
53
|
-
const k2c = getColors(categories.length);
|
|
54
|
-
for (const category of categories) {
|
|
55
|
-
const color = q.colorColumn.colorMap?.[category] || k2c(category);
|
|
56
|
-
colorMap[category] = {
|
|
57
|
-
sampleCount: refSamples.filter((s) => s.category == category).length,
|
|
58
|
-
color,
|
|
59
|
-
key: category
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
const shapeMap = { Ref: { shape: 0, sampleCount: refSamples.length, key: "Ref" } };
|
|
63
|
-
result = {
|
|
64
|
-
Default: {
|
|
65
|
-
samples: refSamples,
|
|
66
|
-
colorLegend: Object.entries(colorMap),
|
|
67
|
-
shapeLegend: Object.entries(shapeMap)
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const samples = [...cohortSamples, ...refSamples];
|
|
73
|
-
let range;
|
|
74
|
-
if (samples.length > 0) {
|
|
75
|
-
if (q.excludeOutliers) {
|
|
76
|
-
const ystats = getDescrStats(
|
|
77
|
-
samples.map((s) => s.y),
|
|
78
|
-
q.excludeOutliers
|
|
79
|
-
);
|
|
80
|
-
cohortSamples = cohortSamples.filter(
|
|
81
|
-
(sample) => sample.y > ystats.outlierMin.value && sample.y < ystats.outlierMax.value
|
|
82
|
-
);
|
|
83
|
-
refSamples = refSamples.filter(
|
|
84
|
-
(sample) => sample.y > ystats.outlierMin.value && sample.y < ystats.outlierMax.value
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
const s0 = samples[0];
|
|
88
|
-
const [xMin, xMax, yMin, yMax] = samples.reduce(
|
|
89
|
-
(s, d) => [
|
|
90
|
-
d.x < s[0] ? d.x : s[0],
|
|
91
|
-
d.x > s[1] ? d.x : s[1],
|
|
92
|
-
d.y < s[2] ? d.y : s[2],
|
|
93
|
-
d.y > s[3] ? d.y : s[3]
|
|
94
|
-
],
|
|
95
|
-
[s0.x, s0.x, s0.y, s0.y]
|
|
96
|
-
);
|
|
97
|
-
range = { xMin, xMax, yMin, yMax };
|
|
98
|
-
}
|
|
99
|
-
if (!result) result = await colorAndShapeSamples(refSamples, cohortSamples, data, q);
|
|
100
|
-
res.send({ result, range });
|
|
101
|
-
} catch (e) {
|
|
102
|
-
if (e.stack) console.log(e.stack);
|
|
103
|
-
res.send({ error: e.message || e });
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
async function getSamples(ds, plot) {
|
|
108
|
-
if (!plot.filterableSamples) await loadFile(plot, ds);
|
|
109
|
-
return [readSamples(plot.referenceSamples), readSamples(plot.filterableSamples)];
|
|
110
|
-
function readSamples(samples) {
|
|
111
|
-
const result = [];
|
|
112
|
-
for (const i of JSON.parse(JSON.stringify(samples))) {
|
|
113
|
-
result.push(i);
|
|
114
|
-
}
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async function colorAndShapeSamples(refSamples, cohortSamples, data, q) {
|
|
119
|
-
const results = {};
|
|
120
|
-
let fCount = 0;
|
|
121
|
-
const hasTerms = Object.keys(data.samples).length > 0;
|
|
122
|
-
for (const sample of cohortSamples) {
|
|
123
|
-
const dbSample = data.samples[sample.sampleId.toString()];
|
|
124
|
-
if (!dbSample && hasTerms) {
|
|
125
|
-
fCount++;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (q.colorTW && !hasValue(dbSample, q.colorTW) || q.shapeTW && !hasValue(dbSample, q.shapeTW)) continue;
|
|
129
|
-
let divideBy = "Default";
|
|
130
|
-
if (q.divideByTW) {
|
|
131
|
-
sample.z = 0;
|
|
132
|
-
if (q.divideByTW.term.type == "geneVariant" && q.divideByTW.q.type == "values") {
|
|
133
|
-
divideBy = getMutation(true, dbSample, q.divideByTW);
|
|
134
|
-
if (divideBy == null) {
|
|
135
|
-
divideBy = getMutation(false, dbSample, q.divideByTW);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
const field = q.divideByTW.$id;
|
|
139
|
-
const key = dbSample[field]?.key;
|
|
140
|
-
if (key == null) continue;
|
|
141
|
-
if (q.divideByTW.q.mode != "continuous") divideBy = q.divideByTW.term.values?.[key]?.label || key;
|
|
142
|
-
else sample.z = key;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (divideBy == null || divideBy == void 0) {
|
|
146
|
-
console.log("divideBy is null/undefined for sample " + JSON.stringify(sample));
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (!results[divideBy]) {
|
|
150
|
-
const samples = refSamples.map((sample2) => ({ ...sample2, category: "Ref", shape: "Ref", z: 0 }));
|
|
151
|
-
results[divideBy] = { samples, colorMap: {}, shapeMap: {} };
|
|
152
|
-
}
|
|
153
|
-
if (!q.divideByTW) sample.z = 0;
|
|
154
|
-
if (!q.scaleDotTW) sample.scale = 1;
|
|
155
|
-
else {
|
|
156
|
-
const value = dbSample?.[q.scaleDotTW.$id]?.key;
|
|
157
|
-
if (!value || !isComputable(q.scaleDotTW.term, value)) continue;
|
|
158
|
-
sample.scale = value;
|
|
159
|
-
}
|
|
160
|
-
sample.cat_info = {};
|
|
161
|
-
sample.hidden = {};
|
|
162
|
-
if (!q.colorTW) {
|
|
163
|
-
sample.category = "Default";
|
|
164
|
-
} else {
|
|
165
|
-
if (q.colorTW?.q?.mode === "continuous") {
|
|
166
|
-
if (dbSample) sample.category = dbSample[q.colorTW.$id].value;
|
|
167
|
-
} else processSample(dbSample, sample, q.colorTW, results[divideBy].colorMap, "category");
|
|
168
|
-
}
|
|
169
|
-
if (q.shapeTW) processSample(dbSample, sample, q.shapeTW, results[divideBy].shapeMap, "shape");
|
|
170
|
-
else sample.shape = "Ref";
|
|
171
|
-
results[divideBy].samples.push(sample);
|
|
172
|
-
}
|
|
173
|
-
if (fCount) console.log(fCount + " samples not in the database or filtered");
|
|
174
|
-
let max = 0;
|
|
175
|
-
for (const [_, result] of Object.entries(results)) max = Math.max(max, Object.keys(result.colorMap).length);
|
|
176
|
-
const k2c = getColors(max);
|
|
177
|
-
const scheme = schemeCategory20;
|
|
178
|
-
for (const [_, result] of Object.entries(results)) {
|
|
179
|
-
if (q.colorTW && q.colorTW.q.mode !== "continuous") {
|
|
180
|
-
let i2 = 20;
|
|
181
|
-
for (const [category, value] of Object.entries(result.colorMap)) {
|
|
182
|
-
delete value["sampleIds"];
|
|
183
|
-
let tvalue;
|
|
184
|
-
if (q.colorTW.term.values?.[value.key]) {
|
|
185
|
-
tvalue = q.colorTW.term.values?.[value.key];
|
|
186
|
-
}
|
|
187
|
-
if (tvalue && "color" in tvalue) {
|
|
188
|
-
value.color = tvalue.color;
|
|
189
|
-
} else if (isNumericTerm(q.colorTW.term)) {
|
|
190
|
-
const term = data.refs.byTermId[q.colorTW.$id] || data.refs.byTermId[q.colorTW.term.id];
|
|
191
|
-
const bins = term.bins;
|
|
192
|
-
const bin = bins.find((bin2) => bin2.label == category);
|
|
193
|
-
if (bin?.color) value.color = bin.color;
|
|
194
|
-
else {
|
|
195
|
-
value.color = scheme[i2 - 1];
|
|
196
|
-
i2--;
|
|
197
|
-
}
|
|
198
|
-
} else if (!(q.colorTW.term.type == "geneVariant" && q.colorTW.q.type == "values")) {
|
|
199
|
-
value.color = k2c(category);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
let i = 0;
|
|
204
|
-
const shapes = Object.entries(result.shapeMap).sort((a, b) => a[0].localeCompare(b[0]));
|
|
205
|
-
for (const [_2, value] of shapes) {
|
|
206
|
-
delete value["sampleIds"];
|
|
207
|
-
if ("shape" in value) continue;
|
|
208
|
-
if (q.shapeTW.term.values?.[value.key]?.shape != void 0)
|
|
209
|
-
value.shape = q.shapeTW.term.values?.[value.key].shape;
|
|
210
|
-
else value.shape = i;
|
|
211
|
-
i++;
|
|
212
|
-
}
|
|
213
|
-
result.colorLegend = q.colorTW ? order(result.colorMap, q.colorTW, data.refs) : [["Default", { sampleCount: cohortSamples.length, color: "blue", key: "Default" }]];
|
|
214
|
-
result.colorLegend.push([
|
|
215
|
-
"Ref",
|
|
216
|
-
{
|
|
217
|
-
sampleCount: refSamples.length,
|
|
218
|
-
color: q.colorTW?.term.values?.["Ref"] ? q.colorTW.term.values?.["Ref"].color : refColor,
|
|
219
|
-
key: "Ref"
|
|
220
|
-
}
|
|
221
|
-
]);
|
|
222
|
-
result.shapeLegend = shapes;
|
|
223
|
-
result.shapeLegend.push(["Ref", { sampleCount: refSamples.length, shape: 0, key: "Ref" }]);
|
|
224
|
-
}
|
|
225
|
-
return results;
|
|
226
|
-
}
|
|
227
|
-
function hasValue(dbSample, tw) {
|
|
228
|
-
const key = tw && tw.$id !== void 0 ? dbSample?.[tw.$id]?.key : void 0;
|
|
229
|
-
const hasKey = key !== void 0;
|
|
230
|
-
return hasKey;
|
|
231
|
-
}
|
|
232
|
-
function processSample(dbSample, sample, tw, categoryMap, category) {
|
|
233
|
-
let value = null;
|
|
234
|
-
if (tw.term.type == "geneVariant" && tw.q["type"] == "values")
|
|
235
|
-
assignGeneVariantValue(dbSample, sample, tw, categoryMap, category);
|
|
236
|
-
else {
|
|
237
|
-
value = dbSample?.[tw.$id]?.key;
|
|
238
|
-
if (value == null) return;
|
|
239
|
-
if (tw.term.values?.[value]?.label) {
|
|
240
|
-
value = tw.term.values?.[value]?.label;
|
|
241
|
-
sample.hidden[category] = tw.q.hiddenValues ? value in tw.q.hiddenValues : false;
|
|
242
|
-
} else sample.hidden[category] = tw.q.hiddenValues ? dbSample?.[tw.$id]?.key in tw.q.hiddenValues : false;
|
|
243
|
-
if (value) {
|
|
244
|
-
sample[category] = value.toString();
|
|
245
|
-
if (categoryMap[value] == void 0) categoryMap[value] = { sampleCount: 1, key: dbSample?.[tw.$id]?.key };
|
|
246
|
-
else categoryMap[value].sampleCount++;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
function assignGeneVariantValue(dbSample, sample, tw, categoryMap, category) {
|
|
251
|
-
if (tw.term.type == "geneVariant") {
|
|
252
|
-
const mutations = dbSample?.[tw.$id]?.values;
|
|
253
|
-
sample.cat_info[category] = [];
|
|
254
|
-
for (const mutation of mutations) {
|
|
255
|
-
const class_info = mclass[mutation.class];
|
|
256
|
-
const value = getCategory(mutation);
|
|
257
|
-
sample.cat_info[category].push(mutation);
|
|
258
|
-
let mapValue;
|
|
259
|
-
if (categoryMap[value] == void 0) {
|
|
260
|
-
const sampleIds = /* @__PURE__ */ new Set();
|
|
261
|
-
sampleIds.add(dbSample.sample);
|
|
262
|
-
mapValue = { color: class_info.color, sampleCount: 1, mutation, key: value, sampleIds };
|
|
263
|
-
categoryMap[value] = mapValue;
|
|
264
|
-
} else {
|
|
265
|
-
mapValue = categoryMap[value];
|
|
266
|
-
mapValue.sampleIds.add(dbSample.sample);
|
|
267
|
-
mapValue.sampleCount = mapValue.sampleIds.size;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
sample[category] = getMutation(true, dbSample, tw) || getMutation(false, dbSample, tw);
|
|
271
|
-
if (!sample[category]) sample[category] = getCategory(mutations[0]);
|
|
272
|
-
sample.hidden[category] = tw.q.hiddenValues ? sample[category] in tw.q.hiddenValues : false;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
function getMutation(strict, dbSample, tw) {
|
|
276
|
-
const mutations = dbSample?.[tw.$id]?.values;
|
|
277
|
-
for (const [dt, _] of Object.entries(dt2label)) {
|
|
278
|
-
const mutation = mutations.find((mutation2) => {
|
|
279
|
-
const value2 = getCategory(mutation2);
|
|
280
|
-
const visible = !(tw.q.hiddenValues && value2 in tw.q.hiddenValues);
|
|
281
|
-
return mutation2.dt == dt && visible;
|
|
282
|
-
});
|
|
283
|
-
if (!mutation) continue;
|
|
284
|
-
const notImportant = mutation.class == "WT" || mutation.class == "Blank";
|
|
285
|
-
if (strict && notImportant) continue;
|
|
286
|
-
const value = getCategory(mutation);
|
|
287
|
-
return value;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function getCategory(mutation) {
|
|
291
|
-
const dt = mutation.dt;
|
|
292
|
-
const class_info = mclass[mutation.class];
|
|
293
|
-
const origin = morigin[mutation.origin]?.label;
|
|
294
|
-
const dtlabel = origin ? `${origin[0]} ${dt2label[dt]}` : dt2label[dt];
|
|
295
|
-
return `${class_info.label}, ${dtlabel}`;
|
|
296
|
-
}
|
|
297
|
-
function order(map, tw, refs) {
|
|
298
|
-
const hasOrder = tw?.term?.values ? Object.keys(tw.term.values).some((key) => tw.term.values[key].order != void 0) : false;
|
|
299
|
-
let entries = [];
|
|
300
|
-
if (!tw || map.size == 0) return entries;
|
|
301
|
-
entries = Object.entries(map);
|
|
302
|
-
const term = refs?.byTermId[tw.$id] || refs?.byTermId[tw.term.id];
|
|
303
|
-
if (hasOrder) {
|
|
304
|
-
entries.sort((a, b) => {
|
|
305
|
-
let v1, v2;
|
|
306
|
-
for (const key in tw.term.values) {
|
|
307
|
-
const value = tw.term.values[key];
|
|
308
|
-
if (value.label && a[0] == value.label) v1 = value;
|
|
309
|
-
else if (key == a[0]) v1 = value;
|
|
310
|
-
if (value.label && b[0] == value.label) v2 = value;
|
|
311
|
-
else if (key == b[0]) v2 = value;
|
|
312
|
-
}
|
|
313
|
-
if (v1?.order < v2?.order) return -1;
|
|
314
|
-
else if (v1?.order > v2?.order) return 1;
|
|
315
|
-
else if (v1 > v2) return 1;
|
|
316
|
-
else if (v1 < v2) return -1;
|
|
317
|
-
return 0;
|
|
318
|
-
});
|
|
319
|
-
} else if (term?.bins) {
|
|
320
|
-
const bins = term.bins;
|
|
321
|
-
entries.sort((a, b) => {
|
|
322
|
-
const binA = bins.findIndex((bin) => bin.label == a[0]);
|
|
323
|
-
const binB = bins.findIndex((bin) => bin.label == b[0]);
|
|
324
|
-
if (binA == -1) return 1;
|
|
325
|
-
if (binB == -1) return -1;
|
|
326
|
-
return binA - binB;
|
|
327
|
-
});
|
|
328
|
-
} else {
|
|
329
|
-
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
330
|
-
}
|
|
331
|
-
return entries;
|
|
332
|
-
}
|
|
333
|
-
async function getSampleCoordinatesByTerms(req, q, ds, data) {
|
|
334
|
-
if (!q.coordTWs || q.coordTWs.length == 0) return [[], data];
|
|
335
|
-
const canDisplay = authApi.canDisplaySampleIds(req, ds);
|
|
336
|
-
const samples = [];
|
|
337
|
-
for (const sampleId in data.samples) {
|
|
338
|
-
const values = data.samples[sampleId];
|
|
339
|
-
const x = values[q.coordTWs[0].$id]?.value;
|
|
340
|
-
let y = values[q.coordTWs[1]?.$id]?.value;
|
|
341
|
-
if (y === void 0 && (q.chartType == "runchart" || q.chartType == "frequencyChart")) y = 0;
|
|
342
|
-
const z = q.divideByTW ? values[q.divideByTW?.$id]?.value : 0;
|
|
343
|
-
if (x == void 0 || y == void 0 || z == void 0) continue;
|
|
344
|
-
if (!isComputable(q.coordTWs[0].term, x) || !isComputable(q.coordTWs[1]?.term, y) || !isComputable(q.divideByTW?.term, z)) {
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
const sample = { sampleId, x: Number(x), y: Number(y), z: Number(z) };
|
|
348
|
-
if (canDisplay) {
|
|
349
|
-
sample.sample = data.refs.bySampleId[sampleId]?.label || sampleId;
|
|
350
|
-
}
|
|
351
|
-
samples.push(sample);
|
|
352
|
-
}
|
|
353
|
-
return [samples, data];
|
|
354
|
-
}
|
|
355
|
-
function isComputable(term, value) {
|
|
356
|
-
if (!term) return true;
|
|
357
|
-
return !term.values?.[value]?.uncomputable;
|
|
358
|
-
}
|
|
359
|
-
async function loadFile(p, ds) {
|
|
360
|
-
const lines = (await read_file(path.join(serverconfig.tpmasterdir, p.file))).trim().split("\n");
|
|
361
|
-
const xColumn = p.coordsColumns?.x || 1;
|
|
362
|
-
const yColumn = p.coordsColumns?.y || 2;
|
|
363
|
-
const headerFields = lines[0].split(" ");
|
|
364
|
-
p.filterableSamples = [];
|
|
365
|
-
p.referenceSamples = [];
|
|
366
|
-
let invalidXY = 0;
|
|
367
|
-
for (let i = 1; i < lines.length; i++) {
|
|
368
|
-
const l = lines[i].trim().split(" ");
|
|
369
|
-
const x = Number(l[xColumn]), y = Number(l[yColumn]);
|
|
370
|
-
if (Number.isNaN(x) || Number.isNaN(y)) {
|
|
371
|
-
invalidXY++;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const sample = { sample: l[0], x, y };
|
|
375
|
-
if (p.colorColumn) {
|
|
376
|
-
sample["sampleId"] = l[0];
|
|
377
|
-
sample.category = l[p.colorColumn.index];
|
|
378
|
-
sample.shape = "Ref";
|
|
379
|
-
sample.z = 0;
|
|
380
|
-
}
|
|
381
|
-
const id = ds.cohort.termdb.q.sampleName2id(l[0]);
|
|
382
|
-
if (id == void 0) {
|
|
383
|
-
if (headerFields[3]) {
|
|
384
|
-
sample.info = {};
|
|
385
|
-
for (let j = 3; j < headerFields.length; j++) {
|
|
386
|
-
sample.info[headerFields[j]] = l[j];
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
p.referenceSamples.push(sample);
|
|
390
|
-
} else {
|
|
391
|
-
sample["sampleId"] = id;
|
|
392
|
-
p.filterableSamples.push(sample);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
console.log(
|
|
396
|
-
p.name + " (prebuilt scatter):",
|
|
397
|
-
p.filterableSamples.length,
|
|
398
|
-
"lines,",
|
|
399
|
-
p.referenceSamples.length,
|
|
400
|
-
"reference cases,",
|
|
401
|
-
invalidXY,
|
|
402
|
-
"lines with invalid X/Y values"
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
async function mayInitiateScatterplots(ds) {
|
|
406
|
-
if (ds.scatterplots) {
|
|
407
|
-
ds.cohort.scatterplots = ds.scatterplots;
|
|
408
|
-
delete ds.scatterplots;
|
|
409
|
-
}
|
|
410
|
-
if (!ds.cohort.scatterplots) return;
|
|
411
|
-
if (typeof ds.cohort.scatterplots.get == "function") {
|
|
412
|
-
}
|
|
413
|
-
if (!Array.isArray(ds.cohort.scatterplots.plots)) throw new Error("cohort.scatterplots.plots is not array");
|
|
414
|
-
for (const p of ds.cohort.scatterplots.plots) {
|
|
415
|
-
if (!p.name) throw new Error(".name missing from one of scatterplots.plots[]");
|
|
416
|
-
if (p.file) {
|
|
417
|
-
} else {
|
|
418
|
-
throw new Error("unknown data source of one of scatterplots.plots[]");
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
async function trigger_getLowessCurve(q, res) {
|
|
423
|
-
const data = q.coords;
|
|
424
|
-
const result = JSON.parse(await run_R("lowess.R", JSON.stringify(data)));
|
|
425
|
-
const lowessCurve = [];
|
|
426
|
-
for (const [i, x] of Object.entries(result.x)) lowessCurve.push([x, result.y[i]]);
|
|
427
|
-
return res.send(lowessCurve);
|
|
428
|
-
}
|
|
429
|
-
export {
|
|
430
|
-
init,
|
|
431
|
-
mayInitiateScatterplots,
|
|
432
|
-
refColor,
|
|
433
|
-
trigger_getLowessCurve
|
|
434
|
-
};
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { getColors, getCoordinate, calculatePadding, xAxisOffSet, yAxisOffSet } from "#shared";
|
|
2
|
-
import { isSingleCellTerm, SINGLECELL_GENE_EXPRESSION, SINGLECELL_CELLTYPE } from "#shared/terms.js";
|
|
3
|
-
import { createCanvas } from "canvas";
|
|
4
|
-
import { scaleLinear } from "d3-scale";
|
|
5
|
-
import { rgb } from "d3-color";
|
|
6
|
-
import { refColor } from "./termdb.sampleScatter.js";
|
|
7
|
-
function init({ genomes }) {
|
|
8
|
-
return async function(req, res) {
|
|
9
|
-
try {
|
|
10
|
-
const q = req.query;
|
|
11
|
-
if (!q.genome || !q.dslabel) {
|
|
12
|
-
throw new Error("Genome and dataset label are required for termdb/singleCellPlots request.");
|
|
13
|
-
}
|
|
14
|
-
const g = genomes[q.genome];
|
|
15
|
-
if (!g) throw new Error("Invalid genome name");
|
|
16
|
-
const ds = g.datasets[q.dslabel];
|
|
17
|
-
if (!ds) throw new Error("Invalid dataset label");
|
|
18
|
-
if (!ds.queries?.singleCell) throw new Error("No single cell data on this dataset");
|
|
19
|
-
return getSingleCellScatter(req, res, ds);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error(err);
|
|
22
|
-
res.status(500).json({ error: err.message || String(err) });
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
async function getSingleCellScatter(req, res, ds) {
|
|
27
|
-
const q = req.query;
|
|
28
|
-
const { name, sample } = q.singleCellPlot;
|
|
29
|
-
try {
|
|
30
|
-
const tw = q.colorTW;
|
|
31
|
-
if (!tw || !isSingleCellTerm(tw.term))
|
|
32
|
-
throw new Error("colorTW must be provided and be a single cell term for single cell scatter plot");
|
|
33
|
-
const arg = { plots: [name], sample };
|
|
34
|
-
if (tw.term.type == SINGLECELL_GENE_EXPRESSION) arg.gene = tw.term.gene;
|
|
35
|
-
else if (tw.term.type == SINGLECELL_CELLTYPE) arg.colorBy = tw.term.name;
|
|
36
|
-
else throw new Error(`unsupported single cell term type: ${tw.term.type}`);
|
|
37
|
-
const data = await ds.queries.singleCell.data.get(arg);
|
|
38
|
-
const plot = data.plots[0];
|
|
39
|
-
const cells = [...plot.expCells, ...plot.noExpCells];
|
|
40
|
-
const groups = tw.q?.customset?.groups;
|
|
41
|
-
const cat2GrpName = /* @__PURE__ */ new Map();
|
|
42
|
-
if (groups) {
|
|
43
|
-
for (const group of groups) {
|
|
44
|
-
for (const value of Object.values(group.values)) {
|
|
45
|
-
cat2GrpName.set(value.key, group.name);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const samples = [];
|
|
50
|
-
const categoryCounts = /* @__PURE__ */ new Map();
|
|
51
|
-
let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, geMin = Infinity, geMax = -Infinity;
|
|
52
|
-
let totalCellCount = 0;
|
|
53
|
-
for (const cell of cells) {
|
|
54
|
-
let category = cell.category;
|
|
55
|
-
const groupName = cat2GrpName.get(category);
|
|
56
|
-
if (groupName !== void 0) category = groupName;
|
|
57
|
-
const isHidden = tw?.q?.hiddenValues ? category in tw.q.hiddenValues : false;
|
|
58
|
-
totalCellCount++;
|
|
59
|
-
categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
|
|
60
|
-
if (cell.x < xMin) xMin = cell.x;
|
|
61
|
-
if (cell.x > xMax) xMax = cell.x;
|
|
62
|
-
if (cell.y < yMin) yMin = cell.y;
|
|
63
|
-
if (cell.y > yMax) yMax = cell.y;
|
|
64
|
-
if (Number.isFinite(cell.geneExp) && cell.geneExp < geMin) geMin = cell.geneExp;
|
|
65
|
-
if (Number.isFinite(cell.geneExp) && cell.geneExp > geMax) geMax = cell.geneExp;
|
|
66
|
-
if (isHidden) continue;
|
|
67
|
-
samples.push({
|
|
68
|
-
sampleId: cell.cellId,
|
|
69
|
-
x: cell.x,
|
|
70
|
-
y: cell.y,
|
|
71
|
-
z: 0,
|
|
72
|
-
category,
|
|
73
|
-
shape: "Ref",
|
|
74
|
-
hidden: { category: false },
|
|
75
|
-
geneExp: cell.geneExp
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const colorMap = {};
|
|
79
|
-
if (tw.term.type == SINGLECELL_CELLTYPE) {
|
|
80
|
-
const defaultK2c = getColors(categoryCounts.size);
|
|
81
|
-
const dsTerm = ds.queries.singleCell?.terms ? ds.queries.singleCell.terms.find((t) => t.name == tw.term.name) : void 0;
|
|
82
|
-
for (const [category, count] of categoryCounts) {
|
|
83
|
-
const color = tw.term.values?.[category]?.color || dsTerm?.values?.[category]?.color || defaultK2c(category);
|
|
84
|
-
colorMap[category] = { sampleCount: count, color, key: category };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const shapeLegend = [["Ref", { sampleCount: totalCellCount, shape: 0, key: "Ref" }]];
|
|
88
|
-
const colorLegend = Object.entries(colorMap);
|
|
89
|
-
const output = {
|
|
90
|
-
range: { xMin, xMax, yMin, yMax, geMin, geMax },
|
|
91
|
-
//There should only be one chart
|
|
92
|
-
result: { Default: { colorLegend, shapeLegend } }
|
|
93
|
-
};
|
|
94
|
-
if (totalCellCount >= q.canvasSettings.cutoff) {
|
|
95
|
-
const { src, canvasWidth, canvasHeight } = await makeCanvas(
|
|
96
|
-
q,
|
|
97
|
-
samples,
|
|
98
|
-
colorMap,
|
|
99
|
-
{ xMin, xMax, yMin, yMax, geMin, geMax },
|
|
100
|
-
tw.term.type
|
|
101
|
-
);
|
|
102
|
-
output.result.Default.src = src;
|
|
103
|
-
output.result.Default.canvasWidth = canvasWidth;
|
|
104
|
-
output.result.Default.canvasHeight = canvasHeight;
|
|
105
|
-
output.result.Default.totalSampleCount = totalCellCount;
|
|
106
|
-
} else {
|
|
107
|
-
output.result.Default.samples = samples;
|
|
108
|
-
}
|
|
109
|
-
res.send(output);
|
|
110
|
-
} catch (e) {
|
|
111
|
-
console.log(e);
|
|
112
|
-
res.send({ error: e.message || e });
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async function makeCanvas(q, samples, colorMap, range, termType) {
|
|
116
|
-
const settings = q.canvasSettings;
|
|
117
|
-
const dpr = settings.devicePixelRatio || 1;
|
|
118
|
-
const extraSpaceX = calculatePadding(settings.minXScale, settings.maxXScale, range.xMin, range.xMax);
|
|
119
|
-
const extraSpaceY = calculatePadding(settings.minYScale, settings.maxYScale, range.yMin, range.yMax);
|
|
120
|
-
const width = settings.width + xAxisOffSet + extraSpaceX + 20;
|
|
121
|
-
const height = settings.height + yAxisOffSet + extraSpaceY + 20;
|
|
122
|
-
const canvas = createCanvas(width * dpr, height * dpr);
|
|
123
|
-
const ctx = canvas.getContext("2d");
|
|
124
|
-
if (dpr > 1) ctx.scale(dpr, dpr);
|
|
125
|
-
const xScale = scaleLinear().domain([range.xMin - extraSpaceX, range.xMax + extraSpaceX]).range([xAxisOffSet, settings.width + xAxisOffSet]);
|
|
126
|
-
const yScale = scaleLinear().domain([range.yMax + extraSpaceY, range.yMin - extraSpaceY]).range([yAxisOffSet, settings.height + yAxisOffSet]);
|
|
127
|
-
let colorGenerator;
|
|
128
|
-
if (Number.isFinite(range.geMin) && Number.isFinite(range.geMax)) {
|
|
129
|
-
colorGenerator = scaleLinear().domain([range.geMin, range.geMax]).range([settings.startColor, settings.stopColor]);
|
|
130
|
-
}
|
|
131
|
-
const color = (sample) => {
|
|
132
|
-
if (termType == SINGLECELL_GENE_EXPRESSION) {
|
|
133
|
-
if (!Number.isFinite(sample.geneExp)) return settings.startColor;
|
|
134
|
-
else if (sample.geneExp > range.geMax) return settings.stopColor;
|
|
135
|
-
else return colorGenerator(sample.geneExp);
|
|
136
|
-
}
|
|
137
|
-
return colorMap[sample.category] ? colorMap[sample.category].color : refColor;
|
|
138
|
-
};
|
|
139
|
-
const x = (sample) => {
|
|
140
|
-
const tmp = getCoordinate(sample.x, settings.minXScale, settings.maxXScale);
|
|
141
|
-
return xScale(tmp);
|
|
142
|
-
};
|
|
143
|
-
const y = (sample) => {
|
|
144
|
-
const tmp = getCoordinate(sample.y, settings.minYScale, settings.maxYScale);
|
|
145
|
-
return yScale(tmp);
|
|
146
|
-
};
|
|
147
|
-
for (const sample of samples) {
|
|
148
|
-
const c = rgb(color(sample));
|
|
149
|
-
c.opacity = settings.opacity;
|
|
150
|
-
ctx.fillStyle = c.toString();
|
|
151
|
-
ctx.beginPath();
|
|
152
|
-
ctx.arc(x(sample), y(sample), settings.radius, 0, Math.PI * 2);
|
|
153
|
-
ctx.fill();
|
|
154
|
-
}
|
|
155
|
-
return { src: canvas.toDataURL(), canvasWidth: width, canvasHeight: height };
|
|
156
|
-
}
|
|
157
|
-
export {
|
|
158
|
-
init
|
|
159
|
-
};
|