@genspectrum/dashboard-components 0.6.10 → 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 +1006 -618
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +7 -7
- package/dist/style.css +147 -1
- package/package.json +2 -2
- package/src/preact/aggregatedData/aggregate.tsx +12 -4
- 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/info.tsx +88 -1
- 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 +33 -3
- 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 +43 -8
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +2 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +44 -53
- 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/web-components/visualization/gs-prevalence-over-time.tsx +2 -4
- package/src/utils/Map2d.ts +0 -75
|
@@ -0,0 +1,115 @@
|
|
|
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 {
|
|
7
|
+
type DisplayedMutationType,
|
|
8
|
+
MutationTypeSelector,
|
|
9
|
+
type MutationTypeSelectorProps,
|
|
10
|
+
} from './mutation-type-selector';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<MutationTypeSelectorProps> = {
|
|
13
|
+
title: 'Component/Mutation type selector',
|
|
14
|
+
component: MutationTypeSelector,
|
|
15
|
+
parameters: { fetchMock: {} },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
|
|
20
|
+
const WrapperWithState: FunctionComponent<{
|
|
21
|
+
displayedMutationTypes: DisplayedMutationType[];
|
|
22
|
+
}> = ({ displayedMutationTypes: initialMutationTypes }) => {
|
|
23
|
+
const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>(initialMutationTypes);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<MutationTypeSelector
|
|
27
|
+
displayedMutationTypes={displayedMutationTypes}
|
|
28
|
+
setDisplayedMutationTypes={(mutationTypes: DisplayedMutationType[]) => {
|
|
29
|
+
setDisplayedMutationTypes(mutationTypes);
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const MutationTypeSelectorStory: StoryObj<MutationTypeSelectorProps> = {
|
|
36
|
+
render: (args) => {
|
|
37
|
+
return <WrapperWithState {...args} />;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const AllMutationTypesSelected: StoryObj<MutationTypeSelectorProps> = {
|
|
42
|
+
...MutationTypeSelectorStory,
|
|
43
|
+
args: {
|
|
44
|
+
displayedMutationTypes: [
|
|
45
|
+
{
|
|
46
|
+
label: 'Substitution',
|
|
47
|
+
type: 'substitution',
|
|
48
|
+
checked: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: 'Deletion',
|
|
52
|
+
type: 'deletion',
|
|
53
|
+
checked: true,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
play: async ({ canvasElement, step }) => {
|
|
58
|
+
const canvas = within(canvasElement);
|
|
59
|
+
|
|
60
|
+
await step('Show short form of types as label', async () => {
|
|
61
|
+
await expect(canvas.getByText('Subst., Del.')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const NoMutationTypesSelected: StoryObj<MutationTypeSelectorProps> = {
|
|
67
|
+
...MutationTypeSelectorStory,
|
|
68
|
+
args: {
|
|
69
|
+
displayedMutationTypes: [
|
|
70
|
+
{
|
|
71
|
+
label: 'Substitution',
|
|
72
|
+
type: 'substitution',
|
|
73
|
+
checked: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Deletion',
|
|
77
|
+
type: 'deletion',
|
|
78
|
+
checked: false,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
play: async ({ canvasElement, step }) => {
|
|
83
|
+
const canvas = within(canvasElement);
|
|
84
|
+
|
|
85
|
+
await step("Show 'No types' as label", async () => {
|
|
86
|
+
await expect(canvas.getByText('No types')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const OneTypesSelected: StoryObj<MutationTypeSelectorProps> = {
|
|
92
|
+
...MutationTypeSelectorStory,
|
|
93
|
+
args: {
|
|
94
|
+
displayedMutationTypes: [
|
|
95
|
+
{
|
|
96
|
+
label: 'Substitution',
|
|
97
|
+
type: 'substitution',
|
|
98
|
+
checked: true,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: 'Deletion',
|
|
102
|
+
type: 'deletion',
|
|
103
|
+
checked: false,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
play: async ({ canvasElement, step }) => {
|
|
108
|
+
const canvas = within(canvasElement);
|
|
109
|
+
|
|
110
|
+
await step('Show the selected type as label', async () => {
|
|
111
|
+
const substitutionElements = await canvas.getAllByText('Substitution');
|
|
112
|
+
await expect(substitutionElements.length).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -16,14 +16,39 @@ export const MutationTypeSelector: FunctionComponent<MutationTypeSelectorProps>
|
|
|
16
16
|
displayedMutationTypes,
|
|
17
17
|
setDisplayedMutationTypes,
|
|
18
18
|
}) => {
|
|
19
|
-
const checkedLabels = displayedMutationTypes.filter((type) => type.checked).map((type) => type.label);
|
|
20
|
-
const mutationTypesSelectorLabel = `Types: ${checkedLabels.length > 0 ? checkedLabels.join(', ') : 'None'}`;
|
|
21
|
-
|
|
22
19
|
return (
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
<div className='w-[6rem]'>
|
|
21
|
+
<CheckboxSelector
|
|
22
|
+
items={displayedMutationTypes}
|
|
23
|
+
label={getMutationTypesSelectorLabel(displayedMutationTypes)}
|
|
24
|
+
setItems={(items) => setDisplayedMutationTypes(items)}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
28
27
|
);
|
|
29
28
|
};
|
|
29
|
+
|
|
30
|
+
const getMutationTypesSelectorLabel = (displayedMutationTypes: DisplayedMutationType[]) => {
|
|
31
|
+
const checkedLabels = displayedMutationTypes.filter((displayedMutationType) => displayedMutationType.checked);
|
|
32
|
+
|
|
33
|
+
if (checkedLabels.length === 0) {
|
|
34
|
+
return `No types`;
|
|
35
|
+
}
|
|
36
|
+
if (displayedMutationTypes.length === checkedLabels.length) {
|
|
37
|
+
return displayedMutationTypes
|
|
38
|
+
.map((type) => {
|
|
39
|
+
switch (type.type) {
|
|
40
|
+
case 'substitution':
|
|
41
|
+
return 'Subst.';
|
|
42
|
+
case 'deletion':
|
|
43
|
+
return 'Del.';
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.join(', ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return checkedLabels
|
|
50
|
+
.map((type) => {
|
|
51
|
+
return type.label;
|
|
52
|
+
})
|
|
53
|
+
.join(', ');
|
|
54
|
+
};
|
|
@@ -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;
|