@genspectrum/dashboard-components 0.6.11 → 0.6.13

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 (35) hide show
  1. package/dist/dashboard-components.js +1079 -873
  2. package/dist/dashboard-components.js.map +1 -1
  3. package/dist/genspectrum-components.d.ts +3 -3
  4. package/dist/style.css +117 -24
  5. package/package.json +2 -2
  6. package/src/preact/components/checkbox-selector.stories.tsx +93 -11
  7. package/src/preact/components/checkbox-selector.tsx +19 -0
  8. package/src/preact/components/color-scale-selector-dropdown.tsx +5 -3
  9. package/src/preact/components/dropdown.tsx +3 -3
  10. package/src/preact/components/loading-display.tsx +8 -1
  11. package/src/preact/components/mutation-type-selector.stories.tsx +115 -0
  12. package/src/preact/components/mutation-type-selector.tsx +33 -8
  13. package/src/preact/components/percent-input.stories.tsx +93 -0
  14. package/src/preact/components/percent-intput.tsx +4 -0
  15. package/src/preact/components/proportion-selector-dropdown.stories.tsx +2 -2
  16. package/src/preact/components/proportion-selector-dropdown.tsx +9 -7
  17. package/src/preact/components/proportion-selector.stories.tsx +4 -4
  18. package/src/preact/components/proportion-selector.tsx +46 -12
  19. package/src/preact/components/segment-selector.stories.tsx +151 -0
  20. package/src/preact/components/{SegmentSelector.tsx → segment-selector.tsx} +29 -20
  21. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +1 -1
  22. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
  23. package/src/preact/mutationComparison/queryMutationData.ts +1 -1
  24. package/src/preact/mutations/mutations-grid.tsx +5 -1
  25. package/src/preact/mutations/mutations.tsx +1 -1
  26. package/src/preact/mutations/queryMutations.ts +1 -1
  27. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +4 -4
  28. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +3 -2
  29. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
  30. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +3 -2
  31. package/src/preact/useQuery.ts +1 -1
  32. package/src/query/queryMutationsOverTime.ts +3 -3
  33. package/src/utils/map2d.spec.ts +83 -22
  34. package/src/utils/map2d.ts +158 -0
  35. package/src/utils/Map2d.ts +0 -75
@@ -0,0 +1,93 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
+ import { type FunctionComponent } from 'preact';
4
+ import { useState } from 'preact/hooks';
5
+
6
+ import { PercentInput, type PercentInputProps } from './percent-intput';
7
+
8
+ const meta: Meta<PercentInputProps> = {
9
+ title: 'Component/Percent input',
10
+ component: PercentInput,
11
+ parameters: { fetchMock: {} },
12
+ };
13
+
14
+ export default meta;
15
+
16
+ const WrapperWithState: FunctionComponent<{
17
+ value: number;
18
+ setValue: (value: number) => void;
19
+ }> = ({ value: initialValue, setValue: setExternalValue }) => {
20
+ const [value, setValue] = useState(initialValue);
21
+
22
+ return (
23
+ <PercentInput
24
+ percentage={value}
25
+ setPercentage={(value: number) => {
26
+ setValue(value);
27
+ setExternalValue(value);
28
+ }}
29
+ />
30
+ );
31
+ };
32
+
33
+ export const PercentInputStory: StoryObj<{
34
+ value: number;
35
+ setValue: (value: number) => void;
36
+ }> = {
37
+ render: (args) => {
38
+ return <WrapperWithState {...args} />;
39
+ },
40
+ args: {
41
+ value: 5,
42
+ setValue: fn(),
43
+ },
44
+ play: async ({ canvasElement, step, args }) => {
45
+ const canvas = within(canvasElement);
46
+
47
+ const input = () => canvas.getByLabelText('%');
48
+
49
+ await step('Expect initial value to be 5%', async () => {
50
+ await expect(input()).toHaveValue(5);
51
+ });
52
+
53
+ await step('Add digits', async () => {
54
+ await userEvent.type(input(), '1');
55
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledWith(51));
56
+ await expect(input()).toHaveValue(51);
57
+ });
58
+
59
+ await step('Remove digits', async () => {
60
+ await userEvent.type(input(), '{backspace}');
61
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledWith(5));
62
+ await expect(input()).toHaveValue(5);
63
+ });
64
+
65
+ await step('Entering a dot should not trigger the external update function', async () => {
66
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledTimes(2));
67
+ await userEvent.type(input(), '.');
68
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledTimes(2));
69
+ });
70
+
71
+ await step('Deleting all digits should not trigger the external update function', async () => {
72
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledTimes(2));
73
+ await userEvent.clear(input());
74
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledTimes(2));
75
+ });
76
+
77
+ await step('Entering a number outside the range should not trigger the external update function', async () => {
78
+ await userEvent.type(input(), '10');
79
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledTimes(4));
80
+ await userEvent.type(input(), '1');
81
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledTimes(4));
82
+ });
83
+
84
+ await step(
85
+ 'Removing digits until a valid number is reached triggers the external update function',
86
+ async () => {
87
+ await userEvent.type(input(), '{backspace}');
88
+ await waitFor(() => expect(args.setValue).toHaveBeenCalledWith(10));
89
+ await expect(input()).toHaveValue(10);
90
+ },
91
+ );
92
+ },
93
+ };
@@ -21,6 +21,10 @@ export const PercentInput: FunctionComponent<PercentInputProps> = ({ percentage,
21
21
  const input = event.target as HTMLInputElement;
22
22
  const value = Number(input.value);
23
23
 
24
+ if (value === internalPercentage || input.value === '') {
25
+ return;
26
+ }
27
+
24
28
  const inRange = percentageInRange(value);
25
29
 
26
30
  if (inRange) {
@@ -1,5 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect, fn, userEvent, within } from '@storybook/test';
2
+ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
3
  import { type FunctionComponent } from 'preact';
4
4
  import { useState } from 'preact/hooks';
5
5
 
@@ -59,7 +59,7 @@ export const ProportionSelectorStory: StoryObj<ProportionSelectorDropdownProps>
59
59
  await userEvent.clear(minInput);
60
60
  await userEvent.type(minInput, '10');
61
61
 
62
- await expect(button).toHaveTextContent('Proportion 10.0% - 100.0%');
62
+ await waitFor(() => expect(button).toHaveTextContent('Proportion 10.0% - 100.0%'));
63
63
  await expect(args.setMinProportion).toHaveBeenCalledWith(0.1);
64
64
  });
65
65
  },
@@ -13,12 +13,14 @@ export const ProportionSelectorDropdown: FunctionComponent<ProportionSelectorDro
13
13
  const label = `${(proportionInterval.min * 100).toFixed(1)}% - ${(proportionInterval.max * 100).toFixed(1)}%`;
14
14
 
15
15
  return (
16
- <Dropdown buttonTitle={`Proportion ${label}`} placement={'bottom-start'}>
17
- <ProportionSelector
18
- proportionInterval={proportionInterval}
19
- setMinProportion={setMinProportion}
20
- setMaxProportion={setMaxProportion}
21
- />
22
- </Dropdown>
16
+ <div className='w-44'>
17
+ <Dropdown buttonTitle={`Proportion ${label}`} placement={'bottom-start'}>
18
+ <ProportionSelector
19
+ proportionInterval={proportionInterval}
20
+ setMinProportion={setMinProportion}
21
+ setMaxProportion={setMaxProportion}
22
+ />
23
+ </Dropdown>
24
+ </div>
23
25
  );
24
26
  };
@@ -54,27 +54,27 @@ export const ProportionSelectorStory: StoryObj<ProportionSelectorProps> = {
54
54
  const minInput = canvas.getAllByLabelText('%')[0];
55
55
  await userEvent.clear(minInput);
56
56
  await userEvent.type(minInput, '10');
57
- await expect(args.setMinProportion).toHaveBeenCalledWith(0.1);
57
+ await waitFor(() => expect(args.setMinProportion).toHaveBeenCalledWith(0.1));
58
58
  });
59
59
 
60
60
  await step('Change max proportion to 50%', async () => {
61
61
  const maxInput = canvas.getAllByLabelText('%')[1];
62
62
  await userEvent.clear(maxInput);
63
63
  await userEvent.type(maxInput, '50');
64
- await expect(args.setMaxProportion).toHaveBeenCalledWith(0.5);
64
+ await waitFor(() => expect(args.setMaxProportion).toHaveBeenCalledWith(0.5));
65
65
  });
66
66
 
67
67
  await step('Move min proportion silder to 20%', async () => {
68
68
  const minSlider = canvas.getAllByRole('slider')[0];
69
69
  await fireEvent.input(minSlider, { target: { value: '20' } });
70
- await expect(args.setMinProportion).toHaveBeenCalledWith(0.2);
70
+ await waitFor(() => expect(args.setMinProportion).toHaveBeenCalledWith(0.2));
71
71
  await waitFor(() => expect(canvas.getAllByLabelText('%')[0]).toHaveValue(20));
72
72
  });
73
73
 
74
74
  await step('Move max proportion silder to 80%', async () => {
75
75
  const maxSlider = canvas.getAllByRole('slider')[1];
76
76
  await fireEvent.input(maxSlider, { target: { value: '80' } });
77
- await expect(args.setMaxProportion).toHaveBeenCalledWith(0.8);
77
+ await waitFor(() => expect(args.setMaxProportion).toHaveBeenCalledWith(0.8));
78
78
  await waitFor(() => expect(canvas.getAllByLabelText('%')[1]).toHaveValue(80));
79
79
  });
80
80
  },
@@ -1,4 +1,5 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { useEffect, useRef, useState } from 'preact/hooks';
2
3
 
3
4
  import { MinMaxRangeSlider } from './min-max-range-slider';
4
5
  import { PercentInput } from './percent-intput';
@@ -11,31 +12,64 @@ export interface ProportionSelectorProps {
11
12
  setMaxProportion: (maxProportion: number) => void;
12
13
  }
13
14
 
15
+ function useUpdateExternalValueInIntervals(
16
+ setExternalValue: (minProportion: number) => void,
17
+ updateIntervalInMs: number,
18
+ internalValue: number,
19
+ ) {
20
+ const hasMounted = useRef(false);
21
+
22
+ useEffect(() => {
23
+ if (!hasMounted.current) {
24
+ hasMounted.current = true;
25
+ return;
26
+ }
27
+
28
+ const minTimeout = setTimeout(() => {
29
+ setExternalValue(internalValue);
30
+ }, updateIntervalInMs);
31
+
32
+ return () => clearTimeout(minTimeout);
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect when the internal value changes
34
+ }, [internalValue]);
35
+ }
36
+
14
37
  export const ProportionSelector: FunctionComponent<ProportionSelectorProps> = ({
15
38
  proportionInterval,
16
39
  setMinProportion,
17
40
  setMaxProportion,
18
41
  }) => {
42
+ const updateIntervalInMs = 300;
19
43
  const { min: minProportion, max: maxProportion } = proportionInterval;
44
+
45
+ const [internalMinProportion, setInternalMinProportion] = useState(minProportion);
46
+ const [internalMaxProportion, setInternalMaxProportion] = useState(maxProportion);
47
+
48
+ useUpdateExternalValueInIntervals(setMinProportion, updateIntervalInMs, internalMinProportion);
49
+ const updateMinPercentage = (minPercentage: number) => {
50
+ const newMinProportion = minPercentage / 100;
51
+ setInternalMinProportion(newMinProportion);
52
+ };
53
+
54
+ useUpdateExternalValueInIntervals(setMaxProportion, updateIntervalInMs, internalMaxProportion);
55
+ const updateMaxPercentage = (maxPercentage: number) => {
56
+ const newMaxProportion = maxPercentage / 100;
57
+ setInternalMaxProportion(newMaxProportion);
58
+ };
59
+
20
60
  return (
21
61
  <div class='flex flex-col w-64 mb-2'>
22
62
  <div class='flex items-center '>
23
- <PercentInput
24
- percentage={minProportion * 100}
25
- setPercentage={(percentage) => setMinProportion(percentage / 100)}
26
- />
63
+ <PercentInput percentage={internalMinProportion * 100} setPercentage={updateMinPercentage} />
27
64
  <div class='m-2'>-</div>
28
- <PercentInput
29
- percentage={maxProportion * 100}
30
- setPercentage={(percentage) => setMaxProportion(percentage / 100)}
31
- />
65
+ <PercentInput percentage={internalMaxProportion * 100} setPercentage={updateMaxPercentage} />
32
66
  </div>
33
67
  <div class='my-1'>
34
68
  <MinMaxRangeSlider
35
- min={minProportion * 100}
36
- max={maxProportion * 100}
37
- setMin={(percentage) => setMinProportion(percentage / 100)}
38
- setMax={(percentage) => setMaxProportion(percentage / 100)}
69
+ min={internalMinProportion * 100}
70
+ max={internalMaxProportion * 100}
71
+ setMin={updateMinPercentage}
72
+ setMax={updateMaxPercentage}
39
73
  />
40
74
  </div>
41
75
  </div>
@@ -0,0 +1,151 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, within } from '@storybook/test';
3
+ import { type FunctionComponent } from 'preact';
4
+ import { useState } from 'preact/hooks';
5
+
6
+ import { type DisplayedSegment, SegmentSelector, type SegmentSelectorProps } from './segment-selector';
7
+
8
+ const meta: Meta<SegmentSelectorProps> = {
9
+ title: 'Component/Segment selector',
10
+ component: SegmentSelector,
11
+ parameters: { fetchMock: {} },
12
+ };
13
+
14
+ export default meta;
15
+
16
+ const WrapperWithState: FunctionComponent<{
17
+ displayedSegments: DisplayedSegment[];
18
+ }> = ({ displayedSegments: initialDisplayedSegments }) => {
19
+ const [displayedSegments, setDisplayedSegments] = useState<DisplayedSegment[]>(initialDisplayedSegments);
20
+
21
+ return (
22
+ <SegmentSelector
23
+ displayedSegments={displayedSegments}
24
+ setDisplayedSegments={(items: DisplayedSegment[]) => {
25
+ setDisplayedSegments(items);
26
+ }}
27
+ />
28
+ );
29
+ };
30
+
31
+ export const AllSegmentsSelected: StoryObj<SegmentSelectorProps> = {
32
+ render: (args) => {
33
+ return <WrapperWithState {...args} />;
34
+ },
35
+ args: {
36
+ displayedSegments: [
37
+ {
38
+ segment: 'ORF1a',
39
+ label: 'ORF1a',
40
+ checked: true,
41
+ },
42
+ {
43
+ segment: 'S',
44
+ label: 'S',
45
+ checked: true,
46
+ },
47
+ {
48
+ segment: 'VeryLongSegmentName',
49
+ label: 'VeryLongSegmentName',
50
+ checked: true,
51
+ },
52
+ ],
53
+ },
54
+ play: async ({ canvasElement, step }) => {
55
+ const canvas = within(canvasElement);
56
+
57
+ await step("Show 'All segments' as label", async () => {
58
+ await expect(canvas.getByText('All segments')).toBeInTheDocument();
59
+ });
60
+ },
61
+ };
62
+
63
+ export const NoSegmentsSelected: StoryObj<SegmentSelectorProps> = {
64
+ ...AllSegmentsSelected,
65
+ args: {
66
+ displayedSegments: [
67
+ {
68
+ segment: 'ORF1a',
69
+ label: 'ORF1a',
70
+ checked: false,
71
+ },
72
+ {
73
+ segment: 'S',
74
+ label: 'S',
75
+ checked: false,
76
+ },
77
+ {
78
+ segment: 'VeryLongSegmentName',
79
+ label: 'VeryLongSegmentName',
80
+ checked: false,
81
+ },
82
+ ],
83
+ },
84
+ play: async ({ canvasElement, step }) => {
85
+ const canvas = within(canvasElement);
86
+
87
+ await step("Show 'No segments' as label", async () => {
88
+ await expect(canvas.getByText('No segments')).toBeInTheDocument();
89
+ });
90
+ },
91
+ };
92
+
93
+ export const LongSegmentsSelected: StoryObj<SegmentSelectorProps> = {
94
+ ...AllSegmentsSelected,
95
+ args: {
96
+ displayedSegments: [
97
+ {
98
+ segment: 'ORF1a',
99
+ label: 'ORF1a',
100
+ checked: true,
101
+ },
102
+ {
103
+ segment: 'S',
104
+ label: 'S',
105
+ checked: false,
106
+ },
107
+ {
108
+ segment: 'VeryLongSegmentName',
109
+ label: 'VeryLongSegmentName',
110
+ checked: true,
111
+ },
112
+ ],
113
+ },
114
+ play: async ({ canvasElement, step }) => {
115
+ const canvas = within(canvasElement);
116
+
117
+ await step('Show number of active segments as label', async () => {
118
+ await expect(canvas.getByText('2 segments')).toBeInTheDocument();
119
+ });
120
+ },
121
+ };
122
+
123
+ export const ShortSegmentsSelected: StoryObj<SegmentSelectorProps> = {
124
+ ...AllSegmentsSelected,
125
+ args: {
126
+ displayedSegments: [
127
+ {
128
+ segment: 'ORF1a',
129
+ label: 'ORF1a',
130
+ checked: true,
131
+ },
132
+ {
133
+ segment: 'S',
134
+ label: 'S',
135
+ checked: true,
136
+ },
137
+ {
138
+ segment: 'VeryLongSegmentName',
139
+ label: 'VeryLongSegmentName',
140
+ checked: false,
141
+ },
142
+ ],
143
+ },
144
+ play: async ({ canvasElement, step }) => {
145
+ const canvas = within(canvasElement);
146
+
147
+ await step('Show active segments as label', async () => {
148
+ await expect(canvas.getByText('ORF1a, S')).toBeInTheDocument();
149
+ });
150
+ },
151
+ };
@@ -13,39 +13,48 @@ export type DisplayedSegment = CheckboxItem & {
13
13
  export type SegmentSelectorProps = {
14
14
  displayedSegments: DisplayedSegment[];
15
15
  setDisplayedSegments: (items: DisplayedSegment[]) => void;
16
- prefix?: string;
17
16
  };
18
17
 
19
- const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[], prefix: string) => {
18
+ export const SegmentSelector: FunctionComponent<SegmentSelectorProps> = ({
19
+ displayedSegments,
20
+ setDisplayedSegments,
21
+ }) => {
22
+ if (displayedSegments.length <= 1) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <div className='w-24'>
28
+ <CheckboxSelector
29
+ items={displayedSegments}
30
+ label={getSegmentSelectorLabel(displayedSegments)}
31
+ setItems={(items) => setDisplayedSegments(items)}
32
+ />
33
+ </div>
34
+ );
35
+ };
36
+
37
+ const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[]) => {
20
38
  const allSelectedSelected = displayedSegments
21
39
  .filter((segment) => segment.checked)
22
40
  .map((segment) => segment.segment);
23
41
 
24
42
  if (allSelectedSelected.length === 0) {
25
- return `${prefix}none`;
43
+ return `No segments`;
26
44
  }
27
45
  if (displayedSegments.length === allSelectedSelected.length) {
28
- return `${prefix}all`;
46
+ return `All segments`;
29
47
  }
30
- return prefix + allSelectedSelected.join(', ');
31
- };
32
48
 
33
- export const SegmentSelector: FunctionComponent<SegmentSelectorProps> = ({
34
- displayedSegments,
35
- setDisplayedSegments,
36
- prefix,
37
- }) => {
38
- if (displayedSegments.length <= 1) {
39
- return null;
49
+ const longestDisplayString = `All segments`;
50
+
51
+ const allSelectedSelectedString = allSelectedSelected.join(', ');
52
+
53
+ if (longestDisplayString.length >= allSelectedSelectedString.length) {
54
+ return allSelectedSelectedString;
40
55
  }
41
56
 
42
- return (
43
- <CheckboxSelector
44
- items={displayedSegments}
45
- label={getSegmentSelectorLabel(displayedSegments, prefix || 'Segments: ')}
46
- setItems={(items) => setDisplayedSegments(items)}
47
- />
48
- );
57
+ return `${allSelectedSelected.length} ${allSelectedSelected.length === 1 ? 'segment' : 'segments'}`;
49
58
  };
50
59
 
51
60
  export function useDisplayedSegments(sequenceType: SequenceType) {
@@ -128,7 +128,7 @@ export const FilterForOnlyDeletions: StoryObj<MutationComparisonProps> = {
128
128
  await waitFor(() => expect(someSubstitution()).toBeVisible());
129
129
  await waitFor(() => expect(someDeletion()).toBeVisible());
130
130
 
131
- canvas.getByRole('button', { name: /Types:/ }).click();
131
+ canvas.getByRole('button', { name: 'Subst., Del.' }).click();
132
132
  canvas.getByLabelText('Substitutions').click();
133
133
 
134
134
  await waitFor(() => expect(someSubstitution()).not.toBeInTheDocument());
@@ -7,7 +7,6 @@ import { MutationComparisonVenn } from './mutation-comparison-venn';
7
7
  import { filterMutationData, type MutationData, queryMutationData } from './queryMutationData';
8
8
  import { type NamedLapisFilter, type SequenceType } from '../../types';
9
9
  import { LapisUrlContext } from '../LapisUrlContext';
10
- import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/SegmentSelector';
11
10
  import { CsvDownloadButton } from '../components/csv-download-button';
12
11
  import { ErrorBoundary } from '../components/error-boundary';
13
12
  import { ErrorDisplay } from '../components/error-display';
@@ -19,6 +18,7 @@ import { NoDataDisplay } from '../components/no-data-display';
19
18
  import { type ProportionInterval } from '../components/proportion-selector';
20
19
  import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
21
20
  import { ResizeContainer } from '../components/resize-container';
21
+ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/segment-selector';
22
22
  import Tabs from '../components/tabs';
23
23
  import { useQuery } from '../useQuery';
24
24
 
@@ -1,7 +1,7 @@
1
1
  import { querySubstitutionsOrDeletions } from '../../query/querySubstitutionsOrDeletions';
2
2
  import { type NamedLapisFilter, type SubstitutionOrDeletionEntry } from '../../types';
3
- import { type DisplayedSegment } from '../components/SegmentSelector';
4
3
  import { type DisplayedMutationType } from '../components/mutation-type-selector';
4
+ import { type DisplayedSegment } from '../components/segment-selector';
5
5
 
6
6
  export type MutationData = {
7
7
  displayName: string;
@@ -1,5 +1,6 @@
1
1
  import { type Row } from 'gridjs';
2
2
  import { type FunctionComponent } from 'preact';
3
+ import { useMemo } from 'preact/hooks';
3
4
 
4
5
  import { getMutationsGridData } from './getMutationsGridData';
5
6
  import { type SequenceType, type SubstitutionOrDeletionEntry } from '../../types';
@@ -84,7 +85,10 @@ export const MutationsGrid: FunctionComponent<MutationsGridProps> = ({
84
85
  return {};
85
86
  };
86
87
 
87
- const tableData = getMutationsGridData(data, sequenceType, proportionInterval).map((row) => Object.values(row));
88
+ const tableData = useMemo(
89
+ () => getMutationsGridData(data, sequenceType, proportionInterval).map((row) => Object.values(row)),
90
+ [data, proportionInterval, sequenceType],
91
+ );
88
92
 
89
93
  return <Table data={tableData} columns={getHeaders()} pageSize={pageSize} />;
90
94
  };
@@ -14,7 +14,6 @@ import {
14
14
  type SubstitutionOrDeletionEntry,
15
15
  } from '../../types';
16
16
  import { LapisUrlContext } from '../LapisUrlContext';
17
- import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/SegmentSelector';
18
17
  import { CsvDownloadButton } from '../components/csv-download-button';
19
18
  import { ErrorBoundary } from '../components/error-boundary';
20
19
  import { ErrorDisplay } from '../components/error-display';
@@ -26,6 +25,7 @@ import { NoDataDisplay } from '../components/no-data-display';
26
25
  import type { ProportionInterval } from '../components/proportion-selector';
27
26
  import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
28
27
  import { ResizeContainer } from '../components/resize-container';
28
+ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/segment-selector';
29
29
  import Tabs from '../components/tabs';
30
30
  import { useQuery } from '../useQuery';
31
31
 
@@ -6,8 +6,8 @@ import {
6
6
  type MutationEntry,
7
7
  type SubstitutionOrDeletionEntry,
8
8
  } from '../../types';
9
- import { type DisplayedSegment } from '../components/SegmentSelector';
10
9
  import { type DisplayedMutationType } from '../components/mutation-type-selector';
10
+ import { type DisplayedSegment } from '../components/segment-selector';
11
11
 
12
12
  export async function queryMutationsData(
13
13
  lapisFilter: LapisFilter,
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import { filterDisplayedSegments, filterMutationTypes, filterProportion } from './getFilteredMutationsOverTimeData';
4
4
  import { type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
5
- import { Map2d } from '../../utils/Map2d';
5
+ import { Map2dBase } from '../../utils/map2d';
6
6
  import { Deletion, Substitution } from '../../utils/mutations';
7
7
  import { type Temporal } from '../../utils/temporal';
8
8
  import { yearMonthDay } from '../../utils/temporalTestHelpers';
@@ -10,7 +10,7 @@ import { yearMonthDay } from '../../utils/temporalTestHelpers';
10
10
  describe('getFilteredMutationOverTimeData', () => {
11
11
  describe('filterDisplayedSegments', () => {
12
12
  it('should filter by displayed segments', () => {
13
- const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
13
+ const data = new Map2dBase<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
14
14
 
15
15
  data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
16
16
  count: 1,
@@ -36,7 +36,7 @@ describe('getFilteredMutationOverTimeData', () => {
36
36
 
37
37
  describe('filterMutationTypes', () => {
38
38
  it('should filter by mutation types', () => {
39
- const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
39
+ const data = new Map2dBase<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
40
40
 
41
41
  data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
42
42
  count: 1,
@@ -120,7 +120,7 @@ describe('getFilteredMutationOverTimeData', () => {
120
120
  });
121
121
 
122
122
  function getMutationOverTimeData() {
123
- const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
123
+ const data = new Map2dBase<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
124
124
  data.set(someSubstitution, yearMonthDay('2021-01-01'), { count: 1, proportion: 0.1 });
125
125
  data.set(someSubstitution, yearMonthDay('2021-02-02'), { count: 99, proportion: 0.99 });
126
126
  return data;
@@ -1,8 +1,9 @@
1
1
  import { type Dataset } from '../../operator/Dataset';
2
2
  import { type MutationOverTimeDataGroupedByMutation } from '../../query/queryMutationsOverTime';
3
3
  import { type DeletionEntry, type SubstitutionEntry } from '../../types';
4
- import type { DisplayedSegment } from '../components/SegmentSelector';
4
+ import { Map2dView } from '../../utils/map2d';
5
5
  import type { DisplayedMutationType } from '../components/mutation-type-selector';
6
+ import type { DisplayedSegment } from '../components/segment-selector';
6
7
 
7
8
  export function getFilteredMutationOverTimeData(
8
9
  data: MutationOverTimeDataGroupedByMutation,
@@ -11,7 +12,7 @@ export function getFilteredMutationOverTimeData(
11
12
  displayedMutationTypes: DisplayedMutationType[],
12
13
  proportionInterval: { min: number; max: number },
13
14
  ) {
14
- const filteredData = data.copy();
15
+ const filteredData = new Map2dView(data);
15
16
  filterDisplayedSegments(displayedSegments, filteredData);
16
17
  filterMutationTypes(displayedMutationTypes, filteredData);
17
18
  filterProportion(filteredData, overallMutationData, proportionInterval);
@@ -18,7 +18,6 @@ import {
18
18
  } from '../../types';
19
19
  import { compareTemporal } from '../../utils/temporal';
20
20
  import { LapisUrlContext } from '../LapisUrlContext';
21
- import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/SegmentSelector';
22
21
  import { type ColorScale } from '../components/color-scale-selector';
23
22
  import { ColorScaleSelectorDropdown } from '../components/color-scale-selector-dropdown';
24
23
  import { CsvDownloadButton } from '../components/csv-download-button';
@@ -32,6 +31,7 @@ import { NoDataDisplay } from '../components/no-data-display';
32
31
  import type { ProportionInterval } from '../components/proportion-selector';
33
32
  import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
34
33
  import { ResizeContainer } from '../components/resize-container';
34
+ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/segment-selector';
35
35
  import Tabs from '../components/tabs';
36
36
  import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
37
37
  import { useQuery } from '../useQuery';
@@ -62,8 +62,9 @@ const NumberSequencesOverTimeInner = ({
62
62
  }: NumberSequencesOverTimeInnerProps) => {
63
63
  const lapis = useContext(LapisUrlContext);
64
64
 
65
- const { data, error, isLoading } = useQuery(() =>
66
- queryNumberOfSequencesOverTime(lapis, lapisFilter, lapisDateField, granularity, smoothingWindow),
65
+ const { data, error, isLoading } = useQuery(
66
+ () => queryNumberOfSequencesOverTime(lapis, lapisFilter, lapisDateField, granularity, smoothingWindow),
67
+ [lapis, lapisFilter, lapisDateField, granularity, smoothingWindow],
67
68
  );
68
69
 
69
70
  if (isLoading) {
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
2
 
3
- export function useQuery<Data>(fetchDataCallback: () => Promise<Data>, dependencies: unknown[] = []) {
3
+ export function useQuery<Data>(fetchDataCallback: () => Promise<Data>, dependencies: unknown[]) {
4
4
  const [data, setData] = useState<Data | null>(null);
5
5
  const [error, setError] = useState<Error | null>(null);
6
6
  const [isLoading, setIsLoading] = useState(true);