@genspectrum/dashboard-components 0.1.2 → 0.1.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 (81) hide show
  1. package/README.md +1 -1
  2. package/custom-elements.json +453 -67
  3. package/dist/dashboard-components.js +778 -488
  4. package/dist/dashboard-components.js.map +1 -1
  5. package/dist/genspectrum-components.d.ts +277 -50
  6. package/dist/style.css +132 -139
  7. package/package.json +9 -5
  8. package/src/lapisApi/lapisApi.ts +1 -1
  9. package/src/preact/aggregatedData/__mockData__/aggregated.json +585 -0
  10. package/src/preact/aggregatedData/aggregate-table.tsx +32 -0
  11. package/src/preact/aggregatedData/aggregate.stories.tsx +53 -0
  12. package/src/preact/aggregatedData/aggregate.tsx +102 -0
  13. package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
  14. package/src/preact/components/csv-download-button.tsx +8 -2
  15. package/src/preact/components/headline.tsx +16 -4
  16. package/src/preact/components/min-max-range-slider.tsx +4 -4
  17. package/src/preact/components/percent-intput.tsx +2 -3
  18. package/src/preact/components/resize-container.tsx +23 -0
  19. package/src/preact/components/table.tsx +1 -0
  20. package/src/preact/components/tabs.stories.tsx +2 -2
  21. package/src/preact/components/tabs.tsx +47 -24
  22. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
  23. package/src/preact/dateRangeSelector/date-range-selector.tsx +57 -43
  24. package/src/preact/locationFilter/location-filter.tsx +2 -2
  25. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
  26. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
  27. package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
  28. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
  29. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +8 -1
  30. package/src/preact/mutationComparison/mutation-comparison.tsx +13 -4
  31. package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
  32. package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
  33. package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
  34. package/src/preact/mutations/getInsertionsTableData.ts +1 -1
  35. package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
  36. package/src/preact/mutations/getMutationsTableData.ts +1 -1
  37. package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
  38. package/src/preact/mutations/mutations-table.tsx +3 -1
  39. package/src/preact/mutations/mutations.stories.tsx +8 -1
  40. package/src/preact/mutations/mutations.tsx +16 -5
  41. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
  42. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
  43. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
  44. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -0
  45. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +17 -9
  46. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
  47. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +12 -0
  48. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +13 -8
  49. package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
  50. package/src/preact/shared/sort/sortInsertions.ts +10 -17
  51. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
  52. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
  53. package/src/preact/textInput/text-input.stories.tsx +22 -1
  54. package/src/preact/textInput/text-input.tsx +3 -1
  55. package/src/query/queryAggregateData.spec.ts +32 -0
  56. package/src/query/queryAggregateData.ts +25 -0
  57. package/src/utils/typeAssertions.spec.ts +31 -0
  58. package/src/utils/typeAssertions.ts +16 -0
  59. package/src/web-components/PreactLitAdapter.tsx +0 -1
  60. package/src/web-components/app.stories.ts +129 -0
  61. package/src/web-components/app.ts +27 -6
  62. package/src/web-components/display/aggregate-component.stories.ts +73 -0
  63. package/src/web-components/display/aggregate-component.tsx +58 -0
  64. package/src/web-components/display/index.ts +1 -0
  65. package/src/web-components/display/mutation-comparison-component.stories.ts +29 -11
  66. package/src/web-components/display/mutation-comparison-component.tsx +72 -4
  67. package/src/web-components/display/mutations-component.stories.ts +14 -13
  68. package/src/web-components/display/mutations-component.tsx +14 -1
  69. package/src/web-components/display/prevalence-over-time-component.stories.ts +20 -18
  70. package/src/web-components/display/prevalence-over-time-component.tsx +12 -0
  71. package/src/web-components/display/relative-growth-advantage-component.stories.ts +11 -10
  72. package/src/web-components/display/relative-growth-advantage-component.tsx +12 -0
  73. package/src/web-components/input/date-range-selector-component.stories.ts +35 -8
  74. package/src/web-components/input/date-range-selector-component.tsx +18 -5
  75. package/src/web-components/input/location-filter-component.stories.ts +17 -8
  76. package/src/web-components/input/location-filter-component.tsx +2 -6
  77. package/src/web-components/input/mutation-filter-component.stories.ts +20 -9
  78. package/src/web-components/input/mutation-filter-component.tsx +10 -2
  79. package/src/web-components/input/text-input-component.stories.ts +13 -4
  80. package/src/web-components/input/text-input-component.tsx +11 -2
  81. package/src/web-components/input/location-filter.mdx +0 -25
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'preact';
2
2
 
3
3
  import { getMutationsTableData } from './getMutationsTableData';
4
4
  import { type SubstitutionOrDeletionEntry } from '../../types';
5
+ import { type Deletion, type Substitution } from '../../utils/mutations';
5
6
  import type { ProportionInterval } from '../components/proportion-selector';
6
7
  import { Table } from '../components/table';
7
8
  import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
@@ -18,10 +19,11 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({ data, proporti
18
19
  {
19
20
  name: 'Mutation',
20
21
  sort: {
21
- compare: (a: string, b: string) => {
22
+ compare: (a: Substitution | Deletion, b: Substitution | Deletion) => {
22
23
  return sortSubstitutionsAndDeletions(a, b);
23
24
  },
24
25
  },
26
+ formatter: (cell: Substitution | Deletion) => cell.toString(),
25
27
  },
26
28
  {
27
29
  name: 'Type',
@@ -22,6 +22,7 @@ const meta: Meta<MutationsProps> = {
22
22
  options: ['table', 'grid', 'insertions'],
23
23
  control: { type: 'check' },
24
24
  },
25
+ size: [{ control: 'object' }],
25
26
  },
26
27
  };
27
28
 
@@ -31,7 +32,12 @@ const Template = {
31
32
  render: (args: MutationsProps) => (
32
33
  <LapisUrlContext.Provider value={LAPIS_URL}>
33
34
  <ReferenceGenomeContext.Provider value={referenceGenome}>
34
- <Mutations variant={args.variant} sequenceType={args.sequenceType} views={args.views} />
35
+ <Mutations
36
+ variant={args.variant}
37
+ sequenceType={args.sequenceType}
38
+ views={args.views}
39
+ size={args.size}
40
+ />
35
41
  </ReferenceGenomeContext.Provider>
36
42
  </LapisUrlContext.Provider>
37
43
  ),
@@ -43,6 +49,7 @@ export const Default: StoryObj<MutationsProps> = {
43
49
  variant: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: '2022-01-01' },
44
50
  sequenceType: 'nucleotide',
45
51
  views: ['grid', 'table', 'insertions'],
52
+ size: { width: '100%', height: '700px' },
46
53
  },
47
54
  parameters: {
48
55
  fetchMock: {
@@ -24,6 +24,7 @@ import { type DisplayedMutationType, MutationTypeSelector } from '../components/
24
24
  import { NoDataDisplay } from '../components/no-data-display';
25
25
  import type { ProportionInterval } from '../components/proportion-selector';
26
26
  import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
27
+ import { ResizeContainer, type Size } from '../components/resize-container';
27
28
  import Tabs from '../components/tabs';
28
29
  import { useQuery } from '../useQuery';
29
30
 
@@ -33,9 +34,10 @@ export interface MutationsProps {
33
34
  variant: LapisFilter;
34
35
  sequenceType: SequenceType;
35
36
  views: View[];
37
+ size?: Size;
36
38
  }
37
39
 
38
- export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequenceType, views }) => {
40
+ export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequenceType, views, size }) => {
39
41
  const lapis = useContext(LapisUrlContext);
40
42
  const { data, error, isLoading } = useQuery(async () => {
41
43
  return queryMutationsData(variant, sequenceType, lapis);
@@ -67,9 +69,11 @@ export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequence
67
69
  }
68
70
 
69
71
  return (
70
- <Headline heading={headline}>
71
- <MutationsTabs mutationsData={data} sequenceType={sequenceType} views={views} />
72
- </Headline>
72
+ <ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
73
+ <Headline heading={headline}>
74
+ <MutationsTabs mutationsData={data} sequenceType={sequenceType} views={views} />
75
+ </Headline>
76
+ </ResizeContainer>
73
77
  );
74
78
  };
75
79
 
@@ -182,7 +186,14 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
182
186
  {activeTab === 'Insertions' && (
183
187
  <CsvDownloadButton
184
188
  className='mx-1 btn btn-xs'
185
- getData={() => getInsertionsTableData(filteredData.insertions)}
189
+ getData={() =>
190
+ getInsertionsTableData(filteredData.insertions).map((row) => {
191
+ return {
192
+ insertion: row.insertion.toString(),
193
+ count: row.count,
194
+ };
195
+ })
196
+ }
186
197
  filename='insertions.csv'
187
198
  />
188
199
  )}
@@ -28,6 +28,7 @@ const PrevalenceOverTimeBarChart = ({
28
28
  datasets: data.map((graphData, index) => datasets(graphData, index, confidenceIntervalMethod)),
29
29
  },
30
30
  options: {
31
+ maintainAspectRatio: false,
31
32
  animation: false,
32
33
  scales: {
33
34
  y: getYAxisScale(yAxisScaleType),
@@ -43,6 +43,7 @@ const PrevalenceOverTimeBubbleChart = ({ data, yAxisScaleType }: PrevalenceOverT
43
43
  },
44
44
  options: {
45
45
  animation: false,
46
+ maintainAspectRatio: false,
46
47
  scales: {
47
48
  x: {
48
49
  ticks: {
@@ -36,6 +36,7 @@ const PrevalenceOverTimeLineChart = ({
36
36
  },
37
37
  options: {
38
38
  animation: false,
39
+ maintainAspectRatio: false,
39
40
  scales: {
40
41
  y: getYAxisScale(yAxisScaleType),
41
42
  },
@@ -29,6 +29,7 @@ export default {
29
29
  options: ['wilson'],
30
30
  control: { type: 'check' },
31
31
  },
32
+ size: [{ control: 'object' }],
32
33
  },
33
34
  };
34
35
 
@@ -42,6 +43,7 @@ const Template = {
42
43
  smoothingWindow={args.smoothingWindow}
43
44
  views={args.views}
44
45
  confidenceIntervalMethods={args.confidenceIntervalMethods}
46
+ size={args.size}
45
47
  />
46
48
  </LapisUrlContext.Provider>
47
49
  ),
@@ -59,6 +61,7 @@ export const TwoVariants = {
59
61
  smoothingWindow: 0,
60
62
  views: ['bar', 'line', 'bubble', 'table'],
61
63
  confidenceIntervalMethods: ['wilson'],
64
+ size: { width: '100%', height: '700px' },
62
65
  },
63
66
  parameters: {
64
67
  fetchMock: {
@@ -124,6 +127,7 @@ export const OneVariant = {
124
127
  smoothingWindow: 7,
125
128
  views: ['bar', 'line', 'bubble', 'table'],
126
129
  confidenceIntervalMethods: ['wilson'],
130
+ size: { width: '100%', height: '700px' },
127
131
  },
128
132
  parameters: {
129
133
  fetchMock: {
@@ -16,6 +16,7 @@ import Headline from '../components/headline';
16
16
  import Info 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,7 @@ export interface PrevalenceOverTimeProps {
31
32
  smoothingWindow: number;
32
33
  views: View[];
33
34
  confidenceIntervalMethods: ConfidenceIntervalMethod[];
35
+ size?: Size;
34
36
  }
35
37
 
36
38
  export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
@@ -40,6 +42,7 @@ export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
40
42
  smoothingWindow,
41
43
  views,
42
44
  confidenceIntervalMethods,
45
+ size,
43
46
  }) => {
44
47
  const lapis = useContext(LapisUrlContext);
45
48
 
@@ -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
 
@@ -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,16 @@ 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
+ },
13
23
  };
14
24
 
15
25
  export const Primary = {
@@ -20,6 +30,7 @@ export const Primary = {
20
30
  denominator={args.denominator}
21
31
  generationTime={args.generationTime}
22
32
  views={args.views}
33
+ size={args.size}
23
34
  />
24
35
  </LapisUrlContext.Provider>
25
36
  ),
@@ -28,6 +39,7 @@ export const Primary = {
28
39
  denominator: { country: 'Switzerland', dateFrom: '2020-12-01', dateTo: '2021-03-01' },
29
40
  generationTime: 7,
30
41
  views: ['line'],
42
+ size: { width: '100%', height: '700px' },
31
43
  },
32
44
  parameters: {
33
45
  fetchMock: {
@@ -13,6 +13,7 @@ import Headline from '../components/headline';
13
13
  import Info 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,7 @@ export interface RelativeGrowthAdvantageProps {
25
26
  denominator: LapisFilter;
26
27
  generationTime: number;
27
28
  views: View[];
29
+ size?: Size;
28
30
  }
29
31
 
30
32
  export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageProps> = ({
@@ -32,6 +34,7 @@ export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageP
32
34
  denominator,
33
35
  generationTime,
34
36
  views,
37
+ size,
35
38
  }) => {
36
39
  const lapis = useContext(LapisUrlContext);
37
40
  const [yAxisScaleType, setYAxisScaleType] = useState<ScaleType>('linear');
@@ -67,14 +70,16 @@ export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageP
67
70
  }
68
71
 
69
72
  return (
70
- <Headline heading={headline}>
71
- <RelativeGrowthAdvantageTabs
72
- data={data}
73
- yAxisScaleType={yAxisScaleType}
74
- setYAxisScaleType={setYAxisScaleType}
75
- views={views}
76
- />
77
- </Headline>
73
+ <ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
74
+ <Headline heading={headline}>
75
+ <RelativeGrowthAdvantageTabs
76
+ data={data}
77
+ yAxisScaleType={yAxisScaleType}
78
+ setYAxisScaleType={setYAxisScaleType}
79
+ views={views}
80
+ />
81
+ </Headline>
82
+ </ResizeContainer>
78
83
  );
79
84
  };
80
85
 
@@ -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,32 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { queryAggregateData } from './queryAggregateData';
4
+ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
5
+
6
+ describe('queryAggregateData', () => {
7
+ test('should fetch aggregate data', async () => {
8
+ const fields = ['division', 'host'];
9
+ const filter = { country: 'USA' };
10
+
11
+ lapisRequestMocks.aggregated(
12
+ { fields, ...filter },
13
+ {
14
+ data: [
15
+ { count: 4, region: 'region1', host: 'host1' },
16
+ { count: 4, region: 'region1', host: 'host2' },
17
+ { count: 8, region: 'region2', host: 'host1' },
18
+ { count: 16, region: 'region2', host: 'host2' },
19
+ ],
20
+ },
21
+ );
22
+
23
+ const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL);
24
+
25
+ expect(result).to.deep.equal([
26
+ { proportion: 0.125, count: 4, region: 'region1', host: 'host1' },
27
+ { proportion: 0.125, count: 4, region: 'region1', host: 'host2' },
28
+ { proportion: 0.25, count: 8, region: 'region2', host: 'host1' },
29
+ { proportion: 0.5, count: 16, region: 'region2', host: 'host2' },
30
+ ]);
31
+ });
32
+ });