@genspectrum/dashboard-components 0.6.3 → 0.6.5
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/custom-elements.json +1 -1
- package/dist/dashboard-components.js +121 -41
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +11 -0
- package/dist/style.css +6 -0
- package/package.json +1 -1
- package/src/preact/components/tooltip.stories.tsx +54 -0
- package/src/preact/components/tooltip.tsx +31 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_tooManyMutations.json +1470 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_tooManyMutations.json +16453 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +23 -11
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +6 -2
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +77 -41
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +62 -8
- package/src/query/queryMutationsOverTime.spec.ts +50 -24
- package/src/query/queryMutationsOverTime.ts +20 -5
- package/src/utils/temporal.spec.ts +5 -0
- package/src/utils/temporal.ts +29 -5
- package/src/web-components/visualization/gs-mutations-over-time.tsx +11 -0
|
@@ -12,8 +12,14 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
12
12
|
it('should filter by displayed segments', () => {
|
|
13
13
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
14
14
|
|
|
15
|
-
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'),
|
|
16
|
-
|
|
15
|
+
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
|
|
16
|
+
count: 1,
|
|
17
|
+
proportion: 0.1,
|
|
18
|
+
});
|
|
19
|
+
data.set(new Substitution('someOtherSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
|
|
20
|
+
count: 2,
|
|
21
|
+
proportion: 0.2,
|
|
22
|
+
});
|
|
17
23
|
|
|
18
24
|
filterDisplayedSegments(
|
|
19
25
|
[
|
|
@@ -32,8 +38,14 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
32
38
|
it('should filter by mutation types', () => {
|
|
33
39
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
34
40
|
|
|
35
|
-
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'),
|
|
36
|
-
|
|
41
|
+
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
|
|
42
|
+
count: 1,
|
|
43
|
+
proportion: 0.1,
|
|
44
|
+
});
|
|
45
|
+
data.set(new Deletion('someOtherSegment', 'A', 123), yearMonthDay('2021-01-01'), {
|
|
46
|
+
count: 2,
|
|
47
|
+
proportion: 0.2,
|
|
48
|
+
});
|
|
37
49
|
|
|
38
50
|
filterMutationTypes(
|
|
39
51
|
[
|
|
@@ -52,8 +64,8 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
52
64
|
it('should filter by proportion', () => {
|
|
53
65
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
54
66
|
|
|
55
|
-
const belowFilter = 0.1;
|
|
56
|
-
const aboveFilter = 0.99;
|
|
67
|
+
const belowFilter = { count: 1, proportion: 0.1 };
|
|
68
|
+
const aboveFilter = { count: 99, proportion: 0.99 };
|
|
57
69
|
const proportionInterval = { min: 0.2, max: 0.9 };
|
|
58
70
|
|
|
59
71
|
const someSubstitution = new Substitution('someSegment', 'A', 'T', 123);
|
|
@@ -62,15 +74,15 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
62
74
|
|
|
63
75
|
filterProportion(data, proportionInterval);
|
|
64
76
|
|
|
65
|
-
expect(data.getAsArray(0).length).to.equal(0);
|
|
77
|
+
expect(data.getAsArray({ count: 0, proportion: 0 }).length).to.equal(0);
|
|
66
78
|
});
|
|
67
79
|
|
|
68
80
|
it('should not filter if one proportion is within the interval', () => {
|
|
69
81
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
70
82
|
|
|
71
|
-
const belowFilter = 0.1;
|
|
72
|
-
const aboveFilter = 0.99;
|
|
73
|
-
const inFilter = 0.5;
|
|
83
|
+
const belowFilter = { count: 1, proportion: 0.1 };
|
|
84
|
+
const aboveFilter = { count: 99, proportion: 0.99 };
|
|
85
|
+
const inFilter = { count: 5, proportion: 0.5 };
|
|
74
86
|
const proportionInterval = { min: 0.2, max: 0.9 };
|
|
75
87
|
|
|
76
88
|
const someSubstitution = new Substitution('someSegment', 'A', 'T', 123);
|
|
@@ -80,7 +92,7 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
80
92
|
|
|
81
93
|
filterProportion(data, proportionInterval);
|
|
82
94
|
|
|
83
|
-
expect(data.getRow(someSubstitution, 0).length).to.equal(3);
|
|
95
|
+
expect(data.getRow(someSubstitution, { count: 0, proportion: 0 }).length).to.equal(3);
|
|
84
96
|
});
|
|
85
97
|
});
|
|
86
98
|
});
|
|
@@ -54,8 +54,12 @@ export function filterProportion(
|
|
|
54
54
|
},
|
|
55
55
|
) {
|
|
56
56
|
data.getFirstAxisKeys().forEach((mutation) => {
|
|
57
|
-
const row = data.getRow(mutation, 0);
|
|
58
|
-
if (
|
|
57
|
+
const row = data.getRow(mutation, { count: 0, proportion: 0 });
|
|
58
|
+
if (
|
|
59
|
+
!row.some(
|
|
60
|
+
(value) => value.proportion >= proportionInterval.min && value.proportion <= proportionInterval.max,
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
59
63
|
data.deleteRow(mutation);
|
|
60
64
|
}
|
|
61
65
|
});
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
type MutationOverTimeMutationValue,
|
|
6
6
|
} from '../../query/queryMutationsOverTime';
|
|
7
7
|
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
8
|
-
import { compareTemporal, type Temporal } from '../../utils/temporal';
|
|
8
|
+
import { compareTemporal, type Temporal, YearMonthDay } from '../../utils/temporal';
|
|
9
|
+
import Tooltip from '../components/tooltip';
|
|
9
10
|
import { singleGraphColorRGBByName } from '../shared/charts/colors';
|
|
10
11
|
import { formatProportion } from '../shared/table/formatProportion';
|
|
11
12
|
|
|
@@ -13,42 +14,54 @@ export interface MutationsOverTimeGridProps {
|
|
|
13
14
|
data: MutationOverTimeDataGroupedByMutation;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
const MAX_NUMBER_OF_GRID_ROWS = 100;
|
|
18
|
+
|
|
16
19
|
const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data }) => {
|
|
17
|
-
const
|
|
20
|
+
const allMutations = data.getFirstAxisKeys();
|
|
21
|
+
const shownMutations = allMutations.slice(0, MAX_NUMBER_OF_GRID_ROWS);
|
|
22
|
+
|
|
18
23
|
const dates = data.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
|
|
19
24
|
|
|
20
25
|
return (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
26
|
+
<>
|
|
27
|
+
{allMutations.length > MAX_NUMBER_OF_GRID_ROWS && (
|
|
28
|
+
<div className='pl-2'>
|
|
29
|
+
Showing {MAX_NUMBER_OF_GRID_ROWS} of {allMutations.length} mutations. You can narrow the filter to
|
|
30
|
+
reduce the number of mutations.
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
display: 'grid',
|
|
36
|
+
gridTemplateRows: `repeat(${shownMutations.length}, 24px)`,
|
|
37
|
+
gridTemplateColumns: `8rem repeat(${dates.length}, minmax(1.5rem, 1fr))`,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{shownMutations.map((mutation, i) => {
|
|
41
|
+
return (
|
|
42
|
+
<Fragment key={`fragment-${mutation.toString()}`}>
|
|
43
|
+
<div
|
|
44
|
+
key={`mutation-${mutation.toString()}`}
|
|
45
|
+
style={{ gridRowStart: i + 1, gridColumnStart: 1 }}
|
|
46
|
+
>
|
|
47
|
+
<MutationCell mutation={mutation} />
|
|
48
|
+
</div>
|
|
49
|
+
{dates.map((date, j) => {
|
|
50
|
+
const value = data.get(mutation, date) ?? { proportion: 0, count: 0 };
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
style={{ gridRowStart: i + 1, gridColumnStart: j + 2 }}
|
|
54
|
+
key={`${mutation.toString()}-${date.toString()}`}
|
|
55
|
+
>
|
|
56
|
+
<ProportionCell value={value} date={date} mutation={mutation} />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</Fragment>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</div>
|
|
64
|
+
</>
|
|
52
65
|
);
|
|
53
66
|
};
|
|
54
67
|
|
|
@@ -56,22 +69,45 @@ const ProportionCell: FunctionComponent<{
|
|
|
56
69
|
value: MutationOverTimeMutationValue;
|
|
57
70
|
date: Temporal;
|
|
58
71
|
mutation: Substitution | Deletion;
|
|
59
|
-
}> = ({ value }) => {
|
|
60
|
-
|
|
72
|
+
}> = ({ value, mutation, date }) => {
|
|
73
|
+
const tooltipContent = (
|
|
74
|
+
<div>
|
|
75
|
+
<p>
|
|
76
|
+
<span className='font-bold'>{date.englishName()}</span> ({timeIntervalDisplay(date)})
|
|
77
|
+
</p>
|
|
78
|
+
<p>{mutation.code}</p>
|
|
79
|
+
<p>Proportion: {formatProportion(value.proportion)}</p>
|
|
80
|
+
<p>Count: {value.count}</p>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
|
|
61
84
|
return (
|
|
62
85
|
<>
|
|
63
86
|
<div className={'py-1'}>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
<Tooltip content={tooltipContent}>
|
|
88
|
+
<div
|
|
89
|
+
style={{
|
|
90
|
+
backgroundColor: backgroundColor(value.proportion),
|
|
91
|
+
color: textColor(value.proportion),
|
|
92
|
+
}}
|
|
93
|
+
className='text-center hover:font-bold text-xs'
|
|
94
|
+
>
|
|
95
|
+
{formatProportion(value.proportion, 0)}
|
|
96
|
+
</div>
|
|
97
|
+
</Tooltip>
|
|
70
98
|
</div>
|
|
71
99
|
</>
|
|
72
100
|
);
|
|
73
101
|
};
|
|
74
102
|
|
|
103
|
+
const timeIntervalDisplay = (date: Temporal) => {
|
|
104
|
+
if (date instanceof YearMonthDay) {
|
|
105
|
+
return date.toString();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
|
|
109
|
+
};
|
|
110
|
+
|
|
75
111
|
const backgroundColor = (proportion: number) => {
|
|
76
112
|
// TODO(#353): Make minAlpha and maxAlpha configurable
|
|
77
113
|
const minAlpha = 0.0;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, waitFor } from '@storybook/test';
|
|
2
3
|
|
|
3
4
|
import aggregated_date from './__mockData__/aggregated_date.json';
|
|
5
|
+
import aggregated_tooManyMutations from './__mockData__/aggregated_tooManyMutations.json';
|
|
4
6
|
import nucleotideMutation_01 from './__mockData__/nucleotideMutations_2024_01.json';
|
|
5
7
|
import nucleotideMutation_02 from './__mockData__/nucleotideMutations_2024_02.json';
|
|
6
8
|
import nucleotideMutation_03 from './__mockData__/nucleotideMutations_2024_03.json';
|
|
@@ -8,6 +10,7 @@ import nucleotideMutation_04 from './__mockData__/nucleotideMutations_2024_04.js
|
|
|
8
10
|
import nucleotideMutation_05 from './__mockData__/nucleotideMutations_2024_05.json';
|
|
9
11
|
import nucleotideMutation_06 from './__mockData__/nucleotideMutations_2024_06.json';
|
|
10
12
|
import nucleotideMutation_07 from './__mockData__/nucleotideMutations_2024_07.json';
|
|
13
|
+
import nucleotideMutation_tooManyMutations from './__mockData__/nucleotideMutations_tooManyMutations.json';
|
|
11
14
|
import { MutationsOverTime, type MutationsOverTimeProps } from './mutations-over-time';
|
|
12
15
|
import { AGGREGATED_ENDPOINT, LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
|
|
13
16
|
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
@@ -95,7 +98,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
95
98
|
pangoLineage: 'JN.1*',
|
|
96
99
|
dateFrom: '2024-01-01',
|
|
97
100
|
dateTo: '2024-01-31',
|
|
98
|
-
minProportion: 0,
|
|
101
|
+
minProportion: 0.001,
|
|
99
102
|
},
|
|
100
103
|
},
|
|
101
104
|
response: {
|
|
@@ -111,7 +114,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
111
114
|
pangoLineage: 'JN.1*',
|
|
112
115
|
dateFrom: '2024-02-01',
|
|
113
116
|
dateTo: '2024-02-29',
|
|
114
|
-
minProportion: 0,
|
|
117
|
+
minProportion: 0.001,
|
|
115
118
|
},
|
|
116
119
|
},
|
|
117
120
|
response: {
|
|
@@ -127,7 +130,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
127
130
|
pangoLineage: 'JN.1*',
|
|
128
131
|
dateFrom: '2024-03-01',
|
|
129
132
|
dateTo: '2024-03-31',
|
|
130
|
-
minProportion: 0,
|
|
133
|
+
minProportion: 0.001,
|
|
131
134
|
},
|
|
132
135
|
response: {
|
|
133
136
|
status: 200,
|
|
@@ -143,7 +146,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
143
146
|
pangoLineage: 'JN.1*',
|
|
144
147
|
dateFrom: '2024-04-01',
|
|
145
148
|
dateTo: '2024-04-30',
|
|
146
|
-
minProportion: 0,
|
|
149
|
+
minProportion: 0.001,
|
|
147
150
|
},
|
|
148
151
|
response: {
|
|
149
152
|
status: 200,
|
|
@@ -159,7 +162,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
159
162
|
pangoLineage: 'JN.1*',
|
|
160
163
|
dateFrom: '2024-05-01',
|
|
161
164
|
dateTo: '2024-05-31',
|
|
162
|
-
minProportion: 0,
|
|
165
|
+
minProportion: 0.001,
|
|
163
166
|
},
|
|
164
167
|
response: {
|
|
165
168
|
status: 200,
|
|
@@ -175,7 +178,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
175
178
|
pangoLineage: 'JN.1*',
|
|
176
179
|
dateFrom: '2024-06-01',
|
|
177
180
|
dateTo: '2024-06-30',
|
|
178
|
-
minProportion: 0,
|
|
181
|
+
minProportion: 0.001,
|
|
179
182
|
},
|
|
180
183
|
response: {
|
|
181
184
|
status: 200,
|
|
@@ -183,7 +186,6 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
183
186
|
},
|
|
184
187
|
},
|
|
185
188
|
},
|
|
186
|
-
|
|
187
189
|
{
|
|
188
190
|
matcher: {
|
|
189
191
|
name: 'nucleotideMutations_07',
|
|
@@ -192,7 +194,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
192
194
|
pangoLineage: 'JN.1*',
|
|
193
195
|
dateFrom: '2024-07-01',
|
|
194
196
|
dateTo: '2024-07-31',
|
|
195
|
-
minProportion: 0,
|
|
197
|
+
minProportion: 0.001,
|
|
196
198
|
},
|
|
197
199
|
response: {
|
|
198
200
|
status: 200,
|
|
@@ -204,3 +206,55 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
204
206
|
},
|
|
205
207
|
},
|
|
206
208
|
};
|
|
209
|
+
|
|
210
|
+
export const ShowsMessageWhenTooManyMutations: StoryObj<MutationsOverTimeProps> = {
|
|
211
|
+
...Template,
|
|
212
|
+
args: {
|
|
213
|
+
lapisFilter: { dateFrom: '2023-01-01', dateTo: '2023-12-31' },
|
|
214
|
+
sequenceType: 'nucleotide',
|
|
215
|
+
views: ['grid'],
|
|
216
|
+
width: '100%',
|
|
217
|
+
height: '700px',
|
|
218
|
+
granularity: 'year',
|
|
219
|
+
lapisDateField: 'date',
|
|
220
|
+
},
|
|
221
|
+
parameters: {
|
|
222
|
+
fetchMock: {
|
|
223
|
+
mocks: [
|
|
224
|
+
{
|
|
225
|
+
matcher: {
|
|
226
|
+
name: 'aggregated',
|
|
227
|
+
url: AGGREGATED_ENDPOINT,
|
|
228
|
+
body: {
|
|
229
|
+
dateFrom: '2023-01-01',
|
|
230
|
+
dateTo: '2023-12-31',
|
|
231
|
+
fields: ['date'],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
response: {
|
|
235
|
+
status: 200,
|
|
236
|
+
body: aggregated_tooManyMutations,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
matcher: {
|
|
241
|
+
name: 'nucleotideMutations',
|
|
242
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
243
|
+
body: {
|
|
244
|
+
dateFrom: '2023-01-01',
|
|
245
|
+
dateTo: '2023-12-31',
|
|
246
|
+
minProportion: 0.001,
|
|
247
|
+
},
|
|
248
|
+
response: {
|
|
249
|
+
status: 200,
|
|
250
|
+
body: nucleotideMutation_tooManyMutations,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
play: async ({ canvas }) => {
|
|
258
|
+
await waitFor(() => expect(canvas.getByText('Showing 100 of 137 mutations.', { exact: false })).toBeVisible());
|
|
259
|
+
},
|
|
260
|
+
};
|
|
@@ -27,7 +27,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
27
27
|
dateFieldTo: '2023-01-01',
|
|
28
28
|
minProportion: 0.001,
|
|
29
29
|
},
|
|
30
|
-
response: { data: [getSomeTestMutation(0.1), getSomeOtherTestMutation(0.4)] },
|
|
30
|
+
response: { data: [getSomeTestMutation(0.1, 1), getSomeOtherTestMutation(0.4, 4)] },
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
body: {
|
|
@@ -36,7 +36,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
36
36
|
dateFieldTo: '2023-01-02',
|
|
37
37
|
minProportion: 0.001,
|
|
38
38
|
},
|
|
39
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
39
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
body: {
|
|
@@ -45,7 +45,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
45
45
|
dateFieldTo: '2023-01-03',
|
|
46
46
|
minProportion: 0.001,
|
|
47
47
|
},
|
|
48
|
-
response: { data: [getSomeTestMutation(0.3)] },
|
|
48
|
+
response: { data: [getSomeTestMutation(0.3, 3)] },
|
|
49
49
|
},
|
|
50
50
|
],
|
|
51
51
|
'nucleotide',
|
|
@@ -53,9 +53,17 @@ describe('queryMutationsOverTime', () => {
|
|
|
53
53
|
|
|
54
54
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
55
55
|
|
|
56
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
57
|
-
[
|
|
58
|
-
|
|
56
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
57
|
+
[
|
|
58
|
+
{ proportion: 0.1, count: 1 },
|
|
59
|
+
{ proportion: 0.2, count: 2 },
|
|
60
|
+
{ proportion: 0.3, count: 3 },
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
{ proportion: 0.4, count: 4 },
|
|
64
|
+
{ proportion: 0, count: 0 },
|
|
65
|
+
{ proportion: 0, count: 0 },
|
|
66
|
+
],
|
|
59
67
|
]);
|
|
60
68
|
|
|
61
69
|
const sequences = result.getFirstAxisKeys();
|
|
@@ -91,7 +99,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
91
99
|
dateFieldTo: '2023-01-01',
|
|
92
100
|
minProportion: 0.001,
|
|
93
101
|
},
|
|
94
|
-
response: { data: [getSomeTestMutation(0.1), getSomeOtherTestMutation(0.4)] },
|
|
102
|
+
response: { data: [getSomeTestMutation(0.1, 1), getSomeOtherTestMutation(0.4, 4)] },
|
|
95
103
|
},
|
|
96
104
|
{
|
|
97
105
|
body: {
|
|
@@ -109,7 +117,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
109
117
|
dateFieldTo: '2023-01-03',
|
|
110
118
|
minProportion: 0.001,
|
|
111
119
|
},
|
|
112
|
-
response: { data: [getSomeTestMutation(0.3)] },
|
|
120
|
+
response: { data: [getSomeTestMutation(0.3, 3)] },
|
|
113
121
|
},
|
|
114
122
|
],
|
|
115
123
|
'nucleotide',
|
|
@@ -117,9 +125,17 @@ describe('queryMutationsOverTime', () => {
|
|
|
117
125
|
|
|
118
126
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
119
127
|
|
|
120
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
121
|
-
[
|
|
122
|
-
|
|
128
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
129
|
+
[
|
|
130
|
+
{ proportion: 0.1, count: 1 },
|
|
131
|
+
{ proportion: 0.3, count: 3 },
|
|
132
|
+
{ proportion: 0, count: 0 },
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
{ proportion: 0.4, count: 4 },
|
|
136
|
+
{ proportion: 0, count: 0 },
|
|
137
|
+
{ proportion: 0, count: 0 },
|
|
138
|
+
],
|
|
123
139
|
]);
|
|
124
140
|
|
|
125
141
|
const sequences = result.getFirstAxisKeys();
|
|
@@ -181,7 +197,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
181
197
|
|
|
182
198
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
183
199
|
|
|
184
|
-
expect(result.getAsArray(0)).to.deep.equal([]);
|
|
200
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([]);
|
|
185
201
|
expect(result.getFirstAxisKeys()).to.deep.equal([]);
|
|
186
202
|
expect(result.getSecondAxisKeys()).to.deep.equal([]);
|
|
187
203
|
});
|
|
@@ -209,7 +225,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
209
225
|
dateFieldTo: '2023-01-02',
|
|
210
226
|
minProportion: 0.001,
|
|
211
227
|
},
|
|
212
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
228
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
213
229
|
},
|
|
214
230
|
{
|
|
215
231
|
body: {
|
|
@@ -218,7 +234,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
218
234
|
dateFieldTo: '2023-01-03',
|
|
219
235
|
minProportion: 0.001,
|
|
220
236
|
},
|
|
221
|
-
response: { data: [getSomeTestMutation(0.3)] },
|
|
237
|
+
response: { data: [getSomeTestMutation(0.3, 3)] },
|
|
222
238
|
},
|
|
223
239
|
],
|
|
224
240
|
'nucleotide',
|
|
@@ -226,7 +242,12 @@ describe('queryMutationsOverTime', () => {
|
|
|
226
242
|
|
|
227
243
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
228
244
|
|
|
229
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
245
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
246
|
+
[
|
|
247
|
+
{ proportion: 0.2, count: 2 },
|
|
248
|
+
{ proportion: 0.3, count: 3 },
|
|
249
|
+
],
|
|
250
|
+
]);
|
|
230
251
|
|
|
231
252
|
const sequences = result.getFirstAxisKeys();
|
|
232
253
|
expect(sequences[0].code).toBe('sequenceName:A123T');
|
|
@@ -259,7 +280,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
259
280
|
dateFieldTo: '2023-01-01',
|
|
260
281
|
minProportion: 0.001,
|
|
261
282
|
},
|
|
262
|
-
response: { data: [getSomeTestMutation(0.1)] },
|
|
283
|
+
response: { data: [getSomeTestMutation(0.1, 1)] },
|
|
263
284
|
},
|
|
264
285
|
{
|
|
265
286
|
body: {
|
|
@@ -268,7 +289,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
268
289
|
dateFieldTo: '2023-01-02',
|
|
269
290
|
minProportion: 0.001,
|
|
270
291
|
},
|
|
271
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
292
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
272
293
|
},
|
|
273
294
|
],
|
|
274
295
|
'nucleotide',
|
|
@@ -276,7 +297,12 @@ describe('queryMutationsOverTime', () => {
|
|
|
276
297
|
|
|
277
298
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
278
299
|
|
|
279
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
300
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
301
|
+
[
|
|
302
|
+
{ proportion: 0.1, count: 1 },
|
|
303
|
+
{ proportion: 0.2, count: 2 },
|
|
304
|
+
],
|
|
305
|
+
]);
|
|
280
306
|
|
|
281
307
|
const sequences = result.getFirstAxisKeys();
|
|
282
308
|
expect(sequences[0].code).toBe('sequenceName:A123T');
|
|
@@ -309,7 +335,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
309
335
|
dateFieldTo: '2023-01-02',
|
|
310
336
|
minProportion: 0.001,
|
|
311
337
|
},
|
|
312
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
338
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
313
339
|
},
|
|
314
340
|
],
|
|
315
341
|
'nucleotide',
|
|
@@ -317,7 +343,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
317
343
|
|
|
318
344
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
319
345
|
|
|
320
|
-
expect(result.getAsArray(0)).to.deep.equal([[0.2]]);
|
|
346
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([[{ proportion: 0.2, count: 2 }]]);
|
|
321
347
|
|
|
322
348
|
const sequences = result.getFirstAxisKeys();
|
|
323
349
|
expect(sequences[0].code).toBe('sequenceName:A123T');
|
|
@@ -326,11 +352,11 @@ describe('queryMutationsOverTime', () => {
|
|
|
326
352
|
expect(dates[0].toString()).toBe('2023-01-02');
|
|
327
353
|
});
|
|
328
354
|
|
|
329
|
-
function getSomeTestMutation(proportion: number) {
|
|
355
|
+
function getSomeTestMutation(proportion: number, count: number) {
|
|
330
356
|
return {
|
|
331
357
|
mutation: 'sequenceName:A123T',
|
|
332
358
|
proportion,
|
|
333
|
-
count
|
|
359
|
+
count,
|
|
334
360
|
sequenceName: 'sequenceName',
|
|
335
361
|
mutationFrom: 'A',
|
|
336
362
|
mutationTo: 'T',
|
|
@@ -338,11 +364,11 @@ describe('queryMutationsOverTime', () => {
|
|
|
338
364
|
};
|
|
339
365
|
}
|
|
340
366
|
|
|
341
|
-
function getSomeOtherTestMutation(proportion: number) {
|
|
367
|
+
function getSomeOtherTestMutation(proportion: number, count: number) {
|
|
342
368
|
return {
|
|
343
369
|
mutation: 'otherSequenceName:A123T',
|
|
344
370
|
proportion,
|
|
345
|
-
count
|
|
371
|
+
count,
|
|
346
372
|
sequenceName: 'otherSequenceName',
|
|
347
373
|
mutationFrom: 'G',
|
|
348
374
|
mutationTo: 'C',
|
|
@@ -5,6 +5,7 @@ import { GroupByAndSumOperator } from '../operator/GroupByAndSumOperator';
|
|
|
5
5
|
import { MapOperator } from '../operator/MapOperator';
|
|
6
6
|
import { RenameFieldOperator } from '../operator/RenameFieldOperator';
|
|
7
7
|
import { SortOperator } from '../operator/SortOperator';
|
|
8
|
+
import { UserFacingError } from '../preact/components/error-display';
|
|
8
9
|
import {
|
|
9
10
|
type LapisFilter,
|
|
10
11
|
type SequenceType,
|
|
@@ -26,13 +27,15 @@ export type MutationOverTimeData = {
|
|
|
26
27
|
mutations: SubstitutionOrDeletionEntry[];
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
export type MutationOverTimeMutationValue = number;
|
|
30
|
+
export type MutationOverTimeMutationValue = { proportion: number; count: number };
|
|
30
31
|
export type MutationOverTimeDataGroupedByMutation = Map2d<
|
|
31
32
|
Substitution | Deletion,
|
|
32
33
|
Temporal,
|
|
33
34
|
MutationOverTimeMutationValue
|
|
34
35
|
>;
|
|
35
36
|
|
|
37
|
+
const MAX_NUMBER_OF_GRID_COLUMNS = 200;
|
|
38
|
+
|
|
36
39
|
export async function queryMutationsOverTimeData(
|
|
37
40
|
lapisFilter: LapisFilter,
|
|
38
41
|
sequenceType: 'nucleotide' | 'amino acid',
|
|
@@ -43,6 +46,15 @@ export async function queryMutationsOverTimeData(
|
|
|
43
46
|
) {
|
|
44
47
|
const allDates = await getDatesInDataset(lapisFilter, lapis, granularity, lapisDateField, signal);
|
|
45
48
|
|
|
49
|
+
if (allDates.length > MAX_NUMBER_OF_GRID_COLUMNS) {
|
|
50
|
+
throw new UserFacingError(
|
|
51
|
+
'Too many dates',
|
|
52
|
+
`The dataset would contain ${allDates.length} date intervals. ` +
|
|
53
|
+
`Please reduce the number to below ${MAX_NUMBER_OF_GRID_COLUMNS} to display the data. ` +
|
|
54
|
+
'You can achieve this by either narrowing the date range in the provided LAPIS filter or by selecting a larger granularity.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
const subQueries = allDates.map(async (date) => {
|
|
47
59
|
const dateFrom = date.firstDay.toString();
|
|
48
60
|
const dateTo = date.lastDay.toString();
|
|
@@ -133,14 +145,17 @@ function fetchAndPrepareSubstitutionsOrDeletions(filter: LapisFilter, sequenceTy
|
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
export function groupByMutation(data: MutationOverTimeData[]) {
|
|
136
|
-
const dataArray = new Map2d<Substitution | Deletion, Temporal,
|
|
148
|
+
const dataArray = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>(
|
|
137
149
|
(mutation) => mutation.code,
|
|
138
150
|
(date) => date.toString(),
|
|
139
151
|
);
|
|
140
152
|
|
|
141
153
|
data.forEach((mutationData) => {
|
|
142
154
|
mutationData.mutations.forEach((mutationEntry) => {
|
|
143
|
-
dataArray.set(mutationEntry.mutation, mutationData.date,
|
|
155
|
+
dataArray.set(mutationEntry.mutation, mutationData.date, {
|
|
156
|
+
count: mutationEntry.count,
|
|
157
|
+
proportion: mutationEntry.proportion,
|
|
158
|
+
});
|
|
144
159
|
});
|
|
145
160
|
});
|
|
146
161
|
|
|
@@ -150,14 +165,14 @@ export function groupByMutation(data: MutationOverTimeData[]) {
|
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
function addZeroValuesForDatesWithNoMutationData(
|
|
153
|
-
dataArray: Map2d<Substitution | Deletion, Temporal,
|
|
168
|
+
dataArray: Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>,
|
|
154
169
|
data: MutationOverTimeData[],
|
|
155
170
|
) {
|
|
156
171
|
if (dataArray.getFirstAxisKeys().length !== 0) {
|
|
157
172
|
const someMutation = dataArray.getFirstAxisKeys()[0];
|
|
158
173
|
data.forEach((mutationData) => {
|
|
159
174
|
if (mutationData.mutations.length === 0) {
|
|
160
|
-
dataArray.set(someMutation, mutationData.date, 0);
|
|
175
|
+
dataArray.set(someMutation, mutationData.date, { count: 0, proportion: 0 });
|
|
161
176
|
}
|
|
162
177
|
});
|
|
163
178
|
}
|