@genspectrum/dashboard-components 1.13.0 → 1.14.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 (27) hide show
  1. package/custom-elements.json +393 -2
  2. package/dist/components.d.ts +170 -53
  3. package/dist/components.js +702 -164
  4. package/dist/components.js.map +1 -1
  5. package/dist/util.d.ts +190 -55
  6. package/package.json +1 -1
  7. package/src/lapisApi/lapisTypes.ts +1 -1
  8. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -1
  9. package/src/preact/queriesOverTime/__mockData__/defaultMockData/queriesOverTime.json +32 -0
  10. package/src/preact/queriesOverTime/__mockData__/manyQueries.json +128 -0
  11. package/src/preact/queriesOverTime/__mockData__/request1800s.json +16 -0
  12. package/src/preact/queriesOverTime/__mockData__/withGaps.json +52 -0
  13. package/src/preact/queriesOverTime/getFilteredQueriesOverTimeData.ts +85 -0
  14. package/src/preact/queriesOverTime/queries-over-time-filter.tsx +25 -0
  15. package/src/preact/queriesOverTime/queries-over-time-grid-tooltip.stories.tsx +134 -0
  16. package/src/preact/queriesOverTime/queries-over-time-grid-tooltip.tsx +123 -0
  17. package/src/preact/queriesOverTime/queries-over-time.stories.tsx +481 -0
  18. package/src/preact/queriesOverTime/queries-over-time.tsx +304 -0
  19. package/src/utilEntrypoint.ts +1 -0
  20. package/src/web-components/visualization/gs-mutations-over-time.spec-d.ts +3 -0
  21. package/src/web-components/visualization/gs-mutations-over-time.tsx +1 -1
  22. package/src/web-components/visualization/gs-queries-over-time.spec-d.ts +38 -0
  23. package/src/web-components/visualization/gs-queries-over-time.stories.ts +288 -0
  24. package/src/web-components/visualization/gs-queries-over-time.tsx +154 -0
  25. package/src/web-components/visualization/index.ts +1 -0
  26. package/standalone-bundle/dashboard-components.js +8510 -8069
  27. package/standalone-bundle/dashboard-components.js.map +1 -1
@@ -0,0 +1,481 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, userEvent, waitFor } from '@storybook/test';
3
+ import { type Canvas } from '@storybook/types';
4
+
5
+ import { LAPIS_URL } from '../../constants';
6
+ import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
7
+ import { LapisUrlContextProvider } from '../LapisUrlContext';
8
+ import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
9
+ import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
10
+ import { playThatExpectsFinishedLoadingEvent } from '../shared/stories/expectFinishedLoadingEvent';
11
+ import mockDefaultQueriesOverTime from './__mockData__/defaultMockData/queriesOverTime.json';
12
+ import mockManyQueriesOverTime from './__mockData__/manyQueries.json';
13
+ import mock1800sQueriesOverTime from './__mockData__/request1800s.json';
14
+ import mockWithGapsQueriesOverTime from './__mockData__/withGaps.json';
15
+ import { QueriesOverTime, type QueriesOverTimeProps } from './queries-over-time';
16
+
17
+ const meta: Meta<QueriesOverTimeProps> = {
18
+ title: 'Visualization/Queries over time',
19
+ component: QueriesOverTime,
20
+ argTypes: {
21
+ lapisFilter: { control: 'object' },
22
+ queries: { control: 'object' },
23
+ views: {
24
+ options: ['grid'],
25
+ control: { type: 'check' },
26
+ },
27
+ width: { control: 'text' },
28
+ height: { control: 'text' },
29
+ granularity: {
30
+ options: ['day', 'week', 'month', 'year'],
31
+ control: { type: 'radio' },
32
+ },
33
+ lapisDateField: { control: 'text' },
34
+ initialMeanProportionInterval: { control: 'object' },
35
+ hideGaps: { control: 'boolean' },
36
+ pageSizes: { control: 'object' },
37
+ customColumns: { control: 'object' },
38
+ },
39
+ parameters: {
40
+ fetchMock: {
41
+ mocks: [
42
+ {
43
+ matcher: {
44
+ url: `${LAPIS_URL}/component/queriesOverTime`,
45
+ body: {
46
+ filters: {
47
+ pangoLineage: 'JN.1*',
48
+ dateFrom: '2024-01-15',
49
+ dateTo: '2024-04-30',
50
+ },
51
+ queries: [
52
+ {
53
+ displayLabel: 'S:F456L (single mutation)',
54
+ countQuery: 'S:456L',
55
+ coverageQuery: '!S:456N',
56
+ },
57
+ {
58
+ displayLabel: 'R346T + F456L (combination)',
59
+ countQuery: 'S:346T & S:456L',
60
+ coverageQuery: '!S:346N & !S:456N',
61
+ },
62
+ {
63
+ displayLabel: 'C22916T or T22917G (nucleotide OR)',
64
+ countQuery: 'C22916T | T22917G',
65
+ coverageQuery: '!22916N & !22917N',
66
+ },
67
+ ],
68
+ dateRanges: [
69
+ { dateFrom: '2024-01-01', dateTo: '2024-01-31' },
70
+ { dateFrom: '2024-02-01', dateTo: '2024-02-29' },
71
+ { dateFrom: '2024-03-01', dateTo: '2024-03-31' },
72
+ { dateFrom: '2024-04-01', dateTo: '2024-04-30' },
73
+ ],
74
+ dateField: 'date',
75
+ },
76
+ matchPartialBody: true,
77
+ response: {
78
+ status: 200,
79
+ body: mockDefaultQueriesOverTime,
80
+ },
81
+ },
82
+ },
83
+ ],
84
+ },
85
+ },
86
+ };
87
+
88
+ export default meta;
89
+
90
+ export const Default: StoryObj<QueriesOverTimeProps> = {
91
+ render: (args: QueriesOverTimeProps) => (
92
+ <LapisUrlContextProvider value={LAPIS_URL}>
93
+ <ReferenceGenomeContext.Provider value={referenceGenome}>
94
+ <QueriesOverTime {...args} />
95
+ </ReferenceGenomeContext.Provider>
96
+ </LapisUrlContextProvider>
97
+ ),
98
+ args: {
99
+ lapisFilter: { pangoLineage: 'JN.1*', dateFrom: '2024-01-15', dateTo: '2024-04-30' },
100
+ queries: [
101
+ {
102
+ displayLabel: 'S:F456L (single mutation)',
103
+ countQuery: 'S:456L',
104
+ coverageQuery: '!S:456N',
105
+ },
106
+ {
107
+ displayLabel: 'R346T + F456L (combination)',
108
+ countQuery: 'S:346T & S:456L',
109
+ coverageQuery: '!S:346N & !S:456N',
110
+ },
111
+ {
112
+ displayLabel: 'C22916T or T22917G (nucleotide OR)',
113
+ countQuery: 'C22916T | T22917G',
114
+ coverageQuery: '!22916N & !22917N',
115
+ },
116
+ ],
117
+ views: ['grid'],
118
+ width: '100%',
119
+ granularity: 'month',
120
+ lapisDateField: 'date',
121
+ initialMeanProportionInterval: { min: 0, max: 1 },
122
+ hideGaps: false,
123
+ pageSizes: [10, 20, 30, 40, 50],
124
+ },
125
+ };
126
+
127
+ export const FiresFinishedLoadingEvent: StoryObj<QueriesOverTimeProps> = {
128
+ ...Default,
129
+ play: playThatExpectsFinishedLoadingEvent(),
130
+ };
131
+
132
+ export const ShowsNoDataWhenNoQueriesMatch: StoryObj<QueriesOverTimeProps> = {
133
+ ...Default,
134
+ args: {
135
+ ...Default.args,
136
+ lapisFilter: { dateFrom: '1800-01-01', dateTo: '1800-01-02' },
137
+ height: '700px',
138
+ granularity: 'year',
139
+ },
140
+ parameters: {
141
+ fetchMock: {
142
+ mocks: [
143
+ {
144
+ matcher: {
145
+ url: `${LAPIS_URL}/component/queriesOverTime`,
146
+ body: {
147
+ filters: { dateFrom: '1800-01-01', dateTo: '1800-01-02' },
148
+ dateRanges: [{ dateFrom: '1800-01-01', dateTo: '1800-12-31' }],
149
+ dateField: 'date',
150
+ },
151
+ matchPartialBody: true,
152
+ response: {
153
+ status: 200,
154
+ body: mock1800sQueriesOverTime,
155
+ },
156
+ },
157
+ },
158
+ ],
159
+ },
160
+ },
161
+ play: async ({ canvas }) => {
162
+ await waitFor(() => expect(canvas.getByText('No data available.', { exact: false })).toBeVisible());
163
+ },
164
+ };
165
+
166
+ export const UsesHideGaps: StoryObj<QueriesOverTimeProps> = {
167
+ ...Default,
168
+ args: {
169
+ ...Default.args,
170
+ queries: [
171
+ { displayLabel: 'S:F456L', countQuery: 'S:456L', coverageQuery: '!S:456N' },
172
+ { displayLabel: 'S:R346T', countQuery: 'S:346T', coverageQuery: '!S:346N' },
173
+ { displayLabel: 'S:Q493E', countQuery: 'S:493E', coverageQuery: '!S:493N' },
174
+ ],
175
+ lapisFilter: { pangoLineage: 'JN.1*', dateFrom: '2024-01-15', dateTo: '2024-07-10' },
176
+ granularity: 'month',
177
+ },
178
+ parameters: {
179
+ fetchMock: {
180
+ mocks: [
181
+ {
182
+ matcher: {
183
+ url: `${LAPIS_URL}/component/queriesOverTime`,
184
+ body: {
185
+ filters: { pangoLineage: 'JN.1*', dateFrom: '2024-01-15', dateTo: '2024-07-10' },
186
+ dateRanges: [
187
+ { dateFrom: '2024-01-01', dateTo: '2024-01-31' },
188
+ { dateFrom: '2024-02-01', dateTo: '2024-02-29' },
189
+ { dateFrom: '2024-03-01', dateTo: '2024-03-31' },
190
+ { dateFrom: '2024-04-01', dateTo: '2024-04-30' },
191
+ { dateFrom: '2024-05-01', dateTo: '2024-05-31' },
192
+ { dateFrom: '2024-06-01', dateTo: '2024-06-30' },
193
+ { dateFrom: '2024-07-01', dateTo: '2024-07-31' },
194
+ ],
195
+ queries: [
196
+ { displayLabel: 'S:F456L', countQuery: 'S:456L', coverageQuery: '!S:456N' },
197
+ { displayLabel: 'S:R346T', countQuery: 'S:346T', coverageQuery: '!S:346N' },
198
+ { displayLabel: 'S:Q493E', countQuery: 'S:493E', coverageQuery: '!S:493N' },
199
+ ],
200
+ dateField: 'date',
201
+ },
202
+ response: {
203
+ status: 200,
204
+ body: mockWithGapsQueriesOverTime,
205
+ },
206
+ },
207
+ },
208
+ ],
209
+ },
210
+ },
211
+ play: async ({ canvas, step }) => {
212
+ await expectDateRangeOnPage(canvas, '2024-04');
213
+
214
+ await step('hide gaps', async () => {
215
+ const hideGapsButton = canvas.getByRole('button', { name: 'Hide gaps' });
216
+ await userEvent.click(hideGapsButton);
217
+ });
218
+
219
+ const filteredDateRange = canvas.queryByText('2024-04');
220
+ await expect(filteredDateRange).not.toBeInTheDocument();
221
+
222
+ await step('un-hide gaps', async () => {
223
+ const hideGapsButton = canvas.getByRole('button', { name: 'Gaps hidden' });
224
+ await userEvent.click(hideGapsButton);
225
+ });
226
+
227
+ await expectDateRangeOnPage(canvas, '2024-04');
228
+ },
229
+ };
230
+
231
+ export const UsesPagination: StoryObj<QueriesOverTimeProps> = {
232
+ ...Default,
233
+ args: {
234
+ ...Default.args,
235
+ queries: [
236
+ { displayLabel: 'S:F456L', countQuery: 'S:456L', coverageQuery: '!S:456N' },
237
+ { displayLabel: 'S:R346T', countQuery: 'S:346T', coverageQuery: '!S:346N' },
238
+ { displayLabel: 'S:Q493E', countQuery: 'S:493E', coverageQuery: '!S:493N' },
239
+ { displayLabel: 'S:F486P', countQuery: 'S:486P', coverageQuery: '!S:486N' },
240
+ { displayLabel: 'S:N460K', countQuery: 'S:460K', coverageQuery: '!S:460N' },
241
+ { displayLabel: 'S:L455F', countQuery: 'S:455F', coverageQuery: '!S:455N' },
242
+ { displayLabel: 'S:L455S', countQuery: 'S:455S', coverageQuery: '!S:455N' },
243
+ { displayLabel: 'S:T572I', countQuery: 'S:572I', coverageQuery: '!S:572N' },
244
+ { displayLabel: 'S:R190S', countQuery: 'S:190S', coverageQuery: '!S:190N' },
245
+ { displayLabel: 'S:R190T', countQuery: 'S:190T', coverageQuery: '!S:190N' },
246
+ { displayLabel: 'S:K478T', countQuery: 'S:478T', coverageQuery: '!S:478N' },
247
+ { displayLabel: 'S:T22N', countQuery: 'S:22N', coverageQuery: '!S:22N' },
248
+ { displayLabel: 'S:S31P', countQuery: 'S:31P', coverageQuery: '!S:31N' },
249
+ { displayLabel: 'ORF1b:S997L', countQuery: 'ORF1b:997L', coverageQuery: '!ORF1b:997N' },
250
+ { displayLabel: 'C875A', countQuery: 'C875A', coverageQuery: '!875N' },
251
+ ],
252
+ },
253
+ parameters: {
254
+ fetchMock: {
255
+ mocks: [
256
+ {
257
+ matcher: {
258
+ url: `${LAPIS_URL}/component/queriesOverTime`,
259
+ body: {
260
+ filters: {
261
+ pangoLineage: 'JN.1*',
262
+ dateFrom: '2024-01-15',
263
+ dateTo: '2024-04-30',
264
+ },
265
+ },
266
+ matchPartialBody: true,
267
+ response: {
268
+ status: 200,
269
+ body: mockManyQueriesOverTime,
270
+ },
271
+ },
272
+ },
273
+ ],
274
+ },
275
+ },
276
+ play: async ({ canvas, step }) => {
277
+ const queryOnFirstPage = 'S:F456L';
278
+ const queryOnSecondPage = 'S:K478T';
279
+ await expectQueryOnPage(canvas, queryOnFirstPage);
280
+
281
+ await step('Navigate to next page', async () => {
282
+ canvas.getByRole('button', { name: 'Next page' }).click();
283
+
284
+ await expectQueryOnPage(canvas, queryOnSecondPage);
285
+ });
286
+
287
+ await step('Use goto page input', async () => {
288
+ const gotoPageInput = canvas.getByRole('spinbutton', { name: 'Enter page number to go to' });
289
+ await userEvent.clear(gotoPageInput);
290
+ await userEvent.type(gotoPageInput, '1');
291
+ await userEvent.tab();
292
+
293
+ await expectQueryOnPage(canvas, queryOnFirstPage);
294
+ });
295
+
296
+ await step('Change number of rows per page', async () => {
297
+ const pageSizeSelector = canvas.getByLabelText('Select number of rows per page');
298
+ await userEvent.selectOptions(pageSizeSelector, '20');
299
+
300
+ await expectQueryOnPage(canvas, queryOnFirstPage);
301
+ await expectQueryOnPage(canvas, queryOnSecondPage);
302
+ });
303
+ },
304
+ };
305
+
306
+ export const UsesQueryFilter: StoryObj<QueriesOverTimeProps> = {
307
+ ...Default,
308
+ play: async ({ canvas, step }) => {
309
+ await expectQueryOnPage(canvas, 'S:F456L (single mutation)');
310
+
311
+ await step('input filter', async () => {
312
+ const filterInput = canvas.getByPlaceholderText('Filter queries...');
313
+ await userEvent.type(filterInput, 'nucleotide');
314
+ });
315
+
316
+ await step('should show only matching filter', async () => {
317
+ await expectQueryOnPage(canvas, 'C22916T or T22917G (nucleotide OR)');
318
+
319
+ const filteredQuery = canvas.queryByText('S:F456L (single mutation)');
320
+ await expect(filteredQuery).not.toBeInTheDocument();
321
+ });
322
+ },
323
+ };
324
+
325
+ export const WithCustomColumns: StoryObj<QueriesOverTimeProps> = {
326
+ ...Default,
327
+ args: {
328
+ ...Default.args,
329
+ queries: [
330
+ { displayLabel: 'S:F456L', countQuery: 'S:456L', coverageQuery: '!S:456N' },
331
+ { displayLabel: 'S:R346T', countQuery: 'S:346T', coverageQuery: '!S:346N' },
332
+ { displayLabel: 'S:Q493E', countQuery: 'S:493E', coverageQuery: '!S:493N' },
333
+ ],
334
+ customColumns: [
335
+ {
336
+ header: 'Jaccard Index',
337
+ values: {
338
+ 'S:F456L': 0.75,
339
+ 'S:R346T': 'Foobar',
340
+ },
341
+ },
342
+ ],
343
+ },
344
+ parameters: {
345
+ fetchMock: {
346
+ mocks: [
347
+ {
348
+ matcher: {
349
+ url: `${LAPIS_URL}/component/queriesOverTime`,
350
+ body: {
351
+ filters: {
352
+ pangoLineage: 'JN.1*',
353
+ dateFrom: '2024-01-15',
354
+ dateTo: '2024-04-30',
355
+ },
356
+ },
357
+ matchPartialBody: true,
358
+ response: {
359
+ status: 200,
360
+ body: mockWithGapsQueriesOverTime,
361
+ },
362
+ },
363
+ },
364
+ ],
365
+ },
366
+ },
367
+ play: async ({ canvas }) => {
368
+ await waitFor(() => expect(canvas.getByText('Jaccard Index')).toBeVisible(), {
369
+ timeout: 5000,
370
+ });
371
+
372
+ await waitFor(() => expect(canvas.getByText('0.75')).toBeVisible());
373
+
374
+ await waitFor(() => expect(canvas.getByText('Foobar')).toBeVisible());
375
+ },
376
+ };
377
+
378
+ export const ShowsNoDataMessageWhenThereAreNoDatesInFilter: StoryObj<QueriesOverTimeProps> = {
379
+ ...Default,
380
+ args: {
381
+ ...Default.args,
382
+ lapisFilter: { dateFrom: '2345-01-01', dateTo: '2020-01-02' },
383
+ height: '700px',
384
+ granularity: 'year',
385
+ },
386
+ parameters: {
387
+ fetchMock: {
388
+ mocks: [
389
+ {
390
+ matcher: {
391
+ url: `${LAPIS_URL}/component/queriesOverTime`,
392
+ body: {
393
+ filters: { dateFrom: '2345-01-01', dateTo: '2020-01-02' },
394
+ dateField: 'date',
395
+ },
396
+ matchPartialBody: true,
397
+ response: {
398
+ status: 200,
399
+ body: {
400
+ data: { queries: [], dateRanges: [], data: [], totalCountsByDateRange: [] },
401
+ },
402
+ },
403
+ },
404
+ },
405
+ ],
406
+ },
407
+ },
408
+ play: async ({ canvas }) => {
409
+ await waitFor(() => expect(canvas.getByText('No data available.', { exact: false })).toBeVisible(), {
410
+ timeout: 10000,
411
+ });
412
+ },
413
+ };
414
+
415
+ export const ShowsNoDataMessageForStrictFilters: StoryObj<QueriesOverTimeProps> = {
416
+ ...Default,
417
+ play: async ({ canvas }) => {
418
+ await waitFor(() => expect(canvas.getByText('Grid')).toBeVisible(), { timeout: 10000 });
419
+
420
+ const button = canvas.getByRole('button', { name: 'Mean proportion 0.0% - 100.0%' });
421
+ await userEvent.click(button);
422
+
423
+ const minInput = canvas.getAllByLabelText('%')[0];
424
+ await userEvent.clear(minInput);
425
+ await userEvent.type(minInput, '40');
426
+
427
+ const maxInput = canvas.getAllByLabelText('%')[1];
428
+ await userEvent.clear(maxInput);
429
+ await userEvent.type(maxInput, '41');
430
+
431
+ await waitFor(
432
+ () => expect(canvas.getByText('No data available for your filters.', { exact: false })).toBeVisible(),
433
+ {
434
+ timeout: 10000,
435
+ },
436
+ );
437
+ },
438
+ };
439
+
440
+ export const ShowsNoDataForStrictInitialProportionInterval: StoryObj<QueriesOverTimeProps> = {
441
+ ...ShowsNoDataMessageForStrictFilters,
442
+ args: {
443
+ ...ShowsNoDataMessageForStrictFilters.args,
444
+ initialMeanProportionInterval: { min: 0.4, max: 0.41 },
445
+ },
446
+ play: async ({ canvas }) => {
447
+ await waitFor(
448
+ () => expect(canvas.getByText('No data available for your filters.', { exact: false })).toBeVisible(),
449
+ {
450
+ timeout: 10000,
451
+ },
452
+ );
453
+ },
454
+ };
455
+
456
+ export const WithNoLapisDateFieldField: StoryObj<QueriesOverTimeProps> = {
457
+ ...Default,
458
+ args: {
459
+ ...Default.args,
460
+ lapisDateField: '',
461
+ },
462
+ play: async ({ canvasElement, step }) => {
463
+ await step('expect error message', async () => {
464
+ await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
465
+ });
466
+ },
467
+ };
468
+
469
+ async function expectQueryOnPage(canvas: Canvas, query: string) {
470
+ await waitFor(async () => {
471
+ const queryOnPage = canvas.getAllByText(query)[0];
472
+ await expect(queryOnPage).toBeVisible();
473
+ });
474
+ }
475
+
476
+ async function expectDateRangeOnPage(canvas: Canvas, dateRange: string) {
477
+ await waitFor(async () => {
478
+ const dateRangeOnPage = canvas.getAllByText(dateRange)[0];
479
+ await expect(dateRangeOnPage).toBeVisible();
480
+ });
481
+ }