@genspectrum/dashboard-components 1.6.0 → 1.8.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 (44) hide show
  1. package/README.md +4 -0
  2. package/custom-elements.json +82 -1
  3. package/dist/{NumberRangeFilterChangedEvent-CQ32Qy8D.js → NumberRangeFilterChangedEvent-BnPI-Asz.js} +17 -3
  4. package/dist/NumberRangeFilterChangedEvent-BnPI-Asz.js.map +1 -0
  5. package/dist/assets/{mutationOverTimeWorker-BmB6BvVM.js.map → mutationOverTimeWorker-DPS3tmOd.js.map} +1 -1
  6. package/dist/components.d.ts +52 -25
  7. package/dist/components.js +209 -75
  8. package/dist/components.js.map +1 -1
  9. package/dist/util.d.ts +48 -24
  10. package/dist/util.js +2 -1
  11. package/package.json +1 -1
  12. package/src/preact/MutationLinkTemplateContext.tsx +56 -0
  13. package/src/preact/components/annotated-mutation.stories.tsx +69 -17
  14. package/src/preact/components/annotated-mutation.tsx +45 -19
  15. package/src/preact/genomeViewer/CDSPlot.tsx +13 -2
  16. package/src/preact/genomeViewer/loadGff3.ts +6 -0
  17. package/src/preact/mutationComparison/mutation-comparison-table.tsx +3 -0
  18. package/src/preact/mutationFilter/mutation-filter.stories.tsx +2 -1
  19. package/src/preact/mutationFilter/mutation-filter.tsx +24 -27
  20. package/src/preact/mutationFilter/parseAndValidateMutation.ts +11 -11
  21. package/src/preact/mutationFilter/parseMutation.spec.ts +32 -22
  22. package/src/preact/mutations/mutations-table.tsx +3 -0
  23. package/src/preact/mutationsOverTime/mutations-over-time.tsx +7 -4
  24. package/src/preact/shared/stories/expectMutationAnnotation.ts +3 -1
  25. package/src/types.ts +17 -1
  26. package/src/utilEntrypoint.ts +4 -0
  27. package/src/utils/mutations.spec.ts +19 -0
  28. package/src/utils/mutations.ts +57 -10
  29. package/src/web-components/gs-app.spec-d.ts +7 -0
  30. package/src/web-components/gs-app.stories.ts +32 -2
  31. package/src/web-components/gs-app.ts +17 -0
  32. package/src/web-components/input/gs-mutation-filter.stories.ts +2 -1
  33. package/src/web-components/input/gs-mutation-filter.tsx +2 -6
  34. package/src/web-components/mutation-link-template-context.ts +13 -0
  35. package/src/web-components/mutationLinks.mdx +27 -0
  36. package/src/web-components/visualization/gs-mutation-comparison.tsx +18 -8
  37. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +12 -1
  38. package/src/web-components/visualization/gs-mutations-over-time.tsx +24 -14
  39. package/src/web-components/visualization/gs-mutations.tsx +19 -9
  40. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +17 -7
  41. package/standalone-bundle/assets/{mutationOverTimeWorker-B_xP8pIC.js.map → mutationOverTimeWorker-Dp-A14AP.js.map} +1 -1
  42. package/standalone-bundle/dashboard-components.js +7295 -7193
  43. package/standalone-bundle/dashboard-components.js.map +1 -1
  44. package/dist/NumberRangeFilterChangedEvent-CQ32Qy8D.js.map +0 -1
package/dist/util.d.ts CHANGED
@@ -194,6 +194,19 @@ declare const mapSourceSchema: default_2.ZodObject<{
194
194
  topologyObjectsKey: string;
195
195
  }>;
196
196
 
197
+ export declare type MeanProportionInterval = default_2.infer<typeof meanProportionIntervalSchema>;
198
+
199
+ declare const meanProportionIntervalSchema: default_2.ZodObject<{
200
+ min: default_2.ZodNumber;
201
+ max: default_2.ZodNumber;
202
+ }, "strip", default_2.ZodTypeAny, {
203
+ min: number;
204
+ max: number;
205
+ }, {
206
+ min: number;
207
+ max: number;
208
+ }>;
209
+
197
210
  export declare type MutationAnnotation = default_2.infer<typeof mutationAnnotationSchema>;
198
211
 
199
212
  export declare type MutationAnnotations = default_2.infer<typeof mutationAnnotationsSchema>;
@@ -430,6 +443,17 @@ export declare type MutationsView = default_2.infer<typeof mutationsViewSchema>;
430
443
 
431
444
  declare const mutationsViewSchema: default_2.ZodUnion<[default_2.ZodLiteral<"table">, default_2.ZodLiteral<"grid">, default_2.ZodLiteral<"insertions">]>;
432
445
 
446
+ export declare type MutationType = default_2.infer<typeof mutationTypeSchema>;
447
+
448
+ export declare const mutationType: {
449
+ readonly nucleotideMutations: "nucleotideMutations";
450
+ readonly nucleotideInsertions: "nucleotideInsertions";
451
+ readonly aminoAcidMutations: "aminoAcidMutations";
452
+ readonly aminoAcidInsertions: "aminoAcidInsertions";
453
+ };
454
+
455
+ declare const mutationTypeSchema: default_2.ZodEnum<["nucleotideMutations", "nucleotideInsertions", "aminoAcidMutations", "aminoAcidInsertions"]>;
456
+
433
457
  export declare type NamedLapisFilter = default_2.infer<typeof namedLapisFilterSchema>;
434
458
 
435
459
  declare const namedLapisFilterSchema: default_2.ZodObject<{
@@ -917,7 +941,7 @@ declare global {
917
941
 
918
942
  declare global {
919
943
  interface HTMLElementTagNameMap {
920
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
944
+ 'gs-genome-data-viewer': GenomeDataViewerComponent;
921
945
  }
922
946
  }
923
947
 
@@ -925,7 +949,7 @@ declare global {
925
949
  declare global {
926
950
  namespace JSX {
927
951
  interface IntrinsicElements {
928
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
952
+ 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
929
953
  }
930
954
  }
931
955
  }
@@ -947,22 +971,6 @@ declare global {
947
971
  }
948
972
 
949
973
 
950
- declare global {
951
- interface HTMLElementTagNameMap {
952
- 'gs-genome-data-viewer': GenomeDataViewerComponent;
953
- }
954
- }
955
-
956
-
957
- declare global {
958
- namespace JSX {
959
- interface IntrinsicElements {
960
- 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
961
- }
962
- }
963
- }
964
-
965
-
966
974
  declare global {
967
975
  interface HTMLElementTagNameMap {
968
976
  'gs-mutations': MutationsComponent;
@@ -1091,6 +1099,22 @@ declare global {
1091
1099
  }
1092
1100
 
1093
1101
 
1102
+ declare global {
1103
+ interface HTMLElementTagNameMap {
1104
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1105
+ }
1106
+ }
1107
+
1108
+
1109
+ declare global {
1110
+ namespace JSX {
1111
+ interface IntrinsicElements {
1112
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1113
+ }
1114
+ }
1115
+ }
1116
+
1117
+
1094
1118
  declare global {
1095
1119
  interface HTMLElementTagNameMap {
1096
1120
  'gs-date-range-filter': DateRangeFilterComponent;
@@ -1113,10 +1137,10 @@ declare global {
1113
1137
 
1114
1138
  declare global {
1115
1139
  interface HTMLElementTagNameMap {
1116
- 'gs-location-filter': LocationFilterComponent;
1140
+ 'gs-text-filter': TextFilterComponent;
1117
1141
  }
1118
1142
  interface HTMLElementEventMap {
1119
- [gsEventNames.locationChanged]: LocationChangedEvent;
1143
+ [gsEventNames.textFilterChanged]: TextFilterChangedEvent;
1120
1144
  }
1121
1145
  }
1122
1146
 
@@ -1124,7 +1148,7 @@ declare global {
1124
1148
  declare global {
1125
1149
  namespace JSX {
1126
1150
  interface IntrinsicElements {
1127
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1151
+ 'gs-text-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1128
1152
  }
1129
1153
  }
1130
1154
  }
@@ -1132,10 +1156,10 @@ declare global {
1132
1156
 
1133
1157
  declare global {
1134
1158
  interface HTMLElementTagNameMap {
1135
- 'gs-text-filter': TextFilterComponent;
1159
+ 'gs-location-filter': LocationFilterComponent;
1136
1160
  }
1137
1161
  interface HTMLElementEventMap {
1138
- [gsEventNames.textFilterChanged]: TextFilterChangedEvent;
1162
+ [gsEventNames.locationChanged]: LocationChangedEvent;
1139
1163
  }
1140
1164
  }
1141
1165
 
@@ -1143,7 +1167,7 @@ declare global {
1143
1167
  declare global {
1144
1168
  namespace JSX {
1145
1169
  interface IntrinsicElements {
1146
- 'gs-text-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1170
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1147
1171
  }
1148
1172
  }
1149
1173
  }
package/dist/util.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, a, L, N, b, T, d, g, v } from "./NumberRangeFilterChangedEvent-CQ32Qy8D.js";
1
+ import { D, a, L, N, b, T, d, g, m, v } from "./NumberRangeFilterChangedEvent-BnPI-Asz.js";
2
2
  export {
3
3
  D as DateRangeOptionChangedEvent,
4
4
  a as LineageFilterChangedEvent,
@@ -8,6 +8,7 @@ export {
8
8
  T as TextFilterChangedEvent,
9
9
  d as dateRangeOptionPresets,
10
10
  g as gsEventNames,
11
+ m as mutationType,
11
12
  v as views
12
13
  };
13
14
  //# sourceMappingURL=util.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -0,0 +1,56 @@
1
+ import { createContext, type Provider } from 'preact';
2
+ import { useContext, useMemo } from 'preact/hooks';
3
+
4
+ import type { SequenceType } from '../types';
5
+ import type { Deletion, Substitution } from '../utils/mutations';
6
+ import { mutationLinkTemplateSchema } from '../web-components/mutation-link-template-context';
7
+ import { ErrorDisplay } from './components/error-display';
8
+ import { ResizeContainer } from './components/resize-container';
9
+
10
+ type MutationLinkTemplate = {
11
+ nucleotideMutation?: string;
12
+ aminoAcidMutation?: string;
13
+ };
14
+
15
+ const MutationLinkTemplateContext = createContext<MutationLinkTemplate>({
16
+ nucleotideMutation: undefined,
17
+ aminoAcidMutation: undefined,
18
+ });
19
+
20
+ export const MutationLinkTemplateContextProvider: Provider<MutationLinkTemplate> = ({ value, children }) => {
21
+ const parseResult = useMemo(() => mutationLinkTemplateSchema.safeParse(value), [value]);
22
+
23
+ if (!parseResult.success) {
24
+ return (
25
+ <ResizeContainer size={{ width: '100%' }}>
26
+ <ErrorDisplay error={parseResult.error} layout='vertical' />
27
+ </ResizeContainer>
28
+ );
29
+ }
30
+
31
+ return (
32
+ <MutationLinkTemplateContext.Provider value={parseResult.data}>{children}</MutationLinkTemplateContext.Provider>
33
+ );
34
+ };
35
+
36
+ export function useMutationLinkProvider() {
37
+ const linkTemplate = useContext(MutationLinkTemplateContext);
38
+
39
+ return (mutation: Substitution | Deletion, sequenceType: SequenceType) => {
40
+ switch (sequenceType) {
41
+ case 'nucleotide': {
42
+ if (linkTemplate.nucleotideMutation !== undefined) {
43
+ return linkTemplate.nucleotideMutation.replace('{{mutation}}', encodeURIComponent(mutation.code));
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ case 'amino acid': {
49
+ if (linkTemplate.aminoAcidMutation !== undefined) {
50
+ return linkTemplate.aminoAcidMutation.replace('{{mutation}}', encodeURIComponent(mutation.code));
51
+ }
52
+ return undefined;
53
+ }
54
+ }
55
+ };
56
+ }
@@ -2,15 +2,25 @@ import { type Meta, type StoryObj } from '@storybook/preact';
2
2
  import { expect, userEvent, waitFor, within } from '@storybook/test';
3
3
 
4
4
  import { AnnotatedMutation, type AnnotatedMutationProps } from './annotated-mutation';
5
- import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
5
+ import type { MutationAnnotations } from '../../web-components/mutation-annotations-context';
6
+ import type { MutationLinkTemplate } from '../../web-components/mutation-link-template-context';
6
7
  import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
8
+ import { MutationLinkTemplateContextProvider } from '../MutationLinkTemplateContext';
7
9
 
8
- const meta: Meta<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
10
+ type ContextProps = {
11
+ annotations: MutationAnnotations;
12
+ linkTemplate: MutationLinkTemplate;
13
+ };
14
+
15
+ type StoryProps = AnnotatedMutationProps & ContextProps;
16
+
17
+ const meta: Meta<StoryProps> = {
9
18
  title: 'Component/Annotated Mutation',
10
19
  component: AnnotatedMutation,
11
20
  parameters: { fetchMock: {} },
12
21
  argTypes: {
13
22
  annotations: { control: { type: 'object' } },
23
+ linkTemplate: { control: { type: 'object' } },
14
24
  mutation: { control: { type: 'object' } },
15
25
  sequenceType: {
16
26
  options: ['nucleotide', 'amino acid'],
@@ -21,14 +31,16 @@ const meta: Meta<AnnotatedMutationProps & { annotations: MutationAnnotations }>
21
31
 
22
32
  export default meta;
23
33
 
24
- export const MutationWithoutAnnotationEntry: StoryObj<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
34
+ export const MutationWithoutAnnotationEntry: StoryObj<StoryProps> = {
25
35
  render: (args) => {
26
- const { annotations, ...annotatedMutationsArgs } = args;
36
+ const { annotations, linkTemplate, ...annotatedMutationsArgs } = args;
27
37
 
28
38
  return (
29
- <MutationAnnotationsContextProvider value={annotations}>
30
- <AnnotatedMutation {...annotatedMutationsArgs} />
31
- </MutationAnnotationsContextProvider>
39
+ <MutationLinkTemplateContextProvider value={linkTemplate}>
40
+ <MutationAnnotationsContextProvider value={annotations}>
41
+ <AnnotatedMutation {...annotatedMutationsArgs} />
42
+ </MutationAnnotationsContextProvider>
43
+ </MutationLinkTemplateContextProvider>
32
44
  );
33
45
  },
34
46
  args: {
@@ -48,6 +60,7 @@ export const MutationWithoutAnnotationEntry: StoryObj<AnnotatedMutationProps & {
48
60
  nucleotideMutations: ['123T'],
49
61
  },
50
62
  ],
63
+ linkTemplate: {},
51
64
  },
52
65
  play: async ({ canvasElement }) => {
53
66
  const canvas = within(canvasElement);
@@ -58,7 +71,7 @@ export const MutationWithoutAnnotationEntry: StoryObj<AnnotatedMutationProps & {
58
71
  },
59
72
  };
60
73
 
61
- export const MutationWithAnnotationEntry: StoryObj<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
74
+ export const MutationWithAnnotationEntry: StoryObj<StoryProps> = {
62
75
  ...MutationWithoutAnnotationEntry,
63
76
  args: {
64
77
  ...MutationWithoutAnnotationEntry.args,
@@ -77,15 +90,13 @@ export const MutationWithAnnotationEntry: StoryObj<AnnotatedMutationProps & { an
77
90
  await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
78
91
  await expect(getAnnotationIndicator(canvas)).toBeVisible();
79
92
 
80
- await userEvent.click(canvas.getByText('A23403G'));
93
+ await userEvent.click(canvas.getByText('*'));
81
94
  await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
82
95
  await expect(canvas.getByRole('link', { name: 'with a link.' })).toBeVisible();
83
96
  },
84
97
  };
85
98
 
86
- export const MutationWithMultipleAnnotationEntries: StoryObj<
87
- AnnotatedMutationProps & { annotations: MutationAnnotations }
88
- > = {
99
+ export const MutationWithMultipleAnnotationEntries: StoryObj<StoryProps> = {
89
100
  ...MutationWithoutAnnotationEntry,
90
101
  args: {
91
102
  ...MutationWithoutAnnotationEntry.args,
@@ -111,17 +122,16 @@ export const MutationWithMultipleAnnotationEntries: StoryObj<
111
122
  await expect(getAnnotationIndicator(canvas)).toBeVisible();
112
123
  await expect(canvas.queryByText('+')).toBeVisible();
113
124
 
114
- await userEvent.click(canvas.getByText('A23403G'));
125
+ await userEvent.click(canvas.getByText('*'));
115
126
  await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
116
127
  await expect(canvas.queryByText('Another test annotation')).toBeVisible();
117
128
  },
118
129
  };
119
130
 
120
- export const AminoAcidMutationWithAnnotationEntry: StoryObj<
121
- AnnotatedMutationProps & { annotations: MutationAnnotations }
122
- > = {
131
+ export const AminoAcidMutationWithAnnotationEntry: StoryObj<StoryProps> = {
123
132
  ...MutationWithoutAnnotationEntry,
124
133
  args: {
134
+ ...MutationWithoutAnnotationEntry.args,
125
135
  mutation: {
126
136
  type: 'substitution',
127
137
  code: 'S:A501G',
@@ -145,7 +155,7 @@ export const AminoAcidMutationWithAnnotationEntry: StoryObj<
145
155
  await waitFor(() => expect(canvas.getByText('S:A501G')).toBeVisible());
146
156
  await expect(getAnnotationIndicator(canvas)).toBeVisible();
147
157
 
148
- await userEvent.click(canvas.getByText('S:A501G'));
158
+ await userEvent.click(canvas.getByText('*'));
149
159
  await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
150
160
  },
151
161
  };
@@ -157,3 +167,45 @@ function getAnnotationIndicator(canvas: ReturnType<typeof within>) {
157
167
  function getAnnotationName(canvas: ReturnType<typeof within>) {
158
168
  return canvas.queryByText('Test annotation');
159
169
  }
170
+
171
+ export const NucleotideMutationWithLink: StoryObj<StoryProps> = {
172
+ ...MutationWithoutAnnotationEntry,
173
+ args: {
174
+ ...MutationWithoutAnnotationEntry.args,
175
+ linkTemplate: {
176
+ nucleotideMutation: 'http://foo.com/query?nucMut={{mutation}}',
177
+ },
178
+ },
179
+ play: async ({ canvasElement }) => {
180
+ const canvas = within(canvasElement);
181
+
182
+ await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
183
+ const link = canvas.getByText('A23403G').closest('a');
184
+ void expect(link).toHaveAttribute('href', 'http://foo.com/query?nucMut=A23403G');
185
+ },
186
+ };
187
+
188
+ export const AminoAcidMutationWithLink: StoryObj<StoryProps> = {
189
+ ...MutationWithoutAnnotationEntry,
190
+ args: {
191
+ ...MutationWithoutAnnotationEntry.args,
192
+ mutation: {
193
+ type: 'substitution',
194
+ code: 'S:A501G',
195
+ position: 501,
196
+ valueAtReference: 'A',
197
+ substitutionValue: 'G',
198
+ },
199
+ sequenceType: 'amino acid',
200
+ linkTemplate: {
201
+ aminoAcidMutation: 'http://foo.com/query?aaMut={{mutation}}',
202
+ },
203
+ },
204
+ play: async ({ canvasElement }) => {
205
+ const canvas = within(canvasElement);
206
+
207
+ await waitFor(() => expect(canvas.getByText('S:A501G')).toBeVisible());
208
+ const link = canvas.getByText('S:A501G').closest('a');
209
+ void expect(link).toHaveAttribute('href', 'http://foo.com/query?aaMut=S%3AA501G');
210
+ },
211
+ };
@@ -7,6 +7,7 @@ import type { Deletion, Substitution } from '../../utils/mutations';
7
7
  import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
8
8
  import { InfoHeadline1, InfoHeadline2, InfoParagraph } from './info';
9
9
  import { ButtonWithModalDialog, useModalRef } from './modal';
10
+ import { useMutationLinkProvider } from '../MutationLinkTemplateContext';
10
11
 
11
12
  export type AnnotatedMutationProps = {
12
13
  mutation: Substitution | Deletion;
@@ -15,13 +16,22 @@ export type AnnotatedMutationProps = {
15
16
 
16
17
  export const AnnotatedMutation: FunctionComponent<AnnotatedMutationProps> = (props) => {
17
18
  const annotationsProvider = useMutationAnnotationsProvider();
19
+ const linkProvider = useMutationLinkProvider();
18
20
  const modalRef = useModalRef();
19
21
 
20
- return <AnnotatedMutationWithoutContext {...props} annotationsProvider={annotationsProvider} modalRef={modalRef} />;
22
+ return (
23
+ <AnnotatedMutationWithoutContext
24
+ {...props}
25
+ annotationsProvider={annotationsProvider}
26
+ linkProvider={linkProvider}
27
+ modalRef={modalRef}
28
+ />
29
+ );
21
30
  };
22
31
 
23
32
  type GridJsAnnotatedMutationProps = AnnotatedMutationProps & {
24
33
  annotationsProvider: ReturnType<typeof useMutationAnnotationsProvider>;
34
+ linkProvider: ReturnType<typeof useMutationLinkProvider>;
25
35
  };
26
36
 
27
37
  /**
@@ -43,12 +53,26 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
43
53
  mutation,
44
54
  sequenceType,
45
55
  annotationsProvider,
56
+ linkProvider,
46
57
  modalRef,
47
58
  }) => {
59
+ const link = linkProvider(mutation, sequenceType);
60
+ let innerLabel = <>{mutation.code}</>;
61
+ if (link !== undefined) {
62
+ innerLabel = (
63
+ <a
64
+ className='hover:text-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-300 underline'
65
+ href={link}
66
+ >
67
+ {mutation.code}
68
+ </a>
69
+ );
70
+ }
71
+
48
72
  const mutationAnnotations = annotationsProvider(mutation, sequenceType);
49
73
 
50
74
  if (mutationAnnotations === undefined || mutationAnnotations.length === 0) {
51
- return mutation.code;
75
+ return innerLabel;
52
76
  }
53
77
 
54
78
  const modalContent = (
@@ -66,22 +90,24 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
66
90
  );
67
91
 
68
92
  return (
69
- <ButtonWithModalDialog
70
- buttonClassName={'select-text cursor-pointer'}
71
- modalContent={modalContent}
72
- modalRef={modalRef}
73
- >
74
- {mutation.code}
75
- <sup>
76
- {mutationAnnotations
77
- .map((annotation) => annotation.symbol)
78
- .map((symbol, index) => (
79
- <Fragment key={symbol}>
80
- <span className='text-red-600'>{symbol}</span>
81
- {index !== mutationAnnotations.length - 1 && ','}
82
- </Fragment>
83
- ))}
84
- </sup>
85
- </ButtonWithModalDialog>
93
+ <>
94
+ {innerLabel}
95
+ <ButtonWithModalDialog
96
+ buttonClassName={'select-text cursor-pointer'}
97
+ modalContent={modalContent}
98
+ modalRef={modalRef}
99
+ >
100
+ <sup className='hover:underline focus-visible:underline decoration-red-600'>
101
+ {mutationAnnotations
102
+ .map((annotation) => annotation.symbol)
103
+ .map((symbol, index) => (
104
+ <Fragment key={symbol}>
105
+ <span className='text-red-600'>{symbol}</span>
106
+ {index !== mutationAnnotations.length - 1 && ','}
107
+ </Fragment>
108
+ ))}
109
+ </sup>
110
+ </ButtonWithModalDialog>
111
+ </>
86
112
  );
87
113
  };
@@ -20,9 +20,16 @@ function getMaxTickNumber(fullWidth: number): number {
20
20
  }
21
21
 
22
22
  function getTicks(zoomStart: number, zoomEnd: number, fullWidth: number) {
23
- const maxTickNumber = getMaxTickNumber(fullWidth);
23
+ let maxTickNumber = getMaxTickNumber(fullWidth);
24
24
  const length = zoomEnd - zoomStart;
25
- const minTickSize = length / maxTickNumber;
25
+ let minTickSize = length / maxTickNumber;
26
+ if (minTickSize <= 1) {
27
+ maxTickNumber = MIN_TICK_NUMBER;
28
+ minTickSize = length / maxTickNumber;
29
+ }
30
+ if (minTickSize <= 1) {
31
+ return [];
32
+ }
26
33
  let maxTickSize = 10 ** Math.round(Math.log(minTickSize) / Math.log(10));
27
34
  const numTicks = Math.round(length / maxTickSize);
28
35
  if (numTicks > maxTickNumber) {
@@ -78,6 +85,7 @@ const XAxis: FunctionComponent<XAxisProps> = (componentProps) => {
78
85
  width: `calc(${widthPercent}% - 1px)`,
79
86
  }}
80
87
  >
88
+ {/* TODO(#994): determine if text can be shown based on text width */}
81
89
  {width >= averageWidth ? tick.start : ''}
82
90
  </div>
83
91
  );
@@ -142,6 +150,9 @@ const CDSBars: FunctionComponent<CDSBarsProps> = (componentProps) => {
142
150
  if (start >= end) {
143
151
  return null;
144
152
  }
153
+ if (zoomEnd - zoomStart <= 2) {
154
+ return null;
155
+ }
145
156
 
146
157
  const widthPercent = ((end - start) / visibleRegionLength) * 100;
147
158
  const leftPercent = ((start - zoomStart) / visibleRegionLength) * 100;
@@ -22,6 +22,12 @@ export async function loadGff3(gff3Source: string, genomeLength: number | undefi
22
22
  }
23
23
 
24
24
  const response = await fetch(gff3Source);
25
+ if (!response.ok) {
26
+ throw new UserFacingError(
27
+ 'GFF3 download failed',
28
+ `Server returned ${response.status} ${response.statusText} for ${response.url}`,
29
+ );
30
+ }
25
31
  const content = await response.text();
26
32
  genomeLength ??= loadGenomeLength(content);
27
33
  return { features: parseGFF3(content), length: genomeLength };
@@ -6,6 +6,7 @@ import { type Dataset } from '../../operator/Dataset';
6
6
  import type { SequenceType } from '../../types';
7
7
  import { type DeletionClass, type SubstitutionClass } from '../../utils/mutations';
8
8
  import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
9
+ import { useMutationLinkProvider } from '../MutationLinkTemplateContext';
9
10
  import { GridJsAnnotatedMutation } from '../components/annotated-mutation';
10
11
  import { type ProportionInterval } from '../components/proportion-selector';
11
12
  import { Table } from '../components/table';
@@ -26,6 +27,7 @@ export const MutationComparisonTable: FunctionComponent<MutationsTableProps> = (
26
27
  sequenceType,
27
28
  }) => {
28
29
  const annotationsProvider = useMutationAnnotationsProvider();
30
+ const linkProvider = useMutationLinkProvider();
29
31
 
30
32
  const headers = [
31
33
  {
@@ -38,6 +40,7 @@ export const MutationComparisonTable: FunctionComponent<MutationsTableProps> = (
38
40
  mutation={cell}
39
41
  sequenceType={sequenceType}
40
42
  annotationsProvider={annotationsProvider}
43
+ linkProvider={linkProvider}
41
44
  />
42
45
  ),
43
46
  },
@@ -6,6 +6,7 @@ import { MutationFilter, type MutationFilterProps } from './mutation-filter';
6
6
  import { previewHandles } from '../../../.storybook/preview';
7
7
  import { LAPIS_URL } from '../../constants';
8
8
  import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
9
+ import { mutationType } from '../../types';
9
10
  import { gsEventNames } from '../../utils/gsEventNames';
10
11
  import { LapisUrlContextProvider } from '../LapisUrlContext';
11
12
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
@@ -230,7 +231,7 @@ export const FiltersOutDisabledMutationTypes: StoryObj<MutationFilterProps> = {
230
231
  ...Default,
231
232
  args: {
232
233
  ...Default.args,
233
- enabledMutationTypes: ['nucleotideMutations'],
234
+ enabledMutationTypes: [mutationType.nucleotideMutations],
234
235
  },
235
236
  play: async ({ canvasElement, step }) => {
236
237
  const { canvas, changedListenerMock } = await prepare(canvasElement, step);