@sjcrh/proteinpaint-server 2.176.1-0 → 2.177.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 +34 -0
- package/package.json +4 -4
- package/routes/grin2.js +0 -11
- package/routes/termdb.chat.js +18 -16
- package/routes/termdb.cohort.summary.js +0 -2
- package/routes/termdb.cohorts.js +0 -2
- package/routes/termdb.config.js +0 -2
- package/routes/termdb.runChart.js +132 -36
- package/src/app.js +214 -130
package/dataset/termdb.test.js
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
const mafFields = [
|
|
2
|
+
{
|
|
3
|
+
id: "tumor_DNA",
|
|
4
|
+
name: "Tumor DNA",
|
|
5
|
+
parent_id: null,
|
|
6
|
+
child_ids: ["tumor_DNA_WGS"],
|
|
7
|
+
isleaf: true,
|
|
8
|
+
type: "float",
|
|
9
|
+
default: true,
|
|
10
|
+
// indicates default maf term (e.g. will be used by default for making maf filters in predefined groupset)
|
|
11
|
+
min: 0,
|
|
12
|
+
max: 1,
|
|
13
|
+
tvs: {
|
|
14
|
+
ranges: [
|
|
15
|
+
{
|
|
16
|
+
start: 0.1,
|
|
17
|
+
startinclusive: true,
|
|
18
|
+
stopunbounded: true
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
const mafFilter = {
|
|
25
|
+
opts: { joinWith: ["and", "or"] },
|
|
26
|
+
filter: {
|
|
27
|
+
type: "tvslst",
|
|
28
|
+
join: "",
|
|
29
|
+
in: true,
|
|
30
|
+
lst: []
|
|
31
|
+
},
|
|
32
|
+
terms: mafFields
|
|
33
|
+
};
|
|
1
34
|
function termdb_test_default() {
|
|
2
35
|
return {
|
|
3
36
|
isMds3: true,
|
|
@@ -285,6 +318,7 @@ function termdb_test_default() {
|
|
|
285
318
|
}
|
|
286
319
|
]
|
|
287
320
|
},
|
|
321
|
+
mafFilter,
|
|
288
322
|
skewerRim: {
|
|
289
323
|
type: "format",
|
|
290
324
|
formatKey: "origin",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.177.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",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"@sjcrh/augen": "2.143.0",
|
|
65
65
|
"@sjcrh/proteinpaint-python": "2.174.0",
|
|
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.177.0",
|
|
68
|
+
"@sjcrh/proteinpaint-shared": "2.177.0",
|
|
69
|
+
"@sjcrh/proteinpaint-types": "2.177.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/grin2.js
CHANGED
|
@@ -431,17 +431,6 @@ function filterAndConvertSnvIndel(sampleName, entry, options) {
|
|
|
431
431
|
if (!Number.isInteger(entry.pos)) {
|
|
432
432
|
return null;
|
|
433
433
|
}
|
|
434
|
-
if (options.minAltAlleleCount !== void 0 && options.minAltAlleleCount > 0) {
|
|
435
|
-
if (!entry.altCount || entry.altCount < options.minAltAlleleCount) {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (options.minTotalDepth !== void 0 && options.minTotalDepth > 0) {
|
|
440
|
-
const totalDepth = (entry.refCount || 0) + (entry.altCount || 0);
|
|
441
|
-
if (totalDepth < options.minTotalDepth) {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
434
|
const start = entry.pos;
|
|
446
435
|
const end = entry.pos;
|
|
447
436
|
return [sampleName, entry.chr, start, end, dt2lesion[dtsnvindel].lesionTypes[0].lesionType];
|
package/routes/termdb.chat.js
CHANGED
|
@@ -139,8 +139,7 @@ async function run_chat_pipeline(user_prompt, llm, aiRoute, dataset_json, testin
|
|
|
139
139
|
user_prompt,
|
|
140
140
|
llm,
|
|
141
141
|
aiRoute,
|
|
142
|
-
dataset_json
|
|
143
|
-
testing
|
|
142
|
+
dataset_json
|
|
144
143
|
);
|
|
145
144
|
let ai_output_json;
|
|
146
145
|
mayLog("Time taken for classification:", formatElapsedTime(Date.now() - time1));
|
|
@@ -302,7 +301,7 @@ async function readJSONFile(file) {
|
|
|
302
301
|
const json_file = await fs.promises.readFile(file);
|
|
303
302
|
return JSON.parse(json_file.toString());
|
|
304
303
|
}
|
|
305
|
-
async function classify_query_by_dataset_type(user_prompt, llm, aiRoute, dataset_json
|
|
304
|
+
async function classify_query_by_dataset_type(user_prompt, llm, aiRoute, dataset_json) {
|
|
306
305
|
const data = await readJSONFile(aiRoute);
|
|
307
306
|
let contents = data["general"];
|
|
308
307
|
for (const key of Object.keys(data)) {
|
|
@@ -320,11 +319,7 @@ async function classify_query_by_dataset_type(user_prompt, llm, aiRoute, dataset
|
|
|
320
319
|
}
|
|
321
320
|
const template = contents + " training data is as follows:" + training_data + " Question: {" + user_prompt + "} Answer: {answer}";
|
|
322
321
|
const response = await route_to_appropriate_llm_provider(template, llm);
|
|
323
|
-
|
|
324
|
-
return { action: "html", response: JSON.parse(response) };
|
|
325
|
-
} else {
|
|
326
|
-
return JSON.parse(response);
|
|
327
|
-
}
|
|
322
|
+
return JSON.parse(response);
|
|
328
323
|
}
|
|
329
324
|
async function extract_DE_search_terms_from_query(prompt, llm, dataset_db_output, dataset_json, ds, testing) {
|
|
330
325
|
if (dataset_json.hasDE) {
|
|
@@ -567,10 +562,14 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
|
|
|
567
562
|
const pp_plot_json = { chartType: "summary" };
|
|
568
563
|
let html = "";
|
|
569
564
|
if (response_type.html) html = response_type.html;
|
|
570
|
-
if (!response_type.term)
|
|
565
|
+
if (!response_type.term) {
|
|
566
|
+
html += "term type is not present in summary output";
|
|
567
|
+
return { type: "html", html };
|
|
568
|
+
}
|
|
571
569
|
const term1_validation = validate_term(response_type.term, common_genes, dataset_json, ds);
|
|
572
570
|
if (term1_validation.html.length > 0) {
|
|
573
571
|
html += term1_validation.html;
|
|
572
|
+
return { type: "html", html };
|
|
574
573
|
} else {
|
|
575
574
|
pp_plot_json.term = term1_validation.term_type;
|
|
576
575
|
if (term1_validation.category == "float" || term1_validation.category == "integer") {
|
|
@@ -582,6 +581,7 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
|
|
|
582
581
|
const term2_validation = validate_term(response_type.term2, common_genes, dataset_json, ds);
|
|
583
582
|
if (term2_validation.html.length > 0) {
|
|
584
583
|
html += term2_validation.html;
|
|
584
|
+
return { type: "html", html };
|
|
585
585
|
} else {
|
|
586
586
|
pp_plot_json.term2 = term2_validation.term_type;
|
|
587
587
|
if (term2_validation.category == "float" || term2_validation.category == "integer") {
|
|
@@ -594,8 +594,15 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
|
|
|
594
594
|
const resolved = resolveChildType(pp_plot_json.category, pp_plot_json.category2, llmChildType);
|
|
595
595
|
if (resolved.error) {
|
|
596
596
|
html += resolved.error;
|
|
597
|
+
return { type: "html", html };
|
|
597
598
|
} else {
|
|
598
599
|
pp_plot_json.childType = resolved.childType;
|
|
600
|
+
if (pp_plot_json.childType == "barchart") {
|
|
601
|
+
pp_plot_json.term.q = { mode: "discrete" };
|
|
602
|
+
if (pp_plot_json.term2) {
|
|
603
|
+
pp_plot_json.term2.q = { mode: "discrete" };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
599
606
|
if (resolved.bothNumeric && (resolved.childType == "violin" || resolved.childType == "boxplot")) {
|
|
600
607
|
pp_plot_json.term2.q = { mode: "discrete" };
|
|
601
608
|
}
|
|
@@ -606,15 +613,12 @@ function validate_summary_response(response, common_genes, dataset_json, ds) {
|
|
|
606
613
|
const validated_filters = validate_filter(response_type.simpleFilter, ds, "");
|
|
607
614
|
if (validated_filters.html.length > 0) {
|
|
608
615
|
html += validated_filters.html;
|
|
616
|
+
return { type: "html", html };
|
|
609
617
|
} else {
|
|
610
618
|
pp_plot_json.filter = validated_filters.simplefilter;
|
|
611
619
|
}
|
|
612
620
|
}
|
|
613
|
-
|
|
614
|
-
return { type: "html", html };
|
|
615
|
-
} else {
|
|
616
|
-
return { type: "plot", plot: pp_plot_json };
|
|
617
|
-
}
|
|
621
|
+
return { type: "plot", plot: pp_plot_json };
|
|
618
622
|
}
|
|
619
623
|
async function extract_matrix_search_terms_from_query(prompt, llm, dataset_db_output, dataset_json, genes_list, ds, testing) {
|
|
620
624
|
const Schema = {
|
|
@@ -647,8 +651,6 @@ async function extract_matrix_search_terms_from_query(prompt, llm, dataset_db_ou
|
|
|
647
651
|
};
|
|
648
652
|
const common_genes = extractGenesFromPrompt(prompt, genes_list);
|
|
649
653
|
const matrix_ds = dataset_json.charts.filter((chart) => chart.type == "Matrix");
|
|
650
|
-
console.log("matrix_ds", matrix_ds);
|
|
651
|
-
console.log("dataset_json.charts", dataset_json.charts);
|
|
652
654
|
if (matrix_ds.length == 0) throw "Matrix information is not present in the dataset file.";
|
|
653
655
|
if (matrix_ds[0].TrainingData.length == 0) throw "No training data is provided for the matrix agent.";
|
|
654
656
|
const training_data = formatTrainingExamples(matrix_ds[0].TrainingData);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { termdbCohortSummaryPayload } from "#types/checkers";
|
|
2
2
|
import { get_ds_tdb } from "#src/termdb.js";
|
|
3
|
-
import { mayCopyFromCookie } from "#src/utils.js";
|
|
4
3
|
import { get_samples } from "#src/termdb.sql.js";
|
|
5
4
|
import { authApi } from "#src/auth.js";
|
|
6
5
|
const api = {
|
|
@@ -15,7 +14,6 @@ const api = {
|
|
|
15
14
|
function init({ genomes }) {
|
|
16
15
|
return async (req, res) => {
|
|
17
16
|
const q = req.query;
|
|
18
|
-
mayCopyFromCookie(q, req.cookies);
|
|
19
17
|
try {
|
|
20
18
|
const genome = genomes[q.genome];
|
|
21
19
|
if (!genome) throw "invalid genome";
|
package/routes/termdb.cohorts.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { termdbCohortsPayload } from "#types/checkers";
|
|
2
2
|
import { get_ds_tdb } from "#src/termdb.js";
|
|
3
|
-
import { mayCopyFromCookie } from "#src/utils.js";
|
|
4
3
|
const api = {
|
|
5
4
|
endpoint: "termdb/cohorts",
|
|
6
5
|
methods: {
|
|
@@ -13,7 +12,6 @@ const api = {
|
|
|
13
12
|
function init({ genomes }) {
|
|
14
13
|
return async (req, res) => {
|
|
15
14
|
const q = req.query;
|
|
16
|
-
mayCopyFromCookie(q, req.cookies);
|
|
17
15
|
try {
|
|
18
16
|
const genome = genomes[q.genome];
|
|
19
17
|
if (!genome) throw "invalid genome";
|
package/routes/termdb.config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import serverconfig from "#src/serverconfig.js";
|
|
2
2
|
import { authApi } from "#src/auth.js";
|
|
3
3
|
import { get_ds_tdb } from "#src/termdb.js";
|
|
4
|
-
import { mayCopyFromCookie } from "#src/utils.js";
|
|
5
4
|
import { TermTypes } from "#shared/terms.js";
|
|
6
5
|
const api = {
|
|
7
6
|
endpoint: "termdb/config",
|
|
@@ -20,7 +19,6 @@ const api = {
|
|
|
20
19
|
function init({ genomes }) {
|
|
21
20
|
return async (req, res) => {
|
|
22
21
|
const q = req.query;
|
|
23
|
-
mayCopyFromCookie(q, req.cookies);
|
|
24
22
|
try {
|
|
25
23
|
const genome = genomes[q.genome];
|
|
26
24
|
if (!genome) throw "invalid genome";
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { runChartPayload } from "#types/checkers";
|
|
2
|
-
import { getNumberFromDate } from "#shared/terms.js";
|
|
2
|
+
import { getDateFromNumber, getNumberFromDate } from "#shared/terms.js";
|
|
3
|
+
function decimalYearToYearMonth(xRaw) {
|
|
4
|
+
const date = getDateFromNumber(xRaw);
|
|
5
|
+
const t = date.getTime();
|
|
6
|
+
if (Number.isFinite(t)) {
|
|
7
|
+
const yearNum = date.getFullYear();
|
|
8
|
+
const monthNum = date.getMonth() + 1;
|
|
9
|
+
if (Number.isFinite(yearNum) && Number.isFinite(monthNum)) return { yearNum, monthNum };
|
|
10
|
+
}
|
|
11
|
+
const parts = String(xRaw).split(".");
|
|
12
|
+
const year = Number(parts[0]);
|
|
13
|
+
if (!Number.isFinite(year)) return null;
|
|
14
|
+
let month;
|
|
15
|
+
if (parts.length > 1) {
|
|
16
|
+
const decimalPart = parts[1];
|
|
17
|
+
if (decimalPart.length === 2) {
|
|
18
|
+
const monthCandidate = Number(decimalPart);
|
|
19
|
+
month = monthCandidate >= 1 && monthCandidate <= 12 ? monthCandidate : Math.floor((xRaw - year) * 12) + 1;
|
|
20
|
+
} else {
|
|
21
|
+
month = Math.floor((xRaw - year) * 12) + 1;
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
month = 1;
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isFinite(month) || month < 1 || month > 12) return null;
|
|
27
|
+
return { yearNum: year, monthNum: month };
|
|
28
|
+
}
|
|
3
29
|
const api = {
|
|
4
30
|
endpoint: "termdb/runChart",
|
|
5
31
|
methods: {
|
|
@@ -14,9 +40,16 @@ const api = {
|
|
|
14
40
|
}
|
|
15
41
|
};
|
|
16
42
|
async function getRunChart(q, ds) {
|
|
17
|
-
const terms = [q.xtw, q.ytw];
|
|
18
43
|
const xTermId = q.xtw["$id"] ?? q.xtw.term?.id;
|
|
19
|
-
const yTermId = q.ytw["$id"] ?? q.ytw.term?.id;
|
|
44
|
+
const yTermId = q.ytw ? q.ytw["$id"] ?? q.ytw.term?.id : void 0;
|
|
45
|
+
if (xTermId == null || xTermId === "") {
|
|
46
|
+
throw new Error("runChart requires xtw with $id or term.id");
|
|
47
|
+
}
|
|
48
|
+
const isFrequency = !q.ytw;
|
|
49
|
+
if (!isFrequency && (yTermId == null || yTermId === "")) {
|
|
50
|
+
throw new Error("runChart requires ytw with $id or term.id when ytw is provided");
|
|
51
|
+
}
|
|
52
|
+
const terms = isFrequency ? [q.xtw] : [q.xtw, q.ytw];
|
|
20
53
|
const { getData } = await import("../src/termdb.matrix.js");
|
|
21
54
|
const data = await getData(
|
|
22
55
|
{
|
|
@@ -29,7 +62,13 @@ async function getRunChart(q, ds) {
|
|
|
29
62
|
);
|
|
30
63
|
if (data.error) throw new Error(data.error);
|
|
31
64
|
const shouldPartition = q.xtw?.q?.mode === "discrete";
|
|
32
|
-
|
|
65
|
+
if (isFrequency) {
|
|
66
|
+
return buildFrequencyFromData(xTermId, data, shouldPartition, xTermId, q.showCumulativeFrequency === true);
|
|
67
|
+
}
|
|
68
|
+
if (yTermId == null || yTermId === "") {
|
|
69
|
+
throw new Error("runChart requires ytw with $id or term.id when ytw is provided");
|
|
70
|
+
}
|
|
71
|
+
return buildRunChartFromData(q.aggregation ?? "median", xTermId, yTermId, data, shouldPartition, xTermId);
|
|
33
72
|
}
|
|
34
73
|
function buildRunChartFromData(aggregation, xTermId, yTermId, data, shouldPartition, partitionTermId) {
|
|
35
74
|
const allSamples = data.samples || {};
|
|
@@ -56,6 +95,81 @@ function buildRunChartFromData(aggregation, xTermId, yTermId, data, shouldPartit
|
|
|
56
95
|
const one = buildOneSeries(aggregation, xTermId, yTermId, data);
|
|
57
96
|
return { status: "ok", series: [{ ...one }] };
|
|
58
97
|
}
|
|
98
|
+
function buildFrequencyFromData(xTermId, data, shouldPartition, partitionTermId, showCumulativeFrequency) {
|
|
99
|
+
const allSamples = data.samples || {};
|
|
100
|
+
if (shouldPartition && partitionTermId) {
|
|
101
|
+
const period2Samples = {};
|
|
102
|
+
for (const sampleId in allSamples) {
|
|
103
|
+
const sample = allSamples[sampleId];
|
|
104
|
+
const partitionTerm = sample?.[partitionTermId];
|
|
105
|
+
if (partitionTerm?.key == null) continue;
|
|
106
|
+
const periodKey = partitionTerm.key;
|
|
107
|
+
if (!period2Samples[periodKey]) period2Samples[periodKey] = {};
|
|
108
|
+
period2Samples[periodKey][sampleId] = sample;
|
|
109
|
+
}
|
|
110
|
+
const periodKeys = Object.keys(period2Samples).sort();
|
|
111
|
+
const series = periodKeys.map((seriesId) => {
|
|
112
|
+
const subset = { samples: period2Samples[seriesId] };
|
|
113
|
+
const one2 = buildOneSeriesFrequency(xTermId, subset, showCumulativeFrequency === true);
|
|
114
|
+
return { seriesId, ...one2 };
|
|
115
|
+
});
|
|
116
|
+
return { status: "ok", series };
|
|
117
|
+
}
|
|
118
|
+
const one = buildOneSeriesFrequency(xTermId, data, showCumulativeFrequency === true);
|
|
119
|
+
return { status: "ok", series: [{ ...one }] };
|
|
120
|
+
}
|
|
121
|
+
function buildOneSeriesFrequency(xTermId, data, showCumulativeFrequency = false) {
|
|
122
|
+
if (xTermId == null || xTermId === "") {
|
|
123
|
+
throw new Error("buildOneSeriesFrequency requires a defined xTermId (xtw.$id or xtw.term.id)");
|
|
124
|
+
}
|
|
125
|
+
const buckets = {};
|
|
126
|
+
for (const sampleId in data.samples || {}) {
|
|
127
|
+
const sample = data.samples[sampleId];
|
|
128
|
+
const xRaw = sample?.[xTermId]?.value ?? sample?.[xTermId]?.key;
|
|
129
|
+
if (xRaw == null) continue;
|
|
130
|
+
if (typeof xRaw !== "number") {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`x value must be a number for sample ${sampleId}: xTermId=${xTermId}, received type ${typeof xRaw}, value: ${xRaw}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const parsed = decimalYearToYearMonth(xRaw);
|
|
136
|
+
if (!parsed) continue;
|
|
137
|
+
const { yearNum, monthNum } = parsed;
|
|
138
|
+
const bucketKey = `${yearNum}-${String(monthNum).padStart(2, "0")}`;
|
|
139
|
+
const bucketDate = new Date(yearNum, monthNum - 1, 1);
|
|
140
|
+
const xName = bucketDate.toLocaleString("en-US", { month: "long", year: "numeric" });
|
|
141
|
+
if (!buckets[bucketKey]) {
|
|
142
|
+
const midMonthX = getNumberFromDate(new Date(yearNum, monthNum - 1, 15));
|
|
143
|
+
buckets[bucketKey] = {
|
|
144
|
+
x: Math.round(midMonthX * 100) / 100,
|
|
145
|
+
xName,
|
|
146
|
+
count: 0,
|
|
147
|
+
sortKey: yearNum * 100 + monthNum
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
buckets[bucketKey].count += 1;
|
|
151
|
+
}
|
|
152
|
+
let points = Object.values(buckets).sort((a, b) => a.sortKey - b.sortKey).map((b) => ({
|
|
153
|
+
x: b.x,
|
|
154
|
+
xName: b.xName,
|
|
155
|
+
y: b.count,
|
|
156
|
+
sampleCount: b.count
|
|
157
|
+
}));
|
|
158
|
+
if (showCumulativeFrequency) {
|
|
159
|
+
let sum = 0;
|
|
160
|
+
points = points.map((p) => {
|
|
161
|
+
sum += p.y;
|
|
162
|
+
return { ...p, y: sum };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const yValues = points.map((p) => p.y).filter((v) => typeof v === "number" && !Number.isNaN(v));
|
|
166
|
+
const median = yValues.length > 0 ? (() => {
|
|
167
|
+
const sorted = [...yValues].sort((a, b) => a - b);
|
|
168
|
+
const mid = Math.floor(sorted.length / 2);
|
|
169
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
170
|
+
})() : 0;
|
|
171
|
+
return { median, points };
|
|
172
|
+
}
|
|
59
173
|
function buildOneSeries(aggregation, xTermId, yTermId, data) {
|
|
60
174
|
const supportedAggregations = ["proportion", "count", "median"];
|
|
61
175
|
if (!supportedAggregations.includes(aggregation)) {
|
|
@@ -76,39 +190,15 @@ function buildOneSeries(aggregation, xTermId, yTermId, data) {
|
|
|
76
190
|
`x value must be a number for sample ${sampleId}: xTermId=${xTermId}, received type ${typeof xRaw}, value: ${xRaw}`
|
|
77
191
|
);
|
|
78
192
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
year = Number(parts[0]);
|
|
83
|
-
if (parts.length > 1) {
|
|
84
|
-
const decimalPart = parts[1];
|
|
85
|
-
if (decimalPart.length === 2) {
|
|
86
|
-
const monthCandidate = Number(decimalPart);
|
|
87
|
-
if (monthCandidate >= 1 && monthCandidate <= 12) {
|
|
88
|
-
month = monthCandidate;
|
|
89
|
-
} else {
|
|
90
|
-
const frac = xRaw - year;
|
|
91
|
-
month = Math.floor(frac * 12) + 1;
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
const frac = xRaw - year;
|
|
95
|
-
month = Math.floor(frac * 12) + 1;
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
month = 1;
|
|
99
|
-
}
|
|
100
|
-
if (year == null || month == null || Number.isNaN(year) || Number.isNaN(month)) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const yearNum = year;
|
|
104
|
-
const monthNum = month;
|
|
193
|
+
const parsed = decimalYearToYearMonth(xRaw);
|
|
194
|
+
if (!parsed) continue;
|
|
195
|
+
const { yearNum, monthNum } = parsed;
|
|
105
196
|
const bucketKey = `${yearNum}-${String(monthNum).padStart(2, "0")}`;
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const xName = date.toLocaleString("en-US", { month: "long", year: "numeric" });
|
|
197
|
+
const bucketDate = new Date(yearNum, monthNum - 1, 1);
|
|
198
|
+
const xName = bucketDate.toLocaleString("en-US", { month: "long", year: "numeric" });
|
|
109
199
|
if (!buckets[bucketKey]) {
|
|
110
200
|
buckets[bucketKey] = {
|
|
111
|
-
x,
|
|
201
|
+
x: getNumberFromDate(new Date(yearNum, monthNum - 1, 15)),
|
|
112
202
|
xName,
|
|
113
203
|
count: 0,
|
|
114
204
|
missingCount: 0,
|
|
@@ -214,6 +304,9 @@ function buildOneSeries(aggregation, xTermId, yTermId, data) {
|
|
|
214
304
|
})() : 0;
|
|
215
305
|
return { median, points };
|
|
216
306
|
}
|
|
307
|
+
function runChartErrorPayload(message) {
|
|
308
|
+
return { error: String(message), series: [] };
|
|
309
|
+
}
|
|
217
310
|
function init({ genomes }) {
|
|
218
311
|
return async (req, res) => {
|
|
219
312
|
try {
|
|
@@ -225,12 +318,15 @@ function init({ genomes }) {
|
|
|
225
318
|
const result = await getRunChart(q, ds);
|
|
226
319
|
res.send(result);
|
|
227
320
|
} catch (e) {
|
|
228
|
-
res.send(
|
|
321
|
+
res.send(runChartErrorPayload(e.message || e));
|
|
229
322
|
}
|
|
230
323
|
};
|
|
231
324
|
}
|
|
232
325
|
export {
|
|
233
326
|
api,
|
|
327
|
+
buildFrequencyFromData,
|
|
234
328
|
buildRunChartFromData,
|
|
235
|
-
|
|
329
|
+
decimalYearToYearMonth,
|
|
330
|
+
getRunChart,
|
|
331
|
+
runChartErrorPayload
|
|
236
332
|
};
|