@molgenis/vip-report-template 6.0.1 → 6.0.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/package.json +1 -1
- package/src/App.tsx +18 -8
- package/src/__tests__/sample.test.ts +184 -0
- package/src/components/Breadcrumb.tsx +3 -2
- package/src/components/SampleTable.tsx +37 -46
- package/src/components/VariantsSampleTable.tsx +19 -41
- package/src/store/index.tsx +6 -4
- package/src/utils/sample.ts +63 -3
- package/src/views/Help.tsx +131 -6
- package/src/views/Home.tsx +24 -112
- package/src/views/SampleVariant.tsx +1 -1
- package/src/views/SampleVariantConsequence.tsx +1 -1
- package/src/views/SampleVariants.tsx +74 -3
- package/src/views/Samples.tsx +35 -33
- package/src/views/Variants.tsx +6 -0
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -45,19 +45,29 @@ const App: Component = () => {
|
|
|
45
45
|
<nav class="navbar is-fixed-top is-light" role="navigation" aria-label="main navigation">
|
|
46
46
|
<div class="navbar-brand">
|
|
47
47
|
<Link class="navbar-item has-text-weight-semibold" href="/">
|
|
48
|
-
|
|
48
|
+
Variant Interpretation Pipeline
|
|
49
49
|
</Link>
|
|
50
50
|
</div>
|
|
51
51
|
<div class="navbar-menu">
|
|
52
|
-
<div class="navbar-
|
|
53
|
-
<Link class="navbar-
|
|
54
|
-
|
|
52
|
+
<div class="navbar-item has-dropdown is-hoverable">
|
|
53
|
+
<Link class="navbar-link" href="/">
|
|
54
|
+
Report
|
|
55
55
|
</Link>
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
<div class="navbar-dropdown">
|
|
57
|
+
<Link class="navbar-item" href="/samples">
|
|
58
|
+
Samples
|
|
59
|
+
</Link>
|
|
60
|
+
<hr class="navbar-divider" />
|
|
61
|
+
<Link class="navbar-item" href="/variants">
|
|
62
|
+
Variants
|
|
63
|
+
</Link>
|
|
64
|
+
</div>
|
|
60
65
|
</div>
|
|
66
|
+
{api.isDatasetSupport() && (
|
|
67
|
+
<div class="navbar-start">
|
|
68
|
+
<DatasetDropdown />
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
61
71
|
<div class="navbar-end">
|
|
62
72
|
<Link class="navbar-item" href="/help">
|
|
63
73
|
Help
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getSampleAffectedStatusLabel,
|
|
4
|
+
getSampleFamilyMembersWithoutParents,
|
|
5
|
+
getSampleFather,
|
|
6
|
+
getSampleLabel,
|
|
7
|
+
getSampleMother,
|
|
8
|
+
getSampleSexLabel,
|
|
9
|
+
} from "../utils/sample";
|
|
10
|
+
import { Sample } from "@molgenis/vip-report-api/src/Api";
|
|
11
|
+
|
|
12
|
+
describe("sample utilities", () => {
|
|
13
|
+
const sample = {
|
|
14
|
+
person: {
|
|
15
|
+
familyId: "fam0",
|
|
16
|
+
individualId: "0",
|
|
17
|
+
maternalId: "1",
|
|
18
|
+
paternalId: "2",
|
|
19
|
+
},
|
|
20
|
+
} as Sample;
|
|
21
|
+
const motherSample = {
|
|
22
|
+
person: {
|
|
23
|
+
familyId: "fam0",
|
|
24
|
+
individualId: "1",
|
|
25
|
+
},
|
|
26
|
+
} as Sample;
|
|
27
|
+
const fatherSample = {
|
|
28
|
+
person: {
|
|
29
|
+
familyId: "fam0",
|
|
30
|
+
individualId: "2",
|
|
31
|
+
},
|
|
32
|
+
} as Sample;
|
|
33
|
+
const siblingSample = {
|
|
34
|
+
person: {
|
|
35
|
+
familyId: "fam0",
|
|
36
|
+
individualId: "3",
|
|
37
|
+
maternalId: "1",
|
|
38
|
+
paternalId: "2",
|
|
39
|
+
},
|
|
40
|
+
} as Sample;
|
|
41
|
+
const otherMotherSample = {
|
|
42
|
+
person: {
|
|
43
|
+
familyId: "fam1",
|
|
44
|
+
individualId: "1",
|
|
45
|
+
},
|
|
46
|
+
} as Sample;
|
|
47
|
+
const otherFatherSample = {
|
|
48
|
+
person: {
|
|
49
|
+
familyId: "fam1",
|
|
50
|
+
individualId: "2",
|
|
51
|
+
},
|
|
52
|
+
} as Sample;
|
|
53
|
+
const otherSiblingSample = {
|
|
54
|
+
person: {
|
|
55
|
+
familyId: "fam1",
|
|
56
|
+
individualId: "3",
|
|
57
|
+
maternalId: "1",
|
|
58
|
+
paternalId: "2",
|
|
59
|
+
},
|
|
60
|
+
} as Sample;
|
|
61
|
+
|
|
62
|
+
test("getSampleLabel", () => {
|
|
63
|
+
const individualId = "sample_label";
|
|
64
|
+
const sample = {
|
|
65
|
+
person: {
|
|
66
|
+
individualId: individualId,
|
|
67
|
+
},
|
|
68
|
+
} as Sample;
|
|
69
|
+
expect(getSampleLabel(sample)).toBe(individualId);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("getSampleSexLabel:FEMALE", () => {
|
|
73
|
+
const sample = {
|
|
74
|
+
person: {
|
|
75
|
+
sex: "FEMALE",
|
|
76
|
+
},
|
|
77
|
+
} as Sample;
|
|
78
|
+
expect(getSampleSexLabel(sample)).toBe("female");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("getSampleSexLabel:MALE", () => {
|
|
82
|
+
const sample = {
|
|
83
|
+
person: {
|
|
84
|
+
sex: "MALE",
|
|
85
|
+
},
|
|
86
|
+
} as Sample;
|
|
87
|
+
expect(getSampleSexLabel(sample)).toBe("male");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("getSampleSexLabel:OTHER_SEX", () => {
|
|
91
|
+
const sample = {
|
|
92
|
+
person: {
|
|
93
|
+
sex: "OTHER_SEX",
|
|
94
|
+
},
|
|
95
|
+
} as Sample;
|
|
96
|
+
expect(getSampleSexLabel(sample)).toBe("?");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("getSampleSexLabel:UNKNOWN_SEX", () => {
|
|
100
|
+
const sample = {
|
|
101
|
+
person: {
|
|
102
|
+
sex: "UNKNOWN_SEX",
|
|
103
|
+
},
|
|
104
|
+
} as Sample;
|
|
105
|
+
expect(getSampleSexLabel(sample)).toBe("?");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("getSampleAffectedStatusLabel:AFFECTED", () => {
|
|
109
|
+
const sample = {
|
|
110
|
+
person: {
|
|
111
|
+
affectedStatus: "AFFECTED",
|
|
112
|
+
},
|
|
113
|
+
} as Sample;
|
|
114
|
+
expect(getSampleAffectedStatusLabel(sample)).toBe("affected");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("getSampleAffectedStatusLabel:UNAFFECTED", () => {
|
|
118
|
+
const sample = {
|
|
119
|
+
person: {
|
|
120
|
+
affectedStatus: "UNAFFECTED",
|
|
121
|
+
},
|
|
122
|
+
} as Sample;
|
|
123
|
+
expect(getSampleAffectedStatusLabel(sample)).toBe("unaffected");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("getSampleAffectedStatusLabel:MISSING", () => {
|
|
127
|
+
const sample = {
|
|
128
|
+
person: {
|
|
129
|
+
affectedStatus: "MISSING",
|
|
130
|
+
},
|
|
131
|
+
} as Sample;
|
|
132
|
+
expect(getSampleAffectedStatusLabel(sample)).toBe("?");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("getSampleMother", () => {
|
|
136
|
+
expect(
|
|
137
|
+
getSampleMother(sample, [
|
|
138
|
+
sample,
|
|
139
|
+
otherSiblingSample,
|
|
140
|
+
otherFatherSample,
|
|
141
|
+
otherMotherSample,
|
|
142
|
+
siblingSample,
|
|
143
|
+
fatherSample,
|
|
144
|
+
motherSample,
|
|
145
|
+
]),
|
|
146
|
+
).toStrictEqual(motherSample);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("getSampleMother:undefined", () => {
|
|
150
|
+
expect(getSampleMother(sample, [sample, otherSiblingSample, otherFatherSample, otherMotherSample])).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("getSampleFather", () => {
|
|
154
|
+
expect(
|
|
155
|
+
getSampleFather(sample, [
|
|
156
|
+
sample,
|
|
157
|
+
otherSiblingSample,
|
|
158
|
+
otherMotherSample,
|
|
159
|
+
otherFatherSample,
|
|
160
|
+
siblingSample,
|
|
161
|
+
motherSample,
|
|
162
|
+
fatherSample,
|
|
163
|
+
]),
|
|
164
|
+
).toStrictEqual(fatherSample);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("getSampleFather:undefined", () => {
|
|
168
|
+
expect(getSampleFather(sample, [sample, otherSiblingSample, otherMotherSample, otherFatherSample])).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("getSampleFamilyMembersWithoutParents", () => {
|
|
172
|
+
expect(
|
|
173
|
+
getSampleFamilyMembersWithoutParents(sample, [
|
|
174
|
+
sample,
|
|
175
|
+
otherSiblingSample,
|
|
176
|
+
otherMotherSample,
|
|
177
|
+
otherFatherSample,
|
|
178
|
+
siblingSample,
|
|
179
|
+
motherSample,
|
|
180
|
+
fatherSample,
|
|
181
|
+
]),
|
|
182
|
+
).toStrictEqual([siblingSample]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -9,13 +9,14 @@ export const Breadcrumb: Component<{
|
|
|
9
9
|
return (
|
|
10
10
|
<div class="columns is-gapless">
|
|
11
11
|
<div class="column">
|
|
12
|
-
<nav class="breadcrumb">
|
|
12
|
+
<nav class="breadcrumb has-succeeds-separator">
|
|
13
13
|
<ul>
|
|
14
14
|
<li classList={{ "is-active": props.items.length === 0 }}>
|
|
15
15
|
<Link href="/">
|
|
16
|
-
<span class="icon">
|
|
16
|
+
<span class="icon is-small mr-2">
|
|
17
17
|
<i class="fas fa-home" />
|
|
18
18
|
</span>
|
|
19
|
+
<span>Report</span>
|
|
19
20
|
</Link>
|
|
20
21
|
</li>
|
|
21
22
|
<For each={props.items}>
|
|
@@ -1,40 +1,15 @@
|
|
|
1
|
-
import { Link } from "@solidjs/router";
|
|
1
|
+
import { Link, useNavigate } from "@solidjs/router";
|
|
2
2
|
import { Component, createMemo, For, Show } from "solid-js";
|
|
3
3
|
import { Item, Phenotype, PhenotypicFeature, Sample } from "@molgenis/vip-report-api/src/Api";
|
|
4
4
|
import { HpoTerm } from "./HpoTerm";
|
|
5
5
|
import { Anchor } from "./Anchor";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
case "UNAFFECTED":
|
|
14
|
-
label = "Unaffected";
|
|
15
|
-
break;
|
|
16
|
-
default:
|
|
17
|
-
label = "?";
|
|
18
|
-
break;
|
|
19
|
-
}
|
|
20
|
-
return label;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getSexLabel(sex: string) {
|
|
24
|
-
let label;
|
|
25
|
-
switch (sex) {
|
|
26
|
-
case "MALE":
|
|
27
|
-
label = "Male";
|
|
28
|
-
break;
|
|
29
|
-
case "FEMALE":
|
|
30
|
-
label = "Female";
|
|
31
|
-
break;
|
|
32
|
-
default:
|
|
33
|
-
label = "?";
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
return label;
|
|
37
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
getSampleAffectedStatusLabel,
|
|
8
|
+
getSampleFather,
|
|
9
|
+
getSampleLabel,
|
|
10
|
+
getSampleMother,
|
|
11
|
+
getSampleSexLabel,
|
|
12
|
+
} from "../utils/sample";
|
|
38
13
|
|
|
39
14
|
function mapPhenotypes(phenotypes: Item<Phenotype>[]) {
|
|
40
15
|
const phenoMap: { [key: string]: PhenotypicFeature[] } = {};
|
|
@@ -48,11 +23,23 @@ export const SampleTable: Component<{
|
|
|
48
23
|
samples: Item<Sample>[];
|
|
49
24
|
phenotypes: Item<Phenotype>[];
|
|
50
25
|
}> = (props) => {
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
const samples = createMemo(() => props.samples.map((item) => item.data));
|
|
51
28
|
const phenoMap = createMemo(() => mapPhenotypes(props.phenotypes));
|
|
52
29
|
|
|
53
|
-
|
|
54
|
-
return phenoMap()[
|
|
55
|
-
}
|
|
30
|
+
const samplePhenotypes = (sample: Sample): PhenotypicFeature[] => {
|
|
31
|
+
return phenoMap()[sample.person.individualId] ?? [];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const sampleFatherLabel = (sample: Sample): string => {
|
|
35
|
+
const sampleFather = getSampleFather(sample, samples());
|
|
36
|
+
return sampleFather ? getSampleLabel(sampleFather) : "";
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const sampleMotherLabel = (sample: Sample): string => {
|
|
40
|
+
const sampleMother = getSampleMother(sample, samples());
|
|
41
|
+
return sampleMother ? getSampleLabel(sampleMother) : "";
|
|
42
|
+
};
|
|
56
43
|
|
|
57
44
|
return (
|
|
58
45
|
<div style={{ display: "grid" }}>
|
|
@@ -70,6 +57,7 @@ export const SampleTable: Component<{
|
|
|
70
57
|
<th>Affected</th>
|
|
71
58
|
<th>Phenotypes</th>
|
|
72
59
|
<th>VIBE</th>
|
|
60
|
+
<th />
|
|
73
61
|
</tr>
|
|
74
62
|
</thead>
|
|
75
63
|
<tbody>
|
|
@@ -78,15 +66,15 @@ export const SampleTable: Component<{
|
|
|
78
66
|
<tr>
|
|
79
67
|
<td>{sample.data.person.familyId}</td>
|
|
80
68
|
<td>
|
|
81
|
-
<Link href={`/samples/${sample.id}`}>{sample.data
|
|
69
|
+
<Link href={`/samples/${sample.id}`}>{getSampleLabel(sample.data)}</Link>
|
|
82
70
|
</td>
|
|
83
|
-
<td>{sample.data
|
|
84
|
-
<td>{sample.data
|
|
71
|
+
<td>{sampleFatherLabel(sample.data)}</td>
|
|
72
|
+
<td>{sampleMotherLabel(sample.data)}</td>
|
|
85
73
|
<td>{sample.data.proband === true ? "True" : "False"}</td>
|
|
86
|
-
<td>{
|
|
87
|
-
<td>{
|
|
74
|
+
<td>{getSampleSexLabel(sample.data)}</td>
|
|
75
|
+
<td>{getSampleAffectedStatusLabel(sample.data)}</td>
|
|
88
76
|
<td>
|
|
89
|
-
<For each={
|
|
77
|
+
<For each={samplePhenotypes(sample.data)}>
|
|
90
78
|
{(phenotypicFeature: PhenotypicFeature, i) => (
|
|
91
79
|
<>
|
|
92
80
|
{i() > 0 ? ", " : ""}
|
|
@@ -96,11 +84,9 @@ export const SampleTable: Component<{
|
|
|
96
84
|
</For>
|
|
97
85
|
</td>
|
|
98
86
|
<td>
|
|
99
|
-
<Show when={
|
|
87
|
+
<Show when={samplePhenotypes(sample.data).length > 0}>
|
|
100
88
|
<Anchor
|
|
101
|
-
href={`https://vibe.molgeniscloud.org/?phenotypes=${
|
|
102
|
-
sample.data.person.individualId,
|
|
103
|
-
)
|
|
89
|
+
href={`https://vibe.molgeniscloud.org/?phenotypes=${samplePhenotypes(sample.data)
|
|
104
90
|
.map((feature) => feature.type.id)
|
|
105
91
|
.join(",")}`}
|
|
106
92
|
>
|
|
@@ -108,6 +94,11 @@ export const SampleTable: Component<{
|
|
|
108
94
|
</Anchor>
|
|
109
95
|
</Show>
|
|
110
96
|
</td>
|
|
97
|
+
<td>
|
|
98
|
+
<button class="button is-primary py-1" onClick={() => navigate(`/samples/${sample.id}/variants`)}>
|
|
99
|
+
Explore Variants
|
|
100
|
+
</button>
|
|
101
|
+
</td>
|
|
111
102
|
</tr>
|
|
112
103
|
)}
|
|
113
104
|
</For>
|
|
@@ -11,6 +11,14 @@ import { FieldMetadata } from "@molgenis/vip-report-vcf/src/MetadataParser";
|
|
|
11
11
|
import { FieldHeader } from "./FieldHeader";
|
|
12
12
|
import { Abbr } from "./Abbr";
|
|
13
13
|
import { abbreviateHeader } from "../utils/field";
|
|
14
|
+
import {
|
|
15
|
+
getSampleAffectedStatusLabel,
|
|
16
|
+
getSampleFamilyMembersWithoutParents,
|
|
17
|
+
getSampleFather,
|
|
18
|
+
getSampleLabel,
|
|
19
|
+
getSampleMother,
|
|
20
|
+
getSampleSexLabel,
|
|
21
|
+
} from "../utils/sample";
|
|
14
22
|
|
|
15
23
|
export const VariantsSampleTable: Component<{
|
|
16
24
|
item: Item<Sample>;
|
|
@@ -28,26 +36,16 @@ export const VariantsSampleTable: Component<{
|
|
|
28
36
|
const [otherFamilyMembers, setOtherFamilyMembers] = createSignal<Sample[]>([]);
|
|
29
37
|
|
|
30
38
|
onMount(() => {
|
|
31
|
-
const familyMembers: Sample[] = [];
|
|
32
39
|
setProband(props.item.data);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
sample.person.individualId === (proband() as Sample).person.maternalId
|
|
37
|
-
) {
|
|
38
|
-
setMother(sample);
|
|
39
|
-
} else if (
|
|
40
|
-
(proband() as Sample).person.paternalId !== "0" &&
|
|
41
|
-
sample.person.individualId === (proband() as Sample).person.paternalId
|
|
42
|
-
) {
|
|
43
|
-
setFather(sample);
|
|
44
|
-
} else if (sample.person.individualId !== (proband() as Sample).person.individualId) {
|
|
45
|
-
familyMembers.push(sample);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
setOtherFamilyMembers(familyMembers);
|
|
40
|
+
setMother(getSampleMother(proband() as Sample, samples()));
|
|
41
|
+
setFather(getSampleFather(proband() as Sample, samples()));
|
|
42
|
+
setOtherFamilyMembers(getSampleFamilyMembersWithoutParents(proband() as Sample, samples()));
|
|
49
43
|
});
|
|
50
44
|
|
|
45
|
+
function getSampleHeaderDescription(sample: Sample): string {
|
|
46
|
+
return `${getSampleLabel(sample)}: ${getSampleSexLabel(sample)}, ${getSampleAffectedStatusLabel(sample)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
51
49
|
return (
|
|
52
50
|
<div style={{ display: "grid" }}>
|
|
53
51
|
{/* workaround for https://github.com/jgthms/bulma/issues/2572#issuecomment-523099776 */}
|
|
@@ -60,48 +58,28 @@ export const VariantsSampleTable: Component<{
|
|
|
60
58
|
<Show when={proband()} keyed>
|
|
61
59
|
{(proband) => (
|
|
62
60
|
<th>
|
|
63
|
-
<Abbr
|
|
64
|
-
title={`${
|
|
65
|
-
proband.person.individualId
|
|
66
|
-
}: ${proband.person.sex.toLowerCase()}, ${proband.person.affectedStatus.toLowerCase()}`}
|
|
67
|
-
value="Proband"
|
|
68
|
-
/>
|
|
61
|
+
<Abbr title={getSampleHeaderDescription(proband)} value="Proband" />
|
|
69
62
|
</th>
|
|
70
63
|
)}
|
|
71
64
|
</Show>
|
|
72
65
|
<Show when={mother()} keyed>
|
|
73
66
|
{(mother) => (
|
|
74
67
|
<th>
|
|
75
|
-
<Abbr
|
|
76
|
-
title={`${
|
|
77
|
-
mother.person.individualId
|
|
78
|
-
}: ${mother.person.sex.toLowerCase()}, ${mother.person.affectedStatus.toLowerCase()}`}
|
|
79
|
-
value="Mother"
|
|
80
|
-
/>
|
|
68
|
+
<Abbr title={getSampleHeaderDescription(mother)} value="Mother" />
|
|
81
69
|
</th>
|
|
82
70
|
)}
|
|
83
71
|
</Show>
|
|
84
72
|
<Show when={father()} keyed>
|
|
85
73
|
{(father) => (
|
|
86
74
|
<th>
|
|
87
|
-
<Abbr
|
|
88
|
-
title={`${
|
|
89
|
-
father.person.individualId
|
|
90
|
-
}: ${father.person.sex.toLowerCase()}, ${father.person.affectedStatus.toLowerCase()}`}
|
|
91
|
-
value="Father"
|
|
92
|
-
/>
|
|
75
|
+
<Abbr title={getSampleHeaderDescription(father)} value="Father" />
|
|
93
76
|
</th>
|
|
94
77
|
)}
|
|
95
78
|
</Show>
|
|
96
79
|
<For each={otherFamilyMembers()}>
|
|
97
80
|
{(sample: Sample) => (
|
|
98
81
|
<th>
|
|
99
|
-
<Abbr
|
|
100
|
-
title={`${
|
|
101
|
-
sample.person.individualId
|
|
102
|
-
}: ${sample.person.sex.toLowerCase()}, ${sample.person.affectedStatus.toLowerCase()}`}
|
|
103
|
-
value={abbreviateHeader(sample.person.individualId)}
|
|
104
|
-
/>
|
|
82
|
+
<Abbr title={getSampleHeaderDescription(sample)} value={abbreviateHeader(getSampleLabel(sample))} />
|
|
105
83
|
</th>
|
|
106
84
|
)}
|
|
107
85
|
</For>
|
package/src/store/index.tsx
CHANGED
|
@@ -67,16 +67,17 @@ export const Provider: ParentComponent = (props) => {
|
|
|
67
67
|
setState({ variants: { ...(state.variants || {}), page } });
|
|
68
68
|
},
|
|
69
69
|
setVariantsPageSize(pageSize: number) {
|
|
70
|
-
setState({ variants: { ...(state.variants || {}), pageSize } });
|
|
70
|
+
setState({ variants: { ...(state.variants || {}), pageSize, page: undefined } });
|
|
71
71
|
},
|
|
72
72
|
setVariantsSearchQuery(searchQuery: string) {
|
|
73
|
-
setState({ variants: { ...(state.variants || {}), searchQuery } });
|
|
73
|
+
setState({ variants: { ...(state.variants || {}), searchQuery, page: undefined } });
|
|
74
74
|
},
|
|
75
75
|
setVariantsFilterQuery(query: Query, key: string) {
|
|
76
76
|
setState({
|
|
77
77
|
variants: {
|
|
78
78
|
...(state.variants || {}),
|
|
79
79
|
filterQueries: { ...(state.variants?.filterQueries || {}), [key]: query },
|
|
80
|
+
page: undefined,
|
|
80
81
|
},
|
|
81
82
|
});
|
|
82
83
|
},
|
|
@@ -85,6 +86,7 @@ export const Provider: ParentComponent = (props) => {
|
|
|
85
86
|
variants: {
|
|
86
87
|
...(state.variants || {}),
|
|
87
88
|
filterQueries: { ...(state.variants?.filterQueries || {}), [key]: undefined },
|
|
89
|
+
page: undefined,
|
|
88
90
|
},
|
|
89
91
|
});
|
|
90
92
|
},
|
|
@@ -162,10 +164,10 @@ export const Provider: ParentComponent = (props) => {
|
|
|
162
164
|
setState({ samples: { ...(state.samples || {}), page } });
|
|
163
165
|
},
|
|
164
166
|
setSampleSearchQuery(searchQuery: string) {
|
|
165
|
-
setState({ samples: { ...(state.samples || {}), searchQuery } });
|
|
167
|
+
setState({ samples: { ...(state.samples || {}), searchQuery, page: undefined } });
|
|
166
168
|
},
|
|
167
169
|
setSampleProbandFilterValue(probandFilterValue: boolean) {
|
|
168
|
-
setState({ samples: { ...(state.samples || {}), probandFilterValue } });
|
|
170
|
+
setState({ samples: { ...(state.samples || {}), probandFilterValue, page: undefined } });
|
|
169
171
|
},
|
|
170
172
|
};
|
|
171
173
|
const store: AppStore = [state, actions];
|
package/src/utils/sample.ts
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Sample } from "@molgenis/vip-report-api/src/Api";
|
|
2
2
|
|
|
3
|
-
export function getSampleLabel(sample:
|
|
4
|
-
return sample.
|
|
3
|
+
export function getSampleLabel(sample: Sample) {
|
|
4
|
+
return sample.person.individualId;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getSampleSexLabel(sample: Sample): string {
|
|
8
|
+
switch (sample.person.sex) {
|
|
9
|
+
case "FEMALE":
|
|
10
|
+
return "female";
|
|
11
|
+
case "MALE":
|
|
12
|
+
return "male";
|
|
13
|
+
default:
|
|
14
|
+
return "?";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSampleAffectedStatusLabel(sample: Sample): string {
|
|
19
|
+
switch (sample.person.affectedStatus) {
|
|
20
|
+
case "AFFECTED":
|
|
21
|
+
return "affected";
|
|
22
|
+
case "UNAFFECTED":
|
|
23
|
+
return "unaffected";
|
|
24
|
+
default:
|
|
25
|
+
return "?";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isFamily(sample: Sample, samples: Sample): boolean {
|
|
30
|
+
return sample.person.familyId === samples.person.familyId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isSampleMother(sample: Sample, samples: Sample): boolean {
|
|
34
|
+
return isFamily(sample, samples) && samples.person.individualId === sample.person.maternalId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getSampleMother(sample: Sample, samples: Sample[]): Sample | undefined {
|
|
38
|
+
if (sample.person.maternalId !== "0") {
|
|
39
|
+
for (const otherSample of samples) {
|
|
40
|
+
if (isSampleMother(sample, otherSample)) return otherSample;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isSampleFather(sample: Sample, samples: Sample): boolean {
|
|
46
|
+
return isFamily(sample, samples) && samples.person.individualId === sample.person.paternalId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getSampleFather(sample: Sample, samples: Sample[]): Sample | undefined {
|
|
50
|
+
if (sample.person.paternalId !== "0") {
|
|
51
|
+
for (const otherSample of samples) {
|
|
52
|
+
if (isSampleFather(sample, otherSample)) return otherSample;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getSampleFamilyMembersWithoutParents(sample: Sample, samples: Sample[]): Sample[] {
|
|
58
|
+
const familyMembersWithoutParents: Sample[] = [];
|
|
59
|
+
for (const otherSample of samples) {
|
|
60
|
+
if (isFamily(sample, otherSample) && !isSampleFather(sample, otherSample) && !isSampleMother(sample, otherSample)) {
|
|
61
|
+
if (sample.person.individualId !== otherSample.person.individualId) familyMembersWithoutParents.push(otherSample);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return familyMembersWithoutParents;
|
|
5
65
|
}
|
package/src/views/Help.tsx
CHANGED
|
@@ -1,13 +1,138 @@
|
|
|
1
|
-
import { Component } from "solid-js";
|
|
1
|
+
import { Component, createResource, createSignal, For, Show } from "solid-js";
|
|
2
2
|
import { Anchor } from "../components/Anchor";
|
|
3
|
+
import {
|
|
4
|
+
EMPTY_APP_METADATA,
|
|
5
|
+
EMPTY_HTS_FILE_METADATA,
|
|
6
|
+
EMPTY_PARAMS,
|
|
7
|
+
EMPTY_PHENOTYPES,
|
|
8
|
+
EMPTY_RECORDS_METADATA,
|
|
9
|
+
EMPTY_RECORDS_PAGE,
|
|
10
|
+
EMPTY_SAMPLES_PAGE,
|
|
11
|
+
fetchAppMetadata,
|
|
12
|
+
fetchHtsFileMetadata,
|
|
13
|
+
fetchPhenotypes,
|
|
14
|
+
fetchRecords,
|
|
15
|
+
fetchRecordsMeta,
|
|
16
|
+
fetchSamples,
|
|
17
|
+
} from "../utils/ApiUtils";
|
|
18
|
+
import { Loader } from "../components/Loader";
|
|
19
|
+
import { Breadcrumb } from "../components/Breadcrumb";
|
|
20
|
+
import { VcfHeaderRow } from "../components/VcfHeaderRow";
|
|
21
|
+
import { getHeaderValue } from "../utils/viewUtils";
|
|
22
|
+
import { Item, Phenotype, PhenotypicFeature } from "@molgenis/vip-report-api/src/Api";
|
|
23
|
+
import { HpoTerm } from "../components/HpoTerm";
|
|
3
24
|
export const Help: Component = () => {
|
|
25
|
+
const [params] = createSignal(EMPTY_PARAMS);
|
|
26
|
+
const [samples] = createResource(params, fetchSamples, { initialValue: EMPTY_SAMPLES_PAGE });
|
|
27
|
+
const [records] = createResource(params, fetchRecords, { initialValue: EMPTY_RECORDS_PAGE });
|
|
28
|
+
const [recordsMetadata] = createResource(params, fetchRecordsMeta, { initialValue: EMPTY_RECORDS_METADATA });
|
|
29
|
+
const [phenotypes] = createResource(params, fetchPhenotypes, { initialValue: EMPTY_PHENOTYPES });
|
|
30
|
+
const [htsFileMetadata] = createResource(params, fetchHtsFileMetadata, { initialValue: EMPTY_HTS_FILE_METADATA });
|
|
31
|
+
const [appMetadata] = createResource(params, fetchAppMetadata, { initialValue: EMPTY_APP_METADATA });
|
|
4
32
|
return (
|
|
5
33
|
<>
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
34
|
+
<Breadcrumb items={[{ text: "Help" }]} />
|
|
35
|
+
<div class="columns">
|
|
36
|
+
<div class="column">
|
|
37
|
+
<p class="title is-3">Documentation</p>
|
|
38
|
+
<span>The documentation for the Variant Interpretation Pipeline is located </span>
|
|
39
|
+
<Anchor href={`https://molgenis.github.io/vip/`}>
|
|
40
|
+
<span>here</span>
|
|
41
|
+
</Anchor>
|
|
42
|
+
<span>.</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<Show
|
|
47
|
+
when={
|
|
48
|
+
!samples.loading &&
|
|
49
|
+
!records.loading &&
|
|
50
|
+
!recordsMetadata.loading &&
|
|
51
|
+
!phenotypes.loading &&
|
|
52
|
+
!htsFileMetadata.loading &&
|
|
53
|
+
!appMetadata.loading
|
|
54
|
+
}
|
|
55
|
+
fallback={<Loader />}
|
|
56
|
+
>
|
|
57
|
+
<p class="title is-3">About</p>
|
|
58
|
+
<div class="columns">
|
|
59
|
+
<div class="column">
|
|
60
|
+
<div class="table-container">
|
|
61
|
+
<table class="table is-narrow">
|
|
62
|
+
<thead>
|
|
63
|
+
<tr>
|
|
64
|
+
<th colSpan={2}>Software metadata</th>
|
|
65
|
+
</tr>
|
|
66
|
+
</thead>
|
|
67
|
+
<tbody>
|
|
68
|
+
<tr>
|
|
69
|
+
<th>Name:</th>
|
|
70
|
+
<td>{appMetadata().name}</td>
|
|
71
|
+
</tr>
|
|
72
|
+
<tr>
|
|
73
|
+
<th>Version:</th>
|
|
74
|
+
<td>{appMetadata().version}</td>
|
|
75
|
+
</tr>
|
|
76
|
+
<tr>
|
|
77
|
+
<th>Arguments:</th>
|
|
78
|
+
<td>{appMetadata().args}</td>
|
|
79
|
+
</tr>
|
|
80
|
+
<VcfHeaderRow value={getHeaderValue("VIP_Version", recordsMetadata().lines)} title={"VIP Version"} />
|
|
81
|
+
<VcfHeaderRow value={getHeaderValue("VIP_Command", recordsMetadata().lines)} title={"VIP Command"} />
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="table-container">
|
|
86
|
+
<table class="table is-narrow">
|
|
87
|
+
<thead>
|
|
88
|
+
<tr>
|
|
89
|
+
<th colSpan={2}>Input metadata</th>
|
|
90
|
+
</tr>
|
|
91
|
+
</thead>
|
|
92
|
+
<tbody>
|
|
93
|
+
<tr>
|
|
94
|
+
<th>Filename:</th>
|
|
95
|
+
<td>{htsFileMetadata().uri}</td>
|
|
96
|
+
</tr>
|
|
97
|
+
<tr>
|
|
98
|
+
<th>Assembly:</th>
|
|
99
|
+
<td>{htsFileMetadata().genomeAssembly}</td>
|
|
100
|
+
</tr>
|
|
101
|
+
<tr>
|
|
102
|
+
<th>Number of records:</th>
|
|
103
|
+
<td>{records().total}</td>
|
|
104
|
+
</tr>
|
|
105
|
+
<tr>
|
|
106
|
+
<th>Number of samples:</th>
|
|
107
|
+
<td>{samples().total}</td>
|
|
108
|
+
</tr>
|
|
109
|
+
<tr>
|
|
110
|
+
<th>Phenotypes:</th>
|
|
111
|
+
<td>
|
|
112
|
+
<For each={phenotypes().items}>
|
|
113
|
+
{(item: Item<Phenotype>) => (
|
|
114
|
+
<>
|
|
115
|
+
<b>{item.data.subject.id}: </b>
|
|
116
|
+
<For each={item.data.phenotypicFeaturesList}>
|
|
117
|
+
{(phenotypicFeature: PhenotypicFeature, i) => (
|
|
118
|
+
<>
|
|
119
|
+
{i() > 0 ? ", " : ""}
|
|
120
|
+
<HpoTerm ontologyClass={phenotypicFeature.type} />
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</For>
|
|
124
|
+
<br />
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
</For>
|
|
128
|
+
</td>
|
|
129
|
+
</tr>
|
|
130
|
+
</tbody>
|
|
131
|
+
</table>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</Show>
|
|
11
136
|
</>
|
|
12
137
|
);
|
|
13
138
|
};
|
package/src/views/Home.tsx
CHANGED
|
@@ -1,121 +1,33 @@
|
|
|
1
|
-
import { Component
|
|
1
|
+
import { Component } from "solid-js";
|
|
2
2
|
import { Breadcrumb } from "../components/Breadcrumb";
|
|
3
|
-
import {
|
|
4
|
-
import { HpoTerm } from "../components/HpoTerm";
|
|
5
|
-
import {
|
|
6
|
-
EMPTY_APP_METADATA,
|
|
7
|
-
EMPTY_HTS_FILE_METADATA,
|
|
8
|
-
EMPTY_PARAMS,
|
|
9
|
-
EMPTY_PHENOTYPES,
|
|
10
|
-
EMPTY_RECORDS_METADATA,
|
|
11
|
-
EMPTY_RECORDS_PAGE,
|
|
12
|
-
EMPTY_SAMPLES_PAGE,
|
|
13
|
-
fetchAppMetadata,
|
|
14
|
-
fetchHtsFileMetadata,
|
|
15
|
-
fetchPhenotypes,
|
|
16
|
-
fetchRecords,
|
|
17
|
-
fetchRecordsMeta,
|
|
18
|
-
fetchSamples,
|
|
19
|
-
} from "../utils/ApiUtils";
|
|
20
|
-
import { getHeaderValue } from "../utils/viewUtils";
|
|
21
|
-
import { Loader } from "../components/Loader";
|
|
22
|
-
import { VcfHeaderRow } from "../components/VcfHeaderRow";
|
|
3
|
+
import { useNavigate } from "@solidjs/router";
|
|
23
4
|
|
|
24
5
|
export const Home: Component = () => {
|
|
25
|
-
const
|
|
26
|
-
const [samples] = createResource(params, fetchSamples, { initialValue: EMPTY_SAMPLES_PAGE });
|
|
27
|
-
const [records] = createResource(params, fetchRecords, { initialValue: EMPTY_RECORDS_PAGE });
|
|
28
|
-
const [recordsMetadata] = createResource(params, fetchRecordsMeta, { initialValue: EMPTY_RECORDS_METADATA });
|
|
29
|
-
const [phenotypes] = createResource(params, fetchPhenotypes, { initialValue: EMPTY_PHENOTYPES });
|
|
30
|
-
const [htsFileMetadata] = createResource(params, fetchHtsFileMetadata, { initialValue: EMPTY_HTS_FILE_METADATA });
|
|
31
|
-
const [appMetadata] = createResource(params, fetchAppMetadata, { initialValue: EMPTY_APP_METADATA });
|
|
6
|
+
const navigate = useNavigate();
|
|
32
7
|
|
|
33
8
|
return (
|
|
34
|
-
|
|
35
|
-
when={
|
|
36
|
-
!samples.loading &&
|
|
37
|
-
!records.loading &&
|
|
38
|
-
!recordsMetadata.loading &&
|
|
39
|
-
!phenotypes.loading &&
|
|
40
|
-
!htsFileMetadata.loading &&
|
|
41
|
-
!appMetadata.loading
|
|
42
|
-
}
|
|
43
|
-
fallback={<Loader />}
|
|
44
|
-
>
|
|
9
|
+
<>
|
|
45
10
|
<Breadcrumb items={[]} />
|
|
46
|
-
<div class="
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</tr>
|
|
66
|
-
<VcfHeaderRow value={getHeaderValue("VIP_Version", recordsMetadata().lines)} title={"VIP Version"} />
|
|
67
|
-
<VcfHeaderRow value={getHeaderValue("VIP_Command", recordsMetadata().lines)} title={"VIP Command"} />
|
|
68
|
-
</tbody>
|
|
69
|
-
</table>
|
|
11
|
+
<div class="columns is-centered">
|
|
12
|
+
<div class="column is-three-quarters-widescreen">
|
|
13
|
+
<p class="title is-2">Report</p>
|
|
14
|
+
<p class="subtitle is-4">
|
|
15
|
+
Analyze annotated, classified and filtered variants to solve rare-disease patients
|
|
16
|
+
</p>
|
|
17
|
+
<div class="columns">
|
|
18
|
+
<div class="column">
|
|
19
|
+
<div class="buttons are-large">
|
|
20
|
+
<button class="button is-large" onClick={() => navigate("/variants")}>
|
|
21
|
+
Explore Variants without samples
|
|
22
|
+
</button>
|
|
23
|
+
<button class="button is-large is-primary" onClick={() => navigate("/samples")}>
|
|
24
|
+
Explore Variants for samples
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
70
30
|
</div>
|
|
71
|
-
|
|
72
|
-
<table class="table is-narrow">
|
|
73
|
-
<thead>
|
|
74
|
-
<tr>
|
|
75
|
-
<th colSpan={2}>Input metadata</th>
|
|
76
|
-
</tr>
|
|
77
|
-
</thead>
|
|
78
|
-
<tbody>
|
|
79
|
-
<tr>
|
|
80
|
-
<th>Filename:</th>
|
|
81
|
-
<td>{htsFileMetadata().uri}</td>
|
|
82
|
-
</tr>
|
|
83
|
-
<tr>
|
|
84
|
-
<th>Assembly:</th>
|
|
85
|
-
<td>{htsFileMetadata().genomeAssembly}</td>
|
|
86
|
-
</tr>
|
|
87
|
-
<tr>
|
|
88
|
-
<th>Number of records:</th>
|
|
89
|
-
<td>{records().total}</td>
|
|
90
|
-
</tr>
|
|
91
|
-
<tr>
|
|
92
|
-
<th>Number of samples:</th>
|
|
93
|
-
<td>{samples().total}</td>
|
|
94
|
-
</tr>
|
|
95
|
-
<tr>
|
|
96
|
-
<th>Phenotypes:</th>
|
|
97
|
-
<td>
|
|
98
|
-
<For each={phenotypes().items}>
|
|
99
|
-
{(item: Item<Phenotype>) => (
|
|
100
|
-
<>
|
|
101
|
-
<b>{item.data.subject.id}: </b>
|
|
102
|
-
<For each={item.data.phenotypicFeaturesList}>
|
|
103
|
-
{(phenotypicFeature: PhenotypicFeature, i) => (
|
|
104
|
-
<>
|
|
105
|
-
{i() > 0 ? ", " : ""}
|
|
106
|
-
<HpoTerm ontologyClass={phenotypicFeature.type} />
|
|
107
|
-
</>
|
|
108
|
-
)}
|
|
109
|
-
</For>
|
|
110
|
-
<br />
|
|
111
|
-
</>
|
|
112
|
-
)}
|
|
113
|
-
</For>
|
|
114
|
-
</td>
|
|
115
|
-
</tr>
|
|
116
|
-
</tbody>
|
|
117
|
-
</table>
|
|
118
|
-
</div>
|
|
119
|
-
</Show>
|
|
31
|
+
</>
|
|
120
32
|
);
|
|
121
33
|
};
|
|
@@ -28,7 +28,7 @@ export const SampleVariantView: Component = () => {
|
|
|
28
28
|
<Breadcrumb
|
|
29
29
|
items={[
|
|
30
30
|
{ href: "/samples", text: "Samples" },
|
|
31
|
-
{ href: `/samples/${sample()!.id}`, text: getSampleLabel(sample()
|
|
31
|
+
{ href: `/samples/${sample()!.id}`, text: getSampleLabel(sample()!.data) },
|
|
32
32
|
{ href: `/samples/${sample()!.id}/variants`, text: "Variants" },
|
|
33
33
|
{ text: getRecordLabel(variant()!) },
|
|
34
34
|
]}
|
|
@@ -35,7 +35,7 @@ export const SampleVariantConsequenceView: Component = () => {
|
|
|
35
35
|
<Breadcrumb
|
|
36
36
|
items={[
|
|
37
37
|
{ href: "/samples", text: "Samples" },
|
|
38
|
-
{ href: `/samples/${sample()!.id}`, text: getSampleLabel(sample()
|
|
38
|
+
{ href: `/samples/${sample()!.id}`, text: getSampleLabel(sample()!.data) },
|
|
39
39
|
{ href: `/samples/${sample()!.id}/variants`, text: "Variants" },
|
|
40
40
|
{ href: `/samples/${sample()!.id}/variants/${variant()!.id}`, text: getRecordLabel(variant()!) },
|
|
41
41
|
{ text: `Consequence #${consequenceId}` },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, createMemo, createResource, Show } from "solid-js";
|
|
1
|
+
import { Component, createMemo, createResource, createSignal, onMount, Show } from "solid-js";
|
|
2
2
|
import { useRouteData } from "@solidjs/router";
|
|
3
3
|
import {
|
|
4
4
|
HtsFileMetadata,
|
|
@@ -38,7 +38,14 @@ import { DIRECTION_ASCENDING, DIRECTION_DESCENDING } from "../utils/sortUtils";
|
|
|
38
38
|
import { SampleRouteData } from "./data/SampleData";
|
|
39
39
|
import { useStore } from "../store";
|
|
40
40
|
import { Metadata } from "@molgenis/vip-report-vcf/src/Vcf";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
getSampleAffectedStatusLabel,
|
|
43
|
+
getSampleFamilyMembersWithoutParents,
|
|
44
|
+
getSampleFather,
|
|
45
|
+
getSampleLabel,
|
|
46
|
+
getSampleMother,
|
|
47
|
+
getSampleSexLabel,
|
|
48
|
+
} from "../utils/sample";
|
|
42
49
|
import { arrayEquals } from "../utils/utils";
|
|
43
50
|
import { getAllelicBalanceQuery } from "../components/filter/FilterAllelicBalance";
|
|
44
51
|
import { RecordsPerPage, RecordsPerPageEvent } from "../components/RecordsPerPage";
|
|
@@ -56,7 +63,7 @@ export const SampleVariantsView: Component = () => {
|
|
|
56
63
|
<Breadcrumb
|
|
57
64
|
items={[
|
|
58
65
|
{ href: "/samples", text: "Samples" },
|
|
59
|
-
{ href: `/samples/${sample()!.id}`, text: getSampleLabel(sample()
|
|
66
|
+
{ href: `/samples/${sample()!.id}`, text: getSampleLabel(sample()!.data) },
|
|
60
67
|
{ text: "Variants" },
|
|
61
68
|
]}
|
|
62
69
|
/>
|
|
@@ -82,6 +89,20 @@ export const SampleVariants: Component<{
|
|
|
82
89
|
}> = (props) => {
|
|
83
90
|
const [state, actions] = useStore();
|
|
84
91
|
|
|
92
|
+
const samples = createMemo(() => [props.sample.data, ...props.pedigreeSamples.map((item) => item.data)]);
|
|
93
|
+
|
|
94
|
+
const [proband, setProband] = createSignal<Sample | undefined>();
|
|
95
|
+
const [father, setFather] = createSignal<Sample | undefined>();
|
|
96
|
+
const [mother, setMother] = createSignal<Sample | undefined>();
|
|
97
|
+
const [otherFamilyMembers, setOtherFamilyMembers] = createSignal<Sample[]>([]);
|
|
98
|
+
|
|
99
|
+
onMount(() => {
|
|
100
|
+
setProband(props.sample.data);
|
|
101
|
+
setMother(getSampleMother(proband() as Sample, samples()));
|
|
102
|
+
setFather(getSampleFather(proband() as Sample, samples()));
|
|
103
|
+
setOtherFamilyMembers(getSampleFamilyMembersWithoutParents(proband() as Sample, samples()));
|
|
104
|
+
});
|
|
105
|
+
|
|
85
106
|
function getStateVariants() {
|
|
86
107
|
return state.sampleVariants ? state.sampleVariants[props.sample.id]?.variants : undefined;
|
|
87
108
|
}
|
|
@@ -311,6 +332,48 @@ export const SampleVariants: Component<{
|
|
|
311
332
|
},
|
|
312
333
|
]);
|
|
313
334
|
|
|
335
|
+
function getTitleSampleSexLabel(sample: Sample): string {
|
|
336
|
+
const label = getSampleSexLabel(sample);
|
|
337
|
+
return label !== "?" ? label : "sex:?";
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getTitleAffectedStatusLabel(sample: Sample): string {
|
|
341
|
+
const label = getSampleAffectedStatusLabel(sample);
|
|
342
|
+
return label !== "?" ? label : "affected status:?";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const title = (): string => {
|
|
346
|
+
return `Reported variants for ${getSampleLabel(props.sample.data)} (${getTitleSampleSexLabel(props.sample.data)} ${getTitleAffectedStatusLabel(props.sample.data)})`;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const subtitle = (): string | undefined => {
|
|
350
|
+
const sampleFather = father();
|
|
351
|
+
const sampleMother = mother();
|
|
352
|
+
const sampleOtherFamilyMembers = otherFamilyMembers();
|
|
353
|
+
|
|
354
|
+
if (sampleFather === undefined && sampleMother === undefined && sampleOtherFamilyMembers.length === 0) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const tokens: string[] = [];
|
|
359
|
+
if (sampleMother !== undefined) {
|
|
360
|
+
tokens.push(`mother (${getTitleAffectedStatusLabel(sampleMother)})`);
|
|
361
|
+
}
|
|
362
|
+
if (sampleFather !== undefined) {
|
|
363
|
+
tokens.push(`father (${getTitleAffectedStatusLabel(sampleFather)})`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const familyMember of sampleOtherFamilyMembers) {
|
|
367
|
+
tokens.push(
|
|
368
|
+
`${getSampleLabel(familyMember)} (${getTitleSampleSexLabel(familyMember)} ${getTitleAffectedStatusLabel(familyMember)})`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let str = tokens.pop() as string;
|
|
373
|
+
if (tokens.length > 0) str = `${tokens.join(", ")} and ${str}`;
|
|
374
|
+
return `Includes genotypes for ${str}`;
|
|
375
|
+
};
|
|
376
|
+
|
|
314
377
|
return (
|
|
315
378
|
<div class="columns is-variable is-1">
|
|
316
379
|
<div class="column is-1-fullhd is-2">
|
|
@@ -324,6 +387,14 @@ export const SampleVariants: Component<{
|
|
|
324
387
|
/>
|
|
325
388
|
</div>
|
|
326
389
|
<div class="column">
|
|
390
|
+
<div class="columns is-gapless">
|
|
391
|
+
<div class="column">
|
|
392
|
+
<p class="title is-3">{title()}</p>
|
|
393
|
+
<Show when={subtitle()} keyed>
|
|
394
|
+
{(subtitle) => <p class="subtitle is-5">{subtitle}</p>}
|
|
395
|
+
</Show>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
327
398
|
<div class="columns is-gapless">
|
|
328
399
|
<div class="column is-offset-1-fullhd is-3-fullhd is-4">
|
|
329
400
|
<Show when={records()} fallback={<Loader />} keyed>
|
package/src/views/Samples.tsx
CHANGED
|
@@ -75,42 +75,44 @@ export const Samples: Component = () => {
|
|
|
75
75
|
return (
|
|
76
76
|
<>
|
|
77
77
|
<Breadcrumb items={[{ text: "Samples" }]} />
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
<div class="columns is-centered">
|
|
79
|
+
<div class="column is-three-quarters-widescreen">
|
|
80
|
+
<p class="title is-2">Samples</p>
|
|
81
|
+
<p class="subtitle is-4">Explore samples and sample variants</p>
|
|
82
|
+
<Show when={samples()} fallback={<Loader />} keyed>
|
|
83
|
+
{(samples) => (
|
|
84
|
+
<>
|
|
85
|
+
<div class="columns">
|
|
86
|
+
<div class="column is-4 is-offset-3">{<Pager page={samples.page} onPageChange={onPageChange} />}</div>
|
|
87
|
+
<div class="column is-2 is-offset-1">
|
|
88
|
+
{<span class="is-pulled-right">{samples.page.totalElements} records</span>}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
<div class="columns">
|
|
93
|
+
<div class="column is-2-widescreen is-3">
|
|
94
|
+
<SearchBox onInput={onSearchChange} value={state.samples?.searchQuery} />
|
|
95
|
+
<p class="has-text-weight-semibold">Proband</p>
|
|
96
|
+
<div class="field">
|
|
97
|
+
<div class="control">
|
|
98
|
+
<Checkbox
|
|
99
|
+
value={"proband"}
|
|
100
|
+
label=""
|
|
101
|
+
onChange={onProbandFilterChange}
|
|
102
|
+
checked={state.samples?.probandFilterValue}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="column">
|
|
108
|
+
{!phenotypes.loading && <SampleTable samples={samples.items} phenotypes={phenotypes().items} />}
|
|
100
109
|
</div>
|
|
101
110
|
</div>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
</span>
|
|
108
|
-
{!phenotypes.loading && <SampleTable samples={samples.items} phenotypes={phenotypes().items} />}
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
</>
|
|
112
|
-
)}
|
|
113
|
-
</Show>
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
</Show>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
114
116
|
</>
|
|
115
117
|
);
|
|
116
118
|
};
|
package/src/views/Variants.tsx
CHANGED
|
@@ -93,6 +93,12 @@ export const Variants: Component<{
|
|
|
93
93
|
/>
|
|
94
94
|
</div>
|
|
95
95
|
<div class="column">
|
|
96
|
+
<div class="columns is-gapless">
|
|
97
|
+
<div class="column">
|
|
98
|
+
<p class="title is-3">Reported variants</p>
|
|
99
|
+
<p class="subtitle is-5">Includes all reported variants without genotypes</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
96
102
|
<div class="columns is-gapless">
|
|
97
103
|
<div class="column is-offset-1-fullhd is-3-fullhd is-4">
|
|
98
104
|
<Show when={records()} fallback={<Loader />} keyed>
|