@genspectrum/dashboard-components 0.16.2 → 0.16.4

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 (48) hide show
  1. package/custom-elements.json +72 -7
  2. package/dist/assets/mutationOverTimeWorker-CPfQDLe6.js.map +1 -0
  3. package/dist/components.d.ts +60 -21
  4. package/dist/components.js +804 -608
  5. package/dist/components.js.map +1 -1
  6. package/dist/style.css +21 -0
  7. package/dist/util.d.ts +69 -25
  8. package/package.json +5 -2
  9. package/src/preact/MutationAnnotationsContext.spec.tsx +58 -0
  10. package/src/preact/MutationAnnotationsContext.tsx +72 -0
  11. package/src/preact/components/annotated-mutation.stories.tsx +164 -0
  12. package/src/preact/components/annotated-mutation.tsx +84 -0
  13. package/src/preact/components/error-display.tsx +9 -9
  14. package/src/preact/components/info.tsx +6 -13
  15. package/src/preact/components/modal.stories.tsx +7 -19
  16. package/src/preact/components/modal.tsx +35 -4
  17. package/src/preact/mutationComparison/mutation-comparison-table.tsx +14 -1
  18. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +39 -8
  19. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +36 -12
  20. package/src/preact/mutationComparison/mutation-comparison.tsx +2 -0
  21. package/src/preact/mutations/mutations-table.tsx +14 -2
  22. package/src/preact/mutations/mutations.stories.tsx +33 -1
  23. package/src/preact/mutations/mutations.tsx +1 -0
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +19 -8
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +29 -5
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +13 -1
  27. package/src/preact/sequencesByLocation/sequences-by-location-map.tsx +28 -30
  28. package/src/preact/shared/stories/expectMutationAnnotation.ts +13 -0
  29. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
  30. package/src/utilEntrypoint.ts +2 -0
  31. package/src/web-components/gs-app.spec-d.ts +10 -0
  32. package/src/web-components/gs-app.stories.ts +24 -6
  33. package/src/web-components/gs-app.ts +17 -0
  34. package/src/web-components/mutation-annotations-context.ts +16 -0
  35. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +18 -1
  36. package/src/web-components/visualization/gs-mutation-comparison.tsx +19 -8
  37. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +19 -1
  38. package/src/web-components/visualization/gs-mutations-over-time.tsx +22 -11
  39. package/src/web-components/visualization/gs-mutations.stories.ts +19 -1
  40. package/src/web-components/visualization/gs-mutations.tsx +20 -9
  41. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +11 -1
  42. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +18 -7
  43. package/standalone-bundle/assets/mutationOverTimeWorker-CERZSdcA.js.map +1 -0
  44. package/standalone-bundle/dashboard-components.js +12555 -11853
  45. package/standalone-bundle/dashboard-components.js.map +1 -1
  46. package/standalone-bundle/style.css +1 -1
  47. package/dist/assets/mutationOverTimeWorker-BL50C-yi.js.map +0 -1
  48. package/standalone-bundle/assets/mutationOverTimeWorker-CFB5-Mdk.js.map +0 -1
package/dist/style.css CHANGED
@@ -1073,6 +1073,12 @@ html {
1073
1073
  --tw-bg-opacity: 1;
1074
1074
  background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
1075
1075
  }
1076
+
1077
+ .table-zebra tr.hover:hover,
1078
+ .table-zebra tr.hover:nth-child(even):hover {
1079
+ --tw-bg-opacity: 1;
1080
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
1081
+ }
1076
1082
  }
1077
1083
  .btn {
1078
1084
  display: inline-flex;
@@ -3187,6 +3193,9 @@ input.tab:checked + .tab-content,
3187
3193
  .mt-4 {
3188
3194
  margin-top: 1rem;
3189
3195
  }
3196
+ .block {
3197
+ display: block;
3198
+ }
3190
3199
  .inline {
3191
3200
  display: inline;
3192
3201
  }
@@ -3283,6 +3292,11 @@ input.tab:checked + .tab-content,
3283
3292
  .cursor-pointer {
3284
3293
  cursor: pointer;
3285
3294
  }
3295
+ .select-text {
3296
+ -webkit-user-select: text;
3297
+ -moz-user-select: text;
3298
+ user-select: text;
3299
+ }
3286
3300
  .list-inside {
3287
3301
  list-style-position: inside;
3288
3302
  }
@@ -3492,6 +3506,9 @@ input.tab:checked + .tab-content,
3492
3506
  .font-medium {
3493
3507
  font-weight: 500;
3494
3508
  }
3509
+ .font-normal {
3510
+ font-weight: 400;
3511
+ }
3495
3512
  .font-semibold {
3496
3513
  font-weight: 600;
3497
3514
  }
@@ -3522,6 +3539,10 @@ input.tab:checked + .tab-content,
3522
3539
  --tw-text-opacity: 1;
3523
3540
  color: rgb(115 115 115 / var(--tw-text-opacity, 1));
3524
3541
  }
3542
+ .text-red-600 {
3543
+ --tw-text-opacity: 1;
3544
+ color: rgb(220 38 38 / var(--tw-text-opacity, 1));
3545
+ }
3525
3546
  .text-red-700 {
3526
3547
  --tw-text-opacity: 1;
3527
3548
  color: rgb(185 28 28 / var(--tw-text-opacity, 1));
package/dist/util.d.ts CHANGED
@@ -202,6 +202,50 @@ declare const mapSourceSchema: default_2.ZodObject<{
202
202
  topologyObjectsKey: string;
203
203
  }>;
204
204
 
205
+ export declare type MutationAnnotation = default_2.infer<typeof mutationAnnotationSchema>;
206
+
207
+ export declare type MutationAnnotations = default_2.infer<typeof mutationAnnotationsSchema>;
208
+
209
+ declare const mutationAnnotationSchema: default_2.ZodObject<{
210
+ name: default_2.ZodString;
211
+ description: default_2.ZodString;
212
+ symbol: default_2.ZodString;
213
+ nucleotideMutations: default_2.ZodArray<default_2.ZodString, "many">;
214
+ aminoAcidMutations: default_2.ZodArray<default_2.ZodString, "many">;
215
+ }, "strip", default_2.ZodTypeAny, {
216
+ symbol: string;
217
+ name: string;
218
+ description: string;
219
+ nucleotideMutations: string[];
220
+ aminoAcidMutations: string[];
221
+ }, {
222
+ symbol: string;
223
+ name: string;
224
+ description: string;
225
+ nucleotideMutations: string[];
226
+ aminoAcidMutations: string[];
227
+ }>;
228
+
229
+ declare const mutationAnnotationsSchema: default_2.ZodArray<default_2.ZodObject<{
230
+ name: default_2.ZodString;
231
+ description: default_2.ZodString;
232
+ symbol: default_2.ZodString;
233
+ nucleotideMutations: default_2.ZodArray<default_2.ZodString, "many">;
234
+ aminoAcidMutations: default_2.ZodArray<default_2.ZodString, "many">;
235
+ }, "strip", default_2.ZodTypeAny, {
236
+ symbol: string;
237
+ name: string;
238
+ description: string;
239
+ nucleotideMutations: string[];
240
+ aminoAcidMutations: string[];
241
+ }, {
242
+ symbol: string;
243
+ name: string;
244
+ description: string;
245
+ nucleotideMutations: string[];
246
+ aminoAcidMutations: string[];
247
+ }>, "many">;
248
+
205
249
  export declare type MutationComparisonProps = default_2.infer<typeof mutationComparisonPropsSchema>;
206
250
 
207
251
  declare const mutationComparisonPropsSchema: default_2.ZodObject<{
@@ -247,6 +291,7 @@ declare const mutationComparisonPropsSchema: default_2.ZodObject<{
247
291
  pageSize: default_2.ZodUnion<[default_2.ZodBoolean, default_2.ZodNumber]>;
248
292
  }, "strip", default_2.ZodTypeAny, {
249
293
  width: string;
294
+ sequenceType: "nucleotide" | "amino acid";
250
295
  pageSize: number | boolean;
251
296
  lapisFilters: {
252
297
  lapisFilter: Record<string, string | number | boolean | string[] | null | undefined> & {
@@ -257,11 +302,11 @@ declare const mutationComparisonPropsSchema: default_2.ZodObject<{
257
302
  };
258
303
  displayName: string;
259
304
  }[];
260
- sequenceType: "nucleotide" | "amino acid";
261
305
  views: ("table" | "venn")[];
262
306
  height?: string | undefined;
263
307
  }, {
264
308
  width: string;
309
+ sequenceType: "nucleotide" | "amino acid";
265
310
  pageSize: number | boolean;
266
311
  lapisFilters: {
267
312
  lapisFilter: Record<string, string | number | boolean | string[] | null | undefined> & {
@@ -272,7 +317,6 @@ declare const mutationComparisonPropsSchema: default_2.ZodObject<{
272
317
  };
273
318
  displayName: string;
274
319
  }[];
275
- sequenceType: "nucleotide" | "amino acid";
276
320
  views: ("table" | "venn")[];
277
321
  height?: string | undefined;
278
322
  }>;
@@ -348,8 +392,8 @@ declare const mutationsPropsSchema: default_2.ZodObject<{
348
392
  aminoAcidInsertions?: string[] | undefined;
349
393
  };
350
394
  width: string;
351
- pageSize: number | boolean;
352
395
  sequenceType: "nucleotide" | "amino acid";
396
+ pageSize: number | boolean;
353
397
  views: ("table" | "grid" | "insertions")[];
354
398
  height?: string | undefined;
355
399
  baselineLapisFilter?: (Record<string, string | number | boolean | string[] | null | undefined> & {
@@ -366,8 +410,8 @@ declare const mutationsPropsSchema: default_2.ZodObject<{
366
410
  aminoAcidInsertions?: string[] | undefined;
367
411
  };
368
412
  width: string;
369
- pageSize: number | boolean;
370
413
  sequenceType: "nucleotide" | "amino acid";
414
+ pageSize: number | boolean;
371
415
  views: ("table" | "grid" | "insertions")[];
372
416
  height?: string | undefined;
373
417
  baselineLapisFilter?: (Record<string, string | number | boolean | string[] | null | undefined> & {
@@ -912,7 +956,7 @@ declare global {
912
956
 
913
957
  declare global {
914
958
  interface HTMLElementTagNameMap {
915
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
959
+ 'gs-aggregate': AggregateComponent;
916
960
  }
917
961
  }
918
962
 
@@ -920,7 +964,7 @@ declare global {
920
964
  declare global {
921
965
  namespace JSX {
922
966
  interface IntrinsicElements {
923
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
967
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
924
968
  }
925
969
  }
926
970
  }
@@ -928,7 +972,7 @@ declare global {
928
972
 
929
973
  declare global {
930
974
  interface HTMLElementTagNameMap {
931
- 'gs-aggregate': AggregateComponent;
975
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
932
976
  }
933
977
  }
934
978
 
@@ -936,7 +980,7 @@ declare global {
936
980
  declare global {
937
981
  namespace JSX {
938
982
  interface IntrinsicElements {
939
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
983
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
940
984
  }
941
985
  }
942
986
  }
@@ -992,10 +1036,11 @@ declare global {
992
1036
 
993
1037
  declare global {
994
1038
  interface HTMLElementTagNameMap {
995
- 'gs-location-filter': LocationFilterComponent;
1039
+ 'gs-date-range-filter': DateRangeFilterComponent;
996
1040
  }
997
1041
  interface HTMLElementEventMap {
998
- 'gs-location-changed': LocationChangedEvent;
1042
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1043
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
999
1044
  }
1000
1045
  }
1001
1046
 
@@ -1003,7 +1048,7 @@ declare global {
1003
1048
  declare global {
1004
1049
  namespace JSX {
1005
1050
  interface IntrinsicElements {
1006
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1051
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1007
1052
  }
1008
1053
  }
1009
1054
  }
@@ -1011,11 +1056,7 @@ declare global {
1011
1056
 
1012
1057
  declare global {
1013
1058
  interface HTMLElementTagNameMap {
1014
- 'gs-date-range-filter': DateRangeFilterComponent;
1015
- }
1016
- interface HTMLElementEventMap {
1017
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1018
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1059
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1019
1060
  }
1020
1061
  }
1021
1062
 
@@ -1023,7 +1064,7 @@ declare global {
1023
1064
  declare global {
1024
1065
  namespace JSX {
1025
1066
  interface IntrinsicElements {
1026
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1067
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1027
1068
  }
1028
1069
  }
1029
1070
  }
@@ -1031,10 +1072,10 @@ declare global {
1031
1072
 
1032
1073
  declare global {
1033
1074
  interface HTMLElementTagNameMap {
1034
- 'gs-text-filter': TextFilterComponent;
1075
+ 'gs-location-filter': LocationFilterComponent;
1035
1076
  }
1036
1077
  interface HTMLElementEventMap {
1037
- 'gs-text-filter-changed': TextFilterChangedEvent;
1078
+ 'gs-location-changed': LocationChangedEvent;
1038
1079
  }
1039
1080
  }
1040
1081
 
@@ -1042,7 +1083,7 @@ declare global {
1042
1083
  declare global {
1043
1084
  namespace JSX {
1044
1085
  interface IntrinsicElements {
1045
- 'gs-text-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1086
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1046
1087
  }
1047
1088
  }
1048
1089
  }
@@ -1050,10 +1091,10 @@ declare global {
1050
1091
 
1051
1092
  declare global {
1052
1093
  interface HTMLElementTagNameMap {
1053
- 'gs-mutation-filter': MutationFilterComponent;
1094
+ 'gs-text-filter': TextFilterComponent;
1054
1095
  }
1055
1096
  interface HTMLElementEventMap {
1056
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1097
+ 'gs-text-filter-changed': TextFilterChangedEvent;
1057
1098
  }
1058
1099
  }
1059
1100
 
@@ -1061,7 +1102,7 @@ declare global {
1061
1102
  declare global {
1062
1103
  namespace JSX {
1063
1104
  interface IntrinsicElements {
1064
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1105
+ 'gs-text-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1065
1106
  }
1066
1107
  }
1067
1108
  }
@@ -1088,7 +1129,10 @@ declare global {
1088
1129
 
1089
1130
  declare global {
1090
1131
  interface HTMLElementTagNameMap {
1091
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1132
+ 'gs-mutation-filter': MutationFilterComponent;
1133
+ }
1134
+ interface HTMLElementEventMap {
1135
+ 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1092
1136
  }
1093
1137
  }
1094
1138
 
@@ -1096,7 +1140,7 @@ declare global {
1096
1140
  declare global {
1097
1141
  namespace JSX {
1098
1142
  interface IntrinsicElements {
1099
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1143
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1100
1144
  }
1101
1145
  }
1102
1146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -51,7 +51,7 @@
51
51
  "scripts": {
52
52
  "build": "vite --config vite.release.config.ts build && npm run generate-manifest && vite --config vite.release-standalone.config.ts build",
53
53
  "build-and-pack": "npm run build && npm pack | xargs -I {} cp {} genspectrum-dashboard-components.tgz",
54
- "test": "vitest",
54
+ "test": "vitest --typecheck",
55
55
  "lint": "npm run lint:lit-analyzer && npm run lint:eslint",
56
56
  "lint:eslint": "eslint 'src/**/*.{ts,tsx}'",
57
57
  "lint:lit-analyzer": "lit-analyzer",
@@ -87,6 +87,7 @@
87
87
  "chartjs-chart-error-bars": "^4.4.0",
88
88
  "chartjs-chart-venn": "^4.3.0",
89
89
  "dayjs": "^1.11.10",
90
+ "dompurify": "^3.2.4",
90
91
  "downshift": "^9.0.8",
91
92
  "flatpickr": "^4.6.13",
92
93
  "gridjs": "^6.2.0",
@@ -115,6 +116,7 @@
115
116
  "@storybook/web-components": "^8.0.9",
116
117
  "@storybook/web-components-vite": "^8.0.9",
117
118
  "@tailwindcss/container-queries": "^0.1.1",
119
+ "@testing-library/preact": "^3.2.4",
118
120
  "@types/geojson": "^7946.0.15",
119
121
  "@types/leaflet": "^1.9.15",
120
122
  "@types/node": "^22.0.0",
@@ -130,6 +132,7 @@
130
132
  "eslint-plugin-import": "^2.29.1",
131
133
  "eslint-plugin-jest": "^28.2.0",
132
134
  "eslint-plugin-storybook": "^0.11.0",
135
+ "happy-dom": "^17.1.1",
133
136
  "http-server": "^14.1.1",
134
137
  "lit-analyzer": "^2.0.3",
135
138
  "msw": "^2.2.14",
@@ -0,0 +1,58 @@
1
+ import { renderHook } from '@testing-library/preact';
2
+ import { type FunctionalComponent } from 'preact';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { MutationAnnotationsContextProvider, useMutationAnnotationsProvider } from './MutationAnnotationsContext';
6
+ import { type MutationAnnotations } from '../web-components/mutation-annotations-context';
7
+
8
+ describe('useMutationAnnotation', () => {
9
+ const mockAnnotations: MutationAnnotations = [
10
+ {
11
+ name: 'Annotation 1',
12
+ description: 'Description 1',
13
+ symbol: 'A1',
14
+ nucleotideMutations: ['A123', 'A456'],
15
+ aminoAcidMutations: ['B123'],
16
+ },
17
+ {
18
+ name: 'Annotation 2',
19
+ description: 'Description 2',
20
+ symbol: 'A2',
21
+ nucleotideMutations: ['A456', 'A789'],
22
+ aminoAcidMutations: ['B456', 'B789'],
23
+ },
24
+ ];
25
+
26
+ const wrapper: FunctionalComponent = ({ children }) => (
27
+ <MutationAnnotationsContextProvider value={mockAnnotations}>{children}</MutationAnnotationsContextProvider>
28
+ );
29
+
30
+ function renderAnnotationsHook() {
31
+ const { result } = renderHook(() => useMutationAnnotationsProvider(), { wrapper });
32
+ return result.current;
33
+ }
34
+
35
+ it('should return the correct annotation for a given nucleotide mutation', () => {
36
+ const result = renderAnnotationsHook()('A123', 'nucleotide');
37
+
38
+ expect(result).toEqual([mockAnnotations[0]]);
39
+ });
40
+
41
+ it('should return the correct annotations if multiple contain a mutation', () => {
42
+ const result = renderAnnotationsHook()('A456', 'nucleotide');
43
+
44
+ expect(result).toEqual([mockAnnotations[0], mockAnnotations[1]]);
45
+ });
46
+
47
+ it('should return undefined for a non-existent mutation code', () => {
48
+ const result = renderAnnotationsHook()('NON_EXISTENT', 'nucleotide');
49
+
50
+ expect(result).toBeUndefined();
51
+ });
52
+
53
+ it('should return the correct mutation annotation for amino acid mutations', () => {
54
+ const result = renderAnnotationsHook()('B456', 'amino acid');
55
+
56
+ expect(result).toEqual([mockAnnotations[1]]);
57
+ });
58
+ });
@@ -0,0 +1,72 @@
1
+ import { type ComponentProps, createContext, type FunctionalComponent } from 'preact';
2
+ import { useContext, useMemo } from 'preact/hooks';
3
+
4
+ import { type SequenceType } from '../types';
5
+ import {
6
+ type MutationAnnotation,
7
+ type MutationAnnotations,
8
+ mutationAnnotationsSchema,
9
+ } from '../web-components/mutation-annotations-context';
10
+ import { ErrorDisplay } from './components/error-display';
11
+ import { ResizeContainer } from './components/resize-container';
12
+
13
+ const MutationAnnotationsContext = createContext<Record<SequenceType, Map<string, MutationAnnotations>>>({
14
+ nucleotide: new Map(),
15
+ 'amino acid': new Map(),
16
+ });
17
+
18
+ export const MutationAnnotationsContextProvider: FunctionalComponent<
19
+ Omit<ComponentProps<typeof MutationAnnotationsContext.Provider>, 'value'> & { value: MutationAnnotations }
20
+ > = ({ value, children }) => {
21
+ const parseResult = useMemo(() => {
22
+ const parseResult = mutationAnnotationsSchema.safeParse(value);
23
+
24
+ if (!parseResult.success) {
25
+ return parseResult;
26
+ }
27
+
28
+ const nucleotideMap = new Map<string, MutationAnnotations>();
29
+ const aminoAcidMap = new Map<string, MutationAnnotations>();
30
+
31
+ value.forEach((annotation) => {
32
+ new Set(annotation.nucleotideMutations).forEach((code) => {
33
+ addAnnotationToMap(nucleotideMap, code, annotation);
34
+ });
35
+ new Set(annotation.aminoAcidMutations).forEach((code) => {
36
+ addAnnotationToMap(aminoAcidMap, code, annotation);
37
+ });
38
+ });
39
+
40
+ return {
41
+ success: true as const,
42
+ value: {
43
+ nucleotide: nucleotideMap,
44
+ 'amino acid': aminoAcidMap,
45
+ },
46
+ };
47
+ }, [value]);
48
+
49
+ if (!parseResult.success) {
50
+ return (
51
+ <ResizeContainer size={{ width: '100%' }}>
52
+ <ErrorDisplay error={parseResult.error} layout='vertical' />
53
+ </ResizeContainer>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <MutationAnnotationsContext.Provider value={parseResult.value}>{children}</MutationAnnotationsContext.Provider>
59
+ );
60
+ };
61
+
62
+ function addAnnotationToMap(map: Map<string, MutationAnnotations>, code: string, annotation: MutationAnnotation) {
63
+ const oldAnnotations = map.get(code.toUpperCase()) ?? [];
64
+ map.set(code.toUpperCase(), [...oldAnnotations, annotation]);
65
+ }
66
+
67
+ export function useMutationAnnotationsProvider() {
68
+ const mutationAnnotations = useContext(MutationAnnotationsContext);
69
+
70
+ return (mutationCode: string, sequenceType: SequenceType) =>
71
+ mutationAnnotations[sequenceType].get(mutationCode.toUpperCase());
72
+ }
@@ -0,0 +1,164 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, userEvent, waitFor, within } from '@storybook/test';
3
+
4
+ import { AnnotatedMutation, type AnnotatedMutationProps } from './annotated-mutation';
5
+ import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
6
+ import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
7
+
8
+ const meta: Meta<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
9
+ title: 'Component/Annotated Mutation',
10
+ component: AnnotatedMutation,
11
+ parameters: { fetchMock: {} },
12
+ argTypes: {
13
+ annotations: { control: { type: 'object' } },
14
+ mutation: { control: { type: 'object' } },
15
+ sequenceType: {
16
+ options: ['nucleotide', 'amino acid'],
17
+ control: { type: 'radio' },
18
+ },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+
24
+ export const MutationWithoutAnnotationEntry: StoryObj<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
25
+ render: (args) => {
26
+ const { annotations, ...annotatedMutationsArgs } = args;
27
+
28
+ return (
29
+ <MutationAnnotationsContextProvider value={annotations}>
30
+ <AnnotatedMutation {...annotatedMutationsArgs} />
31
+ </MutationAnnotationsContextProvider>
32
+ );
33
+ },
34
+ args: {
35
+ mutation: {
36
+ type: 'substitution',
37
+ code: 'A23403G',
38
+ position: 23403,
39
+ valueAtReference: 'A',
40
+ substitutionValue: 'G',
41
+ },
42
+ sequenceType: 'nucleotide',
43
+ annotations: [
44
+ {
45
+ name: 'Test annotation',
46
+ description: 'This is a test annotation',
47
+ symbol: '*',
48
+ nucleotideMutations: ['123T'],
49
+ aminoAcidMutations: [],
50
+ },
51
+ ],
52
+ },
53
+ play: async ({ canvasElement }) => {
54
+ const canvas = within(canvasElement);
55
+
56
+ await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
57
+ await expect(getAnnotationIndicator(canvas)).not.toBeInTheDocument();
58
+ await expect(getAnnotationName(canvas)).not.toBeInTheDocument();
59
+ },
60
+ };
61
+
62
+ export const MutationWithAnnotationEntry: StoryObj<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
63
+ ...MutationWithoutAnnotationEntry,
64
+ args: {
65
+ ...MutationWithoutAnnotationEntry.args,
66
+ annotations: [
67
+ {
68
+ name: 'Test annotation',
69
+ description: 'This is a test annotation <a class="link" href="/">with a link.</a>',
70
+ symbol: '*',
71
+ nucleotideMutations: ['A23403G'],
72
+ aminoAcidMutations: [],
73
+ },
74
+ ],
75
+ },
76
+ play: async ({ canvasElement }) => {
77
+ const canvas = within(canvasElement);
78
+
79
+ await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
80
+ await expect(getAnnotationIndicator(canvas)).toBeVisible();
81
+
82
+ await userEvent.click(canvas.getByText('A23403G'));
83
+ await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
84
+ await expect(canvas.getByRole('link', { name: 'with a link.' })).toBeVisible();
85
+ },
86
+ };
87
+
88
+ export const MutationWithMultipleAnnotationEntries: StoryObj<
89
+ AnnotatedMutationProps & { annotations: MutationAnnotations }
90
+ > = {
91
+ ...MutationWithoutAnnotationEntry,
92
+ args: {
93
+ ...MutationWithoutAnnotationEntry.args,
94
+ annotations: [
95
+ {
96
+ name: 'Test annotation',
97
+ description: 'This is a test annotation',
98
+ symbol: '*',
99
+ nucleotideMutations: ['A23403G'],
100
+ aminoAcidMutations: [],
101
+ },
102
+ {
103
+ name: 'Another test annotation',
104
+ description: 'This is a test annotation',
105
+ symbol: '+',
106
+ nucleotideMutations: ['A23403G'],
107
+ aminoAcidMutations: [],
108
+ },
109
+ ],
110
+ },
111
+ play: async ({ canvasElement }) => {
112
+ const canvas = within(canvasElement);
113
+
114
+ await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
115
+ await expect(getAnnotationIndicator(canvas)).toBeVisible();
116
+ await expect(canvas.queryByText('+')).toBeVisible();
117
+
118
+ await userEvent.click(canvas.getByText('A23403G'));
119
+ await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
120
+ await expect(canvas.queryByText('Another test annotation')).toBeVisible();
121
+ },
122
+ };
123
+
124
+ export const AminoAcidMutationWithAnnotationEntry: StoryObj<
125
+ AnnotatedMutationProps & { annotations: MutationAnnotations }
126
+ > = {
127
+ ...MutationWithoutAnnotationEntry,
128
+ args: {
129
+ mutation: {
130
+ type: 'substitution',
131
+ code: 'S:A501G',
132
+ position: 501,
133
+ valueAtReference: 'A',
134
+ substitutionValue: 'G',
135
+ },
136
+ sequenceType: 'amino acid',
137
+ annotations: [
138
+ {
139
+ name: 'Test annotation',
140
+ description: 'This is a test annotation',
141
+ symbol: '*',
142
+ nucleotideMutations: [],
143
+ aminoAcidMutations: ['S:A501G'],
144
+ },
145
+ ],
146
+ },
147
+ play: async ({ canvasElement }) => {
148
+ const canvas = within(canvasElement);
149
+
150
+ await waitFor(() => expect(canvas.getByText('S:A501G')).toBeVisible());
151
+ await expect(getAnnotationIndicator(canvas)).toBeVisible();
152
+
153
+ await userEvent.click(canvas.getByText('S:A501G'));
154
+ await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
155
+ },
156
+ };
157
+
158
+ function getAnnotationIndicator(canvas: ReturnType<typeof within>) {
159
+ return canvas.queryByText('*');
160
+ }
161
+
162
+ function getAnnotationName(canvas: ReturnType<typeof within>) {
163
+ return canvas.queryByText('Test annotation');
164
+ }