@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.
- package/LICENSE +661 -0
- package/README.md +109 -0
- package/custom-elements.json +1587 -0
- package/dist/dashboard-components.js +7322 -0
- package/dist/dashboard-components.js.map +1 -0
- package/dist/genspectrum-components.d.ts +298 -0
- package/dist/style.css +2930 -0
- package/package.json +109 -0
- package/src/constants.ts +6 -0
- package/src/index.ts +1 -0
- package/src/lapisApi/ReferenceGenome.ts +30 -0
- package/src/lapisApi/__mockData__/referenceGenome.json +58 -0
- package/src/lapisApi/lapisApi.ts +99 -0
- package/src/lapisApi/lapisTypes.ts +51 -0
- package/src/operator/Dataset.ts +3 -0
- package/src/operator/DivisionOperator.spec.ts +27 -0
- package/src/operator/DivisionOperator.ts +60 -0
- package/src/operator/FetchAggregatedOperator.ts +44 -0
- package/src/operator/FetchInsertionsOperator.ts +24 -0
- package/src/operator/FetchSubstitutionsOrDeletionsOperator.ts +49 -0
- package/src/operator/FillMissingOperator.spec.ts +26 -0
- package/src/operator/FillMissingOperator.ts +30 -0
- package/src/operator/GroupByAndSumOperator.spec.ts +26 -0
- package/src/operator/GroupByAndSumOperator.ts +26 -0
- package/src/operator/GroupByOperator.spec.ts +43 -0
- package/src/operator/GroupByOperator.ts +32 -0
- package/src/operator/MapOperator.spec.ts +13 -0
- package/src/operator/MapOperator.ts +16 -0
- package/src/operator/MockOperator.spec.ts +11 -0
- package/src/operator/MockOperator.ts +12 -0
- package/src/operator/Operator.ts +5 -0
- package/src/operator/SlidingOperator.spec.ts +52 -0
- package/src/operator/SlidingOperator.ts +23 -0
- package/src/operator/SortOperator.spec.ts +13 -0
- package/src/operator/SortOperator.ts +16 -0
- package/src/preact/LapisUrlContext.ts +3 -0
- package/src/preact/ReferenceGenomeContext.ts +5 -0
- package/src/preact/components/SegmentSelector.tsx +62 -0
- package/src/preact/components/chart.stories.tsx +42 -0
- package/src/preact/components/chart.tsx +32 -0
- package/src/preact/components/checkbox-selector.stories.tsx +56 -0
- package/src/preact/components/checkbox-selector.tsx +46 -0
- package/src/preact/components/confidence-interval-selector.tsx +45 -0
- package/src/preact/components/csv-download-button.stories.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +51 -0
- package/src/preact/components/error-display.stories.tsx +22 -0
- package/src/preact/components/error-display.tsx +5 -0
- package/src/preact/components/headline.stories.tsx +29 -0
- package/src/preact/components/headline.tsx +16 -0
- package/src/preact/components/info.stories.tsx +22 -0
- package/src/preact/components/info.tsx +16 -0
- package/src/preact/components/loading-display.stories.tsx +20 -0
- package/src/preact/components/loading-display.tsx +5 -0
- package/src/preact/components/min-max-percent-slider.css +40 -0
- package/src/preact/components/min-max-range-slider.tsx +95 -0
- package/src/preact/components/mutation-type-selector.tsx +30 -0
- package/src/preact/components/no-data-display.stories.tsx +20 -0
- package/src/preact/components/no-data-display.tsx +5 -0
- package/src/preact/components/percent-intput.tsx +49 -0
- package/src/preact/components/proportion-selector-dropdown.stories.tsx +66 -0
- package/src/preact/components/proportion-selector-dropdown.tsx +33 -0
- package/src/preact/components/proportion-selector.stories.tsx +81 -0
- package/src/preact/components/proportion-selector.tsx +43 -0
- package/src/preact/components/scaling-selector.stories.tsx +25 -0
- package/src/preact/components/scaling-selector.tsx +36 -0
- package/src/preact/components/select.stories.tsx +42 -0
- package/src/preact/components/select.tsx +21 -0
- package/src/preact/components/table.stories.tsx +24 -0
- package/src/preact/components/table.tsx +51 -0
- package/src/preact/components/tabs.stories.tsx +60 -0
- package/src/preact/components/tabs.tsx +49 -0
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +32 -0
- package/src/preact/dateRangeSelector/date-range-selector.tsx +228 -0
- package/src/preact/dateRangeSelector/dateConversion.ts +8 -0
- package/src/preact/locationFilter/__mockData__/aggregated.json +775 -0
- package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +36 -0
- package/src/preact/locationFilter/fetchAutocompletionList.ts +43 -0
- package/src/preact/locationFilter/location-filter.stories.tsx +50 -0
- package/src/preact/locationFilter/location-filter.tsx +112 -0
- package/src/preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json +295 -0
- package/src/preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json +304 -0
- package/src/preact/mutationComparison/fetchMutationData.spec.ts +118 -0
- package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +125 -0
- package/src/preact/mutationComparison/getMutationComparisonTableData.ts +40 -0
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +43 -0
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +122 -0
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +152 -0
- package/src/preact/mutationComparison/mutation-comparison.tsx +179 -0
- package/src/preact/mutationComparison/queryMutationData.ts +53 -0
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +164 -0
- package/src/preact/mutationFilter/mutation-filter.tsx +268 -0
- package/src/preact/mutationFilter/parseAndValidateMutation.ts +54 -0
- package/src/preact/mutationFilter/parseMutation.spec.ts +150 -0
- package/src/preact/mutationFilter/sequenceTypeFromSegment.spec.ts +66 -0
- package/src/preact/mutationFilter/sequenceTypeFromSegment.ts +20 -0
- package/src/preact/mutations/__mockData__/nucleotideInsertions.json +252 -0
- package/src/preact/mutations/__mockData__/nucleotideMutations.json +880 -0
- package/src/preact/mutations/getInsertionsTableData.spec.ts +36 -0
- package/src/preact/mutations/getInsertionsTableData.ts +10 -0
- package/src/preact/mutations/getMutationsGridData.spec.ts +135 -0
- package/src/preact/mutations/getMutationsGridData.ts +92 -0
- package/src/preact/mutations/getMutationsTableData.spec.ts +94 -0
- package/src/preact/mutations/getMutationsTableData.ts +17 -0
- package/src/preact/mutations/mutations-grid.tsx +84 -0
- package/src/preact/mutations/mutations-insertions-table.tsx +33 -0
- package/src/preact/mutations/mutations-table.tsx +47 -0
- package/src/preact/mutations/mutations.stories.tsx +95 -0
- package/src/preact/mutations/mutations.tsx +192 -0
- package/src/preact/mutations/queryMutations.ts +55 -0
- package/src/preact/prevalenceOverTime/__mockData__/denominator.json +1700 -0
- package/src/preact/prevalenceOverTime/__mockData__/denominatorOneVariant.json +608 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorEG.json +1560 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorJN1.json +592 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorOneVariant.json +604 -0
- package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.spec.ts +67 -0
- package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.ts +18 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +105 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +86 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +141 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-table.tsx +46 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +165 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +202 -0
- package/src/preact/relativeGrowthAdvantage/__mockData__/denominator.json +376 -0
- package/src/preact/relativeGrowthAdvantage/__mockData__/numerator.json +332 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +138 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +71 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +136 -0
- package/src/preact/shared/charts/LogitScale.ts +48 -0
- package/src/preact/shared/charts/colors.ts +26 -0
- package/src/preact/shared/charts/confideceInterval.ts +29 -0
- package/src/preact/shared/charts/getYAxisScale.ts +16 -0
- package/src/preact/shared/charts/scales.ts +16 -0
- package/src/preact/shared/icons/DeleteIcon.tsx +17 -0
- package/src/preact/shared/sort/sortInsertions.spec.ts +47 -0
- package/src/preact/shared/sort/sortInsertions.ts +21 -0
- package/src/preact/shared/sort/sortMutationPositions.spec.ts +31 -0
- package/src/preact/shared/sort/sortMutationPositions.ts +14 -0
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +47 -0
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +17 -0
- package/src/preact/shared/table/formatProportion.ts +3 -0
- package/src/preact/textInput/__mockData__/aggregated_hosts.json +24 -0
- package/src/preact/textInput/fetchAutocompleteList.ts +9 -0
- package/src/preact/textInput/text-input.stories.tsx +49 -0
- package/src/preact/textInput/text-input.tsx +73 -0
- package/src/preact/useQuery.ts +27 -0
- package/src/query/queryInsertions.ts +14 -0
- package/src/query/queryPrevalenceOverTime.ts +126 -0
- package/src/query/queryRelativeGrowthAdvantage.ts +131 -0
- package/src/query/querySubstitutionsOrDeletions.ts +19 -0
- package/src/styles/tailwind.css +3 -0
- package/src/styles/tailwind.d.ts +3 -0
- package/src/types.ts +23 -0
- package/src/utils/mutations.spec.ts +64 -0
- package/src/utils/mutations.ts +165 -0
- package/src/utils/temporal.spec.ts +97 -0
- package/src/utils/temporal.ts +348 -0
- package/src/utils/test-utils.ts +5 -0
- package/src/utils/type-utils.ts +15 -0
- package/src/utils/utils.spec.ts +16 -0
- package/src/utils/utils.ts +38 -0
- package/src/web-components/PreactLitAdapter.tsx +62 -0
- package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +12 -0
- package/src/web-components/app.ts +51 -0
- package/src/web-components/display/index.ts +4 -0
- package/src/web-components/display/mutation-comparison-component.stories.ts +138 -0
- package/src/web-components/display/mutation-comparison-component.tsx +31 -0
- package/src/web-components/display/mutations-component.stories.ts +107 -0
- package/src/web-components/display/mutations-component.tsx +27 -0
- package/src/web-components/display/prevalence-over-time-component.stories.ts +205 -0
- package/src/web-components/display/prevalence-over-time-component.tsx +46 -0
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +89 -0
- package/src/web-components/display/relative-growth-advantage-component.tsx +37 -0
- package/src/web-components/index.ts +3 -0
- package/src/web-components/input/date-range-selector-component.stories.ts +53 -0
- package/src/web-components/input/date-range-selector-component.tsx +33 -0
- package/src/web-components/input/index.ts +4 -0
- package/src/web-components/input/location-filter-component.stories.ts +184 -0
- package/src/web-components/input/location-filter-component.tsx +68 -0
- package/src/web-components/input/location-filter.mdx +25 -0
- package/src/web-components/input/mutation-filter-component.stories.ts +97 -0
- package/src/web-components/input/mutation-filter-component.tsx +27 -0
- package/src/web-components/input/text-input-component.stories.ts +92 -0
- package/src/web-components/input/text-input-component.tsx +30 -0
- package/src/web-components/lapis-context.ts +3 -0
- package/src/web-components/reference-genome-context.ts +5 -0
- 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
|
+
};
|