@sjcrh/proteinpaint-server 2.132.1-0 → 2.132.1-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 -1
- package/package.json +4 -4
- package/routes/gdc.grin2.run.js +61 -18
- package/routes/profileFilters.js +68 -0
- package/routes/profileFormScores.js +117 -0
- package/routes/profileScores.js +101 -0
- package/src/app.js +424 -46
package/dataset/termdb.test.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.132.1-
|
|
3
|
+
"version": "2.132.1-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",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@sjcrh/augen": "2.121.0",
|
|
64
|
-
"@sjcrh/proteinpaint-python": "2.
|
|
64
|
+
"@sjcrh/proteinpaint-python": "2.132.1-1",
|
|
65
65
|
"@sjcrh/proteinpaint-r": "2.130.0",
|
|
66
66
|
"@sjcrh/proteinpaint-rust": "2.132.1-0",
|
|
67
|
-
"@sjcrh/proteinpaint-shared": "2.132.
|
|
68
|
-
"@sjcrh/proteinpaint-types": "2.132.1-
|
|
67
|
+
"@sjcrh/proteinpaint-shared": "2.132.1-2",
|
|
68
|
+
"@sjcrh/proteinpaint-types": "2.132.1-1",
|
|
69
69
|
"@types/express": "^5.0.0",
|
|
70
70
|
"@types/express-session": "^1.18.1",
|
|
71
71
|
"better-sqlite3": "^9.4.1",
|
package/routes/gdc.grin2.run.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { runGRIN2Payload } from "#types/checkers";
|
|
2
2
|
import { stream_rust } from "@sjcrh/proteinpaint-rust";
|
|
3
|
-
import { run_R } from "@sjcrh/proteinpaint-r";
|
|
4
3
|
import serverconfig from "#src/serverconfig.js";
|
|
5
4
|
import path from "path";
|
|
6
5
|
import { formatElapsedTime } from "@sjcrh/proteinpaint-shared/time.js";
|
|
6
|
+
import { run_python } from "@sjcrh/proteinpaint-python";
|
|
7
7
|
const api = {
|
|
8
8
|
endpoint: "gdc/runGRIN2",
|
|
9
9
|
methods: {
|
|
@@ -22,20 +22,54 @@ function parseJsonlOutput(rustOutput) {
|
|
|
22
22
|
const allSuccessfulData = [];
|
|
23
23
|
let finalSummary = null;
|
|
24
24
|
let processedFiles = 0;
|
|
25
|
+
const MAX_RECORDS = 1e5;
|
|
26
|
+
let totalRecordsProcessed = 0;
|
|
27
|
+
let isCapReached = false;
|
|
25
28
|
for (const line of lines) {
|
|
26
29
|
const trimmedLine = line.trim();
|
|
27
30
|
if (trimmedLine) {
|
|
28
31
|
try {
|
|
29
32
|
const data = JSON.parse(trimmedLine);
|
|
30
33
|
if (data.type === "data") {
|
|
34
|
+
if (isCapReached) {
|
|
35
|
+
console.log(`[GRIN2] Skipping file ${data.case_id} - record cap of ${MAX_RECORDS} already reached`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const remainingCapacity = MAX_RECORDS - totalRecordsProcessed;
|
|
39
|
+
const incomingRecords = data.data.length;
|
|
40
|
+
let recordsToProcess;
|
|
41
|
+
let recordsProcessedThisFile;
|
|
42
|
+
if (incomingRecords <= remainingCapacity) {
|
|
43
|
+
recordsToProcess = data.data;
|
|
44
|
+
recordsProcessedThisFile = incomingRecords;
|
|
45
|
+
} else {
|
|
46
|
+
recordsToProcess = data.data.slice(0, remainingCapacity);
|
|
47
|
+
recordsProcessedThisFile = remainingCapacity;
|
|
48
|
+
isCapReached = true;
|
|
49
|
+
console.log(
|
|
50
|
+
`[GRIN2] Record cap reached! Processing only ${recordsProcessedThisFile} of ${incomingRecords} records from file ${data.case_id}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
31
53
|
processedFiles++;
|
|
32
|
-
allSuccessfulData.push(
|
|
54
|
+
allSuccessfulData.push(recordsToProcess);
|
|
55
|
+
totalRecordsProcessed += recordsProcessedThisFile;
|
|
56
|
+
console.log(totalRecordsProcessed, MAX_RECORDS);
|
|
33
57
|
console.log(
|
|
34
|
-
`[GRIN2] Processed file ${processedFiles}: ${data.case_id} (${data.data_type}) - ${
|
|
58
|
+
`[GRIN2] Processed file ${processedFiles}: ${data.case_id} (${data.data_type}) - ${recordsProcessedThisFile} records`
|
|
35
59
|
);
|
|
60
|
+
console.log(`[GRIN2] Total records processed: ${totalRecordsProcessed}/${MAX_RECORDS}`);
|
|
61
|
+
if (isCapReached) {
|
|
62
|
+
console.log(
|
|
63
|
+
`[GRIN2] RECORD CAP REACHED: ${MAX_RECORDS} records processed. Subsequent files will be skipped.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
36
66
|
} else if (data.type === "summary") {
|
|
37
67
|
finalSummary = data;
|
|
38
68
|
console.log(`[GRIN2] Download complete: ${data.successful_files}/${data.total_files} files successful`);
|
|
69
|
+
if (isCapReached) {
|
|
70
|
+
console.log(`[GRIN2] Processing stopped due to record cap of ${MAX_RECORDS}`);
|
|
71
|
+
console.log(`[GRIN2] Total records collected: ${totalRecordsProcessed}`);
|
|
72
|
+
}
|
|
39
73
|
if (data.failed_files > 0) {
|
|
40
74
|
console.log(`[GRIN2] ${data.failed_files} files failed`);
|
|
41
75
|
}
|
|
@@ -127,41 +161,50 @@ function init({ genomes }) {
|
|
|
127
161
|
throw new Error("Failed to process MAF files: No result from Rust");
|
|
128
162
|
}
|
|
129
163
|
const parsedRustResult = rustResult;
|
|
130
|
-
let
|
|
164
|
+
let dataForPython = [];
|
|
131
165
|
try {
|
|
132
166
|
if (parsedRustResult.successful_data && Array.isArray(parsedRustResult.successful_data)) {
|
|
133
|
-
|
|
134
|
-
console.log(`[GRIN2] Extracted ${
|
|
167
|
+
dataForPython = parsedRustResult.successful_data.flat();
|
|
168
|
+
console.log(`[GRIN2] Extracted ${dataForPython.length} records for python script`);
|
|
135
169
|
console.log(
|
|
136
170
|
`[GRIN2] Success: ${parsedRustResult.summary.successful_files}, Failed: ${parsedRustResult.summary.failed_files}`
|
|
137
171
|
);
|
|
138
172
|
} else {
|
|
139
173
|
console.warn("[GRIN2] Unexpected Rust result format");
|
|
140
|
-
|
|
174
|
+
dataForPython = [];
|
|
141
175
|
}
|
|
142
176
|
} catch (parseError) {
|
|
143
177
|
console.error("[GRIN2] Error processing Rust result:", parseError);
|
|
144
|
-
|
|
178
|
+
dataForPython = [];
|
|
145
179
|
}
|
|
146
180
|
const genedbfile = path.join(serverconfig.tpmasterdir, g.genedb.dbfile);
|
|
147
|
-
const
|
|
148
|
-
const rInput = JSON.stringify({
|
|
181
|
+
const pyInput = JSON.stringify({
|
|
149
182
|
genedb: genedbfile,
|
|
150
183
|
chromosomelist: g.majorchr,
|
|
151
|
-
|
|
152
|
-
lesion: dataForR
|
|
184
|
+
lesion: dataForPython
|
|
153
185
|
// The mutation string from Rust
|
|
154
186
|
});
|
|
155
|
-
console.log("[GRIN2] Executing
|
|
187
|
+
console.log("[GRIN2] Executing python script...");
|
|
156
188
|
const grin2AnalysisStart = Date.now();
|
|
157
|
-
|
|
158
|
-
|
|
189
|
+
let pyResult;
|
|
190
|
+
try {
|
|
191
|
+
pyResult = await run_python("gdcGRIN2.py", pyInput);
|
|
192
|
+
} catch (pyError) {
|
|
193
|
+
console.error("[GRIN2] Python execution failed:", pyError);
|
|
194
|
+
if (pyError && typeof pyError === "object" && "message" in pyError) {
|
|
195
|
+
throw new Error(`Python script failed: ${pyError.message}`);
|
|
196
|
+
} else {
|
|
197
|
+
throw new Error(`Python script failed: ${String(pyError)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
console.log("[GRIN2] python execution completed");
|
|
201
|
+
console.log(`[GRIN2] Python stderr: ${pyResult.stderr}`);
|
|
159
202
|
const grin2AnalysisTime = formatElapsedTime(Date.now() - grin2AnalysisStart);
|
|
160
203
|
console.log(`[GRIN2] Rust processing took ${grin2AnalysisTime}`);
|
|
161
204
|
let resultData;
|
|
162
205
|
try {
|
|
163
|
-
resultData = JSON.parse(
|
|
164
|
-
console.log("[GRIN2] Finished
|
|
206
|
+
resultData = JSON.parse(pyResult);
|
|
207
|
+
console.log("[GRIN2] Finished python analysis");
|
|
165
208
|
const pngImg = resultData.png[0];
|
|
166
209
|
const topGeneTable = resultData.topGeneTable || null;
|
|
167
210
|
const analysisStats = parsedRustResult.summary || {};
|
|
@@ -180,7 +223,7 @@ function init({ genomes }) {
|
|
|
180
223
|
status: "success"
|
|
181
224
|
});
|
|
182
225
|
} catch (parseError) {
|
|
183
|
-
console.error("[GRIN2] Error parsing
|
|
226
|
+
console.error("[GRIN2] Error parsing python result:", parseError);
|
|
184
227
|
}
|
|
185
228
|
} catch (e) {
|
|
186
229
|
console.error("[GRIN2] Error running analysis:", e);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ProfileFiltersPayload } from "#types/checkers";
|
|
2
|
+
import { getData, getSamplesPerFilter } from "../src/termdb.matrix.js";
|
|
3
|
+
const api = {
|
|
4
|
+
endpoint: "profileFilters",
|
|
5
|
+
methods: {
|
|
6
|
+
get: {
|
|
7
|
+
...ProfileFiltersPayload,
|
|
8
|
+
init
|
|
9
|
+
},
|
|
10
|
+
post: {
|
|
11
|
+
...ProfileFiltersPayload,
|
|
12
|
+
init
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function init({ genomes }) {
|
|
17
|
+
return async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const g = genomes[req.query.genome];
|
|
20
|
+
if (!g)
|
|
21
|
+
throw "invalid genome name";
|
|
22
|
+
const ds = g.datasets?.[req.query.dslabel];
|
|
23
|
+
getFilters(req.query, ds, g, res);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.log(e);
|
|
26
|
+
res.send({ status: "error", error: e.message || e });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function getList(samplesPerFilter, filtersData, tw) {
|
|
31
|
+
const values = Object.values(tw.term.values);
|
|
32
|
+
values.sort((v1, v2) => v1.label.localeCompare(v2.label));
|
|
33
|
+
const twSamples = samplesPerFilter[tw.term.id];
|
|
34
|
+
const data = [];
|
|
35
|
+
for (const sample of twSamples) {
|
|
36
|
+
data.push(filtersData.samples[sample]);
|
|
37
|
+
}
|
|
38
|
+
const sampleValues = Array.from(new Set(data.map((sample) => sample[tw.$id]?.value)));
|
|
39
|
+
for (const value of values) {
|
|
40
|
+
value.value = value.label;
|
|
41
|
+
value.disabled = !sampleValues.includes(value.label);
|
|
42
|
+
}
|
|
43
|
+
values.unshift({ label: "", value: "" });
|
|
44
|
+
return values;
|
|
45
|
+
}
|
|
46
|
+
async function getFilters(query, ds, genome, res) {
|
|
47
|
+
try {
|
|
48
|
+
const samplesPerFilter = await getSamplesPerFilter(query, ds);
|
|
49
|
+
const filtersData = await getData(
|
|
50
|
+
{
|
|
51
|
+
terms: query.terms
|
|
52
|
+
},
|
|
53
|
+
ds,
|
|
54
|
+
genome
|
|
55
|
+
);
|
|
56
|
+
const tw2List = {};
|
|
57
|
+
for (const tw of query.terms) {
|
|
58
|
+
tw2List[tw.term.id] = getList(samplesPerFilter, filtersData, tw);
|
|
59
|
+
}
|
|
60
|
+
res.send({ ...tw2List });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.log(e);
|
|
63
|
+
res.send({ error: e.message || e });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
api
|
|
68
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ProfileFormScoresPayload } from "#types/checkers";
|
|
2
|
+
import { getData } from "../src/termdb.matrix.js";
|
|
3
|
+
const api = {
|
|
4
|
+
endpoint: "profileFormScores",
|
|
5
|
+
methods: {
|
|
6
|
+
get: {
|
|
7
|
+
...ProfileFormScoresPayload,
|
|
8
|
+
init
|
|
9
|
+
},
|
|
10
|
+
post: {
|
|
11
|
+
...ProfileFormScoresPayload,
|
|
12
|
+
init
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function init({ genomes }) {
|
|
17
|
+
return async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const g = genomes[req.query.genome];
|
|
20
|
+
if (!g)
|
|
21
|
+
throw "invalid genome name";
|
|
22
|
+
const ds = g.datasets?.[req.query.dslabel];
|
|
23
|
+
const result = await getScoresDict(req.query, ds, g);
|
|
24
|
+
res.send(result);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.log(e);
|
|
27
|
+
res.send({ status: "error", error: e.message || e });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function getScoresDict(query, ds, genome) {
|
|
32
|
+
const terms = [...query.scoreTerms];
|
|
33
|
+
if (query.scScoreTerms)
|
|
34
|
+
terms.push(...query.scScoreTerms);
|
|
35
|
+
const data = await getData(
|
|
36
|
+
{
|
|
37
|
+
terms,
|
|
38
|
+
filter: query.filter
|
|
39
|
+
//if isRadarFacility and site is specified, do not apply the filter
|
|
40
|
+
},
|
|
41
|
+
ds,
|
|
42
|
+
genome
|
|
43
|
+
);
|
|
44
|
+
const lst = Object.values(data.samples);
|
|
45
|
+
let sites = lst.map((s) => {
|
|
46
|
+
return { label: data.refs.bySampleId[s.sample].label, value: s.sample };
|
|
47
|
+
});
|
|
48
|
+
sites = lst.map((s) => {
|
|
49
|
+
return { label: data.refs.bySampleId[s.sample].label, value: s.sample };
|
|
50
|
+
});
|
|
51
|
+
if (query.userSites) {
|
|
52
|
+
sites = sites.filter((s) => query.userSites.includes(s.label));
|
|
53
|
+
}
|
|
54
|
+
let userSite;
|
|
55
|
+
if (query.userSites) {
|
|
56
|
+
const siteName = query.userSites[0];
|
|
57
|
+
userSite = ds.sampleName2Id.get(siteName);
|
|
58
|
+
if (!userSite) {
|
|
59
|
+
throw `Invalid user site: ${siteName}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const site = query.isAggregate ? query.site : userSite;
|
|
63
|
+
const sampleData = data.samples[site] || null;
|
|
64
|
+
const samples = Object.values(data.samples);
|
|
65
|
+
const term2Score = {};
|
|
66
|
+
for (const d of query.scoreTerms) {
|
|
67
|
+
const samples2 = sampleData ? [sampleData] : Object.values(data.samples);
|
|
68
|
+
const getDictFunc = (sample) => getDict(d.$id, sample);
|
|
69
|
+
const percents = getPercentsDict(getDictFunc, samples2);
|
|
70
|
+
term2Score[d.term.id] = percents;
|
|
71
|
+
}
|
|
72
|
+
if (query.scScoreTerms)
|
|
73
|
+
for (const d of query.scScoreTerms) {
|
|
74
|
+
const samples2 = sampleData ? [sampleData] : Object.values(data.samples);
|
|
75
|
+
const percents = getSCPercentsDict(d, samples2);
|
|
76
|
+
term2Score[d.term.id] = percents;
|
|
77
|
+
}
|
|
78
|
+
const hospital = sampleData?.[query.facilityTW.$id]?.value;
|
|
79
|
+
return { term2Score, sites, hospital, n: samples.length };
|
|
80
|
+
}
|
|
81
|
+
function getDict(key, sample) {
|
|
82
|
+
if (!sample[key])
|
|
83
|
+
return null;
|
|
84
|
+
const termData = sample[key].value;
|
|
85
|
+
return JSON.parse(termData);
|
|
86
|
+
}
|
|
87
|
+
function getPercentsDict(getDictFunc, samples) {
|
|
88
|
+
const percentageDict = {};
|
|
89
|
+
for (const sample of samples) {
|
|
90
|
+
const percents = getDictFunc(sample);
|
|
91
|
+
if (!percents)
|
|
92
|
+
continue;
|
|
93
|
+
for (const key in percents) {
|
|
94
|
+
const value = percents[key];
|
|
95
|
+
if (!percentageDict[key])
|
|
96
|
+
percentageDict[key] = 0;
|
|
97
|
+
percentageDict[key] += value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return percentageDict;
|
|
101
|
+
}
|
|
102
|
+
function getSCPercentsDict(tw, samples) {
|
|
103
|
+
if (!tw)
|
|
104
|
+
throw "tw not defined";
|
|
105
|
+
const percentageDict = {};
|
|
106
|
+
for (const sample of samples) {
|
|
107
|
+
const twData = sample[tw.$id];
|
|
108
|
+
const key = twData?.value;
|
|
109
|
+
if (!percentageDict[key])
|
|
110
|
+
percentageDict[key] = 0;
|
|
111
|
+
percentageDict[key] += 1;
|
|
112
|
+
}
|
|
113
|
+
return percentageDict;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
api
|
|
117
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ProfileScoresPayload } from "#types/checkers";
|
|
2
|
+
import { getData } from "../src/termdb.matrix.js";
|
|
3
|
+
const api = {
|
|
4
|
+
endpoint: "profileScores",
|
|
5
|
+
methods: {
|
|
6
|
+
get: {
|
|
7
|
+
...ProfileScoresPayload,
|
|
8
|
+
init
|
|
9
|
+
},
|
|
10
|
+
post: {
|
|
11
|
+
...ProfileScoresPayload,
|
|
12
|
+
init
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function init({ genomes }) {
|
|
17
|
+
return async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const g = genomes[req.query.genome];
|
|
20
|
+
if (!g)
|
|
21
|
+
throw "invalid genome name";
|
|
22
|
+
const ds = g.datasets?.[req.query.dslabel];
|
|
23
|
+
const result = await getScores(req.query, ds, g);
|
|
24
|
+
res.send(result);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.log(e);
|
|
27
|
+
res.send({ status: "error", error: e.message || e });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function getScores(query, ds, genome) {
|
|
32
|
+
const isRadarFacility = query.isRadarFacility;
|
|
33
|
+
const terms = [query.facilityTW];
|
|
34
|
+
for (const term of query.scoreTerms) {
|
|
35
|
+
terms.push(term.score);
|
|
36
|
+
if (term.maxScore) {
|
|
37
|
+
terms.push(term.maxScore);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const data = await getData(
|
|
41
|
+
{
|
|
42
|
+
terms,
|
|
43
|
+
filter: isRadarFacility && query.site ? void 0 : query.filter
|
|
44
|
+
//if isRadarFacility and site is specified, do not apply the filter
|
|
45
|
+
},
|
|
46
|
+
ds,
|
|
47
|
+
genome
|
|
48
|
+
);
|
|
49
|
+
const lst = Object.values(data.samples);
|
|
50
|
+
let sites = lst.map((s) => {
|
|
51
|
+
return { label: data.refs.bySampleId[s.sample].label, value: s.sample };
|
|
52
|
+
});
|
|
53
|
+
sites = lst.map((s) => {
|
|
54
|
+
return { label: data.refs.bySampleId[s.sample].label, value: s.sample };
|
|
55
|
+
});
|
|
56
|
+
if (query.userSites) {
|
|
57
|
+
sites = sites.filter((s) => query.userSites.includes(s.label));
|
|
58
|
+
}
|
|
59
|
+
let userSite;
|
|
60
|
+
if (query.userSites) {
|
|
61
|
+
const siteName = query.userSites[0];
|
|
62
|
+
userSite = ds.sampleName2Id.get(siteName);
|
|
63
|
+
if (!userSite) {
|
|
64
|
+
throw `Invalid user site: ${siteName}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let site;
|
|
68
|
+
if (isRadarFacility)
|
|
69
|
+
site = query.site;
|
|
70
|
+
else
|
|
71
|
+
site = query.isAggregate ? query.site : userSite;
|
|
72
|
+
const sampleData = data.samples[site] || null;
|
|
73
|
+
const samples = Object.values(data.samples);
|
|
74
|
+
const term2Score = {};
|
|
75
|
+
for (const d of query.scoreTerms) {
|
|
76
|
+
term2Score[d.score.term.id] = getPercentage(d, samples, sampleData);
|
|
77
|
+
}
|
|
78
|
+
const hospital = sampleData?.[query.facilityTW.$id]?.value;
|
|
79
|
+
return { term2Score, sites, hospital, n: samples.length };
|
|
80
|
+
}
|
|
81
|
+
function getPercentage(d, samples, sampleData) {
|
|
82
|
+
if (!d)
|
|
83
|
+
return null;
|
|
84
|
+
const isAggregate = sampleData == null;
|
|
85
|
+
if (isAggregate) {
|
|
86
|
+
const maxScore = d.maxScore.term ? samples[0]?.[d.maxScore.$id]?.value : d.maxScore;
|
|
87
|
+
const scores = samples.map((sample) => sample[d.score.$id]?.value / maxScore * 100);
|
|
88
|
+
scores.sort((s1, s2) => s1 - s2);
|
|
89
|
+
const middle = Math.floor(scores.length / 2);
|
|
90
|
+
const score = scores.length % 2 !== 0 ? scores[middle] : (scores[middle - 1] + scores[middle]) / 2;
|
|
91
|
+
return Math.round(score);
|
|
92
|
+
} else {
|
|
93
|
+
const score = sampleData[d.score.$id]?.value;
|
|
94
|
+
const maxScore = d.maxScore.term ? sampleData[d.maxScore.$id]?.value : d.maxScore;
|
|
95
|
+
const percentage = score / maxScore * 100;
|
|
96
|
+
return Math.round(percentage);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
api
|
|
101
|
+
};
|