@sjcrh/proteinpaint-server 2.196.0 → 2.197.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.
@@ -1,543 +0,0 @@
1
- import { scaleLinear, scaleLog } from "d3";
2
- import { run_rust } from "@sjcrh/proteinpaint-rust";
3
- import { run_R } from "@sjcrh/proteinpaint-r";
4
- import { getData } from "../src/termdb.matrix.js";
5
- import { createCanvas } from "canvas";
6
- import { getOrderedLabels } from "../src/termdb.barchart.js";
7
- import { getDescrStats, getStdDev, getMean } from "#routes/termdb.descrstats.ts";
8
- import { isNumericTerm } from "#shared/terms.js";
9
- import { boxplot_getvalue } from "../src/utils.js";
10
- import { roundValueAuto } from "#shared/roundValue.js";
11
- import { mayLog } from "#src/helpers.ts";
12
- function init({ genomes }) {
13
- return async (req, res) => {
14
- const q = req.query;
15
- let result;
16
- try {
17
- const g = genomes[q.genome];
18
- if (!g) throw new Error("invalid genome name");
19
- const ds = g.datasets?.[q.dslabel];
20
- if (!ds) throw new Error("invalid ds");
21
- if (typeof q.tw?.term != "object" || typeof q.tw?.q != "object") throw new Error("q.tw not of {term,q}");
22
- const term = q.tw.term;
23
- if (!isNumericTerm(term) && term.type !== "survival" && term.type !== "termCollection")
24
- throw new Error("term type is not numeric, survival, or termCollection");
25
- const arg = {
26
- terms: [q.tw],
27
- filter: q.filter,
28
- filter0: q.filter0,
29
- currentGeneNames: q.currentGeneNames,
30
- __protected__: q.__protected__,
31
- __abortSignal: q.__abortSignal
32
- };
33
- if (q.overlayTw) arg.terms.push(q.overlayTw);
34
- if (q.divideTw) arg.terms.push(q.divideTw);
35
- const data = await getData(arg, ds);
36
- if (!data) throw new Error("getData() returns nothing");
37
- if (data.error) throw new Error(data.error);
38
- if (q.tw.term.type === "termCollection") expandNumericTermCollection(q, data);
39
- if (q.plotType === "violin") {
40
- result = await getViolin(q, data, ds);
41
- } else if (q.plotType === "box") {
42
- result = await getBoxPlot(q, data);
43
- } else {
44
- throw new Error("invalid plotType");
45
- }
46
- } catch (e) {
47
- result = { error: e?.message || e };
48
- if (e instanceof Error && e.stack) console.log(e);
49
- }
50
- res.send(result);
51
- };
52
- }
53
- function expandNumericTermCollection(q, data) {
54
- const term = q.tw.term;
55
- if (term.memberType !== "numeric") throw new Error("only numeric termCollection is supported for violinBox");
56
- if (q.overlayTw)
57
- throw new Error("overlayTw is not supported with numeric termCollection; member terms are used as the overlay");
58
- if (q.divideTw) throw new Error("divideTw is not supported with numeric termCollection");
59
- const termlst = term.termlst || [];
60
- mayLog(
61
- `Expanding numeric termCollection with ${termlst.length} member terms and ${Object.keys(data.samples).length} samples`
62
- );
63
- const propsByTermId = term.propsByTermId || {};
64
- const tcId = q.tw.$id;
65
- const memberNameById = {};
66
- const overlayValues = {};
67
- for (const mt of termlst) {
68
- const name = mt.name || mt.id;
69
- memberNameById[mt.id] = name;
70
- overlayValues[name] = { label: name, color: propsByTermId[mt.id]?.color };
71
- }
72
- const newSamples = {};
73
- for (const [sampleId, sampleData] of Object.entries(data.samples)) {
74
- const tcEntry = sampleData[tcId];
75
- const memberValues = tcEntry?.value;
76
- if (!memberValues || typeof memberValues !== "object") continue;
77
- for (const [memberId, memberVal] of Object.entries(memberValues)) {
78
- if (typeof memberVal !== "number" || !Number.isFinite(memberVal)) continue;
79
- const memberName = memberNameById[memberId] || memberId;
80
- newSamples[`${sampleId}__${memberId}`] = {
81
- ...sampleData,
82
- [tcId]: { key: memberVal, value: memberVal },
83
- ["__tcOverlay"]: { key: memberName, value: memberName }
84
- };
85
- }
86
- }
87
- data.samples = newSamples;
88
- q.overlayTw = {
89
- $id: "__tcOverlay",
90
- term: { type: "categorical", values: overlayValues, name: term.name },
91
- q: {}
92
- };
93
- data.refs.byTermId["__tcOverlay"] = {
94
- keyOrder: termlst.map((t) => t.name || t.id)
95
- };
96
- }
97
- async function getViolin(q, data, ds) {
98
- const samples = Object.values(data.samples);
99
- const values = extractNumericValues(samples, q.tw, q.isLogScale);
100
- const descrStats = getDescrStats(values);
101
- const sampleType = computeSampleType(data);
102
- if (q.overlayTw && data.refs.byTermId[q.overlayTw.$id]) {
103
- ;
104
- data.refs.byTermId[q.overlayTw.$id].orderedLabels = getOrderedLabels(
105
- q.overlayTw.term,
106
- data.refs.byTermId[q.overlayTw.$id]?.bins || [],
107
- void 0,
108
- q.overlayTw.q
109
- );
110
- }
111
- if (q.scale) setScaleData(q, data, q.tw);
112
- const valuesObject = divideValues(q, data, sampleType);
113
- const result = setViolinResponse(valuesObject, data, q);
114
- if (q.overlayTw) await getViolinWilcoxonData(result);
115
- await createCanvasImg(q, result, ds);
116
- result["descrStats"] = descrStats;
117
- return result;
118
- }
119
- function setScaleData(q, data, tw) {
120
- if (!q.scale) return;
121
- const scale = Number(q.scale);
122
- for (const val of Object.values(data.samples)) {
123
- if (!tw.$id || !val[tw.$id]) continue;
124
- if (tw.term.values?.[val[tw.$id]?.value]?.uncomputable) continue;
125
- val[tw.$id].key = val[tw.$id].key / scale;
126
- val[tw.$id].value = val[tw.$id].value / scale;
127
- }
128
- }
129
- function divideValues(q, data, sampleType) {
130
- const overlayTerm = q.overlayTw;
131
- const divideTerm = q.divideTw;
132
- const useLog = q.isLogScale;
133
- const { absMax, absMin, chart2plot2values, uncomputableValues } = parseValues(
134
- q,
135
- data,
136
- sampleType,
137
- !!useLog,
138
- overlayTerm,
139
- divideTerm
140
- );
141
- return {
142
- chart2plot2values,
143
- min: absMin,
144
- max: absMax,
145
- uncomputableValues: sortObj(uncomputableValues)
146
- };
147
- }
148
- function sortObj(object) {
149
- return Object.fromEntries(Object.entries(object).sort(([, a], [, b]) => a - b));
150
- }
151
- function sortPlot2Values(data, plot2values, overlayTerm) {
152
- const orderedLabels = overlayTerm?.$id ? data.refs.byTermId[overlayTerm.$id]?.keyOrder : void 0;
153
- plot2values = new Map(
154
- [...plot2values].sort(
155
- 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 })
156
- )
157
- );
158
- return plot2values;
159
- }
160
- function setViolinResponse(valuesObject, data, q) {
161
- const charts = {};
162
- const overlayTerm = q.overlayTw;
163
- const divideTw = q.divideTw;
164
- for (const [chart, plot2values] of valuesObject.chart2plot2values) {
165
- const plots = [];
166
- for (const [plot, values] of sortPlot2Values(data, plot2values, overlayTerm)) {
167
- plots.push({
168
- label: String(overlayTerm?.term?.values?.[plot]?.label || plot),
169
- values,
170
- seriesId: String(plot),
171
- chartId: String(chart),
172
- plotValueCount: values?.length,
173
- color: overlayTerm?.term?.values?.[plot]?.color || ""
174
- });
175
- }
176
- charts[String(chart)] = { chartId: String(chart), plots };
177
- }
178
- const bins = buildBins(q.tw, data, overlayTerm, divideTw);
179
- const result = {
180
- min: valuesObject.min,
181
- max: valuesObject.max,
182
- bins,
183
- charts,
184
- uncomputableValues: Object.keys(valuesObject.uncomputableValues).length > 0 ? valuesObject.uncomputableValues : null
185
- };
186
- return result;
187
- }
188
- async function createCanvasImg(q, result, ds) {
189
- if (!q.radius) q.radius = 5;
190
- if (q.radius <= 0) throw new Error("q.radius is not a number");
191
- else q.radius = +q.radius;
192
- const isH = q.orientation == "horizontal";
193
- for (const k of Object.keys(result.charts)) {
194
- const chart = result.charts[k];
195
- const plot2Values = {};
196
- for (const plot of chart.plots) plot2Values[plot.label] = plot.values;
197
- const useLog = q.isLogScale;
198
- const logBase = ds.cohort.termdb.logscaleBase2 ? 2 : 10;
199
- const densities = await getDensities(plot2Values, useLog, logBase);
200
- let axisScale;
201
- if (useLog) {
202
- axisScale = scaleLog().base(ds.cohort.termdb.logscaleBase2 ? 2 : 10).domain([result.min, result.max]).range(isH ? [0, q.svgw] : [q.svgw, 0]);
203
- } else {
204
- axisScale = scaleLinear().domain([result.min, result.max]).range(isH ? [0, q.svgw] : [q.svgw, 0]);
205
- }
206
- const [width, height] = isH ? [q.svgw * q.devicePixelRatio, q.radius * q.devicePixelRatio] : [q.radius * q.devicePixelRatio, q.svgw * q.devicePixelRatio];
207
- for (const plot of chart.plots) {
208
- plot.density = densities[plot.label];
209
- const canvas = createCanvas(width, height);
210
- const ctx = canvas.getContext("2d");
211
- if (q.devicePixelRatio != 1) ctx.scale(q.devicePixelRatio, q.devicePixelRatio);
212
- ctx.strokeStyle = "black";
213
- ctx.lineWidth = 1;
214
- if (q.datasymbol === "rug") {
215
- ctx.globalAlpha = 0.8;
216
- plot.values.forEach((i) => {
217
- const s = axisScale(i);
218
- ctx.beginPath();
219
- if (isH) {
220
- ctx.moveTo(s, 0);
221
- ctx.lineTo(s, q.radius);
222
- } else {
223
- ctx.moveTo(0, s);
224
- ctx.lineTo(q.radius, s);
225
- }
226
- ctx.stroke();
227
- });
228
- } else if (q.datasymbol === "bean") {
229
- ctx.globalAlpha = 0.6;
230
- ctx.fillStyle = "#ffe6e6";
231
- plot.values.forEach((i) => {
232
- const s = axisScale(i);
233
- ctx.beginPath();
234
- if (isH) ctx.arc(s, q.radius / 2, q.radius / 2, 0, 2 * Math.PI);
235
- else ctx.arc(q.radius / 2, s, q.radius / 2, 0, 2 * Math.PI);
236
- ctx.fill();
237
- ctx.stroke();
238
- });
239
- }
240
- plot.src = canvas.toDataURL();
241
- plot.summaryStats = getDescrStats(plot.values);
242
- }
243
- }
244
- }
245
- async function getViolinWilcoxonData(result) {
246
- await runWilcoxonTests(
247
- result.charts,
248
- { getGroupId: (plot) => plot.label, getGroupValues: (plot) => plot.values },
249
- "pvalues"
250
- );
251
- }
252
- async function getDensity(values) {
253
- const result = await getDensities({ plot: values });
254
- return result.plot;
255
- }
256
- async function getDensities(plot2Values, useLog = false, logBase = 10) {
257
- let transformedPlot2Values = {};
258
- if (useLog) {
259
- for (const plot in plot2Values) {
260
- transformedPlot2Values[plot] = plot2Values[plot].filter((v) => v > 0).map((v) => Math.log(v) / Math.log(logBase));
261
- }
262
- } else {
263
- transformedPlot2Values = plot2Values;
264
- }
265
- const plot2Density = JSON.parse(
266
- await run_R("density.R", JSON.stringify({ plot2Values: transformedPlot2Values }))
267
- );
268
- const densities = {};
269
- for (const plot in plot2Density) {
270
- const result = plot2Density[plot];
271
- const bins = [];
272
- let densityMin = Infinity;
273
- let densityMax = -Infinity;
274
- let xMin = Infinity;
275
- let xMax = -Infinity;
276
- for (const [i, x] of Object.entries(result.x)) {
277
- const density2 = result.y[i];
278
- const x0 = useLog ? Math.pow(logBase, x) : x;
279
- xMin = Math.min(xMin, x0);
280
- xMax = Math.max(xMax, x0);
281
- densityMin = Math.min(densityMin, density2);
282
- densityMax = Math.max(densityMax, density2);
283
- bins.push({ x0, density: density2 });
284
- }
285
- bins.unshift({ x0: xMin, density: densityMin });
286
- bins.push({ x0: xMax, density: densityMin });
287
- const density = { bins, densityMin, densityMax, minvalue: xMin, maxvalue: xMax };
288
- densities[plot] = density;
289
- }
290
- return densities;
291
- }
292
- async function getBoxPlot(q, data) {
293
- const { absMin, absMax, bins, charts, uncomputableValues, descrStats, outlierMin, outlierMax } = await processBoxPlotData(data, q);
294
- const returnData = {
295
- absMin: q.removeOutliers ? outlierMin : absMin,
296
- absMax: q.removeOutliers ? outlierMax : absMax,
297
- bins,
298
- charts,
299
- uncomputableValues: setUncomputableValues(uncomputableValues),
300
- descrStats
301
- };
302
- return returnData;
303
- }
304
- async function processBoxPlotData(data, q) {
305
- const samples = Object.values(data.samples);
306
- const values = extractNumericValues(samples, q.tw);
307
- const descrStats = getDescrStats(values, q.removeOutliers);
308
- const sampleType = computeSampleType(data);
309
- const overlayTw = q.overlayTw;
310
- const divideTw = q.divideTw;
311
- const { absMin, absMax, chart2plot2values, uncomputableValues } = parseValues(
312
- q,
313
- data,
314
- sampleType,
315
- !!q.isLogScale,
316
- overlayTw,
317
- divideTw
318
- );
319
- if (!absMin && absMin !== 0) throw new Error("absMin is undefined");
320
- if (!absMax && absMax !== 0) throw new Error("absMax is undefined");
321
- const charts = {};
322
- let outlierMin = Number.POSITIVE_INFINITY, outlierMax = Number.NEGATIVE_INFINITY;
323
- for (const [chart, plot2values] of chart2plot2values) {
324
- const plots = [];
325
- for (const [key, values2] of sortPlot2Values(data, plot2values, overlayTw)) {
326
- ;
327
- [outlierMax, outlierMin] = setPlotData(
328
- plots,
329
- values2,
330
- key,
331
- sampleType,
332
- descrStats,
333
- q,
334
- outlierMin,
335
- outlierMax,
336
- overlayTw
337
- );
338
- }
339
- if (q.tw.term?.values) setHiddenPlots(q.tw, plots);
340
- if (overlayTw && overlayTw.term?.values) setHiddenPlots(overlayTw, plots);
341
- if (divideTw && divideTw.term?.values) setHiddenPlots(divideTw, plots);
342
- if (q.orderByMedian == true) {
343
- plots.sort((a, b) => a.boxplot.p50 - b.boxplot.p50);
344
- }
345
- const sampleCount = plots.reduce((total, p) => {
346
- if (p.isHidden) return total;
347
- return total + p.descrStats.total.value;
348
- }, 0);
349
- charts[String(chart)] = { chartId: String(chart), plots, sampleCount };
350
- }
351
- const bins = buildBins(q.tw, data, overlayTw, divideTw);
352
- if (q.showAssocTests && overlayTw) await getBoxPlotWilcoxonData(charts);
353
- Object.keys(charts).forEach((c) => charts[c].plots.forEach((p) => delete p.tempValues));
354
- return { absMin, absMax, bins, charts, uncomputableValues, descrStats, outlierMin, outlierMax };
355
- }
356
- function setPlotData(plots, values, key, sampleType, descrStats, q, outlierMin, outlierMax, overlayTw) {
357
- const sortedValues = values.sort((a, b) => a - b);
358
- const vs = sortedValues.map((v) => {
359
- const value = { value: v };
360
- return value;
361
- });
362
- if (q.removeOutliers) {
363
- outlierMin = Math.min(outlierMin, descrStats.outlierMin.value);
364
- outlierMax = Math.max(outlierMax, descrStats.outlierMax.value);
365
- }
366
- const boxplot = boxplot_getvalue(vs, q.removeOutliers);
367
- if (!boxplot) throw new Error("boxplot_getvalue failed [termdb.violinBox init()]");
368
- const plot = {
369
- boxplot,
370
- descrStats: setIndividualBoxPlotStats(boxplot, sortedValues),
371
- // See comment in processBoxPlotData about tempValues
372
- tempValues: sortedValues
373
- };
374
- if (overlayTw) {
375
- const _key = overlayTw?.term?.values?.[key]?.label || key;
376
- plot.color = overlayTw?.term?.values?.[key]?.color || void 0;
377
- plot.key = String(_key);
378
- plot.seriesId = key;
379
- plot.boxplot.label = `${_key}, n=${values.length}`;
380
- } else {
381
- plot.key = sampleType;
382
- plot.boxplot.label = `${sampleType}, n=${values.length}`;
383
- }
384
- plots.push(plot);
385
- return [outlierMax, outlierMin];
386
- }
387
- function setIndividualBoxPlotStats(boxplot, values) {
388
- const stats = {
389
- total: { key: "total", label: "Total", value: values.length },
390
- min: { key: "min", label: "Minimum", value: values[0] },
391
- p25: { key: "p25", label: "1st quartile", value: boxplot.p25 },
392
- median: { key: "median", label: "Median", value: boxplot.p50 },
393
- p75: { key: "p75", label: "3rd quartile", value: boxplot.p75 },
394
- mean: { key: "mean", label: "Mean", value: getMean(values) },
395
- max: { key: "max", label: "Maximum", value: values[values.length - 1] },
396
- stdDev: { key: "stdDev", label: "Standard deviation", value: getStdDev(values) }
397
- };
398
- for (const key of Object.keys(stats)) {
399
- stats[key].value = roundValueAuto(stats[key].value);
400
- }
401
- return stats;
402
- }
403
- function setHiddenPlots(term, plots) {
404
- for (const v of Object.values(term.term?.values)) {
405
- const plot = plots.find((p) => p.key === v.label);
406
- if (plot) plot.isHidden = v?.uncomputable;
407
- }
408
- if (term.q?.hiddenValues) {
409
- for (const key of Object.keys(term.q.hiddenValues)) {
410
- const plot = plots.find((p) => p.key === key);
411
- if (plot) plot.isHidden = true;
412
- }
413
- }
414
- return plots;
415
- }
416
- function setUncomputableValues(values) {
417
- if (Object.entries(values)?.length) {
418
- return Object.entries(values).map(([label, v]) => ({ label, value: v }));
419
- } else return null;
420
- }
421
- async function getBoxPlotWilcoxonData(charts) {
422
- await runWilcoxonTests(
423
- charts,
424
- {
425
- getGroupId: (plot) => plot.boxplot.label.replace(/, n=\d+$/, ""),
426
- getGroupValues: (plot) => plot.tempValues
427
- },
428
- "wilcoxon"
429
- );
430
- }
431
- async function runWilcoxonTests(charts, accessors, resultKey) {
432
- for (const chart of Object.values(charts)) {
433
- const numPlots = chart.plots?.length;
434
- if (!numPlots || numPlots < 2) continue;
435
- const wilcoxInput = [];
436
- for (let i = 0; i < numPlots; i++) {
437
- const group1_id = accessors.getGroupId(chart.plots[i]);
438
- const group1_values = accessors.getGroupValues(chart.plots[i]);
439
- for (let j = i + 1; j < numPlots; j++) {
440
- const group2_id = accessors.getGroupId(chart.plots[j]);
441
- const group2_values = accessors.getGroupValues(chart.plots[j]);
442
- wilcoxInput.push({ group1_id, group1_values, group2_id, group2_values });
443
- }
444
- }
445
- const wilcoxOutput = JSON.parse(await run_rust("wilcoxon", JSON.stringify(wilcoxInput)));
446
- chart[resultKey] = [];
447
- for (const test of wilcoxOutput) {
448
- if (test.pvalue == null || test.pvalue == "null") {
449
- chart[resultKey].push([{ value: test.group1_id }, { value: test.group2_id }, { html: "NA" }]);
450
- } else {
451
- chart[resultKey].push([
452
- { value: test.group1_id },
453
- { value: test.group2_id },
454
- { html: test.pvalue.toPrecision(4) }
455
- ]);
456
- }
457
- }
458
- }
459
- }
460
- function computeSampleType(data) {
461
- return `All ${data.sampleType?.plural_name || "samples"}`;
462
- }
463
- function buildBins(tw, data, overlayTw, divideTw) {
464
- const bins = {
465
- term1: numericBins(tw, data)
466
- };
467
- if (overlayTw) bins.term2 = numericBins(overlayTw, data);
468
- if (divideTw) bins.term0 = numericBins(divideTw, data);
469
- return bins;
470
- }
471
- function extractNumericValues(samples, tw, isLogScale) {
472
- let values = samples.map((s) => s?.[tw.$id]?.value).filter((v) => typeof v === "number" && !tw.term.values?.[v]?.uncomputable);
473
- if (isLogScale) values = values.filter((v) => v > 0);
474
- return values;
475
- }
476
- function parseValues(q, data, sampleType, isLog, overlayTw, divideTw) {
477
- const chart2plot2values = /* @__PURE__ */ new Map();
478
- const uncomputableValues = {};
479
- function trackUncomputable(tw, key) {
480
- if (!tw?.term?.values?.[key]?.uncomputable) return false;
481
- const label = tw.term.values[key]?.label;
482
- if (label) uncomputableValues[label] = (uncomputableValues[label] || 0) + 1;
483
- return true;
484
- }
485
- let absMin = Infinity, absMax = -Infinity;
486
- for (const val of Object.values(data.samples)) {
487
- const value = val[q.tw.$id];
488
- if (!Number.isFinite(value?.value)) continue;
489
- if (trackUncomputable(q.tw, value.value)) continue;
490
- if (isLog && value.value <= 0) continue;
491
- let chart = "";
492
- let plot = sampleType;
493
- if (divideTw) {
494
- if (!val[divideTw.$id]) continue;
495
- const value0 = val[divideTw.$id];
496
- trackUncomputable(divideTw, value0.key);
497
- chart = value0.key;
498
- }
499
- if (overlayTw) {
500
- if (!val[overlayTw.$id]) continue;
501
- const value2 = val[overlayTw.$id];
502
- trackUncomputable(overlayTw, value2.key);
503
- plot = value2.key;
504
- }
505
- if (!chart2plot2values.has(chart)) chart2plot2values.set(chart, /* @__PURE__ */ new Map());
506
- const plot2values = chart2plot2values.get(chart);
507
- if (!plot2values.has(plot)) plot2values.set(plot, []);
508
- const values = plot2values.get(plot);
509
- values.push(value.value);
510
- if (value.value < absMin) absMin = value.value;
511
- if (value.value > absMax) absMax = value.value;
512
- }
513
- return { absMax, absMin, chart2plot2values, uncomputableValues };
514
- }
515
- function numericBins(tw, data) {
516
- const bins = {};
517
- if (!isNumericTerm(tw?.term)) return bins;
518
- for (const bin of data.refs.byTermId[tw.$id]?.bins || []) {
519
- bins[bin.label] = bin;
520
- }
521
- return bins;
522
- }
523
- export {
524
- buildBins,
525
- computeSampleType,
526
- divideValues,
527
- expandNumericTermCollection,
528
- extractNumericValues,
529
- getDensities,
530
- getDensity,
531
- getViolinWilcoxonData,
532
- init,
533
- numericBins,
534
- parseValues,
535
- setHiddenPlots,
536
- setIndividualBoxPlotStats,
537
- setPlotData,
538
- setScaleData,
539
- setUncomputableValues,
540
- setViolinResponse,
541
- sortObj,
542
- sortPlot2Values
543
- };