@genspectrum/dashboard-components 0.1.3 → 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 (75) hide show
  1. package/custom-elements.json +311 -75
  2. package/dist/dashboard-components.js +622 -434
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +229 -42
  5. package/dist/style.css +132 -139
  6. package/package.json +9 -5
  7. package/src/preact/aggregatedData/aggregate.stories.tsx +5 -5
  8. package/src/preact/aggregatedData/aggregate.tsx +8 -4
  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.tsx +16 -4
  12. package/src/preact/components/min-max-range-slider.tsx +4 -4
  13. package/src/preact/components/percent-intput.tsx +2 -3
  14. package/src/preact/components/resize-container.tsx +23 -0
  15. package/src/preact/components/table.tsx +1 -0
  16. package/src/preact/components/tabs.stories.tsx +2 -2
  17. package/src/preact/components/tabs.tsx +47 -24
  18. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
  19. package/src/preact/dateRangeSelector/date-range-selector.tsx +57 -43
  20. package/src/preact/locationFilter/location-filter.tsx +2 -2
  21. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
  22. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
  23. package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
  24. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
  25. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +8 -1
  26. package/src/preact/mutationComparison/mutation-comparison.tsx +13 -4
  27. package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
  28. package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
  29. package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
  30. package/src/preact/mutations/getInsertionsTableData.ts +1 -1
  31. package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
  32. package/src/preact/mutations/getMutationsTableData.ts +1 -1
  33. package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
  34. package/src/preact/mutations/mutations-table.tsx +3 -1
  35. package/src/preact/mutations/mutations.stories.tsx +8 -1
  36. package/src/preact/mutations/mutations.tsx +16 -5
  37. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
  38. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
  39. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
  40. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -0
  41. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +17 -9
  42. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
  43. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +12 -0
  44. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +13 -8
  45. package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
  46. package/src/preact/shared/sort/sortInsertions.ts +10 -17
  47. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
  48. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
  49. package/src/preact/textInput/text-input.stories.tsx +22 -1
  50. package/src/preact/textInput/text-input.tsx +3 -1
  51. package/src/utils/typeAssertions.spec.ts +31 -0
  52. package/src/utils/typeAssertions.ts +16 -0
  53. package/src/web-components/PreactLitAdapter.tsx +0 -1
  54. package/src/web-components/app.stories.ts +129 -0
  55. package/src/web-components/app.ts +27 -6
  56. package/src/web-components/display/aggregate-component.stories.ts +21 -11
  57. package/src/web-components/display/aggregate-component.tsx +12 -5
  58. package/src/web-components/display/mutation-comparison-component.stories.ts +29 -11
  59. package/src/web-components/display/mutation-comparison-component.tsx +72 -4
  60. package/src/web-components/display/mutations-component.stories.ts +14 -13
  61. package/src/web-components/display/mutations-component.tsx +14 -1
  62. package/src/web-components/display/prevalence-over-time-component.stories.ts +20 -18
  63. package/src/web-components/display/prevalence-over-time-component.tsx +12 -0
  64. package/src/web-components/display/relative-growth-advantage-component.stories.ts +11 -10
  65. package/src/web-components/display/relative-growth-advantage-component.tsx +12 -0
  66. package/src/web-components/input/date-range-selector-component.stories.ts +35 -8
  67. package/src/web-components/input/date-range-selector-component.tsx +18 -5
  68. package/src/web-components/input/location-filter-component.stories.ts +15 -4
  69. package/src/web-components/input/location-filter-component.tsx +2 -6
  70. package/src/web-components/input/mutation-filter-component.stories.ts +20 -9
  71. package/src/web-components/input/mutation-filter-component.tsx +10 -2
  72. package/src/web-components/input/text-input-component.stories.ts +13 -4
  73. package/src/web-components/input/text-input-component.tsx +11 -2
  74. package/src/web-components/display/aggregate-component.mdx +0 -25
  75. package/src/web-components/input/location-filter.mdx +0 -25
@@ -0,0 +1,23 @@
1
+ import { type FunctionComponent } from 'preact';
2
+
3
+ export type Size = {
4
+ width?: string;
5
+ height?: string;
6
+ };
7
+
8
+ export interface ResizeContainerProps {
9
+ size?: Size;
10
+ defaultSize: Size;
11
+ }
12
+
13
+ export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size, defaultSize }) => {
14
+ return <div style={extendByDefault(size, defaultSize)}>{children}</div>;
15
+ };
16
+
17
+ const extendByDefault = (size: Size | undefined, defaultSize: Size) => {
18
+ if (size === undefined) {
19
+ return defaultSize;
20
+ }
21
+
22
+ return { ...defaultSize, ...size };
23
+ };
@@ -3,6 +3,7 @@ import { type OneDArray, type TColumn, type TData } from 'gridjs/dist/src/types'
3
3
  import { type PaginationConfig } from 'gridjs/dist/src/view/plugin/pagination';
4
4
  import { type ComponentChild } from 'preact';
5
5
  import { useEffect, useRef } from 'preact/hooks';
6
+
6
7
  import 'gridjs/dist/theme/mermaid.css';
7
8
 
8
9
  export const tableStyle = {
@@ -51,10 +51,10 @@ export const TabsWithToolbarOnlyShowingOnSecondTab: StoryObj = {
51
51
  play: async ({ canvasElement }) => {
52
52
  const canvas = within(canvasElement);
53
53
 
54
- await waitFor(() => expect(canvas.getByLabelText('FirstTab', { selector: 'input' })).toBeVisible());
54
+ await waitFor(() => expect(canvas.getByRole('button', { name: 'SecondTab' })).toBeVisible());
55
55
  await expect(canvas.queryByText('Toolbar')).not.toBeInTheDocument();
56
56
 
57
- await fireEvent.click(canvas.getByLabelText('SecondTab', { selector: 'input' }));
57
+ await fireEvent.click(canvas.getByRole('button', { name: 'SecondTab' }));
58
58
  await waitFor(() => expect(canvas.getByText('Toolbar')).toBeVisible());
59
59
  },
60
60
  };
@@ -1,5 +1,5 @@
1
1
  import { Fragment, type FunctionComponent } from 'preact';
2
- import { useState } from 'preact/hooks';
2
+ import { useEffect, useRef, useState } from 'preact/hooks';
3
3
  import { type JSXInternal } from 'preact/src/jsx';
4
4
 
5
5
  type Tab = {
@@ -14,34 +14,57 @@ interface ComponentTabsProps {
14
14
 
15
15
  const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
16
16
  const [activeTab, setActiveTab] = useState(tabs[0].title);
17
+ const [heightOfTabs, setHeightOfTabs] = useState('3rem');
18
+ const tabRef = useRef<HTMLDivElement>(null);
17
19
 
18
- const tabNames = tabs.map((tab) => tab.title).join(', ');
19
-
20
- const tabElements = tabs.map((tab) => {
21
- return (
22
- <Fragment key={tab.title}>
23
- <input
24
- type='radio'
25
- name={tabNames}
26
- role='tab'
27
- className='tab'
28
- aria-label={tab.title}
29
- checked={activeTab === tab.title}
30
- onChange={() => setActiveTab(tab.title)}
31
- />
32
- <div role='tabpanel' className='tab-content bg-base-100 border-base-300 rounded-box p-1'>
33
- {tab.content}
34
- </div>
35
- </Fragment>
36
- );
37
- });
20
+ useEffect(() => {
21
+ if (tabRef.current) {
22
+ const heightOfTabs = tabRef.current.getBoundingClientRect().height;
23
+ setHeightOfTabs(`${heightOfTabs}px`);
24
+ }
25
+ }, []);
26
+
27
+ const tabElements = (
28
+ <div className='flex flex-row'>
29
+ {tabs.map((tab) => {
30
+ return (
31
+ <Fragment key={tab.title}>
32
+ <button
33
+ className={`px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 ${
34
+ activeTab === tab.title
35
+ ? 'border-b-2 border-gray-400'
36
+ : 'text-gray-600 hover:bg-gray-100 hover:text-gray-700'
37
+ }`}
38
+ onClick={() => {
39
+ setActiveTab(tab.title);
40
+ }}
41
+ >
42
+ {tab.title}
43
+ </button>
44
+ </Fragment>
45
+ );
46
+ })}
47
+ </div>
48
+ );
38
49
 
39
50
  const toolbarElement = typeof toolbar === 'function' ? toolbar(activeTab) : toolbar;
40
51
 
41
52
  return (
42
- <div role='tablist' className='tabs tabs-lifted'>
43
- {tabElements}
44
- {toolbar && <div className='m-1 col-[9999]'>{toolbarElement}</div>}
53
+ <div className='h-full w-full'>
54
+ <div ref={tabRef} className='flex flex-row justify-between'>
55
+ {tabElements}
56
+ {toolbar && <div className='py-2'>{toolbarElement}</div>}
57
+ </div>
58
+ <div
59
+ className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0].title ? '' : 'rounded-tl-md'}`}
60
+ style={{ height: `calc(100% - ${heightOfTabs})` }}
61
+ >
62
+ {tabs.map((tab) => (
63
+ <div className='h-full overflow-auto' key={tab.title} hidden={activeTab !== tab.title}>
64
+ {tab.content}
65
+ </div>
66
+ ))}
67
+ </div>
45
68
  </div>
46
69
  );
47
70
  };
@@ -1,11 +1,21 @@
1
1
  import { withActions } from '@storybook/addon-actions/decorator';
2
2
  import { type Meta, type StoryObj } from '@storybook/preact';
3
3
 
4
- import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
4
+ import {
5
+ DateRangeSelector,
6
+ type DateRangeSelectorProps,
7
+ PRESET_VALUE_ALL_TIMES,
8
+ PRESET_VALUE_CUSTOM,
9
+ PRESET_VALUE_LAST_2_MONTHS,
10
+ PRESET_VALUE_LAST_2_WEEKS,
11
+ PRESET_VALUE_LAST_3_MONTHS,
12
+ PRESET_VALUE_LAST_6_MONTHS,
13
+ PRESET_VALUE_LAST_MONTH,
14
+ } from './date-range-selector';
5
15
  import { LAPIS_URL } from '../../constants';
6
16
  import { LapisUrlContext } from '../LapisUrlContext';
7
17
 
8
- const meta: Meta<DateRangeSelectorProps> = {
18
+ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
9
19
  title: 'Input/DateRangeSelector',
10
20
  component: DateRangeSelector,
11
21
  parameters: {
@@ -14,19 +24,41 @@ const meta: Meta<DateRangeSelectorProps> = {
14
24
  },
15
25
  fetchMock: {},
16
26
  },
27
+ argTypes: {
28
+ initialValue: {
29
+ control: {
30
+ type: 'select',
31
+ },
32
+ options: [
33
+ PRESET_VALUE_CUSTOM,
34
+ PRESET_VALUE_ALL_TIMES,
35
+ PRESET_VALUE_LAST_2_WEEKS,
36
+ PRESET_VALUE_LAST_MONTH,
37
+ PRESET_VALUE_LAST_2_MONTHS,
38
+ PRESET_VALUE_LAST_3_MONTHS,
39
+ PRESET_VALUE_LAST_6_MONTHS,
40
+ 'CustomDateRange',
41
+ ],
42
+ },
43
+ },
17
44
  args: {
18
45
  customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
19
46
  earliestDate: '1970-01-01',
47
+ initialValue: PRESET_VALUE_LAST_3_MONTHS,
20
48
  },
21
49
  decorators: [withActions],
22
50
  };
23
51
 
24
52
  export default meta;
25
53
 
26
- export const Primary: StoryObj<DateRangeSelectorProps> = {
54
+ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
27
55
  render: (args) => (
28
56
  <LapisUrlContext.Provider value={LAPIS_URL}>
29
- <DateRangeSelector customSelectOptions={args.customSelectOptions} earliestDate={args.earliestDate} />
57
+ <DateRangeSelector
58
+ customSelectOptions={args.customSelectOptions}
59
+ earliestDate={args.earliestDate}
60
+ initialValue={args.initialValue}
61
+ />
30
62
  </LapisUrlContext.Provider>
31
63
  ),
32
64
  };
@@ -1,5 +1,4 @@
1
1
  import flatpickr from 'flatpickr';
2
- import { type FunctionComponent } from 'preact';
3
2
  import 'flatpickr/dist/flatpickr.min.css';
4
3
  import { useEffect, useRef, useState } from 'preact/hooks';
5
4
 
@@ -7,24 +6,53 @@ import { toYYYYMMDD } from './dateConversion';
7
6
  import { Select } from '../components/select';
8
7
  import type { ScaleType } from '../shared/charts/getYAxisScale';
9
8
 
10
- export type CustomSelectOption = { label: string; dateFrom: string; dateTo: string };
9
+ export type CustomSelectOption<CustomLabel extends string> = { label: CustomLabel; dateFrom: string; dateTo: string };
11
10
 
12
- export interface DateRangeSelectorProps {
13
- customSelectOptions: CustomSelectOption[];
11
+ export interface DateRangeSelectorProps<CustomLabel extends string> {
12
+ customSelectOptions: CustomSelectOption<CustomLabel>[];
14
13
  earliestDate?: string;
14
+ initialValue?: PresetOptionValues | CustomLabel;
15
15
  }
16
16
 
17
- export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
17
+ export const PRESET_VALUE_CUSTOM = 'custom';
18
+ export const PRESET_VALUE_ALL_TIMES = 'allTimes';
19
+ export const PRESET_VALUE_LAST_2_WEEKS = 'last2Weeks';
20
+ export const PRESET_VALUE_LAST_MONTH = 'lastMonth';
21
+ export const PRESET_VALUE_LAST_2_MONTHS = 'last2Months';
22
+ export const PRESET_VALUE_LAST_3_MONTHS = 'last3Months';
23
+ export const PRESET_VALUE_LAST_6_MONTHS = 'last6Months';
24
+
25
+ export const presets = {
26
+ [PRESET_VALUE_CUSTOM]: { label: 'Custom' },
27
+ [PRESET_VALUE_ALL_TIMES]: { label: 'All times' },
28
+ [PRESET_VALUE_LAST_2_WEEKS]: { label: 'Last 2 weeks' },
29
+ [PRESET_VALUE_LAST_MONTH]: { label: 'Last month' },
30
+ [PRESET_VALUE_LAST_2_MONTHS]: { label: 'Last 2 months' },
31
+ [PRESET_VALUE_LAST_3_MONTHS]: { label: 'Last 3 months' },
32
+ [PRESET_VALUE_LAST_6_MONTHS]: { label: 'Last 6 months' },
33
+ };
34
+
35
+ export type PresetOptionValues = keyof typeof presets;
36
+
37
+ export const DateRangeSelector = <CustomLabel extends string>({
18
38
  customSelectOptions,
19
39
  earliestDate = '1900-01-01',
20
- }) => {
40
+ initialValue,
41
+ }: DateRangeSelectorProps<CustomLabel>) => {
21
42
  const datePickerRef = useRef<HTMLInputElement>(null);
22
43
  const endDatePickerRef = useRef<HTMLInputElement>(null);
23
44
  const divRef = useRef<HTMLDivElement>(null);
24
45
  const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
25
46
  const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
26
47
 
27
- const [selectedDateRange, setSelectedDateRange] = useState('last6Months');
48
+ const selectableOptions = getSelectableOptions(customSelectOptions);
49
+
50
+ const [selectedDateRange, setSelectedDateRange] = useState<CustomLabel | PresetOptionValues>(
51
+ initialValue !== undefined && selectableOptions.some((option) => option.value === initialValue)
52
+ ? initialValue
53
+ : PRESET_VALUE_LAST_6_MONTHS,
54
+ );
55
+
28
56
  const [selectedDates, setSelectedDates] = useState<{ dateFrom: Date; dateTo: Date }>({
29
57
  dateFrom: getDatesForSelectorValue('last6Months', customSelectOptions, earliestDate).dateFrom,
30
58
  dateTo: getDatesForSelectorValue('last6Months', customSelectOptions, earliestDate).dateTo,
@@ -61,27 +89,7 @@ export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
61
89
  // eslint-disable-next-line react-hooks/exhaustive-deps
62
90
  }, [datePickerRef, endDatePickerRef]);
63
91
 
64
- const selectableOptions = () => {
65
- const presetOptions = [
66
- { label: 'Custom', value: 'custom' },
67
- { label: 'All times', value: 'allTimes' },
68
- { label: 'Last 2 weeks', value: 'last2Weeks' },
69
- { label: 'Last month', value: 'lastMonth' },
70
- { label: 'Last 2 weeks', value: 'last2Weeks' },
71
- { label: 'Last month', value: 'lastMonth' },
72
- { label: 'Last 2 months', value: 'last2Months' },
73
- { label: 'Last 3 months', value: 'last3Months' },
74
- { label: 'Last 6 months', value: 'last6Months' },
75
- ];
76
-
77
- const customOptions = customSelectOptions.map((customSelectOption) => {
78
- return { label: customSelectOption.label, value: customLabelToOptionValue(customSelectOption.label) };
79
- });
80
-
81
- return [...presetOptions, ...customOptions];
82
- };
83
-
84
- const onSelectChange = (value: string) => {
92
+ const onSelectChange = (value: CustomLabel | PresetOptionValues) => {
85
93
  setSelectedDateRange(value);
86
94
 
87
95
  const dateRange = getDatesForSelectorValue(value, customSelectOptions, earliestDate);
@@ -145,14 +153,14 @@ export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
145
153
  return (
146
154
  <div class='join' ref={divRef}>
147
155
  <Select
148
- items={selectableOptions()}
156
+ items={selectableOptions}
149
157
  selected={selectedDateRange}
150
158
  selectStyle='select-bordered rounded-none join-item'
151
159
  onChange={(event: Event) => {
152
160
  event.preventDefault();
153
161
  const select = event.target as HTMLSelectElement;
154
162
  const value = select.value as ScaleType;
155
- onSelectChange(value);
163
+ onSelectChange(value as CustomLabel | PresetOptionValues);
156
164
  }}
157
165
  />
158
166
  <input
@@ -175,51 +183,57 @@ export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
175
183
  );
176
184
  };
177
185
 
178
- const customLabelToOptionValue = (customLabel: string) => {
179
- return `${customLabel}customLabel`;
186
+ const getSelectableOptions = <Label extends string>(customSelectOptions: CustomSelectOption<Label>[]) => {
187
+ const presetOptions = Object.entries(presets).map(([key, value]) => {
188
+ return { label: value.label, value: key };
189
+ });
190
+
191
+ const customOptions = customSelectOptions.map((customSelectOption) => {
192
+ return { label: customSelectOption.label, value: customSelectOption.label };
193
+ });
194
+
195
+ return [...presetOptions, ...customOptions];
180
196
  };
181
197
 
182
- const getDatesForSelectorValue = (
198
+ const getDatesForSelectorValue = <Label extends string>(
183
199
  selectorValue: string,
184
- customSelectOptions: CustomSelectOption[],
200
+ customSelectOptions: CustomSelectOption<Label>[],
185
201
  earliestDate: string,
186
202
  ) => {
187
203
  const today = new Date();
188
204
 
189
- const customSelectOption = customSelectOptions.find(
190
- (option) => customLabelToOptionValue(option.label) === selectorValue,
191
- );
205
+ const customSelectOption = customSelectOptions.find((option) => option.label === selectorValue);
192
206
  if (customSelectOption) {
193
207
  return { dateFrom: new Date(customSelectOption.dateFrom), dateTo: new Date(customSelectOption.dateTo) };
194
208
  }
195
209
 
196
210
  switch (selectorValue) {
197
- case 'last2Weeks': {
211
+ case PRESET_VALUE_LAST_2_WEEKS: {
198
212
  const twoWeeksAgo = new Date(today);
199
213
  twoWeeksAgo.setDate(today.getDate() - 14);
200
214
  return { dateFrom: twoWeeksAgo, dateTo: today };
201
215
  }
202
- case 'lastMonth': {
216
+ case PRESET_VALUE_LAST_MONTH: {
203
217
  const lastMonth = new Date(today);
204
218
  lastMonth.setMonth(today.getMonth() - 1);
205
219
  return { dateFrom: lastMonth, dateTo: today };
206
220
  }
207
- case 'last2Months': {
221
+ case PRESET_VALUE_LAST_2_MONTHS: {
208
222
  const twoMonthsAgo = new Date(today);
209
223
  twoMonthsAgo.setMonth(today.getMonth() - 2);
210
224
  return { dateFrom: twoMonthsAgo, dateTo: today };
211
225
  }
212
- case 'last3Months': {
226
+ case PRESET_VALUE_LAST_3_MONTHS: {
213
227
  const threeMonthsAgo = new Date(today);
214
228
  threeMonthsAgo.setMonth(today.getMonth() - 3);
215
229
  return { dateFrom: threeMonthsAgo, dateTo: today };
216
230
  }
217
- case 'last6Months': {
231
+ case PRESET_VALUE_LAST_6_MONTHS: {
218
232
  const sixMonthsAgo = new Date(today);
219
233
  sixMonthsAgo.setMonth(today.getMonth() - 6);
220
234
  return { dateFrom: sixMonthsAgo, dateTo: today };
221
235
  }
222
- case 'allTimes': {
236
+ case PRESET_VALUE_ALL_TIMES: {
223
237
  return { dateFrom: new Date(earliestDate), dateTo: today };
224
238
  }
225
239
  default:
@@ -5,11 +5,11 @@ import { LapisUrlContext } from '../LapisUrlContext';
5
5
  import { useQuery } from '../useQuery';
6
6
 
7
7
  export type LocationFilterProps = {
8
- value?: string;
8
+ initialValue?: string;
9
9
  fields: string[];
10
10
  };
11
11
 
12
- export const LocationFilter = ({ value: initialValue, fields }: LocationFilterProps) => {
12
+ export const LocationFilter = ({ initialValue, fields }: LocationFilterProps) => {
13
13
  const lapis = useContext(LapisUrlContext);
14
14
 
15
15
  const [value, setValue] = useState(initialValue ?? '');
@@ -51,12 +51,12 @@ describe('getPrevalenceOverTimeTableData', () => {
51
51
 
52
52
  expect(result).toEqual([
53
53
  {
54
- mutation: 'A123T',
54
+ mutation: new Substitution(undefined, 'A', 'T', 123),
55
55
  'Test 1 prevalence': 0.123,
56
56
  'Test 2 prevalence': 0.345,
57
57
  },
58
58
  {
59
- mutation: 'G234A',
59
+ mutation: new Substitution(undefined, 'G', 'A', 234),
60
60
  'Test 1 prevalence': 0.567,
61
61
  'Test 2 prevalence': 0.789,
62
62
  },
@@ -106,17 +106,17 @@ describe('getPrevalenceOverTimeTableData', () => {
106
106
 
107
107
  expect(result).toEqual([
108
108
  {
109
- mutation: 'A200T',
109
+ mutation: new Substitution(undefined, 'A', 'T', 200),
110
110
  'Test 1 prevalence': inRange,
111
111
  'Test 2 prevalence': belowRange,
112
112
  },
113
113
  {
114
- mutation: 'A300T',
114
+ mutation: new Substitution(undefined, 'A', 'T', 300),
115
115
  'Test 1 prevalence': inRange,
116
116
  'Test 2 prevalence': inRange,
117
117
  },
118
118
  {
119
- mutation: 'A400T',
119
+ mutation: new Substitution(undefined, 'A', 'T', 400),
120
120
  'Test 1 prevalence': inRange,
121
121
  'Test 2 prevalence': aboveRange,
122
122
  },
@@ -1,35 +1,53 @@
1
1
  import { type MutationData } from './queryMutationData';
2
2
  import { type Dataset } from '../../operator/Dataset';
3
+ import { type Deletion, type Substitution } from '../../utils/mutations';
3
4
  import { type ProportionInterval } from '../components/proportion-selector';
4
5
 
5
6
  type Proportions = {
6
7
  [displayName: string]: number;
7
8
  };
8
9
 
10
+ type MutationComparisonRow = {
11
+ mutation: Substitution | Deletion;
12
+ proportions: Proportions;
13
+ };
14
+
9
15
  export function getMutationComparisonTableData(data: Dataset<MutationData>, proportionInterval: ProportionInterval) {
10
- const mutationsToProportions = new Map<string, Proportions>();
16
+ const mutationsToProportions = new Map<string, MutationComparisonRow>();
11
17
 
12
18
  for (const mutationData of data.content) {
13
19
  for (const mutationEntry of mutationData.data) {
14
- const mutation = mutationEntry.mutation.toString();
15
- const proportions = mutationsToProportions.get(mutation) || {};
16
- proportions[mutationData.displayName] = mutationEntry.proportion;
17
- mutationsToProportions.set(mutation, proportions);
20
+ const mutationKey = mutationEntry.mutation.toString();
21
+ const existingRow = mutationsToProportions.get(mutationKey);
22
+
23
+ if (!existingRow) {
24
+ mutationsToProportions.set(
25
+ mutationKey,
26
+ initializeMutationRow(mutationEntry.mutation, mutationData.displayName, mutationEntry.proportion),
27
+ );
28
+ } else {
29
+ existingRow.proportions = updateProportions(
30
+ existingRow.proportions,
31
+ mutationData.displayName,
32
+ mutationEntry.proportion,
33
+ );
34
+ mutationsToProportions.set(mutationKey, existingRow);
35
+ }
18
36
  }
19
37
  }
20
38
 
21
- return [...mutationsToProportions.entries()]
22
- .map(([mutation, proportions]) => {
39
+ return [...mutationsToProportions.values()]
40
+ .map((row) => {
23
41
  return {
24
- mutation,
42
+ mutation: row.mutation,
25
43
  ...data.content
26
44
  .map((mutationData) => {
27
45
  return {
28
- [`${mutationData.displayName} prevalence`]: proportions[mutationData.displayName] || 0,
46
+ [`${mutationData.displayName} prevalence`]: row.proportions[mutationData.displayName] || 0,
29
47
  };
30
48
  })
31
49
  .reduce((acc, val) => ({ ...acc, ...val }), {}),
32
- } as { mutation: string } & Proportions;
50
+ } as { mutation: Substitution | Deletion } & Proportions;
33
51
  })
34
52
  .filter((row) =>
35
53
  Object.values(row).some(
@@ -38,3 +56,20 @@ export function getMutationComparisonTableData(data: Dataset<MutationData>, prop
38
56
  ),
39
57
  );
40
58
  }
59
+
60
+ function initializeMutationRow(
61
+ mutation: Substitution | Deletion,
62
+ displayName: string,
63
+ proportion: number,
64
+ ): MutationComparisonRow {
65
+ return {
66
+ mutation,
67
+ proportions: {
68
+ [displayName]: proportion,
69
+ },
70
+ };
71
+ }
72
+
73
+ function updateProportions(proportions: Proportions, displayName: string, proportion: number): Proportions {
74
+ return { ...proportions, [displayName]: proportion };
75
+ }
@@ -3,6 +3,7 @@ import { type FunctionComponent } from 'preact';
3
3
  import { getMutationComparisonTableData } from './getMutationComparisonTableData';
4
4
  import { type MutationData } from './queryMutationData';
5
5
  import { type Dataset } from '../../operator/Dataset';
6
+ import { type Deletion, type Substitution } from '../../utils/mutations';
6
7
  import { type ProportionInterval } from '../components/proportion-selector';
7
8
  import { Table } from '../components/table';
8
9
  import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
@@ -14,30 +15,27 @@ export interface MutationsTableProps {
14
15
  }
15
16
 
16
17
  export const MutationComparisonTable: FunctionComponent<MutationsTableProps> = ({ data, proportionInterval }) => {
17
- const getHeaders = () => {
18
- return [
19
- {
20
- name: 'Mutation',
21
- sort: {
22
- compare: (a: string, b: string) => {
23
- return sortSubstitutionsAndDeletions(a, b);
24
- },
25
- },
18
+ const headers = [
19
+ {
20
+ name: 'Mutation',
21
+ sort: {
22
+ compare: sortSubstitutionsAndDeletions,
26
23
  },
27
- {
28
- name: 'Prevalence',
29
- columns: data.content.map((mutationData) => {
30
- return {
31
- name: mutationData.displayName,
32
- sort: true,
33
- formatter: (cell: number) => formatProportion(cell),
34
- };
35
- }),
36
- },
37
- ];
38
- };
24
+ formatter: (cell: Substitution | Deletion) => cell.toString(),
25
+ },
26
+ {
27
+ name: 'Prevalence',
28
+ columns: data.content.map((mutationData) => {
29
+ return {
30
+ name: mutationData.displayName,
31
+ sort: true,
32
+ formatter: (cell: number) => formatProportion(cell),
33
+ };
34
+ }),
35
+ },
36
+ ];
39
37
 
40
38
  const tableData = getMutationComparisonTableData(data, proportionInterval).map((row) => Object.values(row));
41
39
 
42
- return <Table data={tableData} columns={getHeaders()} pagination={true} />;
40
+ return <Table data={tableData} columns={headers} pagination={true} />;
43
41
  };
@@ -64,6 +64,7 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
64
64
  type: 'venn',
65
65
  data: sets,
66
66
  options: {
67
+ maintainAspectRatio: false,
67
68
  scales: {
68
69
  x: {
69
70
  ticks: {
@@ -114,9 +115,11 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
114
115
  }
115
116
 
116
117
  return (
117
- <>
118
- <GsChart configuration={config} />
118
+ <div className='h-full flex flex-col'>
119
+ <div className='flex-1'>
120
+ <GsChart configuration={config} />
121
+ </div>
119
122
  <div class='flex flex-wrap break-words m-2' ref={divRef} />
120
- </>
123
+ </div>
121
124
  );
122
125
  };
@@ -27,6 +27,7 @@ const meta: Meta<MutationComparisonProps> = {
27
27
  options: ['table', 'venn'],
28
28
  control: { type: 'check' },
29
29
  },
30
+ size: [{ control: 'object' }],
30
31
  },
31
32
  parameters: {
32
33
  fetchMock: {
@@ -75,7 +76,12 @@ const Template: StoryObj<MutationComparisonProps> = {
75
76
  render: (args) => (
76
77
  <LapisUrlContext.Provider value={LAPIS_URL}>
77
78
  <ReferenceGenomeContext.Provider value={referenceGenome}>
78
- <MutationComparison variants={args.variants} sequenceType={args.sequenceType} views={args.views} />
79
+ <MutationComparison
80
+ variants={args.variants}
81
+ sequenceType={args.sequenceType}
82
+ views={args.views}
83
+ size={args.size}
84
+ />
79
85
  </ReferenceGenomeContext.Provider>
80
86
  </LapisUrlContext.Provider>
81
87
  ),
@@ -101,6 +107,7 @@ export const TwoVariants: StoryObj<MutationComparisonProps> = {
101
107
  ],
102
108
  sequenceType: 'nucleotide',
103
109
  views: ['table', 'venn'],
110
+ size: { width: '100%', height: '700px' },
104
111
  },
105
112
  };
106
113