@genspectrum/dashboard-components 0.1.3 → 0.1.5

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 (78) hide show
  1. package/custom-elements.json +488 -117
  2. package/dist/dashboard-components.js +904 -466
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +473 -67
  5. package/dist/style.css +273 -153
  6. package/package.json +11 -7
  7. package/src/preact/aggregatedData/aggregate.stories.tsx +7 -5
  8. package/src/preact/aggregatedData/aggregate.tsx +16 -7
  9. package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
  10. package/src/preact/components/csv-download-button.tsx +8 -2
  11. package/src/preact/components/headline.stories.tsx +19 -1
  12. package/src/preact/components/headline.tsx +25 -5
  13. package/src/preact/components/info.stories.tsx +24 -3
  14. package/src/preact/components/info.tsx +49 -5
  15. package/src/preact/components/min-max-range-slider.tsx +4 -4
  16. package/src/preact/components/percent-intput.tsx +2 -3
  17. package/src/preact/components/resize-container.tsx +23 -0
  18. package/src/preact/components/table.tsx +1 -0
  19. package/src/preact/components/tabs.stories.tsx +2 -2
  20. package/src/preact/components/tabs.tsx +47 -24
  21. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
  22. package/src/preact/dateRangeSelector/date-range-selector.tsx +67 -53
  23. package/src/preact/locationFilter/location-filter.tsx +2 -2
  24. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
  25. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
  26. package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
  27. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
  28. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +11 -1
  29. package/src/preact/mutationComparison/mutation-comparison.tsx +16 -7
  30. package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
  31. package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
  32. package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
  33. package/src/preact/mutations/getInsertionsTableData.ts +1 -1
  34. package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
  35. package/src/preact/mutations/getMutationsTableData.ts +1 -1
  36. package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
  37. package/src/preact/mutations/mutations-table.tsx +3 -1
  38. package/src/preact/mutations/mutations.stories.tsx +11 -1
  39. package/src/preact/mutations/mutations.tsx +24 -7
  40. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
  41. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
  42. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
  43. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +8 -0
  44. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +31 -13
  45. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
  46. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +15 -0
  47. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +62 -12
  48. package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
  49. package/src/preact/shared/sort/sortInsertions.ts +10 -17
  50. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
  51. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
  52. package/src/preact/textInput/text-input.stories.tsx +22 -1
  53. package/src/preact/textInput/text-input.tsx +3 -1
  54. package/src/utils/typeAssertions.spec.ts +31 -0
  55. package/src/utils/typeAssertions.ts +16 -0
  56. package/src/web-components/PreactLitAdapter.tsx +0 -1
  57. package/src/web-components/app.stories.ts +129 -0
  58. package/src/web-components/app.ts +27 -6
  59. package/src/web-components/display/aggregate-component.stories.ts +24 -11
  60. package/src/web-components/display/aggregate-component.tsx +26 -5
  61. package/src/web-components/display/mutation-comparison-component.stories.ts +32 -11
  62. package/src/web-components/display/mutation-comparison-component.tsx +79 -4
  63. package/src/web-components/display/mutations-component.stories.ts +40 -19
  64. package/src/web-components/display/mutations-component.tsx +71 -4
  65. package/src/web-components/display/prevalence-over-time-component.stories.ts +44 -18
  66. package/src/web-components/display/prevalence-over-time-component.tsx +105 -5
  67. package/src/web-components/display/relative-growth-advantage-component.stories.ts +32 -10
  68. package/src/web-components/display/relative-growth-advantage-component.tsx +66 -3
  69. package/src/web-components/input/date-range-selector-component.stories.ts +51 -9
  70. package/src/web-components/input/date-range-selector-component.tsx +69 -4
  71. package/src/web-components/input/location-filter-component.stories.ts +15 -4
  72. package/src/web-components/input/location-filter-component.tsx +2 -6
  73. package/src/web-components/input/mutation-filter-component.stories.ts +33 -12
  74. package/src/web-components/input/mutation-filter-component.tsx +60 -4
  75. package/src/web-components/input/text-input-component.stories.ts +26 -6
  76. package/src/web-components/input/text-input-component.tsx +34 -3
  77. package/src/web-components/display/aggregate-component.mdx +0 -25
  78. package/src/web-components/input/location-filter.mdx +0 -25
@@ -13,9 +13,10 @@ import { ConfidenceIntervalSelector } from '../components/confidence-interval-se
13
13
  import { CsvDownloadButton } from '../components/csv-download-button';
14
14
  import { ErrorDisplay } from '../components/error-display';
15
15
  import Headline from '../components/headline';
16
- import Info from '../components/info';
16
+ import Info, { InfoHeadline1, InfoParagraph } from '../components/info';
17
17
  import { LoadingDisplay } from '../components/loading-display';
18
18
  import { NoDataDisplay } from '../components/no-data-display';
19
+ import { ResizeContainer, type Size } from '../components/resize-container';
19
20
  import { ScalingSelector } from '../components/scaling-selector';
20
21
  import Tabs from '../components/tabs';
21
22
  import { type ConfidenceIntervalMethod } from '../shared/charts/confideceInterval';
@@ -31,6 +32,8 @@ export interface PrevalenceOverTimeProps {
31
32
  smoothingWindow: number;
32
33
  views: View[];
33
34
  confidenceIntervalMethods: ConfidenceIntervalMethod[];
35
+ size?: Size;
36
+ headline?: string;
34
37
  }
35
38
 
36
39
  export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
@@ -40,6 +43,8 @@ export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
40
43
  smoothingWindow,
41
44
  views,
42
45
  confidenceIntervalMethods,
46
+ size,
47
+ headline = 'Prevalence over time',
43
48
  }) => {
44
49
  const lapis = useContext(LapisUrlContext);
45
50
 
@@ -48,8 +53,6 @@ export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
48
53
  [lapis, numerator, denominator, granularity, smoothingWindow],
49
54
  );
50
55
 
51
- const headline = 'Prevalence over time';
52
-
53
56
  if (isLoading) {
54
57
  return (
55
58
  <Headline heading={headline}>
@@ -75,14 +78,16 @@ export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
75
78
  }
76
79
 
77
80
  return (
78
- <Headline heading={headline}>
79
- <PrevalenceOverTimeTabs
80
- views={views}
81
- data={data}
82
- granularity={granularity}
83
- confidenceIntervalMethods={confidenceIntervalMethods}
84
- />
85
- </Headline>
81
+ <ResizeContainer size={size} defaultSize={{ height: '600px', width: '100%' }}>
82
+ <Headline heading={headline}>
83
+ <PrevalenceOverTimeTabs
84
+ views={views}
85
+ data={data}
86
+ granularity={granularity}
87
+ confidenceIntervalMethods={confidenceIntervalMethods}
88
+ />
89
+ </Headline>
90
+ </ResizeContainer>
86
91
  );
87
92
  };
88
93
 
@@ -134,7 +139,10 @@ const PrevalenceOverTimeTabs: FunctionComponent<PrevalenceOverTimeTabsProps> = (
134
139
  content: <PrevalenceOverTimeBubbleChart data={data} yAxisScaleType={yAxisScaleType} />,
135
140
  };
136
141
  case 'table':
137
- return { title: 'Table', content: <PrevalenceOverTimeTable data={data} granularity={granularity} /> };
142
+ return {
143
+ title: 'Table',
144
+ content: <PrevalenceOverTimeTable data={data} granularity={granularity} />,
145
+ };
138
146
  }
139
147
  };
140
148
 
@@ -194,9 +202,19 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
194
202
  getData={() => getPrevalenceOverTimeTableData(data, granularity)}
195
203
  filename='prevalence-over-time.csv'
196
204
  />
197
- <Info className='ml-1' content='Info for prevalence over time' />
205
+
206
+ <PrevalenceOverTimeInfo />
198
207
  </div>
199
208
  );
200
209
  };
201
210
 
211
+ const PrevalenceOverTimeInfo: FunctionComponent = () => {
212
+ return (
213
+ <Info size={{ width: '600px', height: '30vh' }}>
214
+ <InfoHeadline1>Prevalence over time</InfoHeadline1>
215
+ <InfoParagraph>Prevalence over time info.</InfoParagraph>
216
+ </Info>
217
+ );
218
+ };
219
+
202
220
  export default PrevalenceOverTime;
@@ -37,6 +37,7 @@ const RelativeGrowthAdvantageChart = ({ data, yAxisScaleType }: RelativeGrowthAd
37
37
  datasets: datasets(data),
38
38
  },
39
39
  options: {
40
+ maintainAspectRatio: false,
40
41
  animation: false,
41
42
  scales: {
42
43
  y: getYAxisScale(yAxisScaleType),
@@ -51,13 +52,15 @@ const RelativeGrowthAdvantageChart = ({ data, yAxisScaleType }: RelativeGrowthAd
51
52
  };
52
53
 
53
54
  return (
54
- <>
55
- <GsChart configuration={config} />
56
- <div>
55
+ <div className='flex flex-col h-full'>
56
+ <div className='flex-1'>
57
+ <GsChart configuration={config} />
58
+ </div>
59
+ <p>
57
60
  Advantage: {(data.params.fd.value * 100).toFixed(2)}% ({(data.params.fd.ciLower * 100).toFixed(2)}% -{' '}
58
61
  {(data.params.fd.ciUpper * 100).toFixed(2)}%)
59
- </div>
60
- </>
62
+ </p>
63
+ </div>
61
64
  );
62
65
  };
63
66
 
@@ -10,6 +10,17 @@ export default {
10
10
  parameters: {
11
11
  fetchMock: {},
12
12
  },
13
+ argTypes: {
14
+ numerator: { control: 'object' },
15
+ denominator: { control: 'object' },
16
+ generationTime: { control: 'number' },
17
+ views: {
18
+ options: ['line'],
19
+ control: { type: 'check' },
20
+ },
21
+ size: [{ control: 'object' }],
22
+ headline: { control: 'text' },
23
+ },
13
24
  };
14
25
 
15
26
  export const Primary = {
@@ -20,6 +31,8 @@ export const Primary = {
20
31
  denominator={args.denominator}
21
32
  generationTime={args.generationTime}
22
33
  views={args.views}
34
+ size={args.size}
35
+ headline={args.headline}
23
36
  />
24
37
  </LapisUrlContext.Provider>
25
38
  ),
@@ -28,6 +41,8 @@ export const Primary = {
28
41
  denominator: { country: 'Switzerland', dateFrom: '2020-12-01', dateTo: '2021-03-01' },
29
42
  generationTime: 7,
30
43
  views: ['line'],
44
+ size: { width: '100%', height: '700px' },
45
+ headline: 'Relative growth advantage',
31
46
  },
32
47
  parameters: {
33
48
  fetchMock: {
@@ -10,9 +10,10 @@ import { type LapisFilter } from '../../types';
10
10
  import { LapisUrlContext } from '../LapisUrlContext';
11
11
  import { ErrorDisplay } from '../components/error-display';
12
12
  import Headline from '../components/headline';
13
- import Info from '../components/info';
13
+ import Info, { InfoHeadline1, InfoHeadline2, InfoLink, InfoParagraph } from '../components/info';
14
14
  import { LoadingDisplay } from '../components/loading-display';
15
15
  import { NoDataDisplay } from '../components/no-data-display';
16
+ import { ResizeContainer, type Size } from '../components/resize-container';
16
17
  import { ScalingSelector } from '../components/scaling-selector';
17
18
  import Tabs from '../components/tabs';
18
19
  import { type ScaleType } from '../shared/charts/getYAxisScale';
@@ -25,6 +26,8 @@ export interface RelativeGrowthAdvantageProps {
25
26
  denominator: LapisFilter;
26
27
  generationTime: number;
27
28
  views: View[];
29
+ size?: Size;
30
+ headline?: string;
28
31
  }
29
32
 
30
33
  export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageProps> = ({
@@ -32,6 +35,8 @@ export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageP
32
35
  denominator,
33
36
  generationTime,
34
37
  views,
38
+ size,
39
+ headline = 'Relative growth advantage',
35
40
  }) => {
36
41
  const lapis = useContext(LapisUrlContext);
37
42
  const [yAxisScaleType, setYAxisScaleType] = useState<ScaleType>('linear');
@@ -41,7 +46,6 @@ export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageP
41
46
  [lapis, numerator, denominator, generationTime, views],
42
47
  );
43
48
 
44
- const headline = 'Relative growth advantage';
45
49
  if (isLoading) {
46
50
  return (
47
51
  <Headline heading={headline}>
@@ -67,14 +71,17 @@ export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageP
67
71
  }
68
72
 
69
73
  return (
70
- <Headline heading={headline}>
71
- <RelativeGrowthAdvantageTabs
72
- data={data}
73
- yAxisScaleType={yAxisScaleType}
74
- setYAxisScaleType={setYAxisScaleType}
75
- views={views}
76
- />
77
- </Headline>
74
+ <ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
75
+ <Headline heading={headline}>
76
+ <RelativeGrowthAdvantageTabs
77
+ data={data}
78
+ yAxisScaleType={yAxisScaleType}
79
+ setYAxisScaleType={setYAxisScaleType}
80
+ views={views}
81
+ generationTime={generationTime}
82
+ />
83
+ </Headline>
84
+ </ResizeContainer>
78
85
  );
79
86
  };
80
87
 
@@ -83,6 +90,7 @@ type RelativeGrowthAdvantageTabsProps = {
83
90
  yAxisScaleType: ScaleType;
84
91
  setYAxisScaleType: (scaleType: ScaleType) => void;
85
92
  views: View[];
93
+ generationTime: number;
86
94
  };
87
95
 
88
96
  const RelativeGrowthAdvantageTabs: FunctionComponent<RelativeGrowthAdvantageTabsProps> = ({
@@ -90,6 +98,7 @@ const RelativeGrowthAdvantageTabs: FunctionComponent<RelativeGrowthAdvantageTabs
90
98
  yAxisScaleType,
91
99
  setYAxisScaleType,
92
100
  views,
101
+ generationTime,
93
102
  }) => {
94
103
  const getTab = (view: View) => {
95
104
  switch (view) {
@@ -112,7 +121,11 @@ const RelativeGrowthAdvantageTabs: FunctionComponent<RelativeGrowthAdvantageTabs
112
121
 
113
122
  const tabs = views.map((view) => getTab(view));
114
123
  const toolbar = () => (
115
- <RelativeGrowthAdvantageToolbar yAxisScaleType={yAxisScaleType} setYAxisScaleType={setYAxisScaleType} />
124
+ <RelativeGrowthAdvantageToolbar
125
+ generationTime={generationTime}
126
+ yAxisScaleType={yAxisScaleType}
127
+ setYAxisScaleType={setYAxisScaleType}
128
+ />
116
129
  );
117
130
 
118
131
  return <Tabs tabs={tabs} toolbar={toolbar} />;
@@ -121,16 +134,53 @@ const RelativeGrowthAdvantageTabs: FunctionComponent<RelativeGrowthAdvantageTabs
121
134
  type RelativeGrowthAdvantageToolbarProps = {
122
135
  yAxisScaleType: ScaleType;
123
136
  setYAxisScaleType: (scaleType: ScaleType) => void;
137
+ generationTime: number;
124
138
  };
125
139
 
126
140
  const RelativeGrowthAdvantageToolbar: FunctionComponent<RelativeGrowthAdvantageToolbarProps> = ({
127
141
  yAxisScaleType,
128
142
  setYAxisScaleType,
143
+ generationTime,
129
144
  }) => {
130
145
  return (
131
146
  <div class='flex'>
132
147
  <ScalingSelector yAxisScaleType={yAxisScaleType} setYAxisScaleType={setYAxisScaleType} />
133
- <Info className='ml-1' content='Line chart' />
148
+ <RelativeGrowthAdvantageInfo generationTime={generationTime} />
134
149
  </div>
135
150
  );
136
151
  };
152
+
153
+ const RelativeGrowthAdvantageInfo: FunctionComponent<{ generationTime: number }> = ({ generationTime }) => {
154
+ return (
155
+ <Info size={{ width: '600px', height: '30vh' }}>
156
+ <InfoHeadline1>Relative growth advantage</InfoHeadline1>
157
+ <InfoParagraph>
158
+ If variants spread pre-dominantly by local transmission across demographic groups, this estimate
159
+ reflects the relative viral intrinsic growth advantage of the focal variant in the selected country and
160
+ time frame. We report the relative growth advantage per {generationTime} days (in percentage; 0% means
161
+ equal growth). Importantly, the relative growth advantage estimate reflects the advantage compared to
162
+ the co-circulating variants. Thus, as new variants spread, the advantage of the focal variant may
163
+ decrease. Different mechanisms can alter the intrinsic growth rate, including an intrinsic transmission
164
+ advantage, immune evasion, and a prolonged infectious period. When absolute numbers of a variant are
165
+ low, the growth advantage may merely reflect the current importance of introductions from abroad or the
166
+ variant spreading in a particular demographic group. In this case, the estimate does not provide
167
+ information on any intrinsic fitness advantages.
168
+ </InfoParagraph>
169
+ <InfoParagraph>
170
+ Example: Assume that 10 infections from the focal variant and 100 infections from the co-circulating
171
+ variants occur today and that the focal variant has a relative growth advantage of 50%. Then, if the
172
+ number of new infections from the co-circulating variants remains at 100 in {generationTime} days from
173
+ today, we expect the number of new infections from the focal variant to be 15.
174
+ </InfoParagraph>
175
+
176
+ <InfoHeadline2>Reference</InfoHeadline2>
177
+ <InfoParagraph>
178
+ Chen, Chaoran, et al. "Quantification of the spread of SARS-CoV-2 variant B.1.1.7 in Switzerland."
179
+ Epidemics (2021); doi:{' '}
180
+ <InfoLink href='https://www.sciencedirect.com/science/article/pii/S1755436521000335?via=ihub'>
181
+ 10.1016/j.epidem.2021.100480
182
+ </InfoLink>
183
+ </InfoParagraph>
184
+ </Info>
185
+ );
186
+ };
@@ -1,19 +1,20 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
 
3
3
  import { sortInsertions } from './sortInsertions';
4
+ import { Insertion } from '../../../utils/mutations';
4
5
 
5
6
  describe('sortInsertions with no segments', () => {
6
7
  test('should sort for positions first', () => {
7
- const a = 'ins_1:A';
8
- const b = 'ins_2:A';
8
+ const a = new Insertion(undefined, 1, 'A');
9
+ const b = new Insertion(undefined, 2, 'A');
9
10
 
10
11
  expect(sortInsertions(a, b)).toBeLessThan(0);
11
12
  expect(sortInsertions(b, a)).toBeGreaterThan(0);
12
13
  });
13
14
 
14
15
  test('should sort for symbols second', () => {
15
- const a = 'ins_1:A';
16
- const b = 'ins_1:B';
16
+ const a = new Insertion(undefined, 1, 'A');
17
+ const b = new Insertion(undefined, 1, 'B');
17
18
 
18
19
  expect(sortInsertions(a, b)).toBeLessThan(0);
19
20
  expect(sortInsertions(b, a)).toBeGreaterThan(0);
@@ -22,24 +23,24 @@ describe('sortInsertions with no segments', () => {
22
23
 
23
24
  describe('sortInsertions with segments', () => {
24
25
  test('should sort for segments first', () => {
25
- const a = 'ins_AA1:1:A';
26
- const b = 'ins_BB2:1:A';
26
+ const a = new Insertion('AA1', 1, 'A');
27
+ const b = new Insertion('BB1', 1, 'A');
27
28
 
28
29
  expect(sortInsertions(a, b)).toBeLessThan(0);
29
30
  expect(sortInsertions(b, a)).toBeGreaterThan(0);
30
31
  });
31
32
 
32
33
  test('should sort for positions second', () => {
33
- const a = 'ins_AA1:1:A';
34
- const b = 'ins_AA1:2:A';
34
+ const a = new Insertion('AA1', 1, 'A');
35
+ const b = new Insertion('AA1', 2, 'A');
35
36
 
36
37
  expect(sortInsertions(a, b)).toBeLessThan(0);
37
38
  expect(sortInsertions(b, a)).toBeGreaterThan(0);
38
39
  });
39
40
 
40
41
  test('should sort for symbols third', () => {
41
- const a = 'ins_AA1:1:A';
42
- const b = 'ins_AA1:1:B';
42
+ const a = new Insertion('AA1', 1, 'A');
43
+ const b = new Insertion('AA1', 1, 'B');
43
44
 
44
45
  expect(sortInsertions(a, b)).toBeLessThan(0);
45
46
  expect(sortInsertions(b, a)).toBeGreaterThan(0);
@@ -1,21 +1,14 @@
1
- import { Insertion } from '../../../utils/mutations';
1
+ import { comparePositions, compareSegments } from './sortSubstitutionsAndDeletions';
2
+ import { type Insertion } from '../../../utils/mutations';
2
3
 
3
- export const sortInsertions = (a: string, b: string) => {
4
- const insertionA = Insertion.parse(a);
5
- const insertionB = Insertion.parse(b);
4
+ export const sortInsertions = (a: Insertion, b: Insertion) => {
5
+ if (a.segment !== b.segment) {
6
+ return compareSegments(a.segment, b.segment);
7
+ }
6
8
 
7
- if (insertionA && insertionB) {
8
- const segmentA = insertionA.segment;
9
- const segmentB = insertionB.segment;
10
- if (segmentA !== undefined && segmentB !== undefined && segmentA !== segmentB) {
11
- return segmentA.localeCompare(segmentB);
12
- }
13
- const positionA = insertionA.position;
14
- const positionB = insertionB.position;
15
- if (positionA !== positionB) {
16
- return positionA - positionB;
17
- }
18
- return insertionA.insertedSymbols.localeCompare(insertionB.insertedSymbols);
9
+ if (a.position !== b.position) {
10
+ return comparePositions(a.position, b.position);
19
11
  }
20
- throw new Error(`Invalid insertion: ${a} or ${b}`);
12
+
13
+ return a.insertedSymbols.localeCompare(b.insertedSymbols);
21
14
  };
@@ -1,19 +1,28 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
 
3
3
  import { sortSubstitutionsAndDeletions } from './sortSubstitutionsAndDeletions';
4
+ import { Deletion, Substitution } from '../../../utils/mutations';
4
5
 
5
6
  describe('sortSubstitutionsAndDeletions with no segments', () => {
6
7
  test('should sort for positions first', () => {
7
- const a = 'A123B';
8
- const b = 'A234B';
8
+ const a = new Substitution(undefined, 'A', 'B', 123);
9
+ const b = new Substitution(undefined, 'A', 'B', 234);
9
10
 
10
11
  expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
11
12
  expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
12
13
  });
13
14
 
14
15
  test('should sort for substitutionValue second', () => {
15
- const a = 'A123A';
16
- const b = 'A123B';
16
+ const a = new Substitution(undefined, 'A', 'A', 123);
17
+ const b = new Substitution(undefined, 'A', 'B', 123);
18
+
19
+ expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
20
+ expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
21
+ });
22
+
23
+ test('should sort for substitutionValue over deletion', () => {
24
+ const a = new Substitution(undefined, 'A', 'A', 123);
25
+ const b = new Deletion(undefined, 'A', 123);
17
26
 
18
27
  expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
19
28
  expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
@@ -22,24 +31,24 @@ describe('sortSubstitutionsAndDeletions with no segments', () => {
22
31
 
23
32
  describe('sortSubstitutionsAndDeletions with segments', () => {
24
33
  test('should sort for segment first', () => {
25
- const a = 'AA1:A123B';
26
- const b = 'BB1:A234B';
34
+ const a = new Substitution('AA1', 'A', 'B', 123);
35
+ const b = new Substitution('BB1', 'A', 'B', 234);
27
36
 
28
37
  expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
29
38
  expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
30
39
  });
31
40
 
32
41
  test('should sort for position second', () => {
33
- const a = 'AA1:A123B';
34
- const b = 'AA1:A234B';
42
+ const a = new Substitution('AA1', 'A', 'B', 123);
43
+ const b = new Substitution('AA1', 'A', 'B', 234);
35
44
 
36
45
  expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
37
46
  expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
38
47
  });
39
48
 
40
49
  test('should sort for substitutionValue third', () => {
41
- const a = 'AA1:A123A';
42
- const b = 'AA1:A123B';
50
+ const a = new Substitution('AA1', 'A', 'A', 123);
51
+ const b = new Substitution('AA1', 'A', 'B', 123);
43
52
 
44
53
  expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
45
54
  expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
@@ -1,17 +1,50 @@
1
- export const substitutionAndDeletionRegex = /(?:([A-Za-z0-9]+):)?([A-Za-z])(\d+)([A-Za-z]|-|\*)/;
1
+ import { Deletion, type Substitution } from '../../../utils/mutations';
2
2
 
3
- export const sortSubstitutionsAndDeletions = (a: string, b: string) => {
4
- const aMatch = a.match(substitutionAndDeletionRegex);
5
- const bMatch = b.match(substitutionAndDeletionRegex);
3
+ export const sortSubstitutionsAndDeletions = (a: Substitution | Deletion, b: Substitution | Deletion) => {
4
+ if (a.segment !== b.segment) {
5
+ compareSegments(a.segment, b.segment);
6
+ }
6
7
 
7
- if (aMatch && bMatch) {
8
- if (aMatch[1] !== bMatch[1]) {
9
- return aMatch[1].localeCompare(bMatch[1]);
10
- }
11
- if (aMatch[3] !== bMatch[3]) {
12
- return parseInt(aMatch[3], 10) - parseInt(bMatch[3], 10);
8
+ if (a.position !== b.position) {
9
+ return comparePositions(a.position, b.position);
10
+ }
11
+
12
+ const aIsDeletion = a instanceof Deletion;
13
+ const bIsDeletion = b instanceof Deletion;
14
+
15
+ if (aIsDeletion !== bIsDeletion) {
16
+ return aIsDeletion ? 1 : -1;
17
+ }
18
+
19
+ if (!aIsDeletion && !bIsDeletion) {
20
+ if (a.substitutionValue !== b.substitutionValue) {
21
+ return compareSubstitutionValues(a.substitutionValue, b.substitutionValue);
13
22
  }
14
- return aMatch[4].localeCompare(bMatch[4]);
15
23
  }
16
- throw new Error(`Invalid substitution or deletion: ${a} or ${b}`);
24
+
25
+ return 0;
26
+ };
27
+
28
+ export const compareSegments = (a: string | undefined, b: string | undefined) => {
29
+ if (a === undefined) {
30
+ return -1;
31
+ }
32
+ if (b === undefined) {
33
+ return 1;
34
+ }
35
+ return a.localeCompare(b);
36
+ };
37
+
38
+ export const comparePositions = (a: number, b: number) => {
39
+ return a - b;
40
+ };
41
+
42
+ const compareSubstitutionValues = (a: string | undefined, b: string | undefined) => {
43
+ if (a === undefined) {
44
+ return -1;
45
+ }
46
+ if (b === undefined) {
47
+ return 1;
48
+ }
49
+ return a.localeCompare(b);
17
50
  };
@@ -1,5 +1,6 @@
1
1
  import { withActions } from '@storybook/addon-actions/decorator';
2
2
  import { type Meta, type StoryObj } from '@storybook/preact';
3
+ import { expect, waitFor, within } from '@storybook/test';
3
4
 
4
5
  import data from './__mockData__/aggregated_hosts.json';
5
6
  import { TextInput, type TextInputProps } from './text-input';
@@ -39,7 +40,11 @@ export default meta;
39
40
  export const Default: StoryObj<TextInputProps> = {
40
41
  render: (args) => (
41
42
  <LapisUrlContext.Provider value={LAPIS_URL}>
42
- <TextInput lapisField={args.lapisField} placeholderText={args.placeholderText} />
43
+ <TextInput
44
+ lapisField={args.lapisField}
45
+ placeholderText={args.placeholderText}
46
+ initialValue={args.initialValue}
47
+ />
43
48
  </LapisUrlContext.Provider>
44
49
  ),
45
50
  args: {
@@ -47,3 +52,19 @@ export const Default: StoryObj<TextInputProps> = {
47
52
  placeholderText: 'Enter a host name',
48
53
  },
49
54
  };
55
+
56
+ export const WithInitialValue: StoryObj<TextInputProps> = {
57
+ ...Default,
58
+ args: {
59
+ ...Default.args,
60
+ initialValue: 'Homo sapiens',
61
+ },
62
+ play: async ({ canvasElement }) => {
63
+ const canvas = within(canvasElement);
64
+
65
+ await waitFor(() => {
66
+ const input = canvas.getByPlaceholderText('Enter a host name', { exact: false });
67
+ expect(input).toHaveValue('Homo sapiens');
68
+ });
69
+ },
70
+ };
@@ -11,9 +11,10 @@ import { useQuery } from '../useQuery';
11
11
  export interface TextInputProps {
12
12
  lapisField: string;
13
13
  placeholderText?: string;
14
+ initialValue?: string;
14
15
  }
15
16
 
16
- export const TextInput: FunctionComponent<TextInputProps> = ({ lapisField, placeholderText }) => {
17
+ export const TextInput: FunctionComponent<TextInputProps> = ({ lapisField, placeholderText, initialValue }) => {
17
18
  const lapis = useContext(LapisUrlContext);
18
19
 
19
20
  const inputRef = useRef<HTMLInputElement>(null);
@@ -62,6 +63,7 @@ export const TextInput: FunctionComponent<TextInputProps> = ({ lapisField, place
62
63
  onInput={onInput}
63
64
  ref={inputRef}
64
65
  list={lapisField}
66
+ value={initialValue}
65
67
  />
66
68
  <datalist id={lapisField}>
67
69
  {data.map((item) => (
@@ -0,0 +1,31 @@
1
+ import { describe, it } from 'vitest';
2
+
3
+ import { type Equals, type Expect } from './typeAssertions';
4
+
5
+ /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */
6
+ describe('Expect', () => {
7
+ it('should only accept true', () => {
8
+ type ShouldBeTrue = Expect<true>;
9
+ // @ts-expect-error Expect should only accept true
10
+ type ShouldFail = Expect<false>;
11
+ });
12
+ });
13
+
14
+ describe('Equals', () => {
15
+ it('should accept equal types', () => {
16
+ type Equals1 = Expect<Equals<1, 1>>;
17
+ type Equals1Or2 = Expect<Equals<1 | 2, 1 | 2>>;
18
+ type EqualsWithObject = Expect<Equals<{ key: string }, { key: string }>>;
19
+ });
20
+
21
+ it('should reject unequal types', () => {
22
+ // @ts-expect-error 1 is not 2
23
+ type Equals1Vs2 = Expect<Equals<1, 2>>;
24
+ // @ts-expect-error disjoint union types
25
+ type EqualsDisjointUnion = Expect<Equals<1 | 2, 1 | 999>>;
26
+ // @ts-expect-error object with different keys
27
+ type EqualsObjectWithDifferentKey = Expect<Equals<{ key: string }, { otherKey: string }>>;
28
+ // @ts-expect-error object with different value types
29
+ type EqualsObjectWithDifferentValue = Expect<Equals<{ key: string }, { key: number }>>;
30
+ });
31
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Those are helpers to make sure that types are equal.
3
+ *
4
+ * #### Why do we need this?
5
+ *
6
+ * The custom element manifest does not fully resolve the types of properties,
7
+ * so Storybook will only show non-resolved types (such as `View[]`).
8
+ * To give users full type information, we have to inline the types in the web component definitions.
9
+ * These assertions help us to make sure that the inlined type is equal to the type
10
+ * that is defined in the Preact components.
11
+ */
12
+
13
+ export type Expect<T extends true> = T;
14
+ export type IsAssignable<X, Y> = X extends Y ? true : false;
15
+ export type And<X, Y> = X extends true ? (Y extends true ? true : false) : false;
16
+ export type Equals<X, Y> = And<IsAssignable<X, Y>, IsAssignable<Y, X>>;
@@ -46,7 +46,6 @@ export abstract class PreactLitAdapter extends ReactiveElement {
46
46
  };
47
47
 
48
48
  override update(changedProperties: PropertyValues) {
49
- console.log('this.lapis', this.lapis);
50
49
  const vdom = (
51
50
  <LapisUrlContext.Provider value={this.lapis}>
52
51
  <ReferenceGenomeContext.Provider value={this.referenceGenome}>