@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molgenis/vip-report-template",
3
- "version": "6.0.1",
3
+ "version": "6.0.2",
4
4
  "description": "Report Template for Variant Call Format (VCF) Report Generator",
5
5
  "scripts": {
6
6
  "build": "vite build",
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
- VCF Report
48
+ Variant Interpretation Pipeline
49
49
  </Link>
50
50
  </div>
51
51
  <div class="navbar-menu">
52
- <div class="navbar-start">
53
- <Link class="navbar-item" href="/samples">
54
- Samples
52
+ <div class="navbar-item has-dropdown is-hoverable">
53
+ <Link class="navbar-link" href="/">
54
+ Report
55
55
  </Link>
56
- <Link class="navbar-item" href="/variants">
57
- Variants
58
- </Link>
59
- {api.isDatasetSupport() && <DatasetDropdown />}
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
- function getAffectedStatusLabel(affectedStatus: string) {
8
- let label;
9
- switch (affectedStatus) {
10
- case "AFFECTED":
11
- label = "Affected";
12
- break;
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
- function getPhenotypes(sampleId: string) {
54
- return phenoMap()[sampleId] !== undefined ? phenoMap()[sampleId] : [];
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.person.individualId}</Link>
69
+ <Link href={`/samples/${sample.id}`}>{getSampleLabel(sample.data)}</Link>
82
70
  </td>
83
- <td>{sample.data.person.paternalId}</td>
84
- <td>{sample.data.person.maternalId}</td>
71
+ <td>{sampleFatherLabel(sample.data)}</td>
72
+ <td>{sampleMotherLabel(sample.data)}</td>
85
73
  <td>{sample.data.proband === true ? "True" : "False"}</td>
86
- <td>{getSexLabel(sample.data.person.sex)}</td>
87
- <td>{getAffectedStatusLabel(sample.data.person.affectedStatus)}</td>
74
+ <td>{getSampleSexLabel(sample.data)}</td>
75
+ <td>{getSampleAffectedStatusLabel(sample.data)}</td>
88
76
  <td>
89
- <For each={getPhenotypes(sample.data.person.individualId)}>
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={getPhenotypes(sample.data.person.individualId).length > 0}>
87
+ <Show when={samplePhenotypes(sample.data).length > 0}>
100
88
  <Anchor
101
- href={`https://vibe.molgeniscloud.org/?phenotypes=${getPhenotypes(
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
- samples().forEach((sample) => {
34
- if (
35
- (proband() as Sample).person.maternalId !== "0" &&
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>
@@ -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];
@@ -1,5 +1,65 @@
1
- import { Item, Sample } from "@molgenis/vip-report-api/src/Api";
1
+ import { Sample } from "@molgenis/vip-report-api/src/Api";
2
2
 
3
- export function getSampleLabel(sample: Item<Sample>) {
4
- return sample.data.person.individualId;
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
  }
@@ -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
- <span>The documentation for the Variant Interpretation Pipeline is located </span>
7
- <Anchor href={`https://molgenis.github.io/vip/`}>
8
- <span>here</span>
9
- </Anchor>
10
- <span>.</span>
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
  };
@@ -1,121 +1,33 @@
1
- import { Component, createResource, createSignal, For, Show } from "solid-js";
1
+ import { Component } from "solid-js";
2
2
  import { Breadcrumb } from "../components/Breadcrumb";
3
- import { Item, Phenotype, PhenotypicFeature } from "@molgenis/vip-report-api/src/Api";
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 [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 });
6
+ const navigate = useNavigate();
32
7
 
33
8
  return (
34
- <Show
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="table-container">
47
- <table class="table is-narrow">
48
- <thead>
49
- <tr>
50
- <th colSpan={2}>Software metadata</th>
51
- </tr>
52
- </thead>
53
- <tbody>
54
- <tr>
55
- <th>Name:</th>
56
- <td>{appMetadata().name}</td>
57
- </tr>
58
- <tr>
59
- <th>Version:</th>
60
- <td>{appMetadata().version}</td>
61
- </tr>
62
- <tr>
63
- <th>Arguments:</th>
64
- <td>{appMetadata().args}</td>
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
- <div class="table-container">
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 { getSampleLabel } from "../utils/sample";
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>
@@ -75,42 +75,44 @@ export const Samples: Component = () => {
75
75
  return (
76
76
  <>
77
77
  <Breadcrumb items={[{ text: "Samples" }]} />
78
- <Show when={samples()} fallback={<Loader />} keyed>
79
- {(samples) => (
80
- <>
81
- <div class="columns">
82
- <div class="column is-4 is-offset-3">{<Pager page={samples.page} onPageChange={onPageChange} />}</div>
83
- <div class="column is-2 is-offset-1">
84
- {<span class="is-pulled-right">{samples.page.totalElements} records</span>}
85
- </div>
86
- </div>
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
- <div class="columns">
89
- <div class="column is-1-fullhd is-2">
90
- <SearchBox onInput={onSearchChange} value={state.samples?.searchQuery} />
91
- <p class="has-text-weight-semibold">Proband</p>
92
- <div class="field">
93
- <div class="control">
94
- <Checkbox
95
- value={"proband"}
96
- label=""
97
- onChange={onProbandFilterChange}
98
- checked={state.samples?.probandFilterValue}
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
- </div>
103
- <div class="column">
104
- <span class="is-italic">
105
- Click on an individual id for detailed information for this sample. In this screen a link to the
106
- variants for this sample is available.
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
  };
@@ -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>