@genspectrum/dashboard-components 0.10.1 → 0.10.3

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 (79) hide show
  1. package/README.md +19 -19
  2. package/custom-elements.json +72 -56
  3. package/dist/assets/{mutationOverTimeWorker-CvZg52rf.js.map → mutationOverTimeWorker-CNg_ztNp.js.map} +1 -1
  4. package/dist/components.d.ts +19 -28
  5. package/dist/components.js +343 -96
  6. package/dist/components.js.map +1 -1
  7. package/dist/{utilEntrypoint-g4DsyhU7.js → dateRangeOption-DjtcAEWq.js} +46 -2
  8. package/dist/dateRangeOption-DjtcAEWq.js.map +1 -0
  9. package/dist/style.css +11 -5
  10. package/dist/util.d.ts +112 -21
  11. package/dist/util.js +3 -2
  12. package/package.json +2 -2
  13. package/src/preact/aggregatedData/aggregate.stories.tsx +14 -0
  14. package/src/preact/aggregatedData/aggregate.tsx +17 -15
  15. package/src/preact/components/color-scale-selector.tsx +7 -3
  16. package/src/preact/components/error-boundary.stories.tsx +24 -3
  17. package/src/preact/components/error-boundary.tsx +38 -5
  18. package/src/preact/components/error-display.tsx +62 -6
  19. package/src/preact/components/tabs.tsx +2 -2
  20. package/src/preact/dateRangeSelector/computeInitialValues.spec.ts +8 -2
  21. package/src/preact/dateRangeSelector/computeInitialValues.ts +6 -0
  22. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +16 -2
  23. package/src/preact/dateRangeSelector/date-range-selector.tsx +20 -15
  24. package/src/preact/dateRangeSelector/dateRangeOption.ts +10 -5
  25. package/src/preact/lineageFilter/lineage-filter.stories.tsx +18 -4
  26. package/src/preact/lineageFilter/lineage-filter.tsx +15 -10
  27. package/src/preact/locationFilter/location-filter.stories.tsx +14 -0
  28. package/src/preact/locationFilter/location-filter.tsx +15 -10
  29. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +17 -18
  30. package/src/preact/mutationComparison/mutation-comparison.tsx +18 -12
  31. package/src/preact/mutationFilter/mutation-filter.tsx +26 -13
  32. package/src/preact/mutations/mutations.tsx +16 -12
  33. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +1326 -9341
  34. package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +615 -4920
  35. package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +2203 -17624
  36. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +16 -8
  37. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +14 -0
  38. package/src/preact/mutationsOverTime/mutations-over-time.tsx +19 -17
  39. package/src/preact/numberSequencesOverTime/number-sequences-over-time.stories.tsx +14 -0
  40. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +22 -14
  41. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -0
  42. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +28 -19
  43. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +14 -0
  44. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +18 -15
  45. package/src/preact/shared/charts/confideceInterval.ts +10 -8
  46. package/src/preact/shared/charts/getYAxisMax.ts +10 -5
  47. package/src/preact/shared/stories/expectInvalidAttributesErrorMessage.ts +13 -0
  48. package/src/preact/statistic/statistics.tsx +10 -8
  49. package/src/preact/textInput/text-input.stories.tsx +14 -0
  50. package/src/preact/textInput/text-input.tsx +16 -11
  51. package/src/preact/webWorkers/useWebWorker.ts +8 -4
  52. package/src/query/queryAggregateData.ts +2 -1
  53. package/src/query/queryMutationsOverTime.spec.ts +12 -27
  54. package/src/query/queryMutationsOverTime.ts +2 -6
  55. package/src/types.ts +31 -7
  56. package/src/utilEntrypoint.ts +15 -0
  57. package/src/utils/map2d.spec.ts +10 -10
  58. package/src/utils/map2d.ts +10 -10
  59. package/src/web-components/app.stories.ts +17 -2
  60. package/src/web-components/app.ts +17 -5
  61. package/src/web-components/input/gs-date-range-selector.stories.ts +2 -2
  62. package/src/web-components/input/gs-date-range-selector.tsx +3 -3
  63. package/src/web-components/input/gs-lineage-filter.tsx +1 -1
  64. package/src/web-components/input/gs-location-filter.tsx +2 -2
  65. package/src/web-components/input/gs-mutation-filter.stories.ts +2 -0
  66. package/src/web-components/input/gs-text-input.tsx +2 -2
  67. package/src/web-components/introduction.mdx +4 -4
  68. package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +3 -3
  69. package/src/web-components/visualization/gs-aggregate.tsx +2 -2
  70. package/src/web-components/visualization/gs-mutations-over-time.tsx +1 -3
  71. package/src/web-components/visualization/gs-mutations.tsx +1 -3
  72. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +1 -3
  73. package/src/web-components/visualization/gs-prevalence-over-time.tsx +3 -6
  74. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +1 -5
  75. package/standalone-bundle/assets/{mutationOverTimeWorker-CypX_PYM.js.map → mutationOverTimeWorker-cIyshfj_.js.map} +1 -1
  76. package/standalone-bundle/dashboard-components.js +8800 -8577
  77. package/standalone-bundle/dashboard-components.js.map +1 -1
  78. package/standalone-bundle/style.css +1 -1
  79. package/dist/utilEntrypoint-g4DsyhU7.js.map +0 -1
@@ -54,7 +54,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
54
54
  <MutationCell mutation={mutation} />
55
55
  </div>
56
56
  {dates.map((date, columnIndex) => {
57
- const value = data.get(mutation, date) ?? { proportion: 0, count: 0, totalCount: 0 };
57
+ const value = data.get(mutation, date) ?? null;
58
58
  const tooltipPosition = getTooltipPosition(
59
59
  rowIndex,
60
60
  shownMutations.length,
@@ -106,10 +106,16 @@ const ProportionCell: FunctionComponent<{
106
106
  </p>
107
107
  <p>({timeIntervalDisplay(dateClass)})</p>
108
108
  <p>{mutation.code}</p>
109
- <p>Proportion: {formatProportion(value.proportion)}</p>
110
- <p>
111
- Count: {value.count} / {value.totalCount} total
112
- </p>
109
+ {value === null ? (
110
+ <p>No data</p>
111
+ ) : (
112
+ <>
113
+ <p>Proportion: {formatProportion(value.proportion)}</p>
114
+ <p>
115
+ Count: {value.count} / {value.totalCount} total
116
+ </p>
117
+ </>
118
+ )}
113
119
  </div>
114
120
  );
115
121
 
@@ -118,12 +124,14 @@ const ProportionCell: FunctionComponent<{
118
124
  <Tooltip content={tooltipContent} position={tooltipPosition}>
119
125
  <div
120
126
  style={{
121
- backgroundColor: getColorWithingScale(value.proportion, colorScale),
122
- color: getTextColorForScale(value.proportion, colorScale),
127
+ backgroundColor: getColorWithingScale(value?.proportion, colorScale),
128
+ color: getTextColorForScale(value?.proportion, colorScale),
123
129
  }}
124
130
  className={`w-full h-full text-center hover:font-bold text-xs group @container`}
125
131
  >
126
- <span className='invisible @[2rem]:visible'>{formatProportion(value.proportion, 0)}</span>
132
+ <span className='invisible @[2rem]:visible'>
133
+ {value === null ? '' : formatProportion(value.proportion, 0)}
134
+ </span>
127
135
  </div>
128
136
  </Tooltip>
129
137
  </div>
@@ -6,6 +6,7 @@ import { LAPIS_URL } from '../../constants';
6
6
  import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
7
7
  import { LapisUrlContext } from '../LapisUrlContext';
8
8
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
9
+ import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
9
10
 
10
11
  const meta: Meta<MutationsOverTimeProps> = {
11
12
  title: 'Visualization/Mutation over time',
@@ -155,3 +156,16 @@ export const ShowsNoDataMessageForStrictFilters: StoryObj<MutationsOverTimeProps
155
156
  );
156
157
  },
157
158
  };
159
+
160
+ export const WithNoLapisDateFieldField: StoryObj<MutationsOverTimeProps> = {
161
+ ...Default,
162
+ args: {
163
+ ...Default.args,
164
+ lapisDateField: '',
165
+ },
166
+ play: async ({ canvasElement, step }) => {
167
+ step('expect error message', async () => {
168
+ await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
169
+ });
170
+ },
171
+ };
@@ -1,5 +1,6 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { type Dispatch, type StateUpdater, useContext, useMemo, useState } from 'preact/hooks';
3
+ import z from 'zod';
3
4
 
4
5
  // @ts-expect-error -- uses subpath imports and vite worker import
5
6
  import MutationOverTimeWorker from '#mutationOverTime?worker&inline';
@@ -9,10 +10,11 @@ import { type MutationOverTimeWorkerResponse } from './mutationOverTimeWorker';
9
10
  import MutationsOverTimeGrid from './mutations-over-time-grid';
10
11
  import { type MutationOverTimeQuery } from '../../query/queryMutationsOverTime';
11
12
  import {
12
- type LapisFilter,
13
- type SequenceType,
13
+ lapisFilterSchema,
14
+ sequenceTypeSchema,
14
15
  type SubstitutionOrDeletionEntry,
15
- type TemporalGranularity,
16
+ temporalGranularitySchema,
17
+ views,
16
18
  } from '../../types';
17
19
  import { type Deletion, type Substitution } from '../../utils/mutations';
18
20
  import { toTemporalClass } from '../../utils/temporalClass';
@@ -33,24 +35,26 @@ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '..
33
35
  import Tabs from '../components/tabs';
34
36
  import { useWebWorker } from '../webWorkers/useWebWorker';
35
37
 
36
- export type View = 'grid';
38
+ const viewSchema = z.literal(views.grid);
39
+ export type View = z.infer<typeof viewSchema>;
37
40
 
38
- export interface MutationsOverTimeProps {
39
- width: string;
40
- height: string;
41
- lapisFilter: LapisFilter;
42
- sequenceType: SequenceType;
43
- views: View[];
44
- granularity: TemporalGranularity;
45
- lapisDateField: string;
46
- }
41
+ const mutationOverTimeSchema = z.object({
42
+ lapisFilter: lapisFilterSchema,
43
+ sequenceType: sequenceTypeSchema,
44
+ views: z.array(viewSchema),
45
+ granularity: temporalGranularitySchema,
46
+ lapisDateField: z.string().min(1),
47
+ width: z.string(),
48
+ height: z.string(),
49
+ });
50
+ export type MutationsOverTimeProps = z.infer<typeof mutationOverTimeSchema>;
47
51
 
48
52
  export const MutationsOverTime: FunctionComponent<MutationsOverTimeProps> = (componentProps) => {
49
53
  const { width, height } = componentProps;
50
54
  const size = { height, width };
51
55
 
52
56
  return (
53
- <ErrorBoundary size={size}>
57
+ <ErrorBoundary size={size} schema={mutationOverTimeSchema} componentProps={componentProps}>
54
58
  <ResizeContainer size={size}>
55
59
  <MutationsOverTimeInner {...componentProps} />
56
60
  </ResizeContainer>
@@ -72,11 +76,9 @@ export const MutationsOverTimeInner: FunctionComponent<MutationsOverTimeProps> =
72
76
  };
73
77
  }, [granularity, lapis, lapisDateField, lapisFilter, sequenceType]);
74
78
 
75
- const worker = useMemo(() => new MutationOverTimeWorker(), []);
76
-
77
79
  const { data, error, isLoading } = useWebWorker<MutationOverTimeQuery, MutationOverTimeWorkerResponse>(
78
80
  messageToWorker,
79
- worker,
81
+ MutationOverTimeWorker,
80
82
  );
81
83
 
82
84
  if (isLoading) {
@@ -6,6 +6,7 @@ import oneVariantEG from '../../preact/numberSequencesOverTime/__mockData__/oneV
6
6
  import twoVariantsEG from '../../preact/numberSequencesOverTime/__mockData__/twoVariantsEG.json';
7
7
  import twoVariantsJN1 from '../../preact/numberSequencesOverTime/__mockData__/twoVariantsJN1.json';
8
8
  import { LapisUrlContext } from '../LapisUrlContext';
9
+ import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
9
10
 
10
11
  export default {
11
12
  title: 'Visualization/NumberSequencesOverTime',
@@ -129,3 +130,16 @@ export const TwoVariants = {
129
130
  },
130
131
  },
131
132
  };
133
+
134
+ export const WithNoLapisDateField: StoryObj<NumberSequencesOverTimeProps> = {
135
+ ...Template,
136
+ args: {
137
+ ...Template.args,
138
+ lapisDateField: '',
139
+ },
140
+ play: async ({ canvasElement, step }) => {
141
+ step('expect error message', async () => {
142
+ await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
143
+ });
144
+ },
145
+ };
@@ -1,5 +1,6 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { useContext, useState } from 'preact/hooks';
3
+ import z from 'zod';
3
4
 
4
5
  import { getNumberOfSequencesOverTimeTableData } from './getNumberOfSequencesOverTimeTableData';
5
6
  import { NumberSequencesOverTimeBarChart } from './number-sequences-over-time-bar-chart';
@@ -9,7 +10,7 @@ import {
9
10
  type NumberOfSequencesDatasets,
10
11
  queryNumberOfSequencesOverTime,
11
12
  } from '../../query/queryNumberOfSequencesOverTime';
12
- import type { NamedLapisFilter, TemporalGranularity } from '../../types';
13
+ import { namedLapisFilterSchema, temporalGranularitySchema, views } from '../../types';
13
14
  import { LapisUrlContext } from '../LapisUrlContext';
14
15
  import { CsvDownloadButton } from '../components/csv-download-button';
15
16
  import { ErrorBoundary } from '../components/error-boundary';
@@ -23,25 +24,32 @@ import Tabs from '../components/tabs';
23
24
  import type { ScaleType } from '../shared/charts/getYAxisScale';
24
25
  import { useQuery } from '../useQuery';
25
26
 
26
- type NumberSequencesOverTimeView = 'bar' | 'line' | 'table';
27
-
28
- export interface NumberSequencesOverTimeProps {
29
- width: string;
30
- height: string;
31
- lapisFilter: NamedLapisFilter | NamedLapisFilter[];
32
- lapisDateField: string;
33
- views: NumberSequencesOverTimeView[];
34
- granularity: TemporalGranularity;
35
- smoothingWindow: number;
36
- pageSize: boolean | number;
37
- }
27
+ const numberSequencesOverTimeViewSchema = z.union([
28
+ z.literal(views.bar),
29
+ z.literal(views.line),
30
+ z.literal(views.table),
31
+ ]);
32
+ export type NumberSequencesOverTimeView = z.infer<typeof numberSequencesOverTimeViewSchema>;
33
+
34
+ const numberSequencesOverTimePropsSchema = z.object({
35
+ width: z.string(),
36
+ height: z.string(),
37
+ lapisFilter: z.union([namedLapisFilterSchema, z.array(namedLapisFilterSchema)]),
38
+ lapisDateField: z.string().min(1),
39
+ views: z.array(numberSequencesOverTimeViewSchema),
40
+ granularity: temporalGranularitySchema,
41
+ smoothingWindow: z.number(),
42
+ pageSize: z.union([z.boolean(), z.number()]),
43
+ });
44
+
45
+ export type NumberSequencesOverTimeProps = z.infer<typeof numberSequencesOverTimePropsSchema>;
38
46
 
39
47
  export const NumberSequencesOverTime = (componentProps: NumberSequencesOverTimeProps) => {
40
48
  const { width, height } = componentProps;
41
49
  const size = { height, width };
42
50
 
43
51
  return (
44
- <ErrorBoundary size={size}>
52
+ <ErrorBoundary size={size} componentProps={componentProps} schema={numberSequencesOverTimePropsSchema}>
45
53
  <ResizeContainer size={size}>
46
54
  <NumberSequencesOverTimeInner {...componentProps} />
47
55
  </ResizeContainer>
@@ -10,6 +10,7 @@ import numeratorFilterNoData from './__mockData__/numeratorFilterNoData.json';
10
10
  import numeratorOneDataset from './__mockData__/numeratorFilterOneDataset.json';
11
11
  import { PrevalenceOverTime, type PrevalenceOverTimeProps } from './prevalence-over-time';
12
12
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
13
+ import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
13
14
 
14
15
  export default {
15
16
  title: 'Visualization/PrevalenceOverTime',
@@ -256,3 +257,16 @@ export const ShowsNoDataBanner: StoryObj<PrevalenceOverTimeProps> = {
256
257
  });
257
258
  },
258
259
  };
260
+
261
+ export const WithNoLapisDateField: StoryObj<PrevalenceOverTimeProps> = {
262
+ ...OneVariant,
263
+ args: {
264
+ ...OneVariant.args,
265
+ lapisDateField: '',
266
+ },
267
+ play: async ({ canvasElement, step }) => {
268
+ step('expect error message', async () => {
269
+ await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
270
+ });
271
+ },
272
+ };
@@ -1,5 +1,6 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { useContext, useEffect, useState } from 'preact/hooks';
3
+ import z from 'zod';
3
4
 
4
5
  import { getPrevalenceOverTimeTableData } from './getPrevalenceOverTimeTableData';
5
6
  import PrevalenceOverTimeBarChart from './prevalence-over-time-bar-chart';
@@ -7,7 +8,7 @@ import PrevalenceOverTimeBubbleChart from './prevalence-over-time-bubble-chart';
7
8
  import PrevalenceOverTimeLineChart from './prevalence-over-time-line-chart';
8
9
  import PrevalenceOverTimeTable from './prevalence-over-time-table';
9
10
  import { type PrevalenceOverTimeData, queryPrevalenceOverTime } from '../../query/queryPrevalenceOverTime';
10
- import { type LapisFilter, type NamedLapisFilter, type TemporalGranularity } from '../../types';
11
+ import { lapisFilterSchema, namedLapisFilterSchema, temporalGranularitySchema, views } from '../../types';
11
12
  import { LapisUrlContext } from '../LapisUrlContext';
12
13
  import { ConfidenceIntervalSelector } from '../components/confidence-interval-selector';
13
14
  import { CsvDownloadButton } from '../components/csv-download-button';
@@ -19,34 +20,42 @@ import { NoDataDisplay } from '../components/no-data-display';
19
20
  import { ResizeContainer } from '../components/resize-container';
20
21
  import { ScalingSelector } from '../components/scaling-selector';
21
22
  import Tabs from '../components/tabs';
22
- import { type ConfidenceIntervalMethod } from '../shared/charts/confideceInterval';
23
- import { type AxisMax } from '../shared/charts/getYAxisMax';
23
+ import { type ConfidenceIntervalMethod, confidenceIntervalMethodSchema } from '../shared/charts/confideceInterval';
24
+ import { axisMaxSchema } from '../shared/charts/getYAxisMax';
24
25
  import { type ScaleType } from '../shared/charts/getYAxisScale';
25
26
  import { useQuery } from '../useQuery';
26
27
 
27
- export type View = 'bar' | 'line' | 'bubble' | 'table';
28
+ const viewSchema = z.union([
29
+ z.literal(views.table),
30
+ z.literal(views.bar),
31
+ z.literal(views.line),
32
+ z.literal(views.bubble),
33
+ ]);
34
+ export type View = z.infer<typeof viewSchema>;
28
35
 
29
- export interface PrevalenceOverTimeProps {
30
- width: string;
31
- height: string;
32
- numeratorFilter: NamedLapisFilter | NamedLapisFilter[];
33
- denominatorFilter: LapisFilter;
34
- granularity: TemporalGranularity;
35
- smoothingWindow: number;
36
- views: View[];
37
- confidenceIntervalMethods: ConfidenceIntervalMethod[];
38
- lapisDateField: string;
39
- pageSize: boolean | number;
40
- yAxisMaxLinear: AxisMax;
41
- yAxisMaxLogarithmic: AxisMax;
42
- }
36
+ const prevalenceOverTimePropsSchema = z.object({
37
+ width: z.string(),
38
+ height: z.string(),
39
+ numeratorFilter: z.union([namedLapisFilterSchema, z.array(namedLapisFilterSchema)]),
40
+ denominatorFilter: lapisFilterSchema,
41
+ granularity: temporalGranularitySchema,
42
+ smoothingWindow: z.number(),
43
+ views: z.array(viewSchema),
44
+ confidenceIntervalMethods: z.array(confidenceIntervalMethodSchema),
45
+ lapisDateField: z.string().min(1),
46
+ pageSize: z.union([z.boolean(), z.number()]),
47
+ yAxisMaxLinear: axisMaxSchema,
48
+ yAxisMaxLogarithmic: axisMaxSchema,
49
+ });
50
+
51
+ export type PrevalenceOverTimeProps = z.infer<typeof prevalenceOverTimePropsSchema>;
43
52
 
44
53
  export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = (componentProps) => {
45
54
  const { width, height } = componentProps;
46
55
  const size = { height, width };
47
56
 
48
57
  return (
49
- <ErrorBoundary size={size}>
58
+ <ErrorBoundary size={size} schema={prevalenceOverTimePropsSchema} componentProps={componentProps}>
50
59
  <ResizeContainer size={size}>
51
60
  <PrevalenceOverTimeInner {...componentProps} />
52
61
  </ResizeContainer>
@@ -6,6 +6,7 @@ import numerator from './__mockData__/numeratorFilter.json';
6
6
  import { RelativeGrowthAdvantage, type RelativeGrowthAdvantageProps } from './relative-growth-advantage';
7
7
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
8
8
  import { LapisUrlContext } from '../LapisUrlContext';
9
+ import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
9
10
 
10
11
  export default {
11
12
  title: 'Visualization/RelativeGrowthAdvantage',
@@ -190,3 +191,16 @@ export const TooFewDataToComputeGrowthAdvantage: StoryObj<RelativeGrowthAdvantag
190
191
  });
191
192
  },
192
193
  };
194
+
195
+ export const WithNoLapisDateField: StoryObj<RelativeGrowthAdvantageProps> = {
196
+ ...Primary,
197
+ args: {
198
+ ...Primary.args,
199
+ lapisDateField: '',
200
+ },
201
+ play: async ({ canvasElement, step }) => {
202
+ step('expect error message', async () => {
203
+ await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
204
+ });
205
+ },
206
+ };
@@ -1,5 +1,6 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { useContext, useState } from 'preact/hooks';
3
+ import z from 'zod';
3
4
 
4
5
  import RelativeGrowthAdvantageChart from './relative-growth-advantage-chart';
5
6
  import {
@@ -7,7 +8,7 @@ import {
7
8
  queryRelativeGrowthAdvantage,
8
9
  type RelativeGrowthAdvantageData,
9
10
  } from '../../query/queryRelativeGrowthAdvantage';
10
- import { type LapisFilter } from '../../types';
11
+ import { lapisFilterSchema, views } from '../../types';
11
12
  import { LapisUrlContext } from '../LapisUrlContext';
12
13
  import { ErrorBoundary } from '../components/error-boundary';
13
14
  import { Fullscreen } from '../components/fullscreen';
@@ -17,29 +18,31 @@ import { NoDataDisplay } from '../components/no-data-display';
17
18
  import { ResizeContainer } from '../components/resize-container';
18
19
  import { ScalingSelector } from '../components/scaling-selector';
19
20
  import Tabs from '../components/tabs';
20
- import { type YAxisMaxConfig } from '../shared/charts/getYAxisMax';
21
+ import { yAxisMaxConfigSchema } from '../shared/charts/getYAxisMax';
21
22
  import { type ScaleType } from '../shared/charts/getYAxisScale';
22
23
  import { useQuery } from '../useQuery';
23
24
 
24
- export type View = 'line';
25
-
26
- export interface RelativeGrowthAdvantageProps {
27
- width: string;
28
- height: string;
29
- numeratorFilter: LapisFilter;
30
- denominatorFilter: LapisFilter;
31
- generationTime: number;
32
- views: View[];
33
- lapisDateField: string;
34
- yAxisMaxConfig: YAxisMaxConfig;
35
- }
25
+ const viewSchema = z.literal(views.line);
26
+ export type View = z.infer<typeof viewSchema>;
27
+
28
+ export const relativeGrowthAdvantagePropsSchema = z.object({
29
+ width: z.string(),
30
+ height: z.string(),
31
+ numeratorFilter: lapisFilterSchema,
32
+ denominatorFilter: lapisFilterSchema,
33
+ generationTime: z.number(),
34
+ views: z.array(viewSchema),
35
+ lapisDateField: z.string().min(1),
36
+ yAxisMaxConfig: yAxisMaxConfigSchema,
37
+ });
38
+ export type RelativeGrowthAdvantageProps = z.infer<typeof relativeGrowthAdvantagePropsSchema>;
36
39
 
37
40
  export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageProps> = (componentProps) => {
38
41
  const { width, height } = componentProps;
39
42
  const size = { height, width };
40
43
 
41
44
  return (
42
- <ErrorBoundary size={size}>
45
+ <ErrorBoundary size={size} schema={relativeGrowthAdvantagePropsSchema} componentProps={componentProps}>
43
46
  <ResizeContainer size={size}>
44
47
  <RelativeGrowthAdvantageInner {...componentProps} />
45
48
  </ResizeContainer>
@@ -1,10 +1,11 @@
1
- // calculateWilsonInterval calculates the Wilson score interval for 95% confidence.
2
- //
3
- // This function is based on https://github.com/erikfox/wilson-interval, but without
4
- // high precision math.
5
- //
6
- // observed - number of observed positive outcomes
7
- // sample - number of experiments or size of the sample
1
+ import z from 'zod';
2
+
3
+ /*
4
+ * calculateWilsonInterval calculates the Wilson score interval for 95% confidence.
5
+ *This function is based on https://github.com/erikfox/wilson-interval, but without high precision math.
6
+ * observed - number of observed positive outcomes
7
+ * sample - number of experiments or size of the sample
8
+ */
8
9
  export function wilson95PercentConfidenceInterval(observed: number, sample: number) {
9
10
  const p = observed / sample;
10
11
  const n = sample;
@@ -31,4 +32,5 @@ export const confidenceIntervalDataLabel = (
31
32
  return `${label}${value.toFixed(3)} (${lowerLimit?.toFixed(3)} - ${upperLimit?.toFixed(3)})`;
32
33
  };
33
34
 
34
- export type ConfidenceIntervalMethod = 'wilson' | 'none';
35
+ export const confidenceIntervalMethodSchema = z.union([z.literal('wilson'), z.literal('none')]);
36
+ export type ConfidenceIntervalMethod = z.infer<typeof confidenceIntervalMethodSchema>;
@@ -1,9 +1,14 @@
1
- export interface YAxisMaxConfig {
2
- linear?: AxisMax;
3
- logarithmic?: AxisMax;
4
- }
1
+ import z from 'zod';
5
2
 
6
- export type AxisMax = 'maxInData' | 'limitTo1' | number;
3
+ export const axisMaxSchema = z.union([z.literal('maxInData'), z.literal('limitTo1'), z.number()]);
4
+ export type AxisMax = z.infer<typeof axisMaxSchema>;
5
+
6
+ export const yAxisMaxConfigSchema = z.object({
7
+ linear: axisMaxSchema.optional(),
8
+ logarithmic: axisMaxSchema.optional(),
9
+ });
10
+
11
+ export type YAxisMaxConfig = z.infer<typeof yAxisMaxConfigSchema>;
7
12
 
8
13
  export const getYAxisMax = (maxInData: number, axisMax?: AxisMax) => {
9
14
  if (!axisMax) {
@@ -0,0 +1,13 @@
1
+ import { expect, waitFor, within } from '@storybook/test';
2
+
3
+ export async function expectInvalidAttributesErrorMessage(canvasElement: HTMLElement, errorMessage: string) {
4
+ const canvas = within(canvasElement);
5
+
6
+ await waitFor(() =>
7
+ expect(canvas.getByText('Error - Invalid component attributes', { exact: false })).toBeInTheDocument(),
8
+ );
9
+
10
+ canvas.getByRole('button', { name: 'Show details.' }).click();
11
+
12
+ await waitFor(() => expect(canvas.getByText(errorMessage, { exact: false })).toBeVisible());
13
+ }
@@ -1,8 +1,9 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { useContext } from 'preact/hooks';
3
+ import z from 'zod';
3
4
 
4
5
  import { queryGeneralStatistics } from '../../query/queryGeneralStatistics';
5
- import { type LapisFilter } from '../../types';
6
+ import { lapisFilterSchema } from '../../types';
6
7
  import { LapisUrlContext } from '../LapisUrlContext';
7
8
  import { ErrorBoundary } from '../components/error-boundary';
8
9
  import { LoadingDisplay } from '../components/loading-display';
@@ -11,19 +12,20 @@ import { ResizeContainer } from '../components/resize-container';
11
12
  import { formatProportion } from '../shared/table/formatProportion';
12
13
  import { useQuery } from '../useQuery';
13
14
 
14
- export type StatisticsProps = {
15
- width: string;
16
- height: string;
17
- numeratorFilter: LapisFilter;
18
- denominatorFilter: LapisFilter;
19
- };
15
+ const statisticsPropsSchema = z.object({
16
+ width: z.string(),
17
+ height: z.string(),
18
+ numeratorFilter: lapisFilterSchema,
19
+ denominatorFilter: lapisFilterSchema,
20
+ });
21
+ export type StatisticsProps = z.infer<typeof statisticsPropsSchema>;
20
22
 
21
23
  export const Statistics: FunctionComponent<StatisticsProps> = (componentProps) => {
22
24
  const { width, height } = componentProps;
23
25
  const size = { height, width };
24
26
 
25
27
  return (
26
- <ErrorBoundary size={size}>
28
+ <ErrorBoundary size={size} schema={statisticsPropsSchema} componentProps={componentProps}>
27
29
  <ResizeContainer size={size}>
28
30
  <StatisticsInner {...componentProps} />
29
31
  </ResizeContainer>
@@ -6,6 +6,7 @@ import { TextInput, type TextInputProps } from './text-input';
6
6
  import { previewHandles } from '../../../.storybook/preview';
7
7
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
8
8
  import { LapisUrlContext } from '../LapisUrlContext';
9
+ import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
9
10
 
10
11
  const meta: Meta<TextInputProps> = {
11
12
  title: 'Input/TextInput',
@@ -93,3 +94,16 @@ export const WithInitialValue: StoryObj<TextInputProps> = {
93
94
  });
94
95
  },
95
96
  };
97
+
98
+ export const WithNoLapisField: StoryObj<TextInputProps> = {
99
+ ...Default,
100
+ args: {
101
+ ...Default.args,
102
+ lapisField: '',
103
+ },
104
+ play: async ({ canvasElement, step }) => {
105
+ step('expect error message', async () => {
106
+ await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
107
+ });
108
+ },
109
+ };
@@ -1,5 +1,6 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { useContext, useRef } from 'preact/hooks';
3
+ import z from 'zod';
3
4
 
4
5
  import { fetchAutocompleteList } from './fetchAutocompleteList';
5
6
  import { LapisUrlContext } from '../LapisUrlContext';
@@ -9,21 +10,25 @@ import { NoDataDisplay } from '../components/no-data-display';
9
10
  import { ResizeContainer } from '../components/resize-container';
10
11
  import { useQuery } from '../useQuery';
11
12
 
12
- export interface TextInputInnerProps {
13
- lapisField: string;
14
- placeholderText: string;
15
- initialValue: string;
16
- }
13
+ const textInputInnerPropsSchema = z.object({
14
+ lapisField: z.string().min(1),
15
+ placeholderText: z.string().optional(),
16
+ initialValue: z.string().optional(),
17
+ });
17
18
 
18
- export interface TextInputProps extends TextInputInnerProps {
19
- width: string;
20
- }
19
+ const textInputPropsSchema = textInputInnerPropsSchema.extend({
20
+ width: z.string(),
21
+ });
21
22
 
22
- export const TextInput: FunctionComponent<TextInputProps> = ({ width, ...innerProps }) => {
23
+ export type TextInputInnerProps = z.infer<typeof textInputInnerPropsSchema>;
24
+ export type TextInputProps = z.infer<typeof textInputPropsSchema>;
25
+
26
+ export const TextInput: FunctionComponent<TextInputProps> = (props) => {
27
+ const { width, ...innerProps } = props;
23
28
  const size = { width, height: '3rem' };
24
29
 
25
30
  return (
26
- <ErrorBoundary size={size} layout='horizontal'>
31
+ <ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={textInputPropsSchema}>
27
32
  <ResizeContainer size={size}>
28
33
  <TextInputInner {...innerProps} />
29
34
  </ResizeContainer>
@@ -76,7 +81,7 @@ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ lapisField, pl
76
81
  <input
77
82
  type='text'
78
83
  class='input input-bordered w-full'
79
- placeholder={placeholderText !== undefined ? placeholderText : lapisField}
84
+ placeholder={placeholderText ?? lapisField}
80
85
  onInput={onInput}
81
86
  ref={inputRef}
82
87
  list={lapisField}
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'preact/hooks';
1
+ import { useEffect, useMemo, useState } from 'preact/hooks';
2
2
 
3
3
  import { UserFacingError } from '../components/error-display';
4
4
 
@@ -23,12 +23,14 @@ export type ErrorWorkerStatus =
23
23
  };
24
24
  export type WorkerStatus<Response> = LoadingWorkerStatus | SuccessWorkerStatus<Response> | ErrorWorkerStatus;
25
25
 
26
- export function useWebWorker<Request, Response>(messageToWorker: Request, worker: Worker) {
26
+ export function useWebWorker<Request, Response>(messageToWorker: Request, WorkerConstructor: new () => Worker) {
27
27
  const [data, setData] = useState<Response | undefined>(undefined);
28
28
  const [error, setError] = useState<Error | undefined>(undefined);
29
29
  const [isLoading, setIsLoading] = useState(true);
30
30
 
31
- useEffect(() => {
31
+ const worker = useMemo(() => {
32
+ const worker = new WorkerConstructor();
33
+
32
34
  worker.onmessage = (event: MessageEvent<WorkerStatus<Response>>) => {
33
35
  const eventData = event.data;
34
36
  const status = eventData.status;
@@ -59,7 +61,9 @@ export function useWebWorker<Request, Response>(messageToWorker: Request, worker
59
61
  setError(new Error(`Worker received a message that it cannot deserialize: ${event.data}`));
60
62
  setIsLoading(false);
61
63
  };
62
- }, [worker]);
64
+
65
+ return worker;
66
+ }, [WorkerConstructor]);
63
67
 
64
68
  useEffect(() => {
65
69
  worker.postMessage(messageToWorker);
@@ -1,8 +1,9 @@
1
1
  import { FetchAggregatedOperator } from '../operator/FetchAggregatedOperator';
2
2
  import { SortOperator } from '../operator/SortOperator';
3
- import { type InitialSort } from '../preact/aggregatedData/aggregate';
4
3
  import { type LapisFilter } from '../types';
5
4
 
5
+ export type InitialSort = { field: string; direction: 'ascending' | 'descending' };
6
+
6
7
  export type AggregateData = (Record<string, string | null | number | boolean> & {
7
8
  count: number;
8
9
  proportion: number;