@genspectrum/dashboard-components 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/custom-elements.json +160 -10
  2. package/dist/{LineageFilterChangedEvent-ixHQkq8y.js → LineageFilterChangedEvent-b0iuroUL.js} +15 -5
  3. package/dist/LineageFilterChangedEvent-b0iuroUL.js.map +1 -0
  4. package/dist/assets/mutationOverTimeWorker-ChQTFL68.js.map +1 -1
  5. package/dist/components.d.ts +71 -25
  6. package/dist/components.js +9047 -8699
  7. package/dist/components.js.map +1 -1
  8. package/dist/util.d.ts +51 -25
  9. package/dist/util.js +2 -1
  10. package/package.json +1 -1
  11. package/src/componentsEntrypoint.ts +3 -1
  12. package/src/preact/components/error-display.stories.tsx +2 -1
  13. package/src/preact/components/error-display.tsx +2 -3
  14. package/src/preact/components/resize-container.tsx +7 -10
  15. package/src/preact/components/tooltip.tsx +7 -4
  16. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +5 -4
  17. package/src/preact/dateRangeFilter/date-range-filter.tsx +2 -1
  18. package/src/preact/dateRangeFilter/dateRangeOption.ts +2 -1
  19. package/src/preact/genomeViewer/CDSPlot.tsx +219 -0
  20. package/src/preact/genomeViewer/genome-data-viewer.stories.tsx +113 -0
  21. package/src/preact/genomeViewer/genome-data-viewer.tsx +69 -0
  22. package/src/preact/genomeViewer/loadGff3.spec.ts +61 -0
  23. package/src/preact/genomeViewer/loadGff3.ts +174 -0
  24. package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +3 -1
  25. package/src/preact/lineageFilter/lineage-filter.stories.tsx +3 -2
  26. package/src/preact/locationFilter/LocationChangedEvent.ts +2 -1
  27. package/src/preact/locationFilter/location-filter.stories.tsx +3 -2
  28. package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -2
  29. package/src/preact/mutationFilter/mutation-filter.tsx +2 -1
  30. package/src/preact/shared/charts/colors.ts +1 -1
  31. package/src/preact/textFilter/TextFilterChangedEvent.ts +3 -1
  32. package/src/preact/textFilter/text-filter.stories.tsx +4 -3
  33. package/src/utilEntrypoint.ts +2 -0
  34. package/src/utils/gsEventNames.ts +9 -0
  35. package/src/web-components/input/gs-date-range-filter.stories.ts +4 -3
  36. package/src/web-components/input/gs-date-range-filter.tsx +3 -2
  37. package/src/web-components/input/gs-lineage-filter.stories.ts +3 -2
  38. package/src/web-components/input/gs-lineage-filter.tsx +2 -1
  39. package/src/web-components/input/gs-location-filter.stories.ts +3 -2
  40. package/src/web-components/input/gs-location-filter.tsx +2 -1
  41. package/src/web-components/input/gs-mutation-filter.stories.ts +3 -2
  42. package/src/web-components/input/gs-mutation-filter.tsx +2 -1
  43. package/src/web-components/input/gs-text-filter.stories.ts +3 -2
  44. package/src/web-components/input/gs-text-filter.tsx +2 -1
  45. package/src/web-components/visualization/gs-genome-data-viewer.spec-d.ts +18 -0
  46. package/src/web-components/visualization/gs-genome-data-viewer.stories.ts +108 -0
  47. package/src/web-components/visualization/gs-genome-data-viewer.tsx +59 -0
  48. package/src/web-components/visualization/index.ts +1 -0
  49. package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
  50. package/standalone-bundle/dashboard-components.js +8275 -8002
  51. package/standalone-bundle/dashboard-components.js.map +1 -1
  52. package/dist/LineageFilterChangedEvent-ixHQkq8y.js.map +0 -1
@@ -0,0 +1,113 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+
3
+ import { GenomeDataViewer, type GenomeDataViewerProps } from './genome-data-viewer';
4
+ import { playThatExpectsErrorMessage } from '../shared/stories/expectErrorMessage';
5
+
6
+ const meta: Meta<GenomeDataViewerProps> = {
7
+ title: 'Visualization/GenomeDataViewer',
8
+ component: GenomeDataViewer,
9
+ argTypes: {
10
+ width: { control: { type: 'text' } },
11
+ },
12
+ };
13
+
14
+ const SimpleData = `
15
+ ##gff-version 3
16
+ #!gff-spec-version 1.21
17
+ #!processor NCBI annotwriter
18
+ ##sequence-region NC_009942.1 1 11029
19
+ ##species https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=11082
20
+ NC_009942.1 RefSeq region 1 11029 . + . ID=NC_009942.1:1..11029;Dbxref=taxon:11082;country=USA;gb-acronym=WNV;gbkey=Src;genome=genomic;isolate=385-99;mol_type=genomic RNA;note=lineage 1%3B Vero cell passage 2 after isolation;strain=NY99
21
+ NC_009942.1 RefSeq gene 97 10395 . + . gene=POLY;ID=gene-WNVNY99_gp1;gbkey=Prot;product=anchored capsid protein ancC;protein_id=YP_005097850.1
22
+ NC_009942.1 RefSeq CDS 97 465 . + . gene=capsid;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=anchored capsid protein ancC;protein_id=YP_005097850.1
23
+ NC_009942.1 RefSeq CDS 466 966 . + . gene=prM;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=protein pr;protein_id=YP_009164953.1
24
+ NC_009942.1 RefSeq CDS 967 2469 . + . gene=env;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=envelope protein E;protein_id=YP_001527880.1
25
+ NC_009942.1 RefSeq CDS 2470 3525 . + . gene=NS1;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=nonstructural protein NS1;protein_id=YP_001527881.1
26
+ NC_009942.1 RefSeq CDS 3526 4218 . + . gene=NS2A;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=nonstructural protein NS2A;protein_id=YP_001527882.1
27
+ NC_009942.1 RefSeq CDS 4219 4611 . + . gene=NS2B;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=nonstructural protein NS2B;protein_id=YP_001527883.1
28
+ NC_009942.1 RefSeq CDS 4612 6468 . + . gene=NS3;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=nonstructural protein NS3;protein_id=YP_001527884.1
29
+ NC_009942.1 RefSeq CDS 6469 6846 . + . gene=NS4A;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=nonstructural protein NS4A;protein_id=YP_001527885.1
30
+ NC_009942.1 RefSeq CDS 6847 6915 . + . gene=2K;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=protein 2K;protein_id=YP_001527885.1
31
+ NC_009942.1 RefSeq CDS 6916 7680 . + . gene=NS4B;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=nonstructural protein NS4B;protein_id=YP_001527886.1
32
+ NC_009942.1 RefSeq CDS 7681 10395 . + . gene=NS5;Parent=gene-WNVNY99_gp1;gbkey=Prot;product=RNA-dependent RNA polymerase NS5;protein_id=YP_001527887.1
33
+ `;
34
+
35
+ const SplicedGeneData = `
36
+ ##gff-version 3
37
+ #!gff-spec-version 1.21
38
+ #!processor NCBI annotwriter
39
+ ##sequence-region NC_026431.1 1 982
40
+ ##species https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=641809
41
+ NC_026431.1 RefSeq region 1 982 . + . ID=NC_026431.1:1..982;Dbxref=taxon:641809;Name=7;collection-date=09-Apr-2009;country=USA: California state;gbkey=Src;genome=genomic;mol_type=viral cRNA;nat-host=Homo sapiens%3B gender M%3B age 54;segment=7;serotype=H1N1;strain=A/California/07/2009
42
+ NC_026431.1 RefSeq CDS 1 26 . + 0 Name=M2;gene=M2;gbkey=CDS;locus_tag=UJ99_s7gp1;protein_id=YP_009118622.1;product=matrix protein 2;ID=cds-YP_009118622.1;Dbxref=GenBank:YP_009118622.1,GeneID:23308108
43
+ NC_026431.1 RefSeq CDS 715 982 . + 1 Name=M2;gene=M2;gbkey=CDS;locus_tag=UJ99_s7gp1;protein_id=YP_009118622.1;product=matrix protein 2;ID=cds-YP_009118622.1;Dbxref=GenBank:YP_009118622.1,GeneID:23308108
44
+ NC_026431.1 RefSeq CDS 1 759 . + 0 Name=M1;gene=M1;gbkey=CDS;locus_tag=UJ99_s7gp2;protein_id=YP_009118623.1;product=matrix protein 1;ID=cds-YP_009118623.1;Dbxref=GenBank:YP_009118623.1,GeneID:23308107
45
+ `;
46
+
47
+ export default meta;
48
+
49
+ const gff3Url = 'http://my.gff.data';
50
+
51
+ export const Default: StoryObj<GenomeDataViewerProps> = {
52
+ render: (args) => <GenomeDataViewer {...args} />,
53
+ args: {
54
+ gff3Source: gff3Url,
55
+ width: '1100px',
56
+ },
57
+ parameters: {
58
+ fetchMock: {
59
+ mocks: [
60
+ {
61
+ matcher: {
62
+ name: 'gff3Data',
63
+ url: gff3Url,
64
+ },
65
+ response: {
66
+ status: 200,
67
+ body: SimpleData,
68
+ headers: {
69
+ 'Content-Type': 'text/plain',
70
+ },
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ },
76
+ };
77
+
78
+ export const InvalidProps: StoryObj<GenomeDataViewerProps> = {
79
+ ...Default,
80
+ args: {
81
+ ...Default.args,
82
+ gff3Source: 'bla',
83
+ },
84
+ play: playThatExpectsErrorMessage('Error - Invalid gff3 source', `Invalid URL passed to parseGFF3: "bla"`),
85
+ };
86
+
87
+ export const SplicedGeneAndOverlap: StoryObj<GenomeDataViewerProps> = {
88
+ render: (args) => <GenomeDataViewer {...args} />,
89
+ args: {
90
+ gff3Source: gff3Url,
91
+ genomeLength: 982,
92
+ width: '1100px',
93
+ },
94
+ parameters: {
95
+ fetchMock: {
96
+ mocks: [
97
+ {
98
+ matcher: {
99
+ name: 'gff3Data',
100
+ url: gff3Url,
101
+ },
102
+ response: {
103
+ status: 200,
104
+ body: SplicedGeneData,
105
+ headers: {
106
+ 'Content-Type': 'text/plain',
107
+ },
108
+ },
109
+ },
110
+ ],
111
+ },
112
+ },
113
+ };
@@ -0,0 +1,69 @@
1
+ import type { FunctionComponent } from 'preact';
2
+ import { useEffect, useRef, useState } from 'preact/hooks';
3
+ import z from 'zod';
4
+
5
+ import { ErrorBoundary } from '../components/error-boundary';
6
+ import { LoadingDisplay } from '../components/loading-display';
7
+ import { ResizeContainer } from '../components/resize-container';
8
+ import { useQuery } from '../useQuery';
9
+ import CDSPlot from './CDSPlot';
10
+ import { loadGff3 } from './loadGff3';
11
+
12
+ const genomeDataViewerPropsSchema = z.object({
13
+ gff3Source: z.string().min(1, 'gff3Source cannot be empty'),
14
+ genomeLength: z.number().gt(0, 'genomeLength must be greater than 0').optional(),
15
+ width: z.string(),
16
+ });
17
+
18
+ interface ExtendedGenomeDataViewerProps extends GenomeDataViewerProps {
19
+ trueWidth: number;
20
+ }
21
+
22
+ export type GenomeDataViewerProps = z.infer<typeof genomeDataViewerPropsSchema>;
23
+
24
+ export const GenomeDataViewer: FunctionComponent<GenomeDataViewerProps> = (componentProps) => {
25
+ const { width } = componentProps;
26
+ const size = { height: '100%', width };
27
+ const containerRef = useRef<HTMLDivElement>(null);
28
+ const [trueWidth, setTrueWidth] = useState(0);
29
+
30
+ useEffect(() => {
31
+ const updateSize = () => {
32
+ if (containerRef.current) {
33
+ setTrueWidth(containerRef.current.getBoundingClientRect().width);
34
+ }
35
+ };
36
+
37
+ updateSize();
38
+ window.addEventListener('resize', updateSize);
39
+ return () => window.removeEventListener('resize', updateSize);
40
+ }, []);
41
+
42
+ return (
43
+ <ErrorBoundary size={size} componentProps={componentProps} schema={genomeDataViewerPropsSchema}>
44
+ <ResizeContainer size={size} ref={containerRef}>
45
+ <GenomeDataViewerInner {...componentProps} trueWidth={trueWidth} />
46
+ </ResizeContainer>
47
+ </ErrorBoundary>
48
+ );
49
+ };
50
+
51
+ const GenomeDataViewerInner: FunctionComponent<ExtendedGenomeDataViewerProps> = (props) => {
52
+ const { gff3Source, genomeLength, trueWidth } = props;
53
+
54
+ const {
55
+ data,
56
+ error,
57
+ isLoading: isLoadingData,
58
+ } = useQuery(() => loadGff3(gff3Source, genomeLength), [gff3Source, genomeLength]);
59
+
60
+ if (isLoadingData) {
61
+ return <LoadingDisplay />;
62
+ }
63
+
64
+ if (error) {
65
+ throw error;
66
+ }
67
+
68
+ return <CDSPlot gffData={data.features} genomeLength={data.length} width={trueWidth} />;
69
+ };
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { type CDSFeature, parseGFF3, loadGenomeLength } from './loadGff3';
4
+
5
+ const SplicedGeneData = `
6
+ ##gff-version 3
7
+ #!gff-spec-version 1.21
8
+ #!processor NCBI annotwriter
9
+ ##sequence-region NC_026431.1 1 982
10
+ ##species https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=641809
11
+ NC_026431.1 RefSeq region 1 982 . + . ID=NC_026431.1:1..982;Dbxref=taxon:641809;Name=7;collection-date=09-Apr-2009;country=USA: California state;gbkey=Src;genome=genomic;mol_type=viral cRNA;nat-host=Homo sapiens%3B gender M%3B age 54;segment=7;serotype=H1N1;strain=A/California/07/2009
12
+ NC_026431.1 RefSeq CDS 1 26 . + 0 Name=M2;gene=M2;gbkey=CDS;locus_tag=UJ99_s7gp1;protein_id=YP_009118622.1;product=matrix protein 2;ID=cds-YP_009118622.1;Dbxref=GenBank:YP_009118622.1,GeneID:23308108
13
+ NC_026431.1 RefSeq CDS 715 982 . + 1 Name=M2;gene=M2;gbkey=CDS;locus_tag=UJ99_s7gp1;protein_id=YP_009118622.1;product=matrix protein 2;ID=cds-YP_009118622.1;Dbxref=GenBank:YP_009118622.1,GeneID:23308108
14
+ NC_026431.1 RefSeq CDS 1 759 . + 0 Name=M1;gene=M1;gbkey=CDS;locus_tag=UJ99_s7gp2;protein_id=YP_009118623.1;product=matrix protein 1;ID=cds-YP_009118623.1;Dbxref=GenBank:YP_009118623.1,GeneID:23308107
15
+ NC_026431.1 RefSeq CDS 760 790 . + 0 Name=fakeGene;gene=fakeGene;gbkey=CDS;locus_tag=UJ99_s7fake;protein_id=YP_009118624.1;product=None;ID=cds-YP_009118624.1;Dbxref=GenBank:YP_009118624.1,GeneID:23308109
16
+ `;
17
+
18
+ describe('parseGFF3', () => {
19
+ test('should parse GFF3 correctly', () => {
20
+ const result: CDSFeature[][] = parseGFF3(SplicedGeneData);
21
+
22
+ expect(result).to.deep.equal([
23
+ [
24
+ {
25
+ color: 'sand',
26
+ positions: [
27
+ { start: 1, end: 26 },
28
+ { start: 715, end: 982 },
29
+ ],
30
+ label: 'M2',
31
+ },
32
+ ],
33
+ [
34
+ { color: 'rose', positions: [{ start: 1, end: 759 }], label: 'M1' },
35
+ { color: 'wine', positions: [{ start: 760, end: 790 }], label: 'fakeGene' },
36
+ ],
37
+ ]);
38
+ });
39
+ });
40
+
41
+ describe('loadGenomeLength', () => {
42
+ test('should calculate genome length correctly', () => {
43
+ const length = loadGenomeLength(SplicedGeneData);
44
+
45
+ expect(length).to.equal(982);
46
+ });
47
+ });
48
+
49
+ const invalidInput = `
50
+ ##gff-version 3
51
+ #!gff-spec-version 1.21
52
+ #!processor NCBI annotwriter
53
+ ##sequence-region NC_026431.1 1`;
54
+
55
+ describe('loadGenomeLength', () => {
56
+ test('should throw an error when passed invalid input', () => {
57
+ expect(() => loadGenomeLength(invalidInput)).toThrow(
58
+ 'No length found in sequence-region: "##sequence-region NC_026431.1 1"',
59
+ );
60
+ });
61
+ });
@@ -0,0 +1,174 @@
1
+ import { UserFacingError } from '../components/error-display';
2
+ import { ColorsRGB, type GraphColor } from '../shared/charts/colors';
3
+
4
+ export type CDSFeature = {
5
+ positions: Position[];
6
+ label: string;
7
+ color: GraphColor;
8
+ };
9
+
10
+ type Position = {
11
+ start: number;
12
+ end: number;
13
+ };
14
+
15
+ type CDSMap = {
16
+ [id: string]: { positions: Position[]; label: string };
17
+ };
18
+
19
+ export async function loadGff3(gff3Source: string, genomeLength: number | undefined) {
20
+ try {
21
+ new URL(gff3Source);
22
+ } catch (_error) {
23
+ throw new UserFacingError('Invalid gff3 source', `Invalid URL passed to parseGFF3: "${gff3Source}"`);
24
+ }
25
+
26
+ const response = await fetch(gff3Source);
27
+ const content = await response.text();
28
+ if (!genomeLength) {
29
+ genomeLength = loadGenomeLength(content);
30
+ }
31
+ return { features: parseGFF3(content), length: genomeLength };
32
+ }
33
+
34
+ export function parseGFF3(content: string): CDSFeature[][] {
35
+ /**
36
+ * Reads in CDS from GFF3 according to nextclade rules:
37
+ * https://docs.nextstrain.org/projects/nextclade/en/stable/user/input-files/03-genome-annotation.html
38
+ * Read in both gene and CDS features
39
+ * If a CDS feature has a gene feature as a parent, remove the gene feature
40
+ * Split the CDS features into non-overlapping features
41
+ */
42
+ const lines = content.split('\n');
43
+
44
+ const map: CDSMap = {};
45
+
46
+ const geneFeatures = getCDSMap(lines, 'gene', map);
47
+ const cdsFeatures = getCDSMap(lines, 'CDS', geneFeatures);
48
+
49
+ return getNonOverlappingCDSFeatures(getSortedCDSFeatures(cdsFeatures));
50
+ }
51
+
52
+ function getAttributes(attributes: string): Map<string, string> {
53
+ const attrPairs = attributes.split(';');
54
+ const attrMap = new Map<string, string>();
55
+ for (const pair of attrPairs) {
56
+ const pairTrimmed = pair.trim();
57
+ const [key, value] = pairTrimmed.split('=');
58
+ attrMap.set(key, value);
59
+ }
60
+ return attrMap;
61
+ }
62
+
63
+ function getCDSMap(lines: string[], genome_type: string, geneMap: CDSMap): CDSMap {
64
+ for (const line of lines) {
65
+ if (line.startsWith('#') || line.trim() === '') {
66
+ continue;
67
+ }
68
+
69
+ const fields = line.split('\t');
70
+ if (fields.length < 9) {
71
+ throw new UserFacingError('Invalid gff3 source', `Gff3 line has less than 9 fields: "${line}"`);
72
+ }
73
+
74
+ const [, , type, startStr, endStr, , , , attributes] = fields;
75
+
76
+ if (type.toLowerCase() !== genome_type.toLowerCase()) {
77
+ continue;
78
+ }
79
+
80
+ const start = parseInt(startStr, 10);
81
+ const end = parseInt(endStr, 10);
82
+
83
+ if (isNaN(start) || isNaN(end)) {
84
+ throw new UserFacingError('Invalid gff3 source', `Invalid start or end position: "${line}"`);
85
+ }
86
+
87
+ const attrPairs = getAttributes(attributes);
88
+ const label = attrPairs.get('Name') || attrPairs.get('gene') || attrPairs.get('gene_name');
89
+ if (!label) {
90
+ throw new UserFacingError(
91
+ 'Invalid gff3 source',
92
+ `No label found for feature: "${line}", must contain label in Name, gene, or gene_name attribute`,
93
+ );
94
+ }
95
+ const id = attrPairs.get('ID') || label;
96
+ if (attrPairs.get('Parent')) {
97
+ const parent = attrPairs.get('Parent');
98
+ if (parent && parent in geneMap) {
99
+ delete geneMap[parent];
100
+ }
101
+ }
102
+ if (id in geneMap) {
103
+ geneMap[id].positions.push({ start, end });
104
+ } else {
105
+ geneMap[id] = { positions: [{ start, end }], label };
106
+ }
107
+ }
108
+ return geneMap;
109
+ }
110
+
111
+ function getSortedCDSFeatures(cdsMap: CDSMap): CDSFeature[] {
112
+ const mapValues = Object.values(cdsMap);
113
+
114
+ mapValues.forEach((feature) => {
115
+ feature.positions.sort((a, b) => a.start - b.start);
116
+ });
117
+
118
+ mapValues.sort((a, b) => {
119
+ return a.positions[0].start - b.positions[0].start;
120
+ });
121
+
122
+ const GraphColorList = Object.keys(ColorsRGB) as GraphColor[];
123
+ const colorIndex = mapValues[0].label[0].toUpperCase().charCodeAt(0);
124
+
125
+ const cdsFeatures: CDSFeature[] = mapValues.map((value, index) => ({
126
+ positions: value.positions,
127
+ label: value.label,
128
+ color: GraphColorList[(colorIndex + index) % GraphColorList.length],
129
+ }));
130
+
131
+ return cdsFeatures;
132
+ }
133
+
134
+ function getNonOverlappingCDSFeatures(cdsFeatures: CDSFeature[]): CDSFeature[][] {
135
+ const nonOverlappingCDSFeatures: CDSFeature[][] = [];
136
+ for (const cdsFeature of cdsFeatures) {
137
+ let added = false;
138
+ for (const cdsList of nonOverlappingCDSFeatures) {
139
+ const lastCds = cdsList[cdsList.length - 1];
140
+ if (cdsFeature.positions[0].start > lastCds.positions[lastCds.positions.length - 1].end) {
141
+ cdsList.push(cdsFeature);
142
+ added = true;
143
+ break;
144
+ }
145
+ }
146
+ if (!added) {
147
+ nonOverlappingCDSFeatures.push([cdsFeature]);
148
+ }
149
+ }
150
+ return nonOverlappingCDSFeatures;
151
+ }
152
+
153
+ export function loadGenomeLength(content: string) {
154
+ const lines = content.split('\n');
155
+ for (const line of lines) {
156
+ if (!line.startsWith('#')) {
157
+ continue;
158
+ }
159
+ const fields = line.split(' ');
160
+ if (fields[0] === '##sequence-region') {
161
+ if (fields.length < 3) {
162
+ throw new UserFacingError('Invalid gff3 source', `No length found in sequence-region: "${line}"`);
163
+ }
164
+ const start = fields.at(-2);
165
+ const end = fields.at(-1);
166
+ const length = parseInt(end!, 10) - parseInt(start!, 10) + 1;
167
+ if (isNaN(length)) {
168
+ throw new UserFacingError('Invalid gff3 source', `No length found in sequence-region: "${line}"`);
169
+ }
170
+ return length;
171
+ }
172
+ }
173
+ throw new UserFacingError('Invalid gff3 source', `No length found in sequence-region`);
174
+ }
@@ -1,8 +1,10 @@
1
+ import { gsEventNames } from '../../utils/gsEventNames';
2
+
1
3
  type LapisLineageFilter = Record<string, string | undefined>;
2
4
 
3
5
  export class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
4
6
  constructor(detail: LapisLineageFilter) {
5
- super('gs-lineage-filter-changed', {
7
+ super(gsEventNames.lineageFilterChanged, {
6
8
  detail,
7
9
  bubbles: true,
8
10
  composed: true,
@@ -6,6 +6,7 @@ import { LineageFilter, type LineageFilterProps } from './lineage-filter';
6
6
  import { previewHandles } from '../../../.storybook/preview';
7
7
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
8
8
  import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
9
+ import { gsEventNames } from '../../utils/gsEventNames';
9
10
  import { LapisUrlContextProvider } from '../LapisUrlContext';
10
11
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
11
12
 
@@ -14,7 +15,7 @@ const meta: Meta = {
14
15
  component: LineageFilter,
15
16
  parameters: {
16
17
  actions: {
17
- handles: ['gs-lineage-filter-changed', ...previewHandles],
18
+ handles: [gsEventNames.lineageFilterChanged, ...previewHandles],
18
19
  },
19
20
  fetchMock: {
20
21
  mocks: [
@@ -155,7 +156,7 @@ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRend
155
156
 
156
157
  const lineageChangedListenerMock = fn();
157
158
  await step('Setup event listener mock', () => {
158
- canvasElement.addEventListener('gs-lineage-filter-changed', lineageChangedListenerMock);
159
+ canvasElement.addEventListener(gsEventNames.lineageFilterChanged, lineageChangedListenerMock);
159
160
  });
160
161
 
161
162
  await step('location filter is rendered with value', async () => {
@@ -1,8 +1,9 @@
1
1
  import { type LapisLocationFilter } from '../../types';
2
+ import { gsEventNames } from '../../utils/gsEventNames';
2
3
 
3
4
  export class LocationChangedEvent extends CustomEvent<LapisLocationFilter> {
4
5
  constructor(detail: LapisLocationFilter) {
5
- super('gs-location-changed', {
6
+ super(gsEventNames.locationChanged, {
6
7
  detail,
7
8
  bubbles: true,
8
9
  composed: true,
@@ -6,6 +6,7 @@ import data from './__mockData__/aggregated.json';
6
6
  import { LocationFilter, type LocationFilterProps } from './location-filter';
7
7
  import { previewHandles } from '../../../.storybook/preview';
8
8
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
9
+ import { gsEventNames } from '../../utils/gsEventNames';
9
10
  import { LapisUrlContextProvider } from '../LapisUrlContext';
10
11
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
11
12
 
@@ -32,7 +33,7 @@ const meta: Meta<LocationFilterProps> = {
32
33
  ],
33
34
  },
34
35
  actions: {
35
- handles: ['gs-location-changed', ...previewHandles],
36
+ handles: [gsEventNames.locationChanged, ...previewHandles],
36
37
  },
37
38
  },
38
39
  args: {
@@ -152,7 +153,7 @@ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRend
152
153
 
153
154
  const locationChangedListenerMock = fn();
154
155
  await step('Setup event listener mock', () => {
155
- canvasElement.addEventListener('gs-location-changed', locationChangedListenerMock);
156
+ canvasElement.addEventListener(gsEventNames.locationChanged, locationChangedListenerMock);
156
157
  });
157
158
 
158
159
  await step('location filter is rendered with value', async () => {
@@ -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 { gsEventNames } from '../../utils/gsEventNames';
9
10
  import { LapisUrlContextProvider } from '../LapisUrlContext';
10
11
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
11
12
  import { playThatExpectsErrorMessage } from '../shared/stories/expectErrorMessage';
@@ -15,7 +16,7 @@ const meta: Meta<MutationFilterProps> = {
15
16
  component: MutationFilter,
16
17
  parameters: {
17
18
  actions: {
18
- handles: ['gs-mutation-filter-changed', ...previewHandles],
19
+ handles: [gsEventNames.mutationFilterChanged, ...previewHandles],
19
20
  },
20
21
  fetchMock: {},
21
22
  },
@@ -357,7 +358,7 @@ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRend
357
358
 
358
359
  const changedListenerMock = fn();
359
360
  await step('Setup event listener mock', () => {
360
- canvasElement.addEventListener('gs-mutation-filter-changed', changedListenerMock);
361
+ canvasElement.addEventListener(gsEventNames.mutationFilterChanged, changedListenerMock);
361
362
  });
362
363
 
363
364
  await step('wait until data is loaded', async () => {
@@ -8,6 +8,7 @@ import { MutationFilterInfo } from './mutation-filter-info';
8
8
  import { parseAndValidateMutation } from './parseAndValidateMutation';
9
9
  import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
10
10
  import { type MutationsFilter, mutationsFilterSchema } from '../../types';
11
+ import { gsEventNames } from '../../utils/gsEventNames';
11
12
  import { type DeletionClass, type InsertionClass, type SubstitutionClass } from '../../utils/mutations';
12
13
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
13
14
  import { ErrorBoundary } from '../components/error-boundary';
@@ -86,7 +87,7 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
86
87
  const detail = mapToMutationFilterStrings(selectedFilters);
87
88
 
88
89
  filterRef.current?.dispatchEvent(
89
- new CustomEvent<MutationsFilter>('gs-mutation-filter-changed', {
90
+ new CustomEvent<MutationsFilter>(gsEventNames.mutationFilterChanged, {
90
91
  detail,
91
92
  bubbles: true,
92
93
  composed: true,
@@ -1,6 +1,6 @@
1
1
  // colorblind friendly colors taken from https://personal.sron.nl/~pault/
2
2
 
3
- const ColorsRGB = {
3
+ export const ColorsRGB = {
4
4
  indigo: [51, 34, 136],
5
5
  green: [17, 119, 51],
6
6
  cyan: [136, 204, 238],
@@ -1,8 +1,10 @@
1
+ import { gsEventNames } from '../../utils/gsEventNames';
2
+
1
3
  type LapisTextFilter = Record<string, string | undefined>;
2
4
 
3
5
  export class TextFilterChangedEvent extends CustomEvent<LapisTextFilter> {
4
6
  constructor(detail: LapisTextFilter) {
5
- super('gs-text-filter-changed', {
7
+ super(gsEventNames.textFilterChanged, {
6
8
  detail,
7
9
  bubbles: true,
8
10
  composed: true,
@@ -5,6 +5,7 @@ import data from './__mockData__/aggregated_hosts.json';
5
5
  import { TextFilter, type TextFilterProps } from './text-filter';
6
6
  import { previewHandles } from '../../../.storybook/preview';
7
7
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
8
+ import { gsEventNames } from '../../utils/gsEventNames';
8
9
  import { LapisUrlContextProvider } from '../LapisUrlContext';
9
10
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
10
11
 
@@ -13,7 +14,7 @@ const meta: Meta<TextFilterProps> = {
13
14
  component: TextFilter,
14
15
  parameters: {
15
16
  actions: {
16
- handles: ['gs-text-filter-changed', ...previewHandles],
17
+ handles: [gsEventNames.textFilterChanged, ...previewHandles],
17
18
  },
18
19
  fetchMock: {
19
20
  mocks: [
@@ -93,7 +94,7 @@ export const RemoveInitialValue: StoryObj<TextFilterProps> = {
93
94
 
94
95
  const changedListenerMock = fn();
95
96
  await step('Setup event listener mock', () => {
96
- canvasElement.addEventListener('gs-text-filter-changed', changedListenerMock);
97
+ canvasElement.addEventListener(gsEventNames.textFilterChanged, changedListenerMock);
97
98
  });
98
99
 
99
100
  await waitFor(async () => {
@@ -136,7 +137,7 @@ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
136
137
 
137
138
  const changedListenerMock = fn();
138
139
  await step('Setup event listener mock', () => {
139
- canvasElement.addEventListener('gs-text-filter-changed', changedListenerMock);
140
+ canvasElement.addEventListener(gsEventNames.textFilterChanged, changedListenerMock);
140
141
  });
141
142
  const inputField = () => canvas.getByPlaceholderText('Enter a host name', { exact: false });
142
143
  async function typeAndBlur(input: string) {
@@ -38,3 +38,5 @@ export { LineageFilterChangedEvent } from './preact/lineageFilter/LineageFilterC
38
38
  export { TextFilterChangedEvent } from './preact/textFilter/TextFilterChangedEvent';
39
39
 
40
40
  export type { MutationAnnotations, MutationAnnotation } from './web-components/mutation-annotations-context';
41
+
42
+ export { gsEventNames } from './utils/gsEventNames';
@@ -0,0 +1,9 @@
1
+ export const gsEventNames = {
2
+ error: 'gs-error',
3
+ dateRangeFilterChanged: 'gs-date-range-filter-changed',
4
+ dateRangeOptionChanged: 'gs-date-range-option-changed',
5
+ mutationFilterChanged: 'gs-mutation-filter-changed',
6
+ lineageFilterChanged: 'gs-lineage-filter-changed',
7
+ locationChanged: 'gs-location-changed',
8
+ textFilterChanged: 'gs-text-filter-changed',
9
+ } as const;
@@ -10,6 +10,7 @@ import './gs-date-range-filter';
10
10
  import '../gs-app';
11
11
  import { toYYYYMMDD } from '../../preact/dateRangeFilter/dateConversion';
12
12
  import { dateRangeOptionPresets } from '../../preact/dateRangeFilter/dateRangeOption';
13
+ import { gsEventNames } from '../../utils/gsEventNames';
13
14
  import { withinShadowRoot } from '../withinShadowRoot.story';
14
15
 
15
16
  const codeExample = String.raw`
@@ -29,7 +30,7 @@ const meta: Meta<Required<DateRangeFilterProps>> = {
29
30
  component: 'gs-date-range-filter',
30
31
  parameters: withComponentDocs({
31
32
  actions: {
32
- handles: ['gs-date-range-filter-changed', 'gs-date-range-option-changed', ...previewHandles],
33
+ handles: [gsEventNames.dateRangeFilterChanged, gsEventNames.dateRangeOptionChanged, ...previewHandles],
33
34
  },
34
35
  fetchMock: {},
35
36
  componentDocs: {
@@ -144,8 +145,8 @@ export const FiresEvents: StoryObj<Required<DateRangeFilterProps>> = {
144
145
  const filterChangedListenerMock = fn();
145
146
  const optionChangedListenerMock = fn();
146
147
  await step('Setup event listener mock', () => {
147
- canvasElement.addEventListener('gs-date-range-filter-changed', filterChangedListenerMock);
148
- canvasElement.addEventListener('gs-date-range-option-changed', optionChangedListenerMock);
148
+ canvasElement.addEventListener(gsEventNames.dateRangeFilterChanged, filterChangedListenerMock);
149
+ canvasElement.addEventListener(gsEventNames.dateRangeOptionChanged, optionChangedListenerMock);
149
150
  });
150
151
 
151
152
  await step('Expect last 6 months to be selected', async () => {
@@ -5,6 +5,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
5
5
 
6
6
  import { DateRangeFilter, type DateRangeFilterProps } from '../../preact/dateRangeFilter/date-range-filter';
7
7
  import { type DateRangeOptionChangedEvent } from '../../preact/dateRangeFilter/dateRangeOption';
8
+ import { type gsEventNames } from '../../utils/gsEventNames';
8
9
  import { type Equals, type Expect } from '../../utils/typeAssertions';
9
10
  import { PreactLitAdapter } from '../PreactLitAdapter';
10
11
 
@@ -143,8 +144,8 @@ declare global {
143
144
  }
144
145
 
145
146
  interface HTMLElementEventMap {
146
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
147
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
147
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
148
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
148
149
  }
149
150
  }
150
151