@genspectrum/dashboard-components 0.6.2 → 0.6.4
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 +220 -0
- package/dist/dashboard-components.js +675 -178
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +70 -2
- package/dist/style.css +10 -4
- package/package.json +3 -1
- package/src/constants.ts +1 -1
- package/src/lapisApi/lapisTypes.ts +1 -0
- package/src/operator/FillMissingOperator.spec.ts +3 -1
- package/src/operator/FillMissingOperator.ts +4 -2
- package/src/preact/components/tooltip.stories.tsx +54 -0
- package/src/preact/components/tooltip.tsx +31 -0
- package/src/preact/mutationComparison/queryMutationData.ts +12 -4
- package/src/preact/mutationsOverTime/__mockData__/aggregated_date.json +642 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_01.json +1747 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_02.json +1774 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_03.json +1819 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_04.json +1864 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_05.json +1927 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_06.json +1864 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_07.json +9 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +98 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +66 -0
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +127 -0
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +206 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +170 -0
- package/src/preact/numberSequencesOverTime/getNumberOfSequencesOverTimeTableData.ts +1 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +1 -0
- package/src/preact/shared/table/formatProportion.ts +2 -2
- package/src/query/queryAggregatedDataOverTime.ts +8 -33
- package/src/query/queryMutationsOverTime.spec.ts +378 -0
- package/src/query/queryMutationsOverTime.ts +179 -0
- package/src/query/queryNumberOfSequencesOverTime.ts +0 -1
- package/src/query/queryRelativeGrowthAdvantage.ts +3 -3
- package/src/utils/Map2d.ts +75 -0
- package/src/utils/map2d.spec.ts +94 -0
- package/src/utils/mutations.ts +5 -1
- package/src/utils/temporal.spec.ts +5 -0
- package/src/utils/temporal.ts +88 -5
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +225 -0
- package/src/web-components/visualization/gs-mutations-over-time.tsx +112 -0
- package/src/web-components/visualization/index.ts +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import hash from 'object-hash';
|
|
2
|
+
|
|
3
|
+
export class Map2d<Key1 extends object | string, Key2 extends object | string, Value> {
|
|
4
|
+
readonly data: Map<string, Map<string, Value>> = new Map<string, Map<string, Value>>();
|
|
5
|
+
readonly keysFirstAxis = new Map<string, Key1>();
|
|
6
|
+
readonly keysSecondAxis = new Map<string, Key2>();
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
readonly serializeFirstAxis: (key: Key1) => string = (key) => (typeof key === 'string' ? key : hash(key)),
|
|
10
|
+
readonly serializeSecondAxis: (key: Key2) => string = (key) => (typeof key === 'string' ? key : hash(key)),
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
get(keyFirstAxis: Key1, keySecondAxis: Key2) {
|
|
14
|
+
const serializedKeyFirstAxis = this.serializeFirstAxis(keyFirstAxis);
|
|
15
|
+
const serializedKeySecondAxis = this.serializeSecondAxis(keySecondAxis);
|
|
16
|
+
return this.data.get(serializedKeyFirstAxis)?.get(serializedKeySecondAxis);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getRow(key: Key1, fillEmptyWith: Value) {
|
|
20
|
+
const serializedKeyFirstAxis = this.serializeFirstAxis(key);
|
|
21
|
+
const row = this.data.get(serializedKeyFirstAxis);
|
|
22
|
+
if (row === undefined) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return Array.from(this.keysSecondAxis.keys()).map((key) => row.get(key) ?? fillEmptyWith);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set(keyFirstAxis: Key1, keySecondAxis: Key2, value: Value) {
|
|
29
|
+
const serializedKeyFirstAxis = this.serializeFirstAxis(keyFirstAxis);
|
|
30
|
+
const serializedKeySecondAxis = this.serializeSecondAxis(keySecondAxis);
|
|
31
|
+
|
|
32
|
+
if (!this.data.has(serializedKeyFirstAxis)) {
|
|
33
|
+
this.data.set(serializedKeyFirstAxis, new Map<string, Value>());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.data.get(serializedKeyFirstAxis)!.set(serializedKeySecondAxis, value);
|
|
37
|
+
|
|
38
|
+
this.keysFirstAxis.set(serializedKeyFirstAxis, keyFirstAxis);
|
|
39
|
+
this.keysSecondAxis.set(serializedKeySecondAxis, keySecondAxis);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
deleteRow(key: Key1) {
|
|
43
|
+
const serializedKeyFirstAxis = this.serializeFirstAxis(key);
|
|
44
|
+
this.data.delete(serializedKeyFirstAxis);
|
|
45
|
+
this.keysFirstAxis.delete(serializedKeyFirstAxis);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getFirstAxisKeys() {
|
|
49
|
+
return Array.from(this.keysFirstAxis.values());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getSecondAxisKeys() {
|
|
53
|
+
return Array.from(this.keysSecondAxis.values());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getAsArray(fillEmptyWith: Value) {
|
|
57
|
+
return this.getFirstAxisKeys().map((firstAxisKey) => {
|
|
58
|
+
return this.getSecondAxisKeys().map((secondAxisKey) => {
|
|
59
|
+
return this.get(firstAxisKey, secondAxisKey) ?? fillEmptyWith;
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
copy() {
|
|
65
|
+
const copy = new Map2d<Key1, Key2, Value>(this.serializeFirstAxis, this.serializeSecondAxis);
|
|
66
|
+
this.data.forEach((value, key) => {
|
|
67
|
+
const keyFirstAxis = this.keysFirstAxis.get(key);
|
|
68
|
+
value.forEach((value, key) => {
|
|
69
|
+
const keySecondAxis = this.keysSecondAxis.get(key);
|
|
70
|
+
copy.set(keyFirstAxis!, keySecondAxis!, value);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return copy;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Map2d } from './Map2d';
|
|
4
|
+
|
|
5
|
+
describe('Map2d', () => {
|
|
6
|
+
it('should add a value and return it', () => {
|
|
7
|
+
const map2d = new Map2d<string, string, number>();
|
|
8
|
+
map2d.set('a', 'b', 2);
|
|
9
|
+
expect(map2d.get('a', 'b')).toBe(2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should update a value', () => {
|
|
13
|
+
const map2d = new Map2d<string, string, number>();
|
|
14
|
+
map2d.set('a', 'b', 2);
|
|
15
|
+
map2d.set('a', 'b', 3);
|
|
16
|
+
expect(map2d.get('a', 'b')).toBe(3);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should return the data as an array', () => {
|
|
20
|
+
const map2d = new Map2d<string, string, number>();
|
|
21
|
+
map2d.set('a', 'b', 1);
|
|
22
|
+
map2d.set('a', 'd', 2);
|
|
23
|
+
map2d.set('c', 'b', 3);
|
|
24
|
+
map2d.set('c', 'd', 4);
|
|
25
|
+
|
|
26
|
+
expect(map2d.getAsArray(0)).toEqual([
|
|
27
|
+
[1, 2],
|
|
28
|
+
[3, 4],
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should fill empty values with the given value', () => {
|
|
33
|
+
const map2d = new Map2d<string, string, number>();
|
|
34
|
+
map2d.set('a', 'b', 2);
|
|
35
|
+
map2d.set('c', 'd', 4);
|
|
36
|
+
expect(map2d.getAsArray(0)).toEqual([
|
|
37
|
+
[2, 0],
|
|
38
|
+
[0, 4],
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return the keys from the first axis', () => {
|
|
43
|
+
const map2d = new Map2d<string, string, number>();
|
|
44
|
+
map2d.set('a', 'b', 2);
|
|
45
|
+
map2d.set('c', 'd', 4);
|
|
46
|
+
|
|
47
|
+
expect(map2d.getFirstAxisKeys()).toEqual(['a', 'c']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should return the keys from the second axis', () => {
|
|
51
|
+
const map2d = new Map2d<string, string, number>();
|
|
52
|
+
map2d.set('a', 'b', 2);
|
|
53
|
+
map2d.set('c', 'd', 4);
|
|
54
|
+
|
|
55
|
+
expect(map2d.getSecondAxisKeys()).toEqual(['b', 'd']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should work with objects as keys', () => {
|
|
59
|
+
const map2d = new Map2d<{ a: string }, { b: string }, number>();
|
|
60
|
+
map2d.set({ a: 'a' }, { b: 'b' }, 2);
|
|
61
|
+
map2d.set({ a: 'second' }, { b: 'second' }, 3);
|
|
62
|
+
|
|
63
|
+
expect(map2d.get({ a: 'a' }, { b: 'b' })).toBe(2);
|
|
64
|
+
expect(map2d.get({ a: 'second' }, { b: 'second' })).toBe(3);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should update a value with objects as keys', () => {
|
|
68
|
+
const map2d = new Map2d<{ a: string }, { b: string }, number>();
|
|
69
|
+
map2d.set({ a: 'a' }, { b: 'b' }, 2);
|
|
70
|
+
map2d.set({ a: 'a' }, { b: 'b' }, 3);
|
|
71
|
+
expect(map2d.get({ a: 'a' }, { b: 'b' })).toBe(3);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should create a deep copy of the map', () => {
|
|
75
|
+
const map2d = new Map2d<string, string, number>();
|
|
76
|
+
map2d.set('a', 'b', 2);
|
|
77
|
+
expect(map2d.get('a', 'b')).toBe(2);
|
|
78
|
+
|
|
79
|
+
const copy = map2d.copy();
|
|
80
|
+
expect(copy.get('a', 'b')).toBe(2);
|
|
81
|
+
|
|
82
|
+
map2d.deleteRow('a');
|
|
83
|
+
expect(map2d.get('a', 'b')).toBe(undefined);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return a row by key', () => {
|
|
87
|
+
const map2d = new Map2d<string, string, number>();
|
|
88
|
+
map2d.set('a', 'b', 2);
|
|
89
|
+
map2d.set('c', 'd', 4);
|
|
90
|
+
|
|
91
|
+
expect(map2d.getRow('a', 0)).toEqual([2, 0]);
|
|
92
|
+
expect(map2d.getRow('c', 0)).toEqual([0, 4]);
|
|
93
|
+
});
|
|
94
|
+
});
|
package/src/utils/mutations.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { type SequenceType } from '../types';
|
|
1
|
+
import { type MutationType, type SequenceType } from '../types';
|
|
2
2
|
|
|
3
3
|
export interface Mutation {
|
|
4
4
|
readonly segment: string | undefined;
|
|
5
5
|
readonly position: number;
|
|
6
6
|
readonly code: string;
|
|
7
|
+
readonly type: MutationType;
|
|
7
8
|
|
|
8
9
|
equals(other: Mutation): boolean;
|
|
9
10
|
|
|
@@ -15,6 +16,7 @@ export const substitutionRegex =
|
|
|
15
16
|
|
|
16
17
|
export class Substitution implements Mutation {
|
|
17
18
|
readonly code;
|
|
19
|
+
readonly type = 'substitution';
|
|
18
20
|
|
|
19
21
|
constructor(
|
|
20
22
|
readonly segment: string | undefined,
|
|
@@ -62,6 +64,7 @@ export const deletionRegex = /^((?<segment>[A-Za-z0-9_-]+)(?=:):)?(?<valueAtRefe
|
|
|
62
64
|
|
|
63
65
|
export class Deletion implements Mutation {
|
|
64
66
|
readonly code;
|
|
67
|
+
readonly type = 'deletion';
|
|
65
68
|
|
|
66
69
|
constructor(
|
|
67
70
|
readonly segment: string | undefined,
|
|
@@ -103,6 +106,7 @@ export const insertionRegexp =
|
|
|
103
106
|
|
|
104
107
|
export class Insertion implements Mutation {
|
|
105
108
|
readonly code;
|
|
109
|
+
readonly type = 'insertion';
|
|
106
110
|
|
|
107
111
|
constructor(
|
|
108
112
|
readonly segment: string | undefined,
|
|
@@ -60,6 +60,8 @@ describe('YearMonthDay', () => {
|
|
|
60
60
|
// seems to be a bug in dayjs: https://github.com/iamkun/dayjs/issues/2620
|
|
61
61
|
expect(underTest.week.text).equal('2019-01');
|
|
62
62
|
expect(underTest.text).equal('2020-01-01');
|
|
63
|
+
expect(underTest.firstDay.text).equal('2020-01-01');
|
|
64
|
+
expect(underTest.lastDay.text).equal('2020-01-01');
|
|
63
65
|
});
|
|
64
66
|
});
|
|
65
67
|
|
|
@@ -71,6 +73,7 @@ describe('YearWeek', () => {
|
|
|
71
73
|
expect(underTest.isoWeekNumber).equal(2);
|
|
72
74
|
expect(underTest.firstDay.text).equal('2020-01-06');
|
|
73
75
|
expect(underTest.text).equal('2020-02');
|
|
76
|
+
expect(underTest.lastDay.text).equal('2020-01-12');
|
|
74
77
|
});
|
|
75
78
|
});
|
|
76
79
|
|
|
@@ -82,6 +85,7 @@ describe('YearMonth', () => {
|
|
|
82
85
|
expect(underTest.monthNumber).equal(1);
|
|
83
86
|
expect(underTest.text).equal('2020-01');
|
|
84
87
|
expect(underTest.firstDay.text).equal('2020-01-01');
|
|
88
|
+
expect(underTest.lastDay.text).equal('2020-01-31');
|
|
85
89
|
});
|
|
86
90
|
});
|
|
87
91
|
|
|
@@ -93,5 +97,6 @@ describe('Year', () => {
|
|
|
93
97
|
expect(underTest.text).equal('2020');
|
|
94
98
|
expect(underTest.firstDay.text).equal('2020-01-01');
|
|
95
99
|
expect(underTest.firstMonth.text).equal('2020-01');
|
|
100
|
+
expect(underTest.lastDay.text).equal('2020-12-31');
|
|
96
101
|
});
|
|
97
102
|
});
|
package/src/utils/temporal.ts
CHANGED
|
@@ -2,6 +2,8 @@ import dayjs from 'dayjs/esm';
|
|
|
2
2
|
import advancedFormat from 'dayjs/esm/plugin/advancedFormat';
|
|
3
3
|
import isoWeek from 'dayjs/esm/plugin/isoWeek';
|
|
4
4
|
|
|
5
|
+
import type { TemporalGranularity } from '../types';
|
|
6
|
+
|
|
5
7
|
dayjs.extend(isoWeek);
|
|
6
8
|
dayjs.extend(advancedFormat);
|
|
7
9
|
|
|
@@ -70,6 +72,18 @@ export class YearMonthDay {
|
|
|
70
72
|
return this.text;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
englishName(): string {
|
|
76
|
+
return this.dayjs.format('dddd, MMMM D, YYYY');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get firstDay(): YearMonthDay {
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get lastDay(): YearMonthDay {
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
get year(): Year {
|
|
74
88
|
return this.cache.getYear(`${this.yearNumber}`);
|
|
75
89
|
}
|
|
@@ -113,6 +127,10 @@ export class YearWeek {
|
|
|
113
127
|
return this.text;
|
|
114
128
|
}
|
|
115
129
|
|
|
130
|
+
englishName(): string {
|
|
131
|
+
return `Week ${this.isoWeekNumber}, ${this.isoYearNumber}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
116
134
|
get firstDay(): YearMonthDay {
|
|
117
135
|
// "The first week of the year, hence, always contains 4 January." https://en.wikipedia.org/wiki/ISO_week_date
|
|
118
136
|
const firstDay = dayjs()
|
|
@@ -124,6 +142,18 @@ export class YearWeek {
|
|
|
124
142
|
return this.cache.getYearMonthDay(firstDay.format('YYYY-MM-DD'));
|
|
125
143
|
}
|
|
126
144
|
|
|
145
|
+
get lastDay(): YearMonthDay {
|
|
146
|
+
const firstDay = dayjs()
|
|
147
|
+
.year(this.isoYearNumber)
|
|
148
|
+
.startOf('year')
|
|
149
|
+
.add((this.isoWeekNumber - 1) * 7, 'day')
|
|
150
|
+
.startOf('week')
|
|
151
|
+
.add(1, 'day');
|
|
152
|
+
const lastDay = firstDay.add(6, 'day');
|
|
153
|
+
|
|
154
|
+
return this.cache.getYearMonthDay(lastDay.format('YYYY-MM-DD'));
|
|
155
|
+
}
|
|
156
|
+
|
|
127
157
|
get year(): Year {
|
|
128
158
|
return this.cache.getYear(`${this.isoYearNumber}`);
|
|
129
159
|
}
|
|
@@ -159,10 +189,20 @@ export class YearMonth {
|
|
|
159
189
|
return this.text;
|
|
160
190
|
}
|
|
161
191
|
|
|
192
|
+
englishName(): string {
|
|
193
|
+
return `${monthName(this.monthNumber)} ${this.yearNumber}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
162
196
|
get firstDay(): YearMonthDay {
|
|
163
197
|
return this.cache.getYearMonthDay(dayjs(`${this.yearNumber}-${this.monthNumber}-01`).format('YYYY-MM-DD'));
|
|
164
198
|
}
|
|
165
199
|
|
|
200
|
+
get lastDay(): YearMonthDay {
|
|
201
|
+
return this.cache.getYearMonthDay(
|
|
202
|
+
dayjs(`${this.yearNumber}-${this.monthNumber}-01`).endOf('month').format('YYYY-MM-DD'),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
166
206
|
get year(): Year {
|
|
167
207
|
return this.cache.getYear(`${this.yearNumber}`);
|
|
168
208
|
}
|
|
@@ -197,14 +237,26 @@ export class Year {
|
|
|
197
237
|
return this.text;
|
|
198
238
|
}
|
|
199
239
|
|
|
240
|
+
englishName(): string {
|
|
241
|
+
return this.year.toString();
|
|
242
|
+
}
|
|
243
|
+
|
|
200
244
|
get firstMonth(): YearMonth {
|
|
201
245
|
return this.cache.getYearMonth(`${this.year}-01`);
|
|
202
246
|
}
|
|
203
247
|
|
|
248
|
+
get lastMonth(): YearMonth {
|
|
249
|
+
return this.cache.getYearMonth(`${this.year}-12`);
|
|
250
|
+
}
|
|
251
|
+
|
|
204
252
|
get firstDay(): YearMonthDay {
|
|
205
253
|
return this.firstMonth.firstDay;
|
|
206
254
|
}
|
|
207
255
|
|
|
256
|
+
get lastDay(): YearMonthDay {
|
|
257
|
+
return this.lastMonth.lastDay;
|
|
258
|
+
}
|
|
259
|
+
|
|
208
260
|
addYears(years: number): Year {
|
|
209
261
|
const date = this.firstDay.dayjs.add(years, 'year');
|
|
210
262
|
const s = date.format('YYYY');
|
|
@@ -221,6 +273,12 @@ export class Year {
|
|
|
221
273
|
}
|
|
222
274
|
}
|
|
223
275
|
|
|
276
|
+
function monthName(month: number): string {
|
|
277
|
+
return dayjs()
|
|
278
|
+
.month(month - 1)
|
|
279
|
+
.format('MMMM');
|
|
280
|
+
}
|
|
281
|
+
|
|
224
282
|
export type Temporal = YearMonthDay | YearWeek | YearMonth | Year;
|
|
225
283
|
|
|
226
284
|
export function generateAllDaysInRange(start: YearMonthDay, end: YearMonthDay): YearMonthDay[] {
|
|
@@ -311,9 +369,9 @@ export function compareTemporal(a: Temporal | null, b: Temporal | null): number
|
|
|
311
369
|
return 0;
|
|
312
370
|
}
|
|
313
371
|
|
|
314
|
-
export function getMinMaxTemporal(values: Iterable<
|
|
315
|
-
let min = null;
|
|
316
|
-
let max = null;
|
|
372
|
+
export function getMinMaxTemporal<T extends Temporal>(values: Iterable<T | null>) {
|
|
373
|
+
let min: T | null = null;
|
|
374
|
+
let max: T | null = null;
|
|
317
375
|
for (const value of values) {
|
|
318
376
|
if (value === null) {
|
|
319
377
|
continue;
|
|
@@ -326,9 +384,9 @@ export function getMinMaxTemporal(values: Iterable<Temporal | null>): [Temporal,
|
|
|
326
384
|
}
|
|
327
385
|
}
|
|
328
386
|
if (min === null || max === null) {
|
|
329
|
-
return null;
|
|
387
|
+
return { min: null, max: null };
|
|
330
388
|
}
|
|
331
|
-
return
|
|
389
|
+
return { min, max };
|
|
332
390
|
}
|
|
333
391
|
|
|
334
392
|
export function addUnit(temporal: Temporal, amount: number): Temporal {
|
|
@@ -346,3 +404,28 @@ export function addUnit(temporal: Temporal, amount: number): Temporal {
|
|
|
346
404
|
}
|
|
347
405
|
throw new Error(`Invalid argument: ${temporal}`);
|
|
348
406
|
}
|
|
407
|
+
|
|
408
|
+
export function parseDateStringToTemporal(date: string, granularity: TemporalGranularity) {
|
|
409
|
+
const cache = TemporalCache.getInstance();
|
|
410
|
+
const day = cache.getYearMonthDay(date);
|
|
411
|
+
switch (granularity) {
|
|
412
|
+
case 'day':
|
|
413
|
+
return day;
|
|
414
|
+
case 'week':
|
|
415
|
+
return day.week;
|
|
416
|
+
case 'month':
|
|
417
|
+
return day.month;
|
|
418
|
+
case 'year':
|
|
419
|
+
return day.year;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function dateRangeCompare(a: { dateRange: Temporal | null }, b: { dateRange: Temporal | null }) {
|
|
424
|
+
if (a.dateRange === null) {
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
if (b.dateRange === null) {
|
|
428
|
+
return -1;
|
|
429
|
+
}
|
|
430
|
+
return compareTemporal(a.dateRange, b.dateRange);
|
|
431
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/web-components';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
|
|
4
|
+
import './gs-mutations-over-time';
|
|
5
|
+
import '../app';
|
|
6
|
+
import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
|
|
7
|
+
import { AGGREGATED_ENDPOINT, LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
|
|
8
|
+
import aggregated_date from '../../preact/mutationsOverTime/__mockData__/aggregated_date.json';
|
|
9
|
+
import nucleotideMutation_01 from '../../preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_01.json';
|
|
10
|
+
import nucleotideMutation_02 from '../../preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_02.json';
|
|
11
|
+
import nucleotideMutation_03 from '../../preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_03.json';
|
|
12
|
+
import nucleotideMutation_04 from '../../preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_04.json';
|
|
13
|
+
import nucleotideMutation_05 from '../../preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_05.json';
|
|
14
|
+
import nucleotideMutation_06 from '../../preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_06.json';
|
|
15
|
+
import nucleotideMutation_07 from '../../preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_07.json';
|
|
16
|
+
import { type MutationsOverTimeProps } from '../../preact/mutationsOverTime/mutations-over-time';
|
|
17
|
+
|
|
18
|
+
const codeExample = String.raw`
|
|
19
|
+
<gs-mutations-over-time
|
|
20
|
+
lapisFilter='{ "pangoLineage": "JN.1*", "dateFrom": "2024-01-15", "dateTo": "2024-07-10" }'
|
|
21
|
+
sequenceType="nucleotide"
|
|
22
|
+
views='["grid"]'
|
|
23
|
+
headline="Mutations over time"
|
|
24
|
+
width='100%'
|
|
25
|
+
height='700px'
|
|
26
|
+
granularity="month"
|
|
27
|
+
lapisDateField="date"
|
|
28
|
+
></gs-mutations-over-time>`;
|
|
29
|
+
|
|
30
|
+
const meta: Meta<Required<MutationsOverTimeProps>> = {
|
|
31
|
+
title: 'Visualization/Mutations over time',
|
|
32
|
+
component: 'gs-mutations-over-time',
|
|
33
|
+
argTypes: {
|
|
34
|
+
lapisFilter: { control: 'object' },
|
|
35
|
+
sequenceType: {
|
|
36
|
+
options: ['nucleotide', 'amino acid'],
|
|
37
|
+
control: { type: 'radio' },
|
|
38
|
+
},
|
|
39
|
+
views: {
|
|
40
|
+
options: ['grid'],
|
|
41
|
+
control: { type: 'check' },
|
|
42
|
+
},
|
|
43
|
+
width: { control: 'text' },
|
|
44
|
+
height: { control: 'text' },
|
|
45
|
+
granularity: {
|
|
46
|
+
options: ['day', 'week', 'month', 'year'],
|
|
47
|
+
control: { type: 'radio' },
|
|
48
|
+
},
|
|
49
|
+
lapisDateField: { control: 'text' },
|
|
50
|
+
},
|
|
51
|
+
args: {
|
|
52
|
+
lapisFilter: { pangoLineage: 'JN.1*', dateFrom: '2024-01-15', dateTo: '2024-07-10' },
|
|
53
|
+
sequenceType: 'nucleotide',
|
|
54
|
+
views: ['grid'],
|
|
55
|
+
width: '100%',
|
|
56
|
+
height: '700px',
|
|
57
|
+
granularity: 'month',
|
|
58
|
+
lapisDateField: 'date',
|
|
59
|
+
},
|
|
60
|
+
parameters: withComponentDocs({
|
|
61
|
+
componentDocs: {
|
|
62
|
+
opensShadowDom: true,
|
|
63
|
+
expectsChildren: false,
|
|
64
|
+
codeExample,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
tags: ['autodocs'],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default meta;
|
|
71
|
+
|
|
72
|
+
const Template: StoryObj<Required<MutationsOverTimeProps>> = {
|
|
73
|
+
render: (args) => html`
|
|
74
|
+
<gs-app lapis="${LAPIS_URL}">
|
|
75
|
+
<gs-mutations-over-time
|
|
76
|
+
.lapisFilter=${args.lapisFilter}
|
|
77
|
+
.sequenceType=${args.sequenceType}
|
|
78
|
+
.views=${args.views}
|
|
79
|
+
.width=${args.width}
|
|
80
|
+
.height=${args.height}
|
|
81
|
+
.granularity=${args.granularity}
|
|
82
|
+
.lapisDateField=${args.lapisDateField}
|
|
83
|
+
></gs-mutations-over-time>
|
|
84
|
+
</gs-app>
|
|
85
|
+
`,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Default: StoryObj<Required<MutationsOverTimeProps>> = {
|
|
89
|
+
...Template,
|
|
90
|
+
parameters: {
|
|
91
|
+
fetchMock: {
|
|
92
|
+
mocks: [
|
|
93
|
+
{
|
|
94
|
+
matcher: {
|
|
95
|
+
name: 'aggregated_dates',
|
|
96
|
+
url: AGGREGATED_ENDPOINT,
|
|
97
|
+
body: {
|
|
98
|
+
dateFrom: '2024-01-15',
|
|
99
|
+
dateTo: '2024-07-10',
|
|
100
|
+
fields: ['date'],
|
|
101
|
+
pangoLineage: 'JN.1*',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
response: {
|
|
105
|
+
status: 200,
|
|
106
|
+
body: aggregated_date,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
matcher: {
|
|
111
|
+
name: 'nucleotideMutations_01',
|
|
112
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
113
|
+
body: {
|
|
114
|
+
pangoLineage: 'JN.1*',
|
|
115
|
+
dateFrom: '2024-01-01',
|
|
116
|
+
dateTo: '2024-01-31',
|
|
117
|
+
minProportion: 0.001,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
response: {
|
|
121
|
+
status: 200,
|
|
122
|
+
body: nucleotideMutation_01,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
matcher: {
|
|
127
|
+
name: 'nucleotideMutations_02',
|
|
128
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
129
|
+
body: {
|
|
130
|
+
pangoLineage: 'JN.1*',
|
|
131
|
+
dateFrom: '2024-02-01',
|
|
132
|
+
dateTo: '2024-02-29',
|
|
133
|
+
minProportion: 0.001,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
response: {
|
|
137
|
+
status: 200,
|
|
138
|
+
body: nucleotideMutation_02,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
matcher: {
|
|
143
|
+
name: 'nucleotideMutations_03',
|
|
144
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
145
|
+
body: {
|
|
146
|
+
pangoLineage: 'JN.1*',
|
|
147
|
+
dateFrom: '2024-03-01',
|
|
148
|
+
dateTo: '2024-03-31',
|
|
149
|
+
minProportion: 0.001,
|
|
150
|
+
},
|
|
151
|
+
response: {
|
|
152
|
+
status: 200,
|
|
153
|
+
body: nucleotideMutation_03,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
matcher: {
|
|
159
|
+
name: 'nucleotideMutations_04',
|
|
160
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
161
|
+
body: {
|
|
162
|
+
pangoLineage: 'JN.1*',
|
|
163
|
+
dateFrom: '2024-04-01',
|
|
164
|
+
dateTo: '2024-04-30',
|
|
165
|
+
minProportion: 0.001,
|
|
166
|
+
},
|
|
167
|
+
response: {
|
|
168
|
+
status: 200,
|
|
169
|
+
body: nucleotideMutation_04,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
matcher: {
|
|
175
|
+
name: 'nucleotideMutations_05',
|
|
176
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
177
|
+
body: {
|
|
178
|
+
pangoLineage: 'JN.1*',
|
|
179
|
+
dateFrom: '2024-05-01',
|
|
180
|
+
dateTo: '2024-05-31',
|
|
181
|
+
minProportion: 0.001,
|
|
182
|
+
},
|
|
183
|
+
response: {
|
|
184
|
+
status: 200,
|
|
185
|
+
body: nucleotideMutation_05,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
matcher: {
|
|
191
|
+
name: 'nucleotideMutations_06',
|
|
192
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
193
|
+
body: {
|
|
194
|
+
pangoLineage: 'JN.1*',
|
|
195
|
+
dateFrom: '2024-06-01',
|
|
196
|
+
dateTo: '2024-06-30',
|
|
197
|
+
minProportion: 0.001,
|
|
198
|
+
},
|
|
199
|
+
response: {
|
|
200
|
+
status: 200,
|
|
201
|
+
body: nucleotideMutation_06,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
matcher: {
|
|
208
|
+
name: 'nucleotideMutations_07',
|
|
209
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
210
|
+
body: {
|
|
211
|
+
pangoLineage: 'JN.1*',
|
|
212
|
+
dateFrom: '2024-07-01',
|
|
213
|
+
dateTo: '2024-07-31',
|
|
214
|
+
minProportion: 0.001,
|
|
215
|
+
},
|
|
216
|
+
response: {
|
|
217
|
+
status: 200,
|
|
218
|
+
body: nucleotideMutation_07,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|