@genspectrum/dashboard-components 1.4.0 → 1.6.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.
- package/custom-elements.json +134 -7
- package/dist/assets/{mutationOverTimeWorker-CQxrFo53.js.map → mutationOverTimeWorker-BmB6BvVM.js.map} +1 -1
- package/dist/components.d.ts +52 -11
- package/dist/components.js +189 -45
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +11 -11
- package/package.json +1 -1
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +24 -0
- package/src/preact/lineageFilter/lineage-filter.tsx +13 -2
- package/src/preact/locationFilter/location-filter.stories.tsx +24 -0
- package/src/preact/locationFilter/location-filter.tsx +19 -3
- package/src/preact/mutationFilter/mutation-filter-info.tsx +2 -2
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +71 -1
- package/src/preact/mutationFilter/mutation-filter.tsx +65 -24
- package/src/preact/mutationsOverTime/__mockData__/withGaps.ts +352 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +38 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +10 -0
- package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +2 -0
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +35 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +28 -4
- package/src/preact/textFilter/text-filter.stories.tsx +29 -4
- package/src/preact/textFilter/text-filter.tsx +13 -2
- package/src/query/queryMutationsOverTime.ts +37 -4
- package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +122 -0
- package/src/utils/map2d.spec.ts +30 -0
- package/src/utils/map2d.ts +14 -1
- package/src/utils/mutations.spec.ts +13 -0
- package/src/utils/mutations.ts +3 -3
- package/src/web-components/input/gs-lineage-filter.stories.ts +7 -0
- package/src/web-components/input/gs-lineage-filter.tsx +8 -0
- package/src/web-components/input/gs-location-filter.stories.ts +7 -0
- package/src/web-components/input/gs-location-filter.tsx +9 -1
- package/src/web-components/input/gs-mutation-filter.stories.ts +29 -1
- package/src/web-components/input/gs-mutation-filter.tsx +29 -2
- package/src/web-components/input/gs-text-filter.stories.ts +7 -0
- package/src/web-components/input/gs-text-filter.tsx +8 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +14 -1
- package/src/web-components/visualization/gs-mutations-over-time.tsx +8 -0
- package/standalone-bundle/assets/{mutationOverTimeWorker-CDACUs6w.js.map → mutationOverTimeWorker-B_xP8pIC.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +3666 -3553
- 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
|
|
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(
|
|
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
|
-
|
|
275
|
-
mutation.
|
|
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: ${code}`);
|
|
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',
|
package/src/utils/map2d.spec.ts
CHANGED
|
@@ -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
|
});
|
package/src/utils/map2d.ts
CHANGED
|
@@ -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.
|
|
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() {
|
|
@@ -8,6 +8,11 @@ describe('SubstitutionClass', () => {
|
|
|
8
8
|
expect(SubstitutionClass.parse('seg1:A1T')).deep.equal(new SubstitutionClass('seg1', 'A', 'T', 1));
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
it('should be parsed with stop codons', () => {
|
|
12
|
+
expect(SubstitutionClass.parse('S:*1247T')).deep.equal(new SubstitutionClass('S', '*', 'T', 1247));
|
|
13
|
+
expect(SubstitutionClass.parse('S:T1247*')).deep.equal(new SubstitutionClass('S', 'T', '*', 1247));
|
|
14
|
+
});
|
|
15
|
+
|
|
11
16
|
it('should render to string correctly', () => {
|
|
12
17
|
const substitutions = [
|
|
13
18
|
{
|
|
@@ -30,6 +35,10 @@ describe('DeletionClass', () => {
|
|
|
30
35
|
expect(DeletionClass.parse('seg1:A1-')).deep.equal(new DeletionClass('seg1', 'A', 1));
|
|
31
36
|
});
|
|
32
37
|
|
|
38
|
+
it('should be parsed with stop codons', () => {
|
|
39
|
+
expect(DeletionClass.parse('seg1:*1-')).deep.equal(new DeletionClass('seg1', '*', 1));
|
|
40
|
+
});
|
|
41
|
+
|
|
33
42
|
it('should render to string correctly', () => {
|
|
34
43
|
const substitutions = [
|
|
35
44
|
{
|
|
@@ -61,4 +70,8 @@ describe('InsertionClass', () => {
|
|
|
61
70
|
expect(InsertionClass.parse('ins_geNe1:1:A')).deep.equal(new InsertionClass('geNe1', 1, 'A'));
|
|
62
71
|
expect(InsertionClass.parse('ins_1:aA')).deep.equal(new InsertionClass(undefined, 1, 'aA'));
|
|
63
72
|
});
|
|
73
|
+
|
|
74
|
+
it('should be parsed with stop codon insertion', () => {
|
|
75
|
+
expect(InsertionClass.parse('ins_134:*')).deep.equal(new InsertionClass(undefined, 134, '*'));
|
|
76
|
+
});
|
|
64
77
|
});
|
package/src/utils/mutations.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface MutationClass extends Mutation {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export const substitutionRegex =
|
|
17
|
-
/^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z])?(?<position>\d+)(?<substitutionValue>[A-Z.*])?$/i;
|
|
17
|
+
/^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(?<substitutionValue>[A-Z.*])?$/i;
|
|
18
18
|
|
|
19
19
|
export interface Substitution extends Mutation {
|
|
20
20
|
type: 'substitution';
|
|
@@ -68,7 +68,7 @@ export class SubstitutionClass implements MutationClass, Substitution {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export const deletionRegex = /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z])?(?<position>\d+)(-)$/i;
|
|
71
|
+
export const deletionRegex = /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(-)$/i;
|
|
72
72
|
|
|
73
73
|
export interface Deletion extends Mutation {
|
|
74
74
|
type: 'deletion';
|
|
@@ -119,7 +119,7 @@ export class DeletionClass implements MutationClass, Deletion {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export const insertionRegexp =
|
|
122
|
-
/^ins_((?<segment>[A-Z0-9_-]+)(?=:):)?(?<position>\d+):(?<insertedSymbols>(([A-Z
|
|
122
|
+
/^ins_((?<segment>[A-Z0-9_-]+)(?=:):)?(?<position>\d+):(?<insertedSymbols>(([A-Z?*]|(\.\*))+))$/i;
|
|
123
123
|
|
|
124
124
|
export interface Insertion extends Mutation {
|
|
125
125
|
type: 'insertion';
|
|
@@ -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
|
}
|
|
@@ -38,6 +38,11 @@ const meta: Meta<MutationFilterProps> = {
|
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
width: { control: 'text' },
|
|
41
|
+
enabledMutationTypes: {
|
|
42
|
+
control: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
41
46
|
},
|
|
42
47
|
tags: ['autodocs'],
|
|
43
48
|
};
|
|
@@ -48,7 +53,11 @@ const Template: StoryObj<MutationFilterProps> = {
|
|
|
48
53
|
render: (args) => {
|
|
49
54
|
return html` <gs-app lapis="${LAPIS_URL}">
|
|
50
55
|
<div class="max-w-(--breakpoint-lg)">
|
|
51
|
-
<gs-mutation-filter
|
|
56
|
+
<gs-mutation-filter
|
|
57
|
+
.initialValue=${args.initialValue}
|
|
58
|
+
.width=${args.width}
|
|
59
|
+
.enabledMutationTypes=${args.enabledMutationTypes}
|
|
60
|
+
></gs-mutation-filter>
|
|
52
61
|
</div>
|
|
53
62
|
</gs-app>`;
|
|
54
63
|
},
|
|
@@ -104,6 +113,25 @@ export const FiresFilterChangedEvent: StoryObj<MutationFilterProps> = {
|
|
|
104
113
|
},
|
|
105
114
|
};
|
|
106
115
|
|
|
116
|
+
export const RestrictEnabledMutationTypes: StoryObj<MutationFilterProps> = {
|
|
117
|
+
...Template,
|
|
118
|
+
args: {
|
|
119
|
+
...Template.args,
|
|
120
|
+
enabledMutationTypes: ['nucleotideMutations', 'aminoAcidMutations'],
|
|
121
|
+
},
|
|
122
|
+
play: async ({ canvasElement }) => {
|
|
123
|
+
const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-filter');
|
|
124
|
+
|
|
125
|
+
const inputField = () => canvas.getByPlaceholderText('Enter a mutation', { exact: false });
|
|
126
|
+
|
|
127
|
+
await waitFor(async () => {
|
|
128
|
+
const placeholderText = inputField().getAttribute('placeholder');
|
|
129
|
+
|
|
130
|
+
await expect(placeholderText).toEqual('Enter a mutation (e.g. 23T, E:57Q)');
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
107
135
|
export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
|
|
108
136
|
...Template,
|
|
109
137
|
args: {
|
|
@@ -2,7 +2,11 @@ import { customElement, property } from 'lit/decorators.js';
|
|
|
2
2
|
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
|
3
3
|
|
|
4
4
|
import { ReferenceGenomesAwaiter } from '../../preact/components/ReferenceGenomesAwaiter';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
MutationFilter,
|
|
7
|
+
type MutationType,
|
|
8
|
+
type MutationFilterProps,
|
|
9
|
+
} from '../../preact/mutationFilter/mutation-filter';
|
|
6
10
|
import type { MutationsFilter } from '../../types';
|
|
7
11
|
import { type gsEventNames } from '../../utils/gsEventNames';
|
|
8
12
|
import type { Equals, Expect } from '../../utils/typeAssertions';
|
|
@@ -43,6 +47,11 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
|
43
47
|
*
|
|
44
48
|
* Examples: `ins_S:614:G`, `ins_614:G`
|
|
45
49
|
*
|
|
50
|
+
* ### Enabled mutation types
|
|
51
|
+
*
|
|
52
|
+
* After parsing, the entered mutation/insertion also has to match the enabled mutation types,
|
|
53
|
+
* which are configured with the `enabledMutationTypes` attribute.
|
|
54
|
+
*
|
|
46
55
|
* @fires {CustomEvent<{
|
|
47
56
|
* nucleotideMutations: string[],
|
|
48
57
|
* aminoAcidMutations: string[],
|
|
@@ -81,10 +90,28 @@ export class MutationFilterComponent extends PreactLitAdapter {
|
|
|
81
90
|
@property({ type: String })
|
|
82
91
|
width: string = '100%';
|
|
83
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Which mutation types this input will accept.
|
|
95
|
+
* Any (or all) of the following can be given in a list:
|
|
96
|
+
*
|
|
97
|
+
* - `nucleotideMutations`
|
|
98
|
+
* - `nucleotideInsertions`
|
|
99
|
+
* - `aminoAcidMutations`
|
|
100
|
+
* - `aminoAcidInsertions`
|
|
101
|
+
*
|
|
102
|
+
* By default or if none are given, all types are accepted.
|
|
103
|
+
*/
|
|
104
|
+
@property({ type: Object })
|
|
105
|
+
enabledMutationTypes: MutationType[] | undefined = undefined;
|
|
106
|
+
|
|
84
107
|
override render() {
|
|
85
108
|
return (
|
|
86
109
|
<ReferenceGenomesAwaiter>
|
|
87
|
-
<MutationFilter
|
|
110
|
+
<MutationFilter
|
|
111
|
+
initialValue={this.initialValue}
|
|
112
|
+
width={this.width}
|
|
113
|
+
enabledMutationTypes={this.enabledMutationTypes}
|
|
114
|
+
/>
|
|
88
115
|
</ReferenceGenomesAwaiter>
|
|
89
116
|
);
|
|
90
117
|
}
|
|
@@ -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
|
},
|