@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,348 @@
1
+ import dayjs from 'dayjs/esm';
2
+ import advancedFormat from 'dayjs/esm/plugin/advancedFormat';
3
+ import isoWeek from 'dayjs/esm/plugin/isoWeek';
4
+
5
+ dayjs.extend(isoWeek);
6
+ dayjs.extend(advancedFormat);
7
+
8
+ export class TemporalCache {
9
+ private yearMonthDayCache = new Map<string, YearMonthDay>();
10
+ private yearWeekCache = new Map<string, YearWeek>();
11
+ private yearMonthCache = new Map<string, YearMonth>();
12
+ private yearCache = new Map<string, Year>();
13
+
14
+ private constructor() {}
15
+
16
+ getYearMonthDay(s: string): YearMonthDay {
17
+ if (!this.yearMonthDayCache.has(s)) {
18
+ this.yearMonthDayCache.set(s, YearMonthDay.parse(s, this));
19
+ }
20
+ return this.yearMonthDayCache.get(s)!;
21
+ }
22
+
23
+ getYearMonth(s: string): YearMonth {
24
+ if (!this.yearMonthCache.has(s)) {
25
+ this.yearMonthCache.set(s, YearMonth.parse(s, this));
26
+ }
27
+ return this.yearMonthCache.get(s)!;
28
+ }
29
+
30
+ getYearWeek(s: string): YearWeek {
31
+ if (!this.yearWeekCache.has(s)) {
32
+ this.yearWeekCache.set(s, YearWeek.parse(s, this));
33
+ }
34
+ return this.yearWeekCache.get(s)!;
35
+ }
36
+
37
+ getYear(s: string): Year {
38
+ if (!this.yearCache.has(s)) {
39
+ this.yearCache.set(s, Year.parse(s, this));
40
+ }
41
+ return this.yearCache.get(s)!;
42
+ }
43
+
44
+ private static instance = new TemporalCache();
45
+
46
+ static getInstance(): TemporalCache {
47
+ return this.instance;
48
+ }
49
+ }
50
+
51
+ export class YearMonthDay {
52
+ readonly date;
53
+ readonly dayjs;
54
+
55
+ constructor(
56
+ readonly yearNumber: number,
57
+ readonly monthNumber: number,
58
+ readonly dayNumber: number,
59
+ readonly cache: TemporalCache,
60
+ ) {
61
+ this.date = new Date(this.yearNumber, this.monthNumber - 1, this.dayNumber);
62
+ this.dayjs = dayjs(this.date);
63
+ }
64
+
65
+ get text(): string {
66
+ return this.dayjs.format('YYYY-MM-DD');
67
+ }
68
+
69
+ toString(): string {
70
+ return this.text;
71
+ }
72
+
73
+ get year(): Year {
74
+ return this.cache.getYear(`${this.yearNumber}`);
75
+ }
76
+
77
+ get month(): YearMonth {
78
+ return this.cache.getYearMonth(this.dayjs.format('YYYY-MM'));
79
+ }
80
+
81
+ get week(): YearWeek {
82
+ return this.cache.getYearWeek(this.dayjs.format('GGGG-WW'));
83
+ }
84
+
85
+ addDays(days: number): YearMonthDay {
86
+ const date = this.dayjs.add(days, 'day');
87
+ const s = date.format('YYYY-MM-DD');
88
+ return this.cache.getYearMonthDay(s);
89
+ }
90
+
91
+ minus(other: YearMonthDay): number {
92
+ return this.dayjs.diff(other.dayjs, 'day');
93
+ }
94
+
95
+ static parse(s: string, cache: TemporalCache): YearMonthDay {
96
+ const [year, month, day] = s.split('-').map((s) => parseInt(s, 10));
97
+ return new YearMonthDay(year, month, day, cache);
98
+ }
99
+ }
100
+
101
+ export class YearWeek {
102
+ constructor(
103
+ readonly isoYearNumber: number,
104
+ readonly isoWeekNumber: number,
105
+ readonly cache: TemporalCache,
106
+ ) {}
107
+
108
+ get text(): string {
109
+ return this.firstDay.dayjs.format('YYYY-WW');
110
+ }
111
+
112
+ toString(): string {
113
+ return this.text;
114
+ }
115
+
116
+ get firstDay(): YearMonthDay {
117
+ // "The first week of the year, hence, always contains 4 January." https://en.wikipedia.org/wiki/ISO_week_date
118
+ const firstDay = dayjs()
119
+ .year(this.isoYearNumber)
120
+ .month(1)
121
+ .date(4)
122
+ .isoWeek(this.isoWeekNumber)
123
+ .startOf('isoWeek');
124
+ return this.cache.getYearMonthDay(firstDay.format('YYYY-MM-DD'));
125
+ }
126
+
127
+ get year(): Year {
128
+ return this.cache.getYear(`${this.isoYearNumber}`);
129
+ }
130
+
131
+ addWeeks(weeks: number): YearWeek {
132
+ const date = this.firstDay.dayjs.add(weeks, 'week');
133
+ const s = date.format('YYYY-WW');
134
+ return this.cache.getYearWeek(s);
135
+ }
136
+
137
+ minus(other: YearWeek): number {
138
+ return this.firstDay.dayjs.diff(other.firstDay.dayjs, 'week');
139
+ }
140
+
141
+ static parse(s: string, cache: TemporalCache): YearWeek {
142
+ const [year, week] = s.split('-').map((s) => parseInt(s, 10));
143
+ return new YearWeek(year, week, cache);
144
+ }
145
+ }
146
+
147
+ export class YearMonth {
148
+ constructor(
149
+ readonly yearNumber: number,
150
+ readonly monthNumber: number,
151
+ readonly cache: TemporalCache,
152
+ ) {}
153
+
154
+ get text(): string {
155
+ return this.firstDay.dayjs.format('YYYY-MM');
156
+ }
157
+
158
+ toString(): string {
159
+ return this.text;
160
+ }
161
+
162
+ get firstDay(): YearMonthDay {
163
+ return this.cache.getYearMonthDay(dayjs(`${this.yearNumber}-${this.monthNumber}-01`).format('YYYY-MM-DD'));
164
+ }
165
+
166
+ get year(): Year {
167
+ return this.cache.getYear(`${this.yearNumber}`);
168
+ }
169
+
170
+ addMonths(months: number): YearMonth {
171
+ const date = this.firstDay.dayjs.add(months, 'month');
172
+ const s = date.format('YYYY-MM');
173
+ return this.cache.getYearMonth(s);
174
+ }
175
+
176
+ minus(other: YearMonth): number {
177
+ return this.firstDay.dayjs.diff(other.firstDay.dayjs, 'month');
178
+ }
179
+
180
+ static parse(s: string, cache: TemporalCache): YearMonth {
181
+ const [year, month] = s.split('-').map((s) => parseInt(s, 10));
182
+ return new YearMonth(year, month, cache);
183
+ }
184
+ }
185
+
186
+ export class Year {
187
+ constructor(
188
+ readonly year: number,
189
+ readonly cache: TemporalCache,
190
+ ) {}
191
+
192
+ get text(): string {
193
+ return this.firstDay.dayjs.format('YYYY');
194
+ }
195
+
196
+ toString(): string {
197
+ return this.text;
198
+ }
199
+
200
+ get firstMonth(): YearMonth {
201
+ return this.cache.getYearMonth(`${this.year}-01`);
202
+ }
203
+
204
+ get firstDay(): YearMonthDay {
205
+ return this.firstMonth.firstDay;
206
+ }
207
+
208
+ addYears(years: number): Year {
209
+ const date = this.firstDay.dayjs.add(years, 'year');
210
+ const s = date.format('YYYY');
211
+ return this.cache.getYear(s);
212
+ }
213
+
214
+ minus(other: Year): number {
215
+ return this.firstDay.dayjs.diff(other.firstDay.dayjs, 'year');
216
+ }
217
+
218
+ static parse(s: string, cache: TemporalCache): Year {
219
+ const year = parseInt(s, 10);
220
+ return new Year(year, cache);
221
+ }
222
+ }
223
+
224
+ export type Temporal = YearMonthDay | YearWeek | YearMonth | Year;
225
+
226
+ export function generateAllDaysInRange(start: YearMonthDay, end: YearMonthDay): YearMonthDay[] {
227
+ const days = [];
228
+ const daysInBetween = end.minus(start);
229
+ for (let i = 0; i <= daysInBetween; i++) {
230
+ days.push(start.addDays(i));
231
+ }
232
+ return days;
233
+ }
234
+
235
+ export function generateAllWeeksInRange(start: YearWeek, end: YearWeek): YearWeek[] {
236
+ const weeks = [];
237
+ const weeksInBetween = end.minus(start);
238
+ for (let i = 0; i <= weeksInBetween; i++) {
239
+ weeks.push(start.addWeeks(i));
240
+ }
241
+ return weeks;
242
+ }
243
+
244
+ export function generateAllMonthsInRange(start: YearMonth, end: YearMonth): YearMonth[] {
245
+ const months = [];
246
+ const monthsInBetween = end.minus(start);
247
+ for (let i = 0; i <= monthsInBetween; i++) {
248
+ months.push(start.addMonths(i));
249
+ }
250
+ return months;
251
+ }
252
+
253
+ export function generateAllYearsInRange(start: Year, end: Year): Year[] {
254
+ const years = [];
255
+ const yearsInBetween = end.minus(start);
256
+ for (let i = 0; i <= yearsInBetween; i++) {
257
+ years.push(start.addYears(i));
258
+ }
259
+ return years;
260
+ }
261
+
262
+ export function generateAllInRange(start: Temporal | null, end: Temporal | null): Temporal[] {
263
+ if (start === null || end === null) {
264
+ return [];
265
+ }
266
+ if (start instanceof YearMonthDay && end instanceof YearMonthDay) {
267
+ return generateAllDaysInRange(start, end);
268
+ }
269
+ if (start instanceof YearWeek && end instanceof YearWeek) {
270
+ return generateAllWeeksInRange(start, end);
271
+ }
272
+ if (start instanceof YearMonth && end instanceof YearMonth) {
273
+ return generateAllMonthsInRange(start, end);
274
+ }
275
+ if (start instanceof Year && end instanceof Year) {
276
+ return generateAllYearsInRange(start, end);
277
+ }
278
+ throw new Error(`Invalid arguments: start and end must be of the same type: ${start}, ${end}`);
279
+ }
280
+
281
+ export function minusTemporal(a: Temporal, b: Temporal): number {
282
+ if (a instanceof YearMonthDay && b instanceof YearMonthDay) {
283
+ return a.minus(b);
284
+ }
285
+ if (a instanceof YearWeek && b instanceof YearWeek) {
286
+ return a.minus(b);
287
+ }
288
+ if (a instanceof YearMonth && b instanceof YearMonth) {
289
+ return a.minus(b);
290
+ }
291
+ if (a instanceof Year && b instanceof Year) {
292
+ return a.minus(b);
293
+ }
294
+ throw new Error(`Cannot compare ${a} and ${b}`);
295
+ }
296
+
297
+ export function compareTemporal(a: Temporal | null, b: Temporal | null): number {
298
+ if (a === null) {
299
+ return 1;
300
+ }
301
+ if (b === null) {
302
+ return -1;
303
+ }
304
+ const diff = minusTemporal(a, b);
305
+ if (diff < 0) {
306
+ return -1;
307
+ }
308
+ if (diff > 0) {
309
+ return 1;
310
+ }
311
+ return 0;
312
+ }
313
+
314
+ export function getMinMaxTemporal(values: Iterable<Temporal | null>): [Temporal, Temporal] | null {
315
+ let min = null;
316
+ let max = null;
317
+ for (const value of values) {
318
+ if (value === null) {
319
+ continue;
320
+ }
321
+ if (min === null || compareTemporal(value, min) < 0) {
322
+ min = value;
323
+ }
324
+ if (max === null || compareTemporal(value, max) > 0) {
325
+ max = value;
326
+ }
327
+ }
328
+ if (min === null || max === null) {
329
+ return null;
330
+ }
331
+ return [min, max];
332
+ }
333
+
334
+ export function addUnit(temporal: Temporal, amount: number): Temporal {
335
+ if (temporal instanceof YearMonthDay) {
336
+ return temporal.addDays(amount);
337
+ }
338
+ if (temporal instanceof YearWeek) {
339
+ return temporal.addWeeks(amount);
340
+ }
341
+ if (temporal instanceof YearMonth) {
342
+ return temporal.addMonths(amount);
343
+ }
344
+ if (temporal instanceof Year) {
345
+ return temporal.addYears(amount);
346
+ }
347
+ throw new Error(`Invalid argument: ${temporal}`);
348
+ }
@@ -0,0 +1,5 @@
1
+ import { expect } from 'vitest';
2
+
3
+ export function expectEqualAfterSorting<T>(actual: T[], expected: T[], compareFn?: (a: T, b: T) => number) {
4
+ return expect(actual.sort(compareFn)).deep.equal(expected.sort(compareFn));
5
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * NumberFields<S> is a type that represents the keys of an object S that have a number value.
3
+ * For example, given `type Person = { age: number }`, it holds `'age' extends NumberFields<Person>`.
4
+ */
5
+ export type NumberFields<S> = keyof S & { [P in keyof S]: S[P] extends number ? P : never }[keyof S];
6
+
7
+ /**
8
+ * MappedType<K, T> is a type that represents an object with keys of type K and values of type T.
9
+ */
10
+ export type MappedType<K extends string | number | symbol, T> = { [P in K]: T };
11
+
12
+ /**
13
+ * MappedNumber<K> is a type that represents an object with keys of type K and values of type number.
14
+ */
15
+ export type MappedNumber<K extends string | number | symbol> = MappedType<K, number>;
@@ -0,0 +1,16 @@
1
+ import { expect, describe, it } from 'vitest';
2
+
3
+ import { mapLapisFilterToUrlParams } from './utils';
4
+
5
+ describe('mapLapisFilterToUrlParams', () => {
6
+ it('should produce correct url params', () => {
7
+ const urlSearchParams = mapLapisFilterToUrlParams({
8
+ string: 'stringValue',
9
+ number: 42,
10
+ boolean: true,
11
+ null: null,
12
+ });
13
+
14
+ expect(urlSearchParams.toString()).equals('string=stringValue&number=42&boolean=true&null=null');
15
+ });
16
+ });
@@ -0,0 +1,38 @@
1
+ import { type LapisFilter } from '../types';
2
+
3
+ export function getMinMaxNumber(values: Iterable<number>): [number, number] | null {
4
+ let min = null;
5
+ let max = null;
6
+ for (const value of values) {
7
+ if (min === null || value < min) {
8
+ min = value;
9
+ }
10
+ if (max === null || value > max) {
11
+ max = value;
12
+ }
13
+ }
14
+ if (min === null || max === null) {
15
+ return null;
16
+ }
17
+ return [min, max];
18
+ }
19
+
20
+ export function mapLapisFilterToUrlParams(filter: LapisFilter): URLSearchParams {
21
+ const params = Object.entries(filter).map(([key, value]) => [key, stringifyLapisFilterValue(value)]);
22
+
23
+ return new URLSearchParams(params);
24
+ }
25
+
26
+ function stringifyLapisFilterValue(value: LapisFilter[string]) {
27
+ if (value === null) {
28
+ return 'null';
29
+ }
30
+ switch (typeof value) {
31
+ case 'boolean':
32
+ return value ? 'true' : 'false';
33
+ case 'number':
34
+ return value.toString();
35
+ case 'string':
36
+ return value;
37
+ }
38
+ }
@@ -0,0 +1,62 @@
1
+ import { consume } from '@lit/context';
2
+ import { type PropertyValues, ReactiveElement } from '@lit/reactive-element';
3
+ import { unsafeCSS } from 'lit';
4
+ import { render } from 'preact';
5
+ import { type JSXInternal } from 'preact/src/jsx';
6
+
7
+ import { lapisContext } from './lapis-context';
8
+ import { referenceGenomeContext } from './reference-genome-context';
9
+ import { type ReferenceGenome } from '../lapisApi/ReferenceGenome';
10
+ import { LapisUrlContext } from '../preact/LapisUrlContext';
11
+ import { ReferenceGenomeContext } from '../preact/ReferenceGenomeContext';
12
+ import minMaxPercentSliderCss from '../preact/components/min-max-percent-slider.css?inline';
13
+ import tailwindStyle from '../styles/tailwind.css?inline';
14
+
15
+ import '../styles/tailwind.css';
16
+ import '../preact/components/min-max-percent-slider.css';
17
+
18
+ const tailwindElementCss = unsafeCSS(tailwindStyle);
19
+ const minMaxPercentSliderElementCss = unsafeCSS(minMaxPercentSliderCss);
20
+
21
+ export abstract class PreactLitAdapter extends ReactiveElement {
22
+ static override styles = [tailwindElementCss, minMaxPercentSliderElementCss];
23
+
24
+ /**
25
+ * @internal
26
+ * The URL of the Lapis instance.
27
+ *
28
+ * This component must be a child of a `gs-app` component.
29
+ * This value will automatically be injected by the parent `gs-app` component.
30
+ */
31
+ @consume({ context: lapisContext })
32
+ lapis: string = '';
33
+
34
+ /**
35
+ * @internal
36
+ * The reference genomes of the underlying organism.
37
+ * These will be fetched from the Lapis instance.
38
+ *
39
+ * This component must be a child of a `gs-app` component.
40
+ * This value will automatically be injected by the parent `gs-app` component.
41
+ */
42
+ @consume({ context: referenceGenomeContext, subscribe: true })
43
+ referenceGenome: ReferenceGenome = {
44
+ nucleotideSequences: [],
45
+ genes: [],
46
+ };
47
+
48
+ override update(changedProperties: PropertyValues) {
49
+ console.log('this.lapis', this.lapis);
50
+ const vdom = (
51
+ <LapisUrlContext.Provider value={this.lapis}>
52
+ <ReferenceGenomeContext.Provider value={this.referenceGenome}>
53
+ {this.render()}
54
+ </ReferenceGenomeContext.Provider>
55
+ </LapisUrlContext.Provider>
56
+ );
57
+ super.update(changedProperties);
58
+ render(vdom, this.renderRoot);
59
+ }
60
+
61
+ protected abstract render(): JSXInternal.Element;
62
+ }
@@ -0,0 +1,12 @@
1
+ import gridJsStyle from 'gridjs/dist/theme/mermaid.css?inline';
2
+ import { unsafeCSS } from 'lit';
3
+
4
+ import { PreactLitAdapter } from './PreactLitAdapter';
5
+
6
+ import 'gridjs/dist/theme/mermaid.css';
7
+
8
+ const gridJsElementCss = unsafeCSS(gridJsStyle);
9
+
10
+ export abstract class PreactLitAdapterWithGridJsStyles extends PreactLitAdapter {
11
+ static override styles = [...PreactLitAdapter.styles, gridJsElementCss];
12
+ }
@@ -0,0 +1,51 @@
1
+ import { provide } from '@lit/context';
2
+ import { Task } from '@lit/task';
3
+ import { html, LitElement } from 'lit';
4
+ import { customElement, property } from 'lit/decorators.js';
5
+
6
+ import { lapisContext } from './lapis-context';
7
+ import { referenceGenomeContext } from './reference-genome-context';
8
+ import { type ReferenceGenome } from '../lapisApi/ReferenceGenome';
9
+ import { fetchReferenceGenome } from '../lapisApi/lapisApi';
10
+
11
+ @customElement('gs-app')
12
+ export class App extends LitElement {
13
+ @provide({ context: lapisContext })
14
+ @property()
15
+ lapis: string = '';
16
+
17
+ @provide({ context: referenceGenomeContext })
18
+ referenceGenome: ReferenceGenome = {
19
+ nucleotideSequences: [],
20
+ genes: [],
21
+ };
22
+
23
+ private updateReferenceGenome = new Task(this, {
24
+ task: async () => {
25
+ this.referenceGenome = await fetchReferenceGenome(this.lapis);
26
+ },
27
+ args: () => [this.lapis],
28
+ });
29
+
30
+ override render() {
31
+ return this.updateReferenceGenome.render({
32
+ complete: () => {
33
+ return html` <slot></slot>`;
34
+ },
35
+ error: () => html`<p>Error</p>`, // TODO(#143): Add more advanced error handling
36
+ pending: () => {
37
+ return html`<p>Loading...</p>`;
38
+ },
39
+ });
40
+ }
41
+
42
+ override createRenderRoot() {
43
+ return this;
44
+ }
45
+ }
46
+
47
+ declare global {
48
+ interface HTMLElementTagNameMap {
49
+ 'gs-app': App;
50
+ }
51
+ }
@@ -0,0 +1,4 @@
1
+ export { MutationComparisonComponent } from './mutation-comparison-component';
2
+ export { MutationsComponent } from './mutations-component';
3
+ export { PrevalenceOverTimeComponent } from './prevalence-over-time-component';
4
+ export { RelativeGrowthAdvantageComponent } from './relative-growth-advantage-component';
@@ -0,0 +1,138 @@
1
+ import { expect, fireEvent, waitFor } from '@storybook/test';
2
+ import type { Meta, StoryObj } from '@storybook/web-components';
3
+ import { html } from 'lit';
4
+
5
+ import './mutation-comparison-component';
6
+ import '../app';
7
+ import { LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
8
+ import nucleotideMutationsOtherVariant from '../../preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json';
9
+ import nucleotideMutationsSomeVariant from '../../preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json';
10
+ import { type MutationComparisonProps } from '../../preact/mutationComparison/mutation-comparison';
11
+ import { withinShadowRoot } from '../withinShadowRoot.story';
12
+
13
+ const meta: Meta<MutationComparisonProps> = {
14
+ title: 'Visualization/Mutation comparison',
15
+ component: 'gs-mutation-comparison-component',
16
+ argTypes: {
17
+ variants: { control: 'object' },
18
+ sequenceType: {
19
+ options: ['nucleotide', 'amino acid'],
20
+ control: { type: 'radio' },
21
+ },
22
+ views: {
23
+ options: ['table', 'venn'],
24
+ control: { type: 'check' },
25
+ },
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+
31
+ const Template: StoryObj<MutationComparisonProps> = {
32
+ render: (args) => html`
33
+ <div class="w-11/12 h-11/12">
34
+ <gs-app lapis="${LAPIS_URL}">
35
+ <gs-mutation-comparison-component
36
+ .variants=${args.variants}
37
+ .sequenceType=${args.sequenceType}
38
+ .views=${args.views}
39
+ ></gs-mutation-comparison-component>
40
+ </gs-app>
41
+ </div>
42
+ `,
43
+ };
44
+
45
+ const dateTo = '2022-01-01';
46
+ const dateFrom = '2021-01-01';
47
+
48
+ export const Default: StoryObj<MutationComparisonProps> = {
49
+ ...Template,
50
+ args: {
51
+ variants: [
52
+ {
53
+ displayName: 'Some variant',
54
+ lapisFilter: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo },
55
+ },
56
+ {
57
+ displayName: 'Other variant',
58
+ lapisFilter: {
59
+ country: 'Switzerland',
60
+ pangoLineage: 'B.1.1.7',
61
+ dateFrom,
62
+ dateTo,
63
+ },
64
+ },
65
+ ],
66
+ sequenceType: 'nucleotide',
67
+ views: ['table', 'venn'],
68
+ },
69
+ parameters: {
70
+ fetchMock: {
71
+ mocks: [
72
+ {
73
+ matcher: {
74
+ name: 'nucleotideMutationsSomeVariant',
75
+ url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
76
+ body: {
77
+ country: 'Switzerland',
78
+ pangoLineage: 'B.1.1.7',
79
+ dateTo,
80
+ minProportion: 0,
81
+ },
82
+ },
83
+ response: {
84
+ status: 200,
85
+ body: nucleotideMutationsSomeVariant,
86
+ },
87
+ },
88
+ {
89
+ matcher: {
90
+ name: 'nucleotideMutationsOtherVariant',
91
+ url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
92
+ body: {
93
+ country: 'Switzerland',
94
+ pangoLineage: 'B.1.1.7',
95
+ dateFrom,
96
+ dateTo,
97
+ minProportion: 0,
98
+ },
99
+ },
100
+ response: {
101
+ status: 200,
102
+ body: nucleotideMutationsOtherVariant,
103
+ },
104
+ },
105
+ ],
106
+ },
107
+ },
108
+ play: async ({ canvasElement, step }) => {
109
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-comparison-component');
110
+
111
+ await step('Min and max proportions should be 50% and 100%', async () => {
112
+ const minInput = () => canvas.getAllByLabelText('%')[0];
113
+ const maxInput = () => canvas.getAllByLabelText('%')[1];
114
+
115
+ await waitFor(() => expect(minInput()).toHaveValue(50));
116
+ await waitFor(() => expect(maxInput()).toHaveValue(100));
117
+ });
118
+ },
119
+ };
120
+
121
+ export const VennDiagram: StoryObj<MutationComparisonProps> = {
122
+ ...Default,
123
+ play: async ({ canvasElement, step }) => {
124
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-comparison-component');
125
+
126
+ await step('Switch to Venn diagram view', async () => {
127
+ await waitFor(() => expect(canvas.getByLabelText('Venn', { selector: 'input' })).toBeInTheDocument());
128
+
129
+ await fireEvent.click(canvas.getByLabelText('Venn', { selector: 'input' }));
130
+
131
+ await waitFor(() =>
132
+ expect(
133
+ canvas.getByText('You have no elements selected. Click in the venn diagram to select.'),
134
+ ).toBeVisible(),
135
+ );
136
+ });
137
+ },
138
+ };