@genspectrum/dashboard-components 0.6.11 → 0.6.12
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.
- package/dist/dashboard-components.js +1072 -853
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +7 -7
- package/dist/style.css +113 -0
- package/package.json +2 -2
- package/src/preact/components/checkbox-selector.stories.tsx +93 -11
- package/src/preact/components/checkbox-selector.tsx +19 -0
- package/src/preact/components/color-scale-selector-dropdown.tsx +5 -3
- package/src/preact/components/dropdown.tsx +3 -3
- package/src/preact/components/mutation-type-selector.stories.tsx +115 -0
- package/src/preact/components/mutation-type-selector.tsx +33 -8
- package/src/preact/components/percent-input.stories.tsx +93 -0
- package/src/preact/components/percent-intput.tsx +4 -0
- package/src/preact/components/proportion-selector-dropdown.stories.tsx +2 -2
- package/src/preact/components/proportion-selector-dropdown.tsx +9 -7
- package/src/preact/components/proportion-selector.stories.tsx +4 -4
- package/src/preact/components/proportion-selector.tsx +46 -12
- package/src/preact/components/segment-selector.stories.tsx +151 -0
- package/src/preact/components/{SegmentSelector.tsx → segment-selector.tsx} +29 -20
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +1 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
- package/src/preact/mutationComparison/queryMutationData.ts +1 -1
- package/src/preact/mutations/mutations-grid.tsx +5 -1
- package/src/preact/mutations/mutations.tsx +1 -1
- package/src/preact/mutations/queryMutations.ts +1 -1
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +4 -4
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +3 -2
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +3 -2
- package/src/preact/useQuery.ts +1 -1
- package/src/query/queryMutationsOverTime.ts +3 -3
- package/src/utils/map2d.spec.ts +83 -22
- package/src/utils/map2d.ts +158 -0
- 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
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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={
|
|
36
|
-
max={
|
|
37
|
-
setMin={
|
|
38
|
-
setMax={
|
|
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
|
|
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
|
|
43
|
+
return `No segments`;
|
|
26
44
|
}
|
|
27
45
|
if (displayedSegments.length === allSelectedSelected.length) {
|
|
28
|
-
return
|
|
46
|
+
return `All segments`;
|
|
29
47
|
}
|
|
30
|
-
return prefix + allSelectedSelected.join(', ');
|
|
31
|
-
};
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|
package/src/preact/useQuery.ts
CHANGED
|
@@ -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);
|