@genspectrum/dashboard-components 1.3.1 → 1.5.0

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 (34) hide show
  1. package/custom-elements.json +97 -5
  2. package/dist/assets/{mutationOverTimeWorker-CQxrFo53.js.map → mutationOverTimeWorker-BJ_P2T8Y.js.map} +1 -1
  3. package/dist/components.d.ts +45 -25
  4. package/dist/components.js +118 -21
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +25 -25
  7. package/package.json +1 -1
  8. package/src/preact/lineageFilter/lineage-filter.stories.tsx +24 -0
  9. package/src/preact/lineageFilter/lineage-filter.tsx +13 -2
  10. package/src/preact/locationFilter/location-filter.stories.tsx +24 -0
  11. package/src/preact/locationFilter/location-filter.tsx +19 -3
  12. package/src/preact/mutationsOverTime/__mockData__/withGaps.ts +352 -0
  13. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +38 -0
  14. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +11 -1
  15. package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +2 -0
  16. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +35 -0
  17. package/src/preact/mutationsOverTime/mutations-over-time.tsx +28 -4
  18. package/src/preact/textFilter/text-filter.stories.tsx +29 -4
  19. package/src/preact/textFilter/text-filter.tsx +13 -2
  20. package/src/query/queryMutationsOverTime.ts +37 -4
  21. package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +122 -0
  22. package/src/utils/map2d.spec.ts +30 -0
  23. package/src/utils/map2d.ts +14 -1
  24. package/src/web-components/input/gs-lineage-filter.stories.ts +7 -0
  25. package/src/web-components/input/gs-lineage-filter.tsx +8 -0
  26. package/src/web-components/input/gs-location-filter.stories.ts +7 -0
  27. package/src/web-components/input/gs-location-filter.tsx +9 -1
  28. package/src/web-components/input/gs-text-filter.stories.ts +7 -0
  29. package/src/web-components/input/gs-text-filter.tsx +8 -0
  30. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +14 -1
  31. package/src/web-components/visualization/gs-mutations-over-time.tsx +8 -0
  32. package/standalone-bundle/assets/{mutationOverTimeWorker-CDACUs6w.js.map → mutationOverTimeWorker-CkeGpKWp.js.map} +1 -1
  33. package/standalone-bundle/dashboard-components.js +1413 -1330
  34. package/standalone-bundle/dashboard-components.js.map +1 -1
@@ -46,6 +46,11 @@ const meta: Meta<TextFilterProps> = {
46
46
  type: 'text',
47
47
  },
48
48
  },
49
+ hideCounts: {
50
+ control: {
51
+ type: 'boolean',
52
+ },
53
+ },
49
54
  value: {
50
55
  control: {
51
56
  type: 'text',
@@ -75,6 +80,7 @@ export const Default: StoryObj<TextFilterProps> = {
75
80
  args: {
76
81
  lapisField: 'host',
77
82
  placeholderText: 'Enter a host name',
83
+ hideCounts: false,
78
84
  value: '',
79
85
  width: '100%',
80
86
  lapisFilter: {
@@ -128,10 +134,6 @@ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
128
134
  </LapisUrlContextProvider>
129
135
  </>
130
136
  ),
131
- args: {
132
- ...Default.args,
133
- value: '',
134
- },
135
137
  play: async ({ canvasElement, step }) => {
136
138
  const canvas = within(canvasElement);
137
139
 
@@ -176,6 +178,29 @@ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
176
178
  },
177
179
  };
178
180
 
181
+ export const WithHideCountsTrue: StoryObj<TextFilterProps> = {
182
+ ...Default,
183
+ args: {
184
+ ...Default.args,
185
+ hideCounts: true,
186
+ },
187
+ play: async ({ canvasElement, step }) => {
188
+ const canvas = within(canvasElement);
189
+ const inputField = () => canvas.getByPlaceholderText('Enter a host name', { exact: false });
190
+
191
+ await waitFor(async () => {
192
+ await expect(inputField()).toHaveValue('');
193
+ });
194
+
195
+ await step('visible without counts', async () => {
196
+ const input = inputField();
197
+ await userEvent.clear(input);
198
+ await userEvent.type(input, 'Homo');
199
+ await expect(canvas.getByRole('option', { name: 'Homo' })).toBeVisible();
200
+ });
201
+ },
202
+ };
203
+
179
204
  export const WithNoLapisField: StoryObj<TextFilterProps> = {
180
205
  ...Default,
181
206
  args: {
@@ -15,6 +15,7 @@ const textSelectorPropsSchema = z.object({
15
15
  lapisField: z.string().min(1),
16
16
  placeholderText: z.string().optional(),
17
17
  value: z.string().optional(),
18
+ hideCounts: z.boolean().optional(),
18
19
  });
19
20
  const textFilterInnerPropsSchema = textSelectorPropsSchema.extend({ lapisFilter: lapisFilterSchema });
20
21
  const textFilterPropsSchema = textFilterInnerPropsSchema.extend({
@@ -42,6 +43,7 @@ const TextFilterInner: FunctionComponent<TextFilterInnerProps> = ({
42
43
  value,
43
44
  lapisField,
44
45
  placeholderText,
46
+ hideCounts,
45
47
  lapisFilter,
46
48
  }) => {
47
49
  const lapis = useLapisUrl();
@@ -59,7 +61,15 @@ const TextFilterInner: FunctionComponent<TextFilterInnerProps> = ({
59
61
  throw error;
60
62
  }
61
63
 
62
- return <TextSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
64
+ return (
65
+ <TextSelector
66
+ lapisField={lapisField}
67
+ value={value}
68
+ placeholderText={placeholderText}
69
+ hideCounts={hideCounts}
70
+ data={data}
71
+ />
72
+ );
63
73
  };
64
74
 
65
75
  type SelectItem = {
@@ -72,6 +82,7 @@ const TextSelector = ({
72
82
  value,
73
83
  placeholderText,
74
84
  data,
85
+ hideCounts = false,
75
86
  }: TextSelectorProps & {
76
87
  data: SelectItem[];
77
88
  }) => {
@@ -89,7 +100,7 @@ const TextSelector = ({
89
100
  return (
90
101
  <p>
91
102
  <span>{item.value}</span>
92
- <span className='ml-2 text-gray-500'>({item.count})</span>
103
+ {!hideCounts && <span className='ml-2 text-gray-500'>({item.count})</span>}
93
104
  </p>
94
105
  );
95
106
  }}
@@ -267,12 +267,33 @@ async function queryMutationsOverTimeDataDirectEndpoint(
267
267
  signal,
268
268
  );
269
269
 
270
+ const responseMutations = apiResult.data.mutations.map(parseMutationCode);
271
+ const mutationEntries: SubstitutionOrDeletionEntry[] = responseMutations.map((mutation, i) => {
272
+ const numbers = {
273
+ count: overallMutationData[i].count,
274
+ proportion: overallMutationData[i].proportion,
275
+ };
276
+ if (mutation.type === 'deletion') {
277
+ return {
278
+ type: 'deletion',
279
+ mutation,
280
+ ...numbers,
281
+ };
282
+ } else {
283
+ return {
284
+ type: 'substitution',
285
+ mutation,
286
+ ...numbers,
287
+ };
288
+ }
289
+ });
290
+
270
291
  const mutationOverTimeData: Map2DContents<Substitution | Deletion, Temporal, MutationOverTimeMutationValue> = {
271
- keysFirstAxis: new Map(overallMutationData.map((value) => [value.mutation.code, value.mutation])),
292
+ keysFirstAxis: new Map(responseMutations.map((mutation) => [mutation.code, mutation])),
272
293
  keysSecondAxis: new Map(allDates.map((date) => [date.dateString, date])),
273
294
  data: new Map(
274
- overallMutationData.map((mutation, i) => [
275
- mutation.mutation.code,
295
+ responseMutations.map((mutation, i) => [
296
+ mutation.code,
276
297
  new Map(
277
298
  allDates.map((date, j): [string, MutationOverTimeMutationValue] => [
278
299
  date.dateString,
@@ -292,10 +313,22 @@ async function queryMutationsOverTimeDataDirectEndpoint(
292
313
 
293
314
  return {
294
315
  mutationOverTimeData: new BaseMutationOverTimeDataMap(mutationOverTimeData),
295
- overallMutationData,
316
+ overallMutationData: mutationEntries,
296
317
  };
297
318
  }
298
319
 
320
+ function parseMutationCode(code: string): SubstitutionClass | DeletionClass {
321
+ const maybeDeletion = DeletionClass.parse(code);
322
+ if (maybeDeletion) {
323
+ return maybeDeletion;
324
+ }
325
+ const maybeSubstitution = SubstitutionClass.parse(code);
326
+ if (maybeSubstitution) {
327
+ return maybeSubstitution;
328
+ }
329
+ throw Error('Given code is not valid');
330
+ }
331
+
299
332
  /**
300
333
  * Returns a list of date ranges as TemporalClass.
301
334
  * Respects date range filters given in the lapisFilter as <lapisDateField>From and <lapisDateField>To.
@@ -1031,6 +1031,128 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
1031
1031
  expect(dates[1].dateString).toBe('2023-02');
1032
1032
  });
1033
1033
 
1034
+ it('should return full mutation codes even if partial includeMutations are given', async () => {
1035
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
1036
+ const dateField = 'dateField';
1037
+
1038
+ lapisRequestMocks.multipleAggregated([
1039
+ {
1040
+ body: { ...lapisFilter, fields: [dateField] },
1041
+ response: {
1042
+ data: [
1043
+ { count: 1, [dateField]: '2023-01-05' },
1044
+ { count: 2, [dateField]: '2023-02-15' },
1045
+ ],
1046
+ },
1047
+ },
1048
+ {
1049
+ body: {
1050
+ ...lapisFilter,
1051
+ dateFieldFrom: '2023-01-01',
1052
+ dateFieldTo: '2023-01-31',
1053
+ fields: [],
1054
+ },
1055
+ response: { data: [{ count: 11 }] },
1056
+ },
1057
+ {
1058
+ body: {
1059
+ ...lapisFilter,
1060
+ dateFieldFrom: '2023-02-01',
1061
+ dateFieldTo: '2023-02-28',
1062
+ fields: [],
1063
+ },
1064
+ response: { data: [{ count: 12 }] },
1065
+ },
1066
+ ]);
1067
+
1068
+ lapisRequestMocks.multipleMutations(
1069
+ [
1070
+ {
1071
+ body: {
1072
+ ...lapisFilter,
1073
+ dateFieldFrom: '2023-01-01',
1074
+ dateFieldTo: '2023-02-28',
1075
+ minProportion: 0.001,
1076
+ },
1077
+ response: {
1078
+ data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
1079
+ },
1080
+ },
1081
+ ],
1082
+ 'nucleotide',
1083
+ );
1084
+
1085
+ const dateRanges = [
1086
+ {
1087
+ dateFrom: '2023-01-01',
1088
+ dateTo: '2023-01-31',
1089
+ },
1090
+ {
1091
+ dateFrom: '2023-02-01',
1092
+ dateTo: '2023-02-28',
1093
+ },
1094
+ ];
1095
+
1096
+ lapisRequestMocks.mutationsOverTime(
1097
+ [
1098
+ {
1099
+ body: {
1100
+ filters: lapisFilter,
1101
+ dateRanges,
1102
+ includeMutations: ['122', 'otherSequenceName:G234C'],
1103
+ dateField,
1104
+ },
1105
+ response: {
1106
+ data: {
1107
+ data: [
1108
+ [
1109
+ { count: 0, coverage: 0 },
1110
+ { count: 0, coverage: 0 },
1111
+ ],
1112
+ [
1113
+ { count: 2, coverage: 10 },
1114
+ { count: 3, coverage: 10 },
1115
+ ],
1116
+ ],
1117
+ dateRanges,
1118
+ mutations: ['A122T', 'otherSequenceName:G234C'],
1119
+ },
1120
+ },
1121
+ },
1122
+ ],
1123
+ 'nucleotide',
1124
+ );
1125
+
1126
+ const { mutationOverTimeData } = await queryMutationsOverTimeData({
1127
+ lapisFilter,
1128
+ sequenceType: 'nucleotide',
1129
+ lapis: DUMMY_LAPIS_URL,
1130
+ lapisDateField: dateField,
1131
+ granularity: 'month',
1132
+ useNewEndpoint: true,
1133
+ displayMutations: ['otherSequenceName:G234C', '122'],
1134
+ });
1135
+
1136
+ expect(mutationOverTimeData.getAsArray()).to.deep.equal([
1137
+ [
1138
+ { type: 'value', proportion: NaN, count: 0, totalCount: 11 },
1139
+ { type: 'value', proportion: NaN, count: 0, totalCount: 12 },
1140
+ ],
1141
+ [
1142
+ { type: 'value', proportion: 0.2, count: 2, totalCount: 11 },
1143
+ { type: 'value', proportion: 0.3, count: 3, totalCount: 12 },
1144
+ ],
1145
+ ]);
1146
+
1147
+ const sequences = mutationOverTimeData.getFirstAxisKeys();
1148
+ expect(sequences[0].code).toBe('A122T');
1149
+ expect(sequences[1].code).toBe('otherSequenceName:G234C');
1150
+
1151
+ const dates = mutationOverTimeData.getSecondAxisKeys();
1152
+ expect(dates[0].dateString).toBe('2023-01');
1153
+ expect(dates[1].dateString).toBe('2023-02');
1154
+ });
1155
+
1034
1156
  function getSomeTestMutation(proportion: number, count: number) {
1035
1157
  return {
1036
1158
  mutation: 'sequenceName:A123T',
@@ -186,6 +186,11 @@ describe('Map2dView', () => {
186
186
  view.deleteRow('c');
187
187
 
188
188
  expect(view.getAsArray()).toEqual([[1, undefined]]);
189
+
190
+ const view2 = new Map2dView<string, string, number>(container);
191
+ view2.deleteColumn('b');
192
+
193
+ expect(view2.getAsArray()).toEqual([[undefined], [4]]);
189
194
  });
190
195
 
191
196
  it('should throw an error when trying to set a value', () => {
@@ -201,12 +206,31 @@ describe('Map2dView', () => {
201
206
  expect(view.getRow('a')).toEqual([1, undefined]);
202
207
  });
203
208
 
209
+ it('should return a column by key', () => {
210
+ const container = createBaseContainer();
211
+ const view = new Map2dView<string, string, number>(container);
212
+
213
+ expect(view.getColumn('b')).toEqual([1, 3]);
214
+ expect(view.getColumn('d')).toEqual([undefined, 4]);
215
+ });
216
+
204
217
  it('should return an empty array when the row does not exist', () => {
205
218
  const container = createBaseContainer();
206
219
  const view = new Map2dView<string, string, number>(container);
207
220
  view.deleteRow('c');
208
221
 
209
222
  expect(view.getRow('c')).toEqual([]);
223
+ expect(view.getColumn('b')).toEqual([1]);
224
+ });
225
+
226
+ it('should return an empty array when the column does not exist', () => {
227
+ const container = createBaseContainer();
228
+ const view = new Map2dView<string, string, number>(container);
229
+ view.deleteColumn('b');
230
+
231
+ expect(view.getColumn('b')).toEqual([]);
232
+ expect(view.getRow('a')).toEqual([undefined]);
233
+ expect(view.getRow('c')).toEqual([4]);
210
234
  });
211
235
 
212
236
  function createBaseContainer() {
@@ -217,6 +241,12 @@ describe('Map2dView', () => {
217
241
  container.set('a', 'b', 1);
218
242
  container.set('c', 'b', 3);
219
243
  container.set('c', 'd', 4);
244
+
245
+ // | | b | d |
246
+ // |---|---|---|
247
+ // | a | 1 | |
248
+ // | c | 3 | 4 |
249
+
220
250
  return container;
221
251
  }
222
252
  });
@@ -168,6 +168,10 @@ export class Map2dView<Key1 extends object | string, Key2 extends object | strin
168
168
  this.keysFirstAxis.delete(this.serializeFirstAxis(key));
169
169
  }
170
170
 
171
+ deleteColumn(key: Key2) {
172
+ this.keysSecondAxis.delete(this.serializeSecondAxis(key));
173
+ }
174
+
171
175
  get(keyFirstAxis: Key1, keySecondAxis: Key2) {
172
176
  const firstAxisKey = this.serializeFirstAxis(keyFirstAxis);
173
177
  const secondAxisKey = this.serializeSecondAxis(keySecondAxis);
@@ -205,7 +209,16 @@ export class Map2dView<Key1 extends object | string, Key2 extends object | strin
205
209
  return [];
206
210
  }
207
211
 
208
- return this.baseMap.getRow(key);
212
+ return this.getSecondAxisKeys().map((k2) => this.baseMap.get(key, k2));
213
+ }
214
+
215
+ getColumn(key: Key2) {
216
+ const serializedKeySecondAxis = this.serializeSecondAxis(key);
217
+ if (!this.keysSecondAxis.has(serializedKeySecondAxis)) {
218
+ return [];
219
+ }
220
+
221
+ return this.getFirstAxisKeys().map((k1) => this.baseMap.get(k1, key));
209
222
  }
210
223
 
211
224
  getContents() {
@@ -92,6 +92,11 @@ const meta: Meta<Required<LineageFilterProps>> = {
92
92
  type: 'object',
93
93
  },
94
94
  },
95
+ hideCounts: {
96
+ control: {
97
+ type: 'boolean',
98
+ },
99
+ },
95
100
  },
96
101
  };
97
102
 
@@ -105,6 +110,7 @@ const Template: StoryObj<Required<LineageFilterProps>> = {
105
110
  .lapisField=${args.lapisField}
106
111
  .lapisFilter=${args.lapisFilter}
107
112
  .placeholderText=${args.placeholderText}
113
+ .hideCounts=${args.hideCounts}
108
114
  .value=${args.value}
109
115
  .width=${args.width}
110
116
  ></gs-lineage-filter>
@@ -119,6 +125,7 @@ const Template: StoryObj<Required<LineageFilterProps>> = {
119
125
  placeholderText: 'Enter a lineage',
120
126
  value: 'B.1.1.7',
121
127
  width: '100%',
128
+ hideCounts: false,
122
129
  },
123
130
  };
124
131
 
@@ -73,6 +73,13 @@ export class LineageFilterComponent extends PreactLitAdapter {
73
73
  @property({ type: String })
74
74
  width: string = '100%';
75
75
 
76
+ /**
77
+ * Whether to hide counts behind lineage options in the drop down selection.
78
+ * Defaults to false.
79
+ */
80
+ @property({ type: Boolean })
81
+ hideCounts: boolean | undefined = false;
82
+
76
83
  override render() {
77
84
  return (
78
85
  <LineageFilter
@@ -81,6 +88,7 @@ export class LineageFilterComponent extends PreactLitAdapter {
81
88
  placeholderText={this.placeholderText}
82
89
  value={this.value}
83
90
  width={this.width}
91
+ hideCounts={this.hideCounts}
84
92
  />
85
93
  );
86
94
  }
@@ -56,6 +56,11 @@ const meta: Meta = {
56
56
  type: 'text',
57
57
  },
58
58
  },
59
+ hideCounts: {
60
+ control: {
61
+ type: 'boolean',
62
+ },
63
+ },
59
64
  lapisFilter: {
60
65
  age: 18,
61
66
  },
@@ -75,6 +80,7 @@ const Template: StoryObj<LocationFilterProps> = {
75
80
  .value=${args.value}
76
81
  .width=${args.width}
77
82
  placeholderText=${ifDefined(args.placeholderText)}
83
+ .hideCounts=${args.hideCounts}
78
84
  ></gs-location-filter>
79
85
  </div>
80
86
  </gs-app>`;
@@ -87,6 +93,7 @@ const Template: StoryObj<LocationFilterProps> = {
87
93
  value: undefined,
88
94
  width: '100%',
89
95
  placeholderText: 'Enter a location',
96
+ hideCounts: false,
90
97
  },
91
98
  };
92
99
 
@@ -74,9 +74,16 @@ export class LocationFilterComponent extends PreactLitAdapter {
74
74
  /**
75
75
  * The placeholder text to display in the input field, if it is empty.
76
76
  */
77
- @property()
77
+ @property({ type: String })
78
78
  placeholderText: string | undefined = undefined;
79
79
 
80
+ /**
81
+ * Whether to hide counts behind location options in the drop down selection.
82
+ * Defaults to false.
83
+ */
84
+ @property({ type: Boolean })
85
+ hideCounts: boolean | undefined = false;
86
+
80
87
  override render() {
81
88
  return (
82
89
  <LocationFilter
@@ -85,6 +92,7 @@ export class LocationFilterComponent extends PreactLitAdapter {
85
92
  lapisFilter={this.lapisFilter}
86
93
  width={this.width}
87
94
  placeholderText={this.placeholderText}
95
+ hideCounts={this.hideCounts}
88
96
  />
89
97
  );
90
98
  }
@@ -63,6 +63,11 @@ const meta: Meta<Required<TextFilterProps>> = {
63
63
  type: 'text',
64
64
  },
65
65
  },
66
+ hideCounts: {
67
+ control: {
68
+ type: 'boolean',
69
+ },
70
+ },
66
71
  value: {
67
72
  control: {
68
73
  type: 'text',
@@ -92,6 +97,7 @@ export const Default: StoryObj<Required<TextFilterProps>> = {
92
97
  .lapisField=${args.lapisField}
93
98
  .lapisFilter=${args.lapisFilter}
94
99
  .placeholderText=${args.placeholderText}
100
+ .hideCounts=${args.hideCounts}
95
101
  .value=${args.value}
96
102
  .width=${args.width}
97
103
  ></gs-text-filter>
@@ -102,6 +108,7 @@ export const Default: StoryObj<Required<TextFilterProps>> = {
102
108
  lapisField: 'host',
103
109
  lapisFilter: { country: 'Germany' },
104
110
  placeholderText: 'Enter host name',
111
+ hideCounts: false,
105
112
  value: 'Homo sapiens',
106
113
  width: '100%',
107
114
  },
@@ -59,6 +59,13 @@ export class TextFilterComponent extends PreactLitAdapter {
59
59
  @property()
60
60
  placeholderText: string | undefined = undefined;
61
61
 
62
+ /**
63
+ * Whether to hide counts behind options in the drop down selection.
64
+ * Defaults to false.
65
+ */
66
+ @property({ type: Boolean })
67
+ hideCounts: boolean | undefined = false;
68
+
62
69
  /**
63
70
  * The width of the component.
64
71
  *
@@ -73,6 +80,7 @@ export class TextFilterComponent extends PreactLitAdapter {
73
80
  lapisField={this.lapisField}
74
81
  lapisFilter={this.lapisFilter}
75
82
  placeholderText={this.placeholderText}
83
+ hideCounts={this.hideCounts}
76
84
  value={this.value}
77
85
  width={this.width}
78
86
  />
@@ -42,6 +42,7 @@ const meta: Meta<Required<MutationsOverTimeProps>> = {
42
42
  lapisDateField: { control: 'text' },
43
43
  displayMutations: { control: 'object' },
44
44
  initialMeanProportionInterval: { control: 'object' },
45
+ hideGaps: { control: 'boolean' },
45
46
  useNewEndpoint: { control: 'boolean' },
46
47
  pageSizes: { control: 'object' },
47
48
  },
@@ -53,6 +54,7 @@ const meta: Meta<Required<MutationsOverTimeProps>> = {
53
54
  granularity: 'month',
54
55
  lapisDateField: 'date',
55
56
  initialMeanProportionInterval: { min: 0.05, max: 0.9 },
57
+ hideGaps: false,
56
58
  useNewEndpoint: false,
57
59
  pageSizes: [10, 20, 30, 40, 50],
58
60
  },
@@ -100,6 +102,7 @@ const Template: StoryObj<Required<MutationsOverTimeProps>> = {
100
102
  .lapisDateField=${args.lapisDateField}
101
103
  .displayMutations=${args.displayMutations}
102
104
  .initialMeanProportionInterval=${args.initialMeanProportionInterval}
105
+ .hideGaps=${args.hideGaps}
103
106
  .pageSizes=${args.pageSizes}
104
107
  .useNewEndpoint=${args.useNewEndpoint}
105
108
  ></gs-mutations-over-time>
@@ -112,7 +115,7 @@ export const ByMonth: StoryObj<Required<MutationsOverTimeProps>> = {
112
115
  ...Template,
113
116
  };
114
117
 
115
- // This test uses mock data: defaultMockData.ts (through mutationOverTimeWorker.mock.ts)
118
+ // This test uses mock data: withDisplayMutations.ts (through mutationOverTimeWorker.mock.ts)
116
119
  export const ByMonthWithFilterOnDisplayedMutations: StoryObj<Required<MutationsOverTimeProps>> = {
117
120
  ...Template,
118
121
  args: {
@@ -121,6 +124,16 @@ export const ByMonthWithFilterOnDisplayedMutations: StoryObj<Required<MutationsO
121
124
  },
122
125
  };
123
126
 
127
+ // This test uses mock data: withGaps.ts (through mutationOverTimeWorker.mock.ts)
128
+ export const ByMonthWithFilterOnDisplayedMutationsAndGaps: StoryObj<Required<MutationsOverTimeProps>> = {
129
+ ...Template,
130
+ args: {
131
+ ...Template.args,
132
+ displayMutations: ['A19722G', 'G21641T', 'T21652-'],
133
+ hideGaps: true,
134
+ },
135
+ };
136
+
124
137
  // This test uses mock data: byWeek.ts (through mutationOverTimeWorker.mock.ts)
125
138
  export const ByWeek: StoryObj<Required<MutationsOverTimeProps>> = {
126
139
  ...Template,
@@ -113,6 +113,13 @@ export class MutationsOverTimeComponent extends PreactLitAdapterWithGridJsStyles
113
113
  @property({ type: Object })
114
114
  initialMeanProportionInterval: { min: number; max: number } = { min: 0.05, max: 0.9 };
115
115
 
116
+ /**
117
+ * If true, date ranges with no data will be hidden initially; if false, not.
118
+ * Can be switched with a button in the toolbar.
119
+ */
120
+ @property({ type: Boolean })
121
+ hideGaps: boolean = false;
122
+
116
123
  /**
117
124
  * Whether to use the mutationsOverTime endpoint from LAPIS.
118
125
  * If true, use the endpoint, if false, compute component data as before.
@@ -146,6 +153,7 @@ export class MutationsOverTimeComponent extends PreactLitAdapterWithGridJsStyles
146
153
  lapisDateField={this.lapisDateField}
147
154
  displayMutations={this.displayMutations}
148
155
  initialMeanProportionInterval={this.initialMeanProportionInterval}
156
+ hideGaps={this.hideGaps}
149
157
  useNewEndpoint={this.useNewEndpoint}
150
158
  pageSizes={this.pageSizes}
151
159
  />