@genspectrum/dashboard-components 0.8.0 → 0.8.2

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 (35) hide show
  1. package/custom-elements.json +1 -1
  2. package/dist/assets/mutationOverTimeWorker-kjUXkRmn.js.map +1 -0
  3. package/dist/dashboard-components.js +142 -70
  4. package/dist/dashboard-components.js.map +1 -1
  5. package/dist/genspectrum-components.d.ts +2 -2
  6. package/dist/style.css +36 -0
  7. package/package.json +1 -1
  8. package/src/lapisApi/lapisApi.ts +59 -34
  9. package/src/preact/aggregatedData/aggregate.stories.tsx +35 -0
  10. package/src/preact/aggregatedData/aggregate.tsx +1 -2
  11. package/src/preact/components/error-boundary.tsx +9 -4
  12. package/src/preact/components/error-display.stories.tsx +23 -3
  13. package/src/preact/components/error-display.tsx +37 -25
  14. package/src/preact/dateRangeSelector/date-range-selector.tsx +1 -1
  15. package/src/preact/lineageFilter/lineage-filter.tsx +2 -3
  16. package/src/preact/locationFilter/location-filter.tsx +2 -3
  17. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -2
  18. package/src/preact/mutationFilter/mutation-filter.tsx +1 -1
  19. package/src/preact/mutations/mutations.tsx +1 -2
  20. package/src/preact/mutationsOverTime/__mockData__/noDataWhenNoMutationsAreInFilter.ts +22 -0
  21. package/src/preact/mutationsOverTime/__mockData__/noDataWhenThereAreNoDatesInFilter.ts +22 -0
  22. package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +2 -0
  23. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +3 -0
  24. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +71 -1
  25. package/src/preact/mutationsOverTime/mutations-over-time.tsx +10 -5
  26. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +1 -2
  27. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -2
  28. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +1 -2
  29. package/src/preact/textInput/text-input.tsx +2 -3
  30. package/src/query/queryMutationsOverTime.spec.ts +30 -0
  31. package/src/query/queryMutationsOverTime.ts +7 -0
  32. package/src/web-components/visualization/gs-mutations-over-time.tsx +2 -2
  33. package/standalone-bundle/dashboard-components.js +4194 -4091
  34. package/standalone-bundle/dashboard-components.js.map +1 -1
  35. package/dist/assets/mutationOverTimeWorker-BOCXtKzd.js.map +0 -1
@@ -547,8 +547,8 @@ export declare class MutationsComponent extends PreactLitAdapterWithGridJsStyles
547
547
  *
548
548
  * The grid view shows the proportion for each mutation over date ranges.
549
549
  *
550
- * The grid limits the number of rows columns for browser performance reasons.
551
- * Too much data might make the browser unresponsive.
550
+ * The grid limits the number of rows and columns for browser performance reasons as
551
+ * too much data might make the browser unresponsive.
552
552
  *
553
553
  * The number of columns is limited to 200.
554
554
  * If this number are exceeded, an error message will be shown.
package/dist/style.css CHANGED
@@ -2525,6 +2525,36 @@ input.tab:checked + .tab-content,
2525
2525
  --togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset,
2526
2526
  var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset;
2527
2527
  }
2528
+ .artboard.phone-1.horizontal,
2529
+ .artboard.phone-1.artboard-horizontal {
2530
+ width: 568px;
2531
+ height: 320px;
2532
+ }
2533
+ .artboard.phone-2.horizontal,
2534
+ .artboard.phone-2.artboard-horizontal {
2535
+ width: 667px;
2536
+ height: 375px;
2537
+ }
2538
+ .artboard.phone-3.horizontal,
2539
+ .artboard.phone-3.artboard-horizontal {
2540
+ width: 736px;
2541
+ height: 414px;
2542
+ }
2543
+ .artboard.phone-4.horizontal,
2544
+ .artboard.phone-4.artboard-horizontal {
2545
+ width: 812px;
2546
+ height: 375px;
2547
+ }
2548
+ .artboard.phone-5.horizontal,
2549
+ .artboard.phone-5.artboard-horizontal {
2550
+ width: 896px;
2551
+ height: 414px;
2552
+ }
2553
+ .artboard.phone-6.horizontal,
2554
+ .artboard.phone-6.artboard-horizontal {
2555
+ width: 1024px;
2556
+ height: 320px;
2557
+ }
2528
2558
  .btm-nav-xs > *:where(.active) {
2529
2559
  border-top-width: 1px;
2530
2560
  }
@@ -2937,6 +2967,9 @@ input.tab:checked + .tab-content,
2937
2967
  .m-2 {
2938
2968
  margin: 0.5rem;
2939
2969
  }
2970
+ .m-4 {
2971
+ margin: 1rem;
2972
+ }
2940
2973
  .mx-1 {
2941
2974
  margin-left: 0.25rem;
2942
2975
  margin-right: 0.25rem;
@@ -3320,6 +3353,9 @@ input.tab:checked + .tab-content,
3320
3353
  .mdi--fullscreen-exit {
3321
3354
  --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M14 14h5v2h-3v3h-2zm-9 0h5v5H8v-3H5zm3-9h2v5H5V8h3zm11 3v2h-5V5h2v3z'/%3E%3C/svg%3E");
3322
3355
  }
3356
+ .mdi--reload {
3357
+ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M2 12a9 9 0 0 0 9 9c2.39 0 4.68-.94 6.4-2.6l-1.5-1.5A6.7 6.7 0 0 1 11 19c-6.24 0-9.36-7.54-4.95-11.95S18 5.77 18 12h-3l4 4h.1l3.9-4h-3a9 9 0 0 0-18 0'/%3E%3C/svg%3E");
3358
+ }
3323
3359
  @media (min-width: 640px) {
3324
3360
 
3325
3361
  .sm\:modal-middle {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -35,16 +35,18 @@ export class LapisError extends Error {
35
35
  }
36
36
 
37
37
  export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest, signal?: AbortSignal) {
38
- const response = await fetch(aggregatedEndpoint(lapisUrl), {
39
- method: 'POST',
40
- headers: {
41
- 'Content-Type': 'application/json',
38
+ const response = await callLapis(
39
+ aggregatedEndpoint(lapisUrl),
40
+ {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ body: JSON.stringify(body),
46
+ signal,
42
47
  },
43
- body: JSON.stringify(body),
44
- signal,
45
- });
46
-
47
- await handleErrors(response, 'aggregated data');
48
+ 'aggregated data',
49
+ );
48
50
 
49
51
  return aggregatedResponse.parse(await response.json());
50
52
  }
@@ -55,16 +57,18 @@ export async function fetchInsertions(
55
57
  sequenceType: SequenceType,
56
58
  signal?: AbortSignal,
57
59
  ) {
58
- const response = await fetch(insertionsEndpoint(lapisUrl, sequenceType), {
59
- method: 'POST',
60
- headers: {
61
- 'Content-Type': 'application/json',
60
+ const response = await callLapis(
61
+ insertionsEndpoint(lapisUrl, sequenceType),
62
+ {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ },
67
+ body: JSON.stringify(body),
68
+ signal,
62
69
  },
63
- body: JSON.stringify(body),
64
- signal,
65
- });
66
-
67
- await handleErrors(response, `${sequenceType} insertions`);
70
+ `${sequenceType} insertions`,
71
+ );
68
72
 
69
73
  return insertionsResponse.parse(await response.json());
70
74
  }
@@ -75,33 +79,54 @@ export async function fetchSubstitutionsOrDeletions(
75
79
  sequenceType: SequenceType,
76
80
  signal?: AbortSignal,
77
81
  ) {
78
- const response = await fetch(substitutionsOrDeletionsEndpoint(lapisUrl, sequenceType), {
79
- method: 'POST',
80
- headers: {
81
- 'Content-Type': 'application/json',
82
+ const response = await callLapis(
83
+ substitutionsOrDeletionsEndpoint(lapisUrl, sequenceType),
84
+ {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ body: JSON.stringify(body),
90
+ signal,
82
91
  },
83
- body: JSON.stringify(body),
84
- signal,
85
- });
86
-
87
- await handleErrors(response, `${sequenceType} mutations`);
92
+ `${sequenceType} mutations`,
93
+ );
88
94
 
89
95
  return mutationsResponse.parse(await response.json());
90
96
  }
91
97
 
92
98
  export async function fetchReferenceGenome(lapisUrl: string, signal?: AbortSignal) {
93
- const response = await fetch(referenceGenomeEndpoint(lapisUrl), {
94
- method: 'GET',
95
- headers: {
96
- 'Content-Type': 'application/json',
99
+ const response = await callLapis(
100
+ referenceGenomeEndpoint(lapisUrl),
101
+ {
102
+ method: 'GET',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ signal,
97
107
  },
98
- signal,
99
- });
108
+ 'the reference genomes',
109
+ );
100
110
 
101
- await handleErrors(response, 'the reference genomes');
102
111
  return referenceGenomeResponse.parse(await response.json());
103
112
  }
104
113
 
114
+ async function callLapis(
115
+ input: Parameters<typeof fetch>[0],
116
+ init: Parameters<typeof fetch>[1],
117
+ requestedDataName: string,
118
+ ) {
119
+ try {
120
+ const response = await fetch(input, init);
121
+
122
+ await handleErrors(response, requestedDataName);
123
+ return response;
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : `${error}`;
126
+ throw new UnknownLapisError(`Failed to connect to LAPIS: ${message}`, 500, requestedDataName);
127
+ }
128
+ }
129
+
105
130
  const handleErrors = async (response: Response, requestedData: string) => {
106
131
  if (!response.ok) {
107
132
  if (response.status >= 400 && response.status < 500) {
@@ -1,4 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, waitFor, within } from '@storybook/test';
2
3
 
3
4
  import aggregatedData from './__mockData__/aggregated.json';
4
5
  import { Aggregate, type AggregateProps } from './aggregate';
@@ -59,3 +60,37 @@ export const Default: StoryObj<AggregateProps> = {
59
60
  pageSize: 10,
60
61
  },
61
62
  };
63
+
64
+ export const FailsLoadingData: StoryObj<AggregateProps> = {
65
+ ...Default,
66
+ parameters: {
67
+ fetchMock: {
68
+ mocks: [
69
+ {
70
+ matcher: {
71
+ name: 'aggregatedData',
72
+ url: AGGREGATED_ENDPOINT,
73
+ },
74
+ response: {
75
+ status: 400,
76
+ body: {
77
+ error: {
78
+ title: 'Bad Request',
79
+ detail: 'Test error',
80
+ status: 400,
81
+ },
82
+ },
83
+ },
84
+ },
85
+ ],
86
+ },
87
+ },
88
+ play: async ({ canvasElement }) => {
89
+ const canvas = within(canvasElement);
90
+
91
+ await waitFor(async () => {
92
+ await expect(canvas.getByText('Error - Failed fetching aggregated data from LAPIS')).toBeInTheDocument();
93
+ await expect(canvas.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
94
+ });
95
+ },
96
+ };
@@ -7,7 +7,6 @@ import { type LapisFilter } from '../../types';
7
7
  import { LapisUrlContext } from '../LapisUrlContext';
8
8
  import { CsvDownloadButton } from '../components/csv-download-button';
9
9
  import { ErrorBoundary } from '../components/error-boundary';
10
- import { ErrorDisplay } from '../components/error-display';
11
10
  import { Fullscreen } from '../components/fullscreen';
12
11
  import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
13
12
  import { LoadingDisplay } from '../components/loading-display';
@@ -56,7 +55,7 @@ export const AggregateInner: FunctionComponent<AggregateProps> = (componentProps
56
55
  }
57
56
 
58
57
  if (error !== null) {
59
- return <ErrorDisplay error={error} />;
58
+ throw error;
60
59
  }
61
60
 
62
61
  if (data === null) {
@@ -1,16 +1,21 @@
1
1
  import type { FunctionComponent } from 'preact';
2
2
  import { useErrorBoundary } from 'preact/hooks';
3
3
 
4
- import { ErrorDisplay } from './error-display';
4
+ import { ErrorDisplay, type ErrorDisplayProps } from './error-display';
5
5
  import { ResizeContainer, type Size } from './resize-container';
6
6
 
7
- export const ErrorBoundary: FunctionComponent<{ size: Size }> = ({ size, children }) => {
8
- const [internalError] = useErrorBoundary();
7
+ type ErrorBoundaryProps = {
8
+ size: Size;
9
+ layout?: ErrorDisplayProps['layout'];
10
+ };
11
+
12
+ export const ErrorBoundary: FunctionComponent<ErrorBoundaryProps> = ({ size, layout, children }) => {
13
+ const [internalError, resetError] = useErrorBoundary();
9
14
 
10
15
  if (internalError) {
11
16
  return (
12
17
  <ResizeContainer size={size}>
13
- <ErrorDisplay error={internalError} />
18
+ <ErrorDisplay error={internalError} resetError={resetError} layout={layout} />
14
19
  </ResizeContainer>
15
20
  );
16
21
  }
@@ -15,7 +15,7 @@ export default meta;
15
15
  export const GenericErrorStory: StoryObj = {
16
16
  render: () => (
17
17
  <ResizeContainer size={{ height: '600px', width: '100%' }}>
18
- <ErrorDisplay error={new Error('some message')} />
18
+ <ErrorDisplay error={new Error('some message')} resetError={() => {}} />
19
19
  </ResizeContainer>
20
20
  ),
21
21
 
@@ -30,7 +30,7 @@ export const GenericErrorStory: StoryObj = {
30
30
  export const UserFacingErrorStory: StoryObj = {
31
31
  render: () => (
32
32
  <ResizeContainer size={{ height: '600px', width: '100%' }}>
33
- <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
33
+ <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} resetError={() => {}} />
34
34
  </ResizeContainer>
35
35
  ),
36
36
 
@@ -52,7 +52,7 @@ export const UserFacingErrorStory: StoryObj = {
52
52
  export const FiresEvent: StoryObj = {
53
53
  render: () => (
54
54
  <ResizeContainer size={{ height: '600px', width: '100%' }}>
55
- <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
55
+ <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} resetError={() => {}} />
56
56
  </ResizeContainer>
57
57
  ),
58
58
 
@@ -66,3 +66,23 @@ export const FiresEvent: StoryObj = {
66
66
  });
67
67
  },
68
68
  };
69
+
70
+ const resetErrorMock = fn();
71
+
72
+ export const TriggersResetErrorOnReloadButton: StoryObj = {
73
+ render: () => (
74
+ <ResizeContainer size={{ height: '600px', width: '100%' }}>
75
+ <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} resetError={resetErrorMock} />
76
+ </ResizeContainer>
77
+ ),
78
+
79
+ play: async ({ canvasElement }) => {
80
+ const canvas = within(canvasElement);
81
+
82
+ await userEvent.click(canvas.getByText('Try again'));
83
+
84
+ await waitFor(() => {
85
+ expect(resetErrorMock).toHaveBeenCalled();
86
+ });
87
+ },
88
+ };
@@ -24,7 +24,13 @@ export class UserFacingError extends Error {
24
24
  }
25
25
  }
26
26
 
27
- export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
27
+ export type ErrorDisplayProps = {
28
+ error: Error;
29
+ resetError: () => void;
30
+ layout?: 'horizontal' | 'vertical';
31
+ };
32
+
33
+ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, resetError, layout }) => {
28
34
  // eslint-disable-next-line no-console -- Currently we use the following statement for our error handling
29
35
  console.error(error);
30
36
 
@@ -40,34 +46,40 @@ export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) =>
40
46
  return (
41
47
  <div
42
48
  ref={containerRef}
43
- className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'
49
+ className={`h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center ${layout === 'horizontal' ? 'flex-row' : 'flex-col'}`}
44
50
  >
45
- <div className='text-red-700 font-bold'>{headline}</div>
46
51
  <div>
47
- Oops! Something went wrong.
48
- {details !== undefined && (
49
- <>
50
- {' '}
51
- <button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
52
- Show details.
53
- </button>
54
- <dialog ref={ref} class='modal'>
55
- <div class='modal-box'>
56
- <form method='dialog'>
57
- <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
58
-
59
- </button>
52
+ <div className='text-red-700 font-bold'>{headline}</div>
53
+ <div>
54
+ Oops! Something went wrong.
55
+ {details !== undefined && (
56
+ <>
57
+ {' '}
58
+ <button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
59
+ Show details.
60
+ </button>
61
+ <dialog ref={ref} class='modal'>
62
+ <div class='modal-box'>
63
+ <form method='dialog'>
64
+ <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
65
+
66
+ </button>
67
+ </form>
68
+ <h1 class='text-lg'>{details.headline}</h1>
69
+ <p class='py-4'>{details.message}</p>
70
+ </div>
71
+ <form method='dialog' class='modal-backdrop'>
72
+ <button>close</button>
60
73
  </form>
61
- <h1 class='text-lg'>{details.headline}</h1>
62
- <p class='py-4'>{details.message}</p>
63
- </div>
64
- <form method='dialog' class='modal-backdrop'>
65
- <button>close</button>
66
- </form>
67
- </dialog>
68
- </>
69
- )}
74
+ </dialog>
75
+ </>
76
+ )}
77
+ </div>
70
78
  </div>
79
+ <button onClick={resetError} className='btn btn-sm flex items-center m-4'>
80
+ <span className='iconify mdi--reload text-lg' />
81
+ Try again
82
+ </button>
71
83
  </div>
72
84
  );
73
85
  };
@@ -29,7 +29,7 @@ export const DateRangeSelector = ({ width, ...innerProps }: DateRangeSelectorPro
29
29
  const size = { width, height: '3rem' };
30
30
 
31
31
  return (
32
- <ErrorBoundary size={size}>
32
+ <ErrorBoundary size={size} layout='horizontal'>
33
33
  <div style={{ width }}>
34
34
  <DateRangeSelectorInner {...innerProps} />
35
35
  </div>
@@ -4,7 +4,6 @@ import { useContext, useRef } from 'preact/hooks';
4
4
  import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
5
5
  import { LapisUrlContext } from '../LapisUrlContext';
6
6
  import { ErrorBoundary } from '../components/error-boundary';
7
- import { ErrorDisplay } from '../components/error-display';
8
7
  import { LoadingDisplay } from '../components/loading-display';
9
8
  import { NoDataDisplay } from '../components/no-data-display';
10
9
  import { ResizeContainer } from '../components/resize-container';
@@ -24,7 +23,7 @@ export const LineageFilter: FunctionComponent<LineageFilterProps> = ({ width, ..
24
23
  const size = { width, height: '3rem' };
25
24
 
26
25
  return (
27
- <ErrorBoundary size={size}>
26
+ <ErrorBoundary size={size} layout='horizontal'>
28
27
  <ResizeContainer size={size}>
29
28
  <LineageFilterInner {...innerProps} />
30
29
  </ResizeContainer>
@@ -51,7 +50,7 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
51
50
  }
52
51
 
53
52
  if (error !== null) {
54
- return <ErrorDisplay error={error} />;
53
+ throw error;
55
54
  }
56
55
 
57
56
  if (data === null) {
@@ -5,7 +5,6 @@ import { type JSXInternal } from 'preact/src/jsx';
5
5
  import { fetchAutocompletionList } from './fetchAutocompletionList';
6
6
  import { LapisUrlContext } from '../LapisUrlContext';
7
7
  import { ErrorBoundary } from '../components/error-boundary';
8
- import { ErrorDisplay } from '../components/error-display';
9
8
  import { LoadingDisplay } from '../components/loading-display';
10
9
  import { ResizeContainer } from '../components/resize-container';
11
10
  import { useQuery } from '../useQuery';
@@ -24,7 +23,7 @@ export const LocationFilter: FunctionComponent<LocationFilterProps> = ({ width,
24
23
  const size = { width, height: '3rem' };
25
24
 
26
25
  return (
27
- <ErrorBoundary size={size}>
26
+ <ErrorBoundary size={size} layout='horizontal'>
28
27
  <ResizeContainer size={size}>
29
28
  <LocationFilterInner {...innerProps} />
30
29
  </ResizeContainer>
@@ -46,7 +45,7 @@ export const LocationFilterInner = ({ initialValue, fields, placeholderText }: L
46
45
  return <LoadingDisplay />;
47
46
  }
48
47
  if (error) {
49
- return <ErrorDisplay error={error} />;
48
+ throw error;
50
49
  }
51
50
 
52
51
  const onInput = (event: JSXInternal.TargetedInputEvent<HTMLInputElement>) => {
@@ -9,7 +9,6 @@ import { type NamedLapisFilter, type SequenceType } from '../../types';
9
9
  import { LapisUrlContext } from '../LapisUrlContext';
10
10
  import { CsvDownloadButton } from '../components/csv-download-button';
11
11
  import { ErrorBoundary } from '../components/error-boundary';
12
- import { ErrorDisplay } from '../components/error-display';
13
12
  import { Fullscreen } from '../components/fullscreen';
14
13
  import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
15
14
  import { LoadingDisplay } from '../components/loading-display';
@@ -59,7 +58,7 @@ export const MutationComparisonInner: FunctionComponent<MutationComparisonProps>
59
58
  }
60
59
 
61
60
  if (error !== null) {
62
- return <ErrorDisplay error={error} />;
61
+ throw error;
63
62
  }
64
63
 
65
64
  if (data === null) {
@@ -35,7 +35,7 @@ export type SelectedMutationFilterStrings = {
35
35
 
36
36
  export const MutationFilter: FunctionComponent<MutationFilterProps> = ({ initialValue, width }) => {
37
37
  return (
38
- <ErrorBoundary size={{ height: '3.375rem', width }}>
38
+ <ErrorBoundary size={{ height: '3.375rem', width }} layout='horizontal'>
39
39
  <div style={width}>
40
40
  <MutationFilterInner initialValue={initialValue} />
41
41
  </div>
@@ -16,7 +16,6 @@ import {
16
16
  import { LapisUrlContext } from '../LapisUrlContext';
17
17
  import { CsvDownloadButton } from '../components/csv-download-button';
18
18
  import { ErrorBoundary } from '../components/error-boundary';
19
- import { ErrorDisplay } from '../components/error-display';
20
19
  import { Fullscreen } from '../components/fullscreen';
21
20
  import Info, { InfoComponentCode, InfoHeadline1, InfoHeadline2, InfoLink, InfoParagraph } from '../components/info';
22
21
  import { LoadingDisplay } from '../components/loading-display';
@@ -66,7 +65,7 @@ export const MutationsInner: FunctionComponent<MutationsProps> = (componentProps
66
65
  }
67
66
 
68
67
  if (error !== null) {
69
- return <ErrorDisplay error={error} />;
68
+ throw error;
70
69
  }
71
70
 
72
71
  if (data === null) {
@@ -0,0 +1,22 @@
1
+ import type { MutationOverTimeMockData } from './mockConversion';
2
+
3
+ export const noDataWhenNoMutationsAreInFilter: MutationOverTimeMockData = {
4
+ query: {
5
+ lapisFilter: {
6
+ dateFrom: '1800-01-01',
7
+ dateTo: '1800-01-02',
8
+ },
9
+ sequenceType: 'nucleotide',
10
+ granularity: 'year',
11
+ lapisDateField: 'date',
12
+ lapis: 'https://lapis.cov-spectrum.org/open/v2',
13
+ },
14
+ response: {
15
+ overallMutationData: [],
16
+ mutationOverTimeSerializedAsArray: {
17
+ keysFirstAxis: [],
18
+ keysSecondAxis: [],
19
+ data: [],
20
+ },
21
+ },
22
+ };
@@ -0,0 +1,22 @@
1
+ import type { MutationOverTimeMockData } from './mockConversion';
2
+
3
+ export const noDataWhenNoMutationsAreInFilter: MutationOverTimeMockData = {
4
+ query: {
5
+ lapisFilter: {
6
+ dateFrom: '2345-01-01',
7
+ dateTo: '2020-01-02',
8
+ },
9
+ sequenceType: 'nucleotide',
10
+ granularity: 'year',
11
+ lapisDateField: 'date',
12
+ lapis: 'https://lapis.cov-spectrum.org/open/v2',
13
+ },
14
+ response: {
15
+ overallMutationData: [],
16
+ mutationOverTimeSerializedAsArray: {
17
+ keysFirstAxis: [],
18
+ keysSecondAxis: [],
19
+ data: [],
20
+ },
21
+ },
22
+ };
@@ -5,6 +5,7 @@ import { workerFunction } from '../webWorkers/workerFunction';
5
5
  import { byWeek } from './__mockData__/byWeek';
6
6
  import { defaultMockData } from './__mockData__/defaultMockData';
7
7
  import { getMutationOverTimeMock } from './__mockData__/mockConversion';
8
+ import { noDataWhenNoMutationsAreInFilter } from './__mockData__/noDataWhenNoMutationsAreInFilter';
8
9
  import { showsMessageWhenTooManyMutations } from './__mockData__/showsMessageWhenTooManyMutations';
9
10
 
10
11
  const mockQueries: { query: MutationOverTimeQuery; response: MutationOverTimeWorkerResponse }[] = [
@@ -12,6 +13,7 @@ const mockQueries: { query: MutationOverTimeQuery; response: MutationOverTimeWor
12
13
  getMutationOverTimeMock(showsMessageWhenTooManyMutations),
13
14
  getMutationOverTimeMock(byWeek),
14
15
  getMutationOverTimeMock(aminoAcidMutationsByDay),
16
+ getMutationOverTimeMock(noDataWhenNoMutationsAreInFilter),
15
17
  ];
16
18
 
17
19
  self.onmessage = async function (event: MessageEvent<MutationOverTimeQuery>) {
@@ -35,6 +35,9 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
35
35
  reduce the number of mutations.
36
36
  </div>
37
37
  )}
38
+ {allMutations.length === 0 && (
39
+ <div className={'flex justify-center'}>No data available for your filters.</div>
40
+ )}
38
41
  <div
39
42
  ref={gridRef}
40
43
  style={{