@genspectrum/dashboard-components 0.1.1

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.
Files changed (186) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +109 -0
  3. package/custom-elements.json +1587 -0
  4. package/dist/dashboard-components.js +7322 -0
  5. package/dist/dashboard-components.js.map +1 -0
  6. package/dist/genspectrum-components.d.ts +298 -0
  7. package/dist/style.css +2930 -0
  8. package/package.json +109 -0
  9. package/src/constants.ts +6 -0
  10. package/src/index.ts +1 -0
  11. package/src/lapisApi/ReferenceGenome.ts +30 -0
  12. package/src/lapisApi/__mockData__/referenceGenome.json +58 -0
  13. package/src/lapisApi/lapisApi.ts +99 -0
  14. package/src/lapisApi/lapisTypes.ts +51 -0
  15. package/src/operator/Dataset.ts +3 -0
  16. package/src/operator/DivisionOperator.spec.ts +27 -0
  17. package/src/operator/DivisionOperator.ts +60 -0
  18. package/src/operator/FetchAggregatedOperator.ts +44 -0
  19. package/src/operator/FetchInsertionsOperator.ts +24 -0
  20. package/src/operator/FetchSubstitutionsOrDeletionsOperator.ts +49 -0
  21. package/src/operator/FillMissingOperator.spec.ts +26 -0
  22. package/src/operator/FillMissingOperator.ts +30 -0
  23. package/src/operator/GroupByAndSumOperator.spec.ts +26 -0
  24. package/src/operator/GroupByAndSumOperator.ts +26 -0
  25. package/src/operator/GroupByOperator.spec.ts +43 -0
  26. package/src/operator/GroupByOperator.ts +32 -0
  27. package/src/operator/MapOperator.spec.ts +13 -0
  28. package/src/operator/MapOperator.ts +16 -0
  29. package/src/operator/MockOperator.spec.ts +11 -0
  30. package/src/operator/MockOperator.ts +12 -0
  31. package/src/operator/Operator.ts +5 -0
  32. package/src/operator/SlidingOperator.spec.ts +52 -0
  33. package/src/operator/SlidingOperator.ts +23 -0
  34. package/src/operator/SortOperator.spec.ts +13 -0
  35. package/src/operator/SortOperator.ts +16 -0
  36. package/src/preact/LapisUrlContext.ts +3 -0
  37. package/src/preact/ReferenceGenomeContext.ts +5 -0
  38. package/src/preact/components/SegmentSelector.tsx +62 -0
  39. package/src/preact/components/chart.stories.tsx +42 -0
  40. package/src/preact/components/chart.tsx +32 -0
  41. package/src/preact/components/checkbox-selector.stories.tsx +56 -0
  42. package/src/preact/components/checkbox-selector.tsx +46 -0
  43. package/src/preact/components/confidence-interval-selector.tsx +45 -0
  44. package/src/preact/components/csv-download-button.stories.tsx +25 -0
  45. package/src/preact/components/csv-download-button.tsx +51 -0
  46. package/src/preact/components/error-display.stories.tsx +22 -0
  47. package/src/preact/components/error-display.tsx +5 -0
  48. package/src/preact/components/headline.stories.tsx +29 -0
  49. package/src/preact/components/headline.tsx +16 -0
  50. package/src/preact/components/info.stories.tsx +22 -0
  51. package/src/preact/components/info.tsx +16 -0
  52. package/src/preact/components/loading-display.stories.tsx +20 -0
  53. package/src/preact/components/loading-display.tsx +5 -0
  54. package/src/preact/components/min-max-percent-slider.css +40 -0
  55. package/src/preact/components/min-max-range-slider.tsx +95 -0
  56. package/src/preact/components/mutation-type-selector.tsx +30 -0
  57. package/src/preact/components/no-data-display.stories.tsx +20 -0
  58. package/src/preact/components/no-data-display.tsx +5 -0
  59. package/src/preact/components/percent-intput.tsx +49 -0
  60. package/src/preact/components/proportion-selector-dropdown.stories.tsx +66 -0
  61. package/src/preact/components/proportion-selector-dropdown.tsx +33 -0
  62. package/src/preact/components/proportion-selector.stories.tsx +81 -0
  63. package/src/preact/components/proportion-selector.tsx +43 -0
  64. package/src/preact/components/scaling-selector.stories.tsx +25 -0
  65. package/src/preact/components/scaling-selector.tsx +36 -0
  66. package/src/preact/components/select.stories.tsx +42 -0
  67. package/src/preact/components/select.tsx +21 -0
  68. package/src/preact/components/table.stories.tsx +24 -0
  69. package/src/preact/components/table.tsx +51 -0
  70. package/src/preact/components/tabs.stories.tsx +60 -0
  71. package/src/preact/components/tabs.tsx +49 -0
  72. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +32 -0
  73. package/src/preact/dateRangeSelector/date-range-selector.tsx +228 -0
  74. package/src/preact/dateRangeSelector/dateConversion.ts +8 -0
  75. package/src/preact/locationFilter/__mockData__/aggregated.json +775 -0
  76. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +36 -0
  77. package/src/preact/locationFilter/fetchAutocompletionList.ts +43 -0
  78. package/src/preact/locationFilter/location-filter.stories.tsx +50 -0
  79. package/src/preact/locationFilter/location-filter.tsx +112 -0
  80. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json +295 -0
  81. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json +304 -0
  82. package/src/preact/mutationComparison/fetchMutationData.spec.ts +118 -0
  83. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +125 -0
  84. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +40 -0
  85. package/src/preact/mutationComparison/mutation-comparison-table.tsx +43 -0
  86. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +122 -0
  87. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +152 -0
  88. package/src/preact/mutationComparison/mutation-comparison.tsx +179 -0
  89. package/src/preact/mutationComparison/queryMutationData.ts +53 -0
  90. package/src/preact/mutationFilter/mutation-filter.stories.tsx +164 -0
  91. package/src/preact/mutationFilter/mutation-filter.tsx +268 -0
  92. package/src/preact/mutationFilter/parseAndValidateMutation.ts +54 -0
  93. package/src/preact/mutationFilter/parseMutation.spec.ts +150 -0
  94. package/src/preact/mutationFilter/sequenceTypeFromSegment.spec.ts +66 -0
  95. package/src/preact/mutationFilter/sequenceTypeFromSegment.ts +20 -0
  96. package/src/preact/mutations/__mockData__/nucleotideInsertions.json +252 -0
  97. package/src/preact/mutations/__mockData__/nucleotideMutations.json +880 -0
  98. package/src/preact/mutations/getInsertionsTableData.spec.ts +36 -0
  99. package/src/preact/mutations/getInsertionsTableData.ts +10 -0
  100. package/src/preact/mutations/getMutationsGridData.spec.ts +135 -0
  101. package/src/preact/mutations/getMutationsGridData.ts +92 -0
  102. package/src/preact/mutations/getMutationsTableData.spec.ts +94 -0
  103. package/src/preact/mutations/getMutationsTableData.ts +17 -0
  104. package/src/preact/mutations/mutations-grid.tsx +84 -0
  105. package/src/preact/mutations/mutations-insertions-table.tsx +33 -0
  106. package/src/preact/mutations/mutations-table.tsx +47 -0
  107. package/src/preact/mutations/mutations.stories.tsx +95 -0
  108. package/src/preact/mutations/mutations.tsx +192 -0
  109. package/src/preact/mutations/queryMutations.ts +55 -0
  110. package/src/preact/prevalenceOverTime/__mockData__/denominator.json +1700 -0
  111. package/src/preact/prevalenceOverTime/__mockData__/denominatorOneVariant.json +608 -0
  112. package/src/preact/prevalenceOverTime/__mockData__/numeratorEG.json +1560 -0
  113. package/src/preact/prevalenceOverTime/__mockData__/numeratorJN1.json +592 -0
  114. package/src/preact/prevalenceOverTime/__mockData__/numeratorOneVariant.json +604 -0
  115. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.spec.ts +67 -0
  116. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.ts +18 -0
  117. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +105 -0
  118. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +86 -0
  119. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +141 -0
  120. package/src/preact/prevalenceOverTime/prevalence-over-time-table.tsx +46 -0
  121. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +165 -0
  122. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +202 -0
  123. package/src/preact/relativeGrowthAdvantage/__mockData__/denominator.json +376 -0
  124. package/src/preact/relativeGrowthAdvantage/__mockData__/numerator.json +332 -0
  125. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +138 -0
  126. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +71 -0
  127. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +136 -0
  128. package/src/preact/shared/charts/LogitScale.ts +48 -0
  129. package/src/preact/shared/charts/colors.ts +26 -0
  130. package/src/preact/shared/charts/confideceInterval.ts +29 -0
  131. package/src/preact/shared/charts/getYAxisScale.ts +16 -0
  132. package/src/preact/shared/charts/scales.ts +16 -0
  133. package/src/preact/shared/icons/DeleteIcon.tsx +17 -0
  134. package/src/preact/shared/sort/sortInsertions.spec.ts +47 -0
  135. package/src/preact/shared/sort/sortInsertions.ts +21 -0
  136. package/src/preact/shared/sort/sortMutationPositions.spec.ts +31 -0
  137. package/src/preact/shared/sort/sortMutationPositions.ts +14 -0
  138. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +47 -0
  139. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +17 -0
  140. package/src/preact/shared/table/formatProportion.ts +3 -0
  141. package/src/preact/textInput/__mockData__/aggregated_hosts.json +24 -0
  142. package/src/preact/textInput/fetchAutocompleteList.ts +9 -0
  143. package/src/preact/textInput/text-input.stories.tsx +49 -0
  144. package/src/preact/textInput/text-input.tsx +73 -0
  145. package/src/preact/useQuery.ts +27 -0
  146. package/src/query/queryInsertions.ts +14 -0
  147. package/src/query/queryPrevalenceOverTime.ts +126 -0
  148. package/src/query/queryRelativeGrowthAdvantage.ts +131 -0
  149. package/src/query/querySubstitutionsOrDeletions.ts +19 -0
  150. package/src/styles/tailwind.css +3 -0
  151. package/src/styles/tailwind.d.ts +3 -0
  152. package/src/types.ts +23 -0
  153. package/src/utils/mutations.spec.ts +64 -0
  154. package/src/utils/mutations.ts +165 -0
  155. package/src/utils/temporal.spec.ts +97 -0
  156. package/src/utils/temporal.ts +348 -0
  157. package/src/utils/test-utils.ts +5 -0
  158. package/src/utils/type-utils.ts +15 -0
  159. package/src/utils/utils.spec.ts +16 -0
  160. package/src/utils/utils.ts +38 -0
  161. package/src/web-components/PreactLitAdapter.tsx +62 -0
  162. package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +12 -0
  163. package/src/web-components/app.ts +51 -0
  164. package/src/web-components/display/index.ts +4 -0
  165. package/src/web-components/display/mutation-comparison-component.stories.ts +138 -0
  166. package/src/web-components/display/mutation-comparison-component.tsx +31 -0
  167. package/src/web-components/display/mutations-component.stories.ts +107 -0
  168. package/src/web-components/display/mutations-component.tsx +27 -0
  169. package/src/web-components/display/prevalence-over-time-component.stories.ts +205 -0
  170. package/src/web-components/display/prevalence-over-time-component.tsx +46 -0
  171. package/src/web-components/display/relative-growth-advantage-component.stories.ts +89 -0
  172. package/src/web-components/display/relative-growth-advantage-component.tsx +37 -0
  173. package/src/web-components/index.ts +3 -0
  174. package/src/web-components/input/date-range-selector-component.stories.ts +53 -0
  175. package/src/web-components/input/date-range-selector-component.tsx +33 -0
  176. package/src/web-components/input/index.ts +4 -0
  177. package/src/web-components/input/location-filter-component.stories.ts +184 -0
  178. package/src/web-components/input/location-filter-component.tsx +68 -0
  179. package/src/web-components/input/location-filter.mdx +25 -0
  180. package/src/web-components/input/mutation-filter-component.stories.ts +97 -0
  181. package/src/web-components/input/mutation-filter-component.tsx +27 -0
  182. package/src/web-components/input/text-input-component.stories.ts +92 -0
  183. package/src/web-components/input/text-input-component.tsx +30 -0
  184. package/src/web-components/lapis-context.ts +3 -0
  185. package/src/web-components/reference-genome-context.ts +5 -0
  186. package/src/web-components/withinShadowRoot.story.ts +34 -0
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { getInsertionsTableData } from './getInsertionsTableData';
4
+ import { Insertion } from '../../utils/mutations';
5
+
6
+ describe('getInsertionsTableData', () => {
7
+ test('should return the correct data', () => {
8
+ const data = [
9
+ {
10
+ type: 'insertion' as const,
11
+ mutation: new Insertion('segment1', 123, 'T'),
12
+ count: 1,
13
+ proportion: 0.1,
14
+ },
15
+ {
16
+ type: 'insertion' as const,
17
+ mutation: new Insertion('segment2', 234, 'AAA'),
18
+ count: 2,
19
+ proportion: 0.2,
20
+ },
21
+ ];
22
+
23
+ const result = getInsertionsTableData(data);
24
+
25
+ expect(result).toEqual([
26
+ {
27
+ insertion: 'ins_segment1:123:T',
28
+ count: 1,
29
+ },
30
+ {
31
+ insertion: 'ins_segment2:234:AAA',
32
+ count: 2,
33
+ },
34
+ ]);
35
+ });
36
+ });
@@ -0,0 +1,10 @@
1
+ import { type InsertionEntry } from '../../types';
2
+
3
+ export function getInsertionsTableData(data: InsertionEntry[]) {
4
+ return data.map((mutationEntry) => {
5
+ return {
6
+ insertion: mutationEntry.mutation.toString(),
7
+ count: mutationEntry.count,
8
+ };
9
+ });
10
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { type BasesData, getMutationsGridData, type MutationsGridDataRow } from './getMutationsGridData';
4
+ import { Deletion, Substitution } from '../../utils/mutations';
5
+
6
+ describe('getMutationsGridData', () => {
7
+ test('should return the correct data', () => {
8
+ const data = [
9
+ {
10
+ type: 'substitution' as const,
11
+ mutation: new Substitution(undefined, 'T', 'C', 123),
12
+ count: 1,
13
+ proportion: 0.9,
14
+ },
15
+ {
16
+ type: 'substitution' as const,
17
+ mutation: new Substitution(undefined, 'T', 'C', 234),
18
+ count: 1,
19
+ proportion: 0.8,
20
+ },
21
+ {
22
+ type: 'substitution' as const,
23
+ mutation: new Substitution(undefined, 'T', 'G', 234),
24
+ count: 1,
25
+ proportion: 0.05,
26
+ },
27
+ {
28
+ type: 'deletion' as const,
29
+ mutation: new Deletion(undefined, 'T', 234),
30
+ count: 2,
31
+ proportion: 0.1,
32
+ },
33
+ ];
34
+
35
+ const proportionInterval = { min: 0, max: 1 };
36
+
37
+ const result = getMutationsGridData(data, 'nucleotide', proportionInterval);
38
+
39
+ const expected = [
40
+ {
41
+ position: '123',
42
+ ...({
43
+ A: { proportion: 0, isReference: false },
44
+ C: { proportion: 0.9, isReference: false },
45
+ G: { proportion: 0, isReference: false },
46
+ T: { proportion: 0.1, isReference: true },
47
+ '-': { proportion: 0, isReference: false },
48
+ } as BasesData),
49
+ },
50
+ {
51
+ position: '234',
52
+ ...({
53
+ A: { proportion: 0, isReference: false },
54
+ C: { proportion: 0.8, isReference: false },
55
+ G: { proportion: 0.05, isReference: false },
56
+ T: { proportion: 0.05, isReference: true },
57
+ '-': { proportion: 0.1, isReference: false },
58
+ } as BasesData),
59
+ },
60
+ ] as MutationsGridDataRow[];
61
+
62
+ expectGridDataIsApproximatelyEqual(result, expected);
63
+ });
64
+
65
+ test('should filter out rows where all entries are below/above proportionInterval', () => {
66
+ const belowInterval = 0.03;
67
+ const aboveInterval = 0.95;
68
+ const inInterval = 0.5;
69
+
70
+ const data = [
71
+ {
72
+ type: 'substitution' as const,
73
+ mutation: new Substitution(undefined, 'T', 'C', 123),
74
+ count: 1,
75
+ proportion: aboveInterval,
76
+ },
77
+ {
78
+ type: 'substitution' as const,
79
+ mutation: new Substitution(undefined, 'T', 'C', 234),
80
+ count: 1,
81
+ proportion: inInterval,
82
+ },
83
+ {
84
+ type: 'substitution' as const,
85
+ mutation: new Substitution(undefined, 'T', 'G', 234),
86
+ count: 1,
87
+ proportion: belowInterval,
88
+ },
89
+ {
90
+ type: 'deletion' as const,
91
+ mutation: new Deletion(undefined, 'T', 234),
92
+ count: 2,
93
+ proportion: belowInterval,
94
+ },
95
+ ];
96
+
97
+ const proportionInterval = { min: 0.05, max: 0.9 };
98
+
99
+ const result = getMutationsGridData(data, 'nucleotide', proportionInterval);
100
+
101
+ const expectedBases: BasesData = {
102
+ A: { proportion: 0, isReference: false },
103
+ C: { proportion: inInterval, isReference: false },
104
+ G: { proportion: belowInterval, isReference: false },
105
+ T: { proportion: 0.44, isReference: true },
106
+ '-': { proportion: belowInterval, isReference: false },
107
+ };
108
+
109
+ const expected = [
110
+ {
111
+ position: '234',
112
+ ...expectedBases,
113
+ },
114
+ ] as MutationsGridDataRow[];
115
+
116
+ expectGridDataIsApproximatelyEqual(result, expected);
117
+ });
118
+
119
+ const expectGridDataIsApproximatelyEqual = (a: MutationsGridDataRow[], b: MutationsGridDataRow[]) => {
120
+ expect(a.length).toBe(b.length);
121
+ a.forEach((row, i) => expectRowsAreApproximatelyEqual(row, b[i]));
122
+ };
123
+
124
+ const expectRowsAreApproximatelyEqual = (a: MutationsGridDataRow, b: MutationsGridDataRow) => {
125
+ const { position: positionA, ...basesA } = a;
126
+ const { position: positionB, ...basesB } = b;
127
+ expect(positionA).toBe(positionB);
128
+
129
+ expect(Object.keys(basesA).length).toBe(Object.keys(basesB).length);
130
+ for (const base in basesA) {
131
+ expect(basesA[base].proportion).toBeCloseTo(basesB[base].proportion);
132
+ expect(basesA[base].isReference).toBe(basesB[base].isReference);
133
+ }
134
+ };
135
+ });
@@ -0,0 +1,92 @@
1
+ import { type BaseCell } from './mutations-grid';
2
+ import type { SequenceType, SubstitutionOrDeletionEntry } from '../../types';
3
+ import { bases } from '../../utils/mutations';
4
+ import { type ProportionInterval } from '../components/proportion-selector';
5
+ import { sortMutationPositions } from '../shared/sort/sortMutationPositions';
6
+
7
+ export const getMutationsGridData = (
8
+ data: SubstitutionOrDeletionEntry[],
9
+ sequenceType: SequenceType,
10
+ proportionInterval: ProportionInterval,
11
+ ) => {
12
+ return accumulateByPosition(data, sequenceType).filter((row) => byProportion(row, proportionInterval));
13
+ };
14
+
15
+ const accumulateByPosition = (data: SubstitutionOrDeletionEntry[], sequenceType: SequenceType) => {
16
+ const basesOfView = bases[sequenceType];
17
+ const positionsToProportionAtBase = new Map<string, Map<string | undefined, number>>();
18
+ const referenceBases = new Map<string, string | undefined>();
19
+
20
+ for (const mutationEntry of data) {
21
+ const position =
22
+ (mutationEntry.mutation.segment ? `${mutationEntry.mutation.segment}:` : '') +
23
+ mutationEntry.mutation.position;
24
+ referenceBases.set(position, mutationEntry.mutation.valueAtReference);
25
+
26
+ const initiallyFillPositionsToProportionAtBase = () => {
27
+ if (!positionsToProportionAtBase.has(position)) {
28
+ const empty = new Map();
29
+ basesOfView.forEach((base) => empty.set(base, 0));
30
+ empty.set(mutationEntry.mutation.valueAtReference, 1);
31
+ positionsToProportionAtBase.set(position, empty);
32
+ }
33
+ };
34
+ initiallyFillPositionsToProportionAtBase();
35
+
36
+ const substitutionValue =
37
+ mutationEntry.type === 'substitution' ? mutationEntry.mutation.substitutionValue : '-';
38
+
39
+ const subtractSubstitutionValue = () => {
40
+ const proportionAtBase = positionsToProportionAtBase.get(position)!;
41
+ proportionAtBase.set(substitutionValue, mutationEntry.proportion);
42
+ proportionAtBase.set(
43
+ mutationEntry.mutation.valueAtReference,
44
+ proportionAtBase.get(mutationEntry.mutation.valueAtReference)! - mutationEntry.proportion,
45
+ );
46
+ };
47
+ subtractSubstitutionValue();
48
+ }
49
+ const orderedPositionsToProportionAtBase = [...positionsToProportionAtBase.entries()]
50
+ .map(([position, proportionsAtBase]) => ({ position, proportions: proportionsAtBase }))
51
+ .sort((a, b) => {
52
+ return sortMutationPositions(a.position, b.position);
53
+ });
54
+
55
+ return orderedPositionsToProportionAtBase.map((proportionsForBaseAtPosition) => {
56
+ const proportions = bases[sequenceType].map((base) => {
57
+ return {
58
+ [base]: {
59
+ proportion: proportionsForBaseAtPosition.proportions.get(base)!,
60
+ isReference: base === referenceBases.get(proportionsForBaseAtPosition.position),
61
+ },
62
+ };
63
+ });
64
+
65
+ return {
66
+ position: proportionsForBaseAtPosition.position,
67
+ ...proportions.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
68
+ } as MutationsGridDataRow;
69
+ });
70
+ };
71
+
72
+ export type BasesData = {
73
+ [base: string]: BaseCell;
74
+ };
75
+ export type MutationsGridDataRow = BasesData & { position: string };
76
+
77
+ const byProportion = (row: MutationsGridDataRow, proportionInterval: ProportionInterval) => {
78
+ const numbersAndIsReference = Object.values(row).filter(
79
+ (
80
+ cell,
81
+ ): cell is {
82
+ proportion: number;
83
+ isReference: boolean;
84
+ } => typeof cell === 'object',
85
+ );
86
+
87
+ return numbersAndIsReference.some((cell) => {
88
+ return (
89
+ !cell.isReference && cell.proportion >= proportionInterval.min && cell.proportion <= proportionInterval.max
90
+ );
91
+ });
92
+ };
@@ -0,0 +1,94 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { getMutationsTableData } from './getMutationsTableData';
4
+ import { Deletion, Substitution } from '../../utils/mutations';
5
+
6
+ describe('getMutationsTableData', () => {
7
+ test('should return the correct data', () => {
8
+ const data = [
9
+ {
10
+ type: 'substitution' as const,
11
+ mutation: new Substitution('segment1', 'A', 'T', 123),
12
+ count: 1,
13
+ proportion: 0.1,
14
+ },
15
+ {
16
+ type: 'deletion' as const,
17
+ mutation: new Deletion('segment2', 'C', 123),
18
+ count: 2,
19
+ proportion: 0.2,
20
+ },
21
+ ];
22
+
23
+ const proportionInterval = { min: 0, max: 1 };
24
+
25
+ const result = getMutationsTableData(data, proportionInterval);
26
+
27
+ expect(result).toEqual([
28
+ {
29
+ mutation: 'segment1:A123T',
30
+ type: 'substitution',
31
+ count: 1,
32
+ proportion: 0.1,
33
+ },
34
+ {
35
+ mutation: 'segment2:C123-',
36
+ type: 'deletion',
37
+ count: 2,
38
+ proportion: 0.2,
39
+ },
40
+ ]);
41
+ });
42
+
43
+ test('should filter out data below/above proportionInterval', () => {
44
+ const belowInterval = 0.03;
45
+ const aboveInterval = 0.95;
46
+ const inInterval = 0.5;
47
+
48
+ const data = [
49
+ {
50
+ type: 'substitution' as const,
51
+ mutation: new Substitution('segment1', 'A', 'T', 123),
52
+ count: 1,
53
+ proportion: inInterval,
54
+ },
55
+ {
56
+ type: 'substitution' as const,
57
+ mutation: new Substitution('segment1', 'A', 'T', 234),
58
+ count: 1,
59
+ proportion: belowInterval,
60
+ },
61
+ {
62
+ type: 'deletion' as const,
63
+ mutation: new Deletion('segment2', 'C', 123),
64
+ count: 2,
65
+ proportion: inInterval,
66
+ },
67
+ {
68
+ type: 'deletion' as const,
69
+ mutation: new Deletion('segment2', 'C', 456),
70
+ count: 2,
71
+ proportion: aboveInterval,
72
+ },
73
+ ];
74
+
75
+ const proportionInterval = { min: 0.05, max: 0.9 };
76
+
77
+ const result = getMutationsTableData(data, proportionInterval);
78
+
79
+ expect(result).toEqual([
80
+ {
81
+ mutation: 'segment1:A123T',
82
+ type: 'substitution',
83
+ count: 1,
84
+ proportion: inInterval,
85
+ },
86
+ {
87
+ mutation: 'segment2:C123-',
88
+ type: 'deletion',
89
+ count: 2,
90
+ proportion: inInterval,
91
+ },
92
+ ]);
93
+ });
94
+ });
@@ -0,0 +1,17 @@
1
+ import { type SubstitutionOrDeletionEntry } from '../../types';
2
+ import { type ProportionInterval } from '../components/proportion-selector';
3
+
4
+ export function getMutationsTableData(data: SubstitutionOrDeletionEntry[], proportionInterval: ProportionInterval) {
5
+ const byProportion = (mutationEntry: SubstitutionOrDeletionEntry) => {
6
+ return mutationEntry.proportion >= proportionInterval.min && mutationEntry.proportion <= proportionInterval.max;
7
+ };
8
+
9
+ return data.filter(byProportion).map((mutationEntry) => {
10
+ return {
11
+ mutation: mutationEntry.mutation.toString(),
12
+ type: mutationEntry.type,
13
+ count: mutationEntry.count,
14
+ proportion: mutationEntry.proportion,
15
+ };
16
+ });
17
+ }
@@ -0,0 +1,84 @@
1
+ import { type Row } from 'gridjs';
2
+ import { type FunctionComponent } from 'preact';
3
+
4
+ import { getMutationsGridData } from './getMutationsGridData';
5
+ import { type SequenceType, type SubstitutionOrDeletionEntry } from '../../types';
6
+ import { bases } from '../../utils/mutations';
7
+ import { type ProportionInterval } from '../components/proportion-selector';
8
+ import { Table, tableStyle } from '../components/table';
9
+ import { sortMutationPositions } from '../shared/sort/sortMutationPositions';
10
+ import { formatProportion } from '../shared/table/formatProportion';
11
+
12
+ interface MutationsGridProps {
13
+ data: SubstitutionOrDeletionEntry[];
14
+ sequenceType: SequenceType;
15
+ proportionInterval: ProportionInterval;
16
+ }
17
+
18
+ export type BaseCell = {
19
+ proportion: number;
20
+ isReference: boolean;
21
+ };
22
+
23
+ export const MutationsGrid: FunctionComponent<MutationsGridProps> = ({ data, sequenceType, proportionInterval }) => {
24
+ const getHeaders = () => {
25
+ return [
26
+ {
27
+ name: 'Position',
28
+ sort: {
29
+ compare: (a: string, b: string) => {
30
+ return sortMutationPositions(a, b);
31
+ },
32
+ },
33
+ },
34
+ ...getBasesHeaders(),
35
+ ];
36
+ };
37
+
38
+ const getBasesHeaders = () => {
39
+ return bases[sequenceType].map((base) => {
40
+ return {
41
+ name: base,
42
+ sort: {
43
+ compare: (a: BaseCell, b: BaseCell) => {
44
+ const aProportion = a.proportion;
45
+ const bProportion = b.proportion;
46
+ if (aProportion < bProportion) {
47
+ return -1;
48
+ }
49
+ if (aProportion > bProportion) {
50
+ return 1;
51
+ }
52
+
53
+ return 0;
54
+ },
55
+ },
56
+ formatter: (cell: BaseCell) => formatProportion(cell.proportion),
57
+ attributes: (cell: BaseCell, row: Row) => {
58
+ // grid-js: the cell and row are null for header cells
59
+ if (row === null) {
60
+ return {};
61
+ }
62
+
63
+ return styleCells(cell);
64
+ },
65
+ };
66
+ });
67
+ };
68
+
69
+ const styleCells = (cell: BaseCell) => {
70
+ if (cell.isReference || cell.proportion < 0.0001) {
71
+ return {
72
+ style: {
73
+ ...tableStyle.td,
74
+ color: 'gray',
75
+ },
76
+ };
77
+ }
78
+ return {};
79
+ };
80
+
81
+ const tableData = getMutationsGridData(data, sequenceType, proportionInterval).map((row) => Object.values(row));
82
+
83
+ return <Table data={tableData} columns={getHeaders()} pagination={true} />;
84
+ };
@@ -0,0 +1,33 @@
1
+ import { type FunctionComponent } from 'preact';
2
+
3
+ import { getInsertionsTableData } from './getInsertionsTableData';
4
+ import { type InsertionEntry } from '../../types';
5
+ import { Table } from '../components/table';
6
+ import { sortInsertions } from '../shared/sort/sortInsertions';
7
+
8
+ export interface InsertionsTableProps {
9
+ data: InsertionEntry[];
10
+ }
11
+
12
+ export const InsertionsTable: FunctionComponent<InsertionsTableProps> = ({ data }) => {
13
+ const getHeaders = () => {
14
+ return [
15
+ {
16
+ name: 'Insertion',
17
+ sort: {
18
+ compare: (a: string, b: string) => {
19
+ return sortInsertions(a, b);
20
+ },
21
+ },
22
+ },
23
+ {
24
+ name: 'Count',
25
+ sort: true,
26
+ },
27
+ ];
28
+ };
29
+
30
+ const tableData = getInsertionsTableData(data).map((row) => Object.values(row));
31
+
32
+ return <Table data={tableData} columns={getHeaders()} pagination={true} />;
33
+ };
@@ -0,0 +1,47 @@
1
+ import { type FunctionComponent } from 'preact';
2
+
3
+ import { getMutationsTableData } from './getMutationsTableData';
4
+ import { type SubstitutionOrDeletionEntry } from '../../types';
5
+ import type { ProportionInterval } from '../components/proportion-selector';
6
+ import { Table } from '../components/table';
7
+ import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
8
+ import { formatProportion } from '../shared/table/formatProportion';
9
+
10
+ export interface MutationsTableProps {
11
+ data: SubstitutionOrDeletionEntry[];
12
+ proportionInterval: ProportionInterval;
13
+ }
14
+
15
+ const MutationsTable: FunctionComponent<MutationsTableProps> = ({ data, proportionInterval }) => {
16
+ const getHeaders = () => {
17
+ return [
18
+ {
19
+ name: 'Mutation',
20
+ sort: {
21
+ compare: (a: string, b: string) => {
22
+ return sortSubstitutionsAndDeletions(a, b);
23
+ },
24
+ },
25
+ },
26
+ {
27
+ name: 'Type',
28
+ sort: true,
29
+ },
30
+ {
31
+ name: 'Count',
32
+ sort: true,
33
+ },
34
+ {
35
+ name: 'Proportion',
36
+ sort: true,
37
+ formatter: (cell: number) => formatProportion(cell),
38
+ },
39
+ ];
40
+ };
41
+
42
+ const tableData = getMutationsTableData(data, proportionInterval).map((row) => Object.values(row));
43
+
44
+ return <Table data={tableData} columns={getHeaders()} pagination={true} />;
45
+ };
46
+
47
+ export default MutationsTable;
@@ -0,0 +1,95 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, waitFor, within } from '@storybook/test';
3
+
4
+ import nucleotideInsertions from './__mockData__/nucleotideInsertions.json';
5
+ import nucleotideMutations from './__mockData__/nucleotideMutations.json';
6
+ import { Mutations, type MutationsProps } from './mutations';
7
+ import { LAPIS_URL, NUCLEOTIDE_INSERTIONS_ENDPOINT, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
8
+ import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
9
+ import { LapisUrlContext } from '../LapisUrlContext';
10
+ import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
11
+
12
+ const meta: Meta<MutationsProps> = {
13
+ title: 'Visualization/Mutations',
14
+ component: Mutations,
15
+ argTypes: {
16
+ variant: { control: 'object' },
17
+ sequenceType: {
18
+ options: ['nucleotide', 'amino acid'],
19
+ control: { type: 'radio' },
20
+ },
21
+ views: {
22
+ options: ['table', 'grid', 'insertions'],
23
+ control: { type: 'check' },
24
+ },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+
30
+ const Template = {
31
+ render: (args: MutationsProps) => (
32
+ <LapisUrlContext.Provider value={LAPIS_URL}>
33
+ <ReferenceGenomeContext.Provider value={referenceGenome}>
34
+ <Mutations variant={args.variant} sequenceType={args.sequenceType} views={args.views} />
35
+ </ReferenceGenomeContext.Provider>
36
+ </LapisUrlContext.Provider>
37
+ ),
38
+ };
39
+
40
+ export const Default: StoryObj<MutationsProps> = {
41
+ ...Template,
42
+ args: {
43
+ variant: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: '2022-01-01' },
44
+ sequenceType: 'nucleotide',
45
+ views: ['grid', 'table', 'insertions'],
46
+ },
47
+ parameters: {
48
+ fetchMock: {
49
+ mocks: [
50
+ {
51
+ matcher: {
52
+ name: 'nucleotideMutations',
53
+ url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
54
+ body: {
55
+ country: 'Switzerland',
56
+ pangoLineage: 'B.1.1.7',
57
+ dateTo: '2022-01-01',
58
+ minProportion: 0,
59
+ },
60
+ },
61
+ response: {
62
+ status: 200,
63
+ body: nucleotideMutations,
64
+ },
65
+ },
66
+ {
67
+ matcher: {
68
+ name: 'nucleotideInsertions',
69
+ url: NUCLEOTIDE_INSERTIONS_ENDPOINT,
70
+ body: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: '2022-01-01' },
71
+ },
72
+ response: {
73
+ status: 200,
74
+ body: nucleotideInsertions,
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ };
81
+
82
+ export const GridTab: StoryObj<MutationsProps> = {
83
+ ...Default,
84
+ play: async ({ canvasElement, step }) => {
85
+ const canvas = within(canvasElement);
86
+
87
+ const mutationAboveThreshold = () => canvas.getAllByText('51.03%');
88
+ const mutationBelowThreshold = () => canvas.getAllByText('3.51%');
89
+
90
+ await step('All proportions are displayed, when one is above threshold', async () => {
91
+ await waitFor(() => expect(mutationAboveThreshold()[0]).toBeVisible());
92
+ await waitFor(() => expect(mutationBelowThreshold()[0]).toBeVisible());
93
+ });
94
+ },
95
+ };