@genspectrum/dashboard-components 1.0.1 → 1.1.0

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/util.d.ts CHANGED
@@ -917,7 +917,7 @@ declare global {
917
917
 
918
918
  declare global {
919
919
  interface HTMLElementTagNameMap {
920
- 'gs-mutation-comparison-component': MutationComparisonComponent;
920
+ 'gs-genome-data-viewer': GenomeDataViewerComponent;
921
921
  }
922
922
  }
923
923
 
@@ -925,7 +925,7 @@ declare global {
925
925
  declare global {
926
926
  namespace JSX {
927
927
  interface IntrinsicElements {
928
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
928
+ 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
929
929
  }
930
930
  }
931
931
  }
@@ -933,7 +933,7 @@ declare global {
933
933
 
934
934
  declare global {
935
935
  interface HTMLElementTagNameMap {
936
- 'gs-genome-data-viewer': GenomeDataViewerComponent;
936
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
937
937
  }
938
938
  }
939
939
 
@@ -941,7 +941,7 @@ declare global {
941
941
  declare global {
942
942
  namespace JSX {
943
943
  interface IntrinsicElements {
944
- 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
944
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
945
945
  }
946
946
  }
947
947
  }
@@ -949,7 +949,7 @@ declare global {
949
949
 
950
950
  declare global {
951
951
  interface HTMLElementTagNameMap {
952
- 'gs-mutations': MutationsComponent;
952
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
953
953
  }
954
954
  }
955
955
 
@@ -957,7 +957,7 @@ declare global {
957
957
  declare global {
958
958
  namespace JSX {
959
959
  interface IntrinsicElements {
960
- 'gs-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
960
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
961
961
  }
962
962
  }
963
963
  }
@@ -965,7 +965,7 @@ declare global {
965
965
 
966
966
  declare global {
967
967
  interface HTMLElementTagNameMap {
968
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
968
+ 'gs-mutations': MutationsComponent;
969
969
  }
970
970
  }
971
971
 
@@ -973,7 +973,7 @@ declare global {
973
973
  declare global {
974
974
  namespace JSX {
975
975
  interface IntrinsicElements {
976
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
976
+ 'gs-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
977
977
  }
978
978
  }
979
979
  }
@@ -981,7 +981,7 @@ declare global {
981
981
 
982
982
  declare global {
983
983
  interface HTMLElementTagNameMap {
984
- 'gs-aggregate': AggregateComponent;
984
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
985
985
  }
986
986
  }
987
987
 
@@ -989,7 +989,7 @@ declare global {
989
989
  declare global {
990
990
  namespace JSX {
991
991
  interface IntrinsicElements {
992
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
992
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
993
993
  }
994
994
  }
995
995
  }
@@ -997,7 +997,7 @@ declare global {
997
997
 
998
998
  declare global {
999
999
  interface HTMLElementTagNameMap {
1000
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1000
+ 'gs-aggregate': AggregateComponent;
1001
1001
  }
1002
1002
  }
1003
1003
 
@@ -1005,7 +1005,7 @@ declare global {
1005
1005
  declare global {
1006
1006
  namespace JSX {
1007
1007
  interface IntrinsicElements {
1008
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1008
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1009
1009
  }
1010
1010
  }
1011
1011
  }
@@ -1029,7 +1029,7 @@ declare global {
1029
1029
 
1030
1030
  declare global {
1031
1031
  interface HTMLElementTagNameMap {
1032
- 'gs-sequences-by-location': SequencesByLocationComponent;
1032
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1033
1033
  }
1034
1034
  }
1035
1035
 
@@ -1037,7 +1037,7 @@ declare global {
1037
1037
  declare global {
1038
1038
  namespace JSX {
1039
1039
  interface IntrinsicElements {
1040
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1040
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1041
1041
  }
1042
1042
  }
1043
1043
  }
@@ -1045,7 +1045,7 @@ declare global {
1045
1045
 
1046
1046
  declare global {
1047
1047
  interface HTMLElementTagNameMap {
1048
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1048
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1049
1049
  }
1050
1050
  }
1051
1051
 
@@ -1053,7 +1053,7 @@ declare global {
1053
1053
  declare global {
1054
1054
  namespace JSX {
1055
1055
  interface IntrinsicElements {
1056
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1056
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
1057
  }
1058
1058
  }
1059
1059
  }
@@ -1077,11 +1077,7 @@ declare global {
1077
1077
 
1078
1078
  declare global {
1079
1079
  interface HTMLElementTagNameMap {
1080
- 'gs-date-range-filter': DateRangeFilterComponent;
1081
- }
1082
- interface HTMLElementEventMap {
1083
- [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1084
- [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1080
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1085
1081
  }
1086
1082
  }
1087
1083
 
@@ -1089,7 +1085,7 @@ declare global {
1089
1085
  declare global {
1090
1086
  namespace JSX {
1091
1087
  interface IntrinsicElements {
1092
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1088
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1093
1089
  }
1094
1090
  }
1095
1091
  }
@@ -1135,10 +1131,11 @@ declare global {
1135
1131
 
1136
1132
  declare global {
1137
1133
  interface HTMLElementTagNameMap {
1138
- 'gs-lineage-filter': LineageFilterComponent;
1134
+ 'gs-date-range-filter': DateRangeFilterComponent;
1139
1135
  }
1140
1136
  interface HTMLElementEventMap {
1141
- [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
1137
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1138
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1142
1139
  }
1143
1140
  }
1144
1141
 
@@ -1146,7 +1143,7 @@ declare global {
1146
1143
  declare global {
1147
1144
  namespace JSX {
1148
1145
  interface IntrinsicElements {
1149
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1146
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1150
1147
  }
1151
1148
  }
1152
1149
  }
@@ -1173,11 +1170,10 @@ declare global {
1173
1170
 
1174
1171
  declare global {
1175
1172
  interface HTMLElementTagNameMap {
1176
- 'gs-number-range-filter': NumberRangeFilterComponent;
1173
+ 'gs-lineage-filter': LineageFilterComponent;
1177
1174
  }
1178
1175
  interface HTMLElementEventMap {
1179
- [gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
1180
- [gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
1176
+ [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
1181
1177
  }
1182
1178
  }
1183
1179
 
@@ -1185,7 +1181,7 @@ declare global {
1185
1181
  declare global {
1186
1182
  namespace JSX {
1187
1183
  interface IntrinsicElements {
1188
- 'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1184
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1189
1185
  }
1190
1186
  }
1191
1187
  }
@@ -1193,7 +1189,11 @@ declare global {
1193
1189
 
1194
1190
  declare global {
1195
1191
  interface HTMLElementTagNameMap {
1196
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1192
+ 'gs-number-range-filter': NumberRangeFilterComponent;
1193
+ }
1194
+ interface HTMLElementEventMap {
1195
+ [gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
1196
+ [gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
1197
1197
  }
1198
1198
  }
1199
1199
 
@@ -1201,7 +1201,7 @@ declare global {
1201
1201
  declare global {
1202
1202
  namespace JSX {
1203
1203
  interface IntrinsicElements {
1204
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1204
+ 'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1205
1205
  }
1206
1206
  }
1207
1207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -16,7 +16,12 @@ type MutationAnnotationPerSequenceType = {
16
16
  position: Map<string, MutationAnnotations>;
17
17
  };
18
18
 
19
- const MutationAnnotationsContext = createContext<Record<SequenceType, MutationAnnotationPerSequenceType>>({
19
+ type MutationAnnotationsContextValue = Record<SequenceType, MutationAnnotationPerSequenceType> & {
20
+ rawAnnotations: MutationAnnotations;
21
+ };
22
+
23
+ const MutationAnnotationsContext = createContext<MutationAnnotationsContextValue>({
24
+ rawAnnotations: [],
20
25
  nucleotide: {
21
26
  mutation: new Map(),
22
27
  position: new Map(),
@@ -75,6 +80,7 @@ export function getMutationAnnotationsContext(value: MutationAnnotations) {
75
80
  });
76
81
 
77
82
  return {
83
+ rawAnnotations: value,
78
84
  nucleotide: { mutation: nucleotideMap, position: nucleotidePositions },
79
85
  'amino acid': { mutation: aminoAcidMap, position: aminoAcidPositions },
80
86
  };
@@ -85,15 +91,17 @@ function addAnnotationToMap(map: Map<string, MutationAnnotations>, code: string,
85
91
  map.set(code.toUpperCase(), [...oldAnnotations, annotation]);
86
92
  }
87
93
 
94
+ export function useRawMutationAnnotations() {
95
+ return useContext(MutationAnnotationsContext).rawAnnotations;
96
+ }
97
+
88
98
  export function useMutationAnnotationsProvider() {
89
99
  const mutationAnnotations = useContext(MutationAnnotationsContext);
90
100
 
91
101
  return getMutationAnnotationsProvider(mutationAnnotations);
92
102
  }
93
103
 
94
- export function getMutationAnnotationsProvider(
95
- mutationAnnotations: Record<SequenceType, MutationAnnotationPerSequenceType>,
96
- ) {
104
+ export function getMutationAnnotationsProvider(mutationAnnotations: MutationAnnotationsContextValue) {
97
105
  return (mutation: Mutation, sequenceType: SequenceType) => {
98
106
  const position =
99
107
  mutation.segment === undefined
@@ -110,11 +118,11 @@ export function getMutationAnnotationsProvider(
110
118
 
111
119
  const uniqueNames = new Set<string>();
112
120
 
113
- return annotations?.filter((item) => {
114
- if (uniqueNames.has(item.name)) {
121
+ return annotations?.filter((annotation) => {
122
+ if (uniqueNames.has(annotation.name)) {
115
123
  return false;
116
124
  }
117
- uniqueNames.add(item.name);
125
+ uniqueNames.add(annotation.name);
118
126
  return true;
119
127
  });
120
128
  };
@@ -0,0 +1,109 @@
1
+ import { type StoryObj } from '@storybook/preact';
2
+ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
+ import { type Meta } from '@storybook/web-components';
4
+ import { useState, type Dispatch, type StateUpdater } from 'preact/hooks';
5
+
6
+ import {
7
+ MutationsOverTimeMutationsFilter,
8
+ type MutationsOverTimeMutationsFilterProps,
9
+ } from './mutations-over-time-mutations-filter';
10
+ import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
11
+ import { type MutationFilter } from '../mutationsOverTime/getFilteredMutationsOverTimeData';
12
+
13
+ const meta: Meta = {
14
+ title: 'Component/Mutations over time mutations filter',
15
+ component: 'MutationsOverTimeTextFilter',
16
+ parameters: { fetchMock: {} },
17
+ };
18
+
19
+ export default meta;
20
+
21
+ const WrapperWithState = ({
22
+ setFilterValue,
23
+ value,
24
+ }: {
25
+ setFilterValue: Dispatch<StateUpdater<MutationFilter>>;
26
+ value: MutationFilter;
27
+ }) => {
28
+ const [state, setState] = useState(value);
29
+
30
+ return (
31
+ <MutationAnnotationsContextProvider
32
+ value={[
33
+ {
34
+ name: 'Test Annotation 1',
35
+ description: 'Test Annotation 1',
36
+ symbol: '#',
37
+ },
38
+ {
39
+ name: 'Test Annotation 2',
40
+ description: 'Test Annotation 2',
41
+ symbol: '+',
42
+ },
43
+ ]}
44
+ >
45
+ <MutationsOverTimeMutationsFilter
46
+ setFilterValue={(value) => {
47
+ setFilterValue(value);
48
+ setState(value);
49
+ }}
50
+ value={state}
51
+ />
52
+ </MutationAnnotationsContextProvider>
53
+ );
54
+ };
55
+
56
+ export const FilterByText: StoryObj<MutationsOverTimeMutationsFilterProps> = {
57
+ render: (args) => {
58
+ return <WrapperWithState setFilterValue={args.setFilterValue} value={args.value} />;
59
+ },
60
+ args: {
61
+ setFilterValue: fn(),
62
+ value: { textFilter: 'Test', annotationNameFilter: new Set([]) },
63
+ },
64
+ play: async ({ canvasElement, step }) => {
65
+ const canvas = within(canvasElement);
66
+
67
+ await step('Expect initial value to show on the button', async () => {
68
+ const button = canvas.getByRole('button');
69
+ await expect(button).toHaveTextContent('Test');
70
+ });
71
+
72
+ await step('Change filter and expect it to show on the button', async () => {
73
+ const button = canvas.getByRole('button');
74
+ await userEvent.click(button);
75
+
76
+ const inputField = canvas.getByRole('textbox');
77
+ await userEvent.clear(inputField);
78
+ await userEvent.type(inputField, 'OtherText');
79
+
80
+ await waitFor(() => expect(button).toHaveTextContent('OtherText'));
81
+ });
82
+ },
83
+ };
84
+
85
+ export const FilterByAnnotation: StoryObj<MutationsOverTimeMutationsFilterProps> = {
86
+ ...FilterByText,
87
+ args: {
88
+ setFilterValue: fn(),
89
+ value: { textFilter: '', annotationNameFilter: new Set() },
90
+ },
91
+ play: async ({ canvasElement, step }) => {
92
+ const canvas = within(canvasElement);
93
+
94
+ await step('Expect default text to show on the button', async () => {
95
+ const button = canvas.getByRole('button');
96
+ await expect(button).toHaveTextContent('Filter mutations');
97
+ });
98
+
99
+ await step('Change filter and expect it to show on the button', async () => {
100
+ const button = canvas.getByRole('button');
101
+ await userEvent.click(button);
102
+
103
+ const inputField = canvas.getByRole('checkbox', { name: /Test Annotation 1/ });
104
+ await userEvent.click(inputField);
105
+
106
+ await waitFor(() => expect(button).toHaveTextContent('Test Annotation 1'));
107
+ });
108
+ },
109
+ };
@@ -0,0 +1,139 @@
1
+ import { type FunctionComponent, type h } from 'preact';
2
+ import { type Dispatch, type StateUpdater, useCallback, useEffect, useState } from 'preact/hooks';
3
+
4
+ import { Dropdown } from './dropdown';
5
+ import { useRawMutationAnnotations } from '../MutationAnnotationsContext';
6
+ import { type MutationFilter } from '../mutationsOverTime/getFilteredMutationsOverTimeData';
7
+ import { DeleteIcon } from '../shared/icons/DeleteIcon';
8
+
9
+ export type MutationsOverTimeMutationsFilterProps = {
10
+ setFilterValue: Dispatch<StateUpdater<MutationFilter>>;
11
+ value: MutationFilter;
12
+ };
13
+
14
+ export function MutationsOverTimeMutationsFilter({ setFilterValue, value }: MutationsOverTimeMutationsFilterProps) {
15
+ return (
16
+ <div className={'w-28 inline-flex'}>
17
+ <Dropdown buttonTitle={getButtonTitle(value)} placement={'bottom-start'}>
18
+ <TextInput value={value} setFilterValue={setFilterValue} />
19
+ <AnnotationCheckboxes value={value} setFilterValue={setFilterValue} />
20
+ </Dropdown>
21
+ </div>
22
+ );
23
+ }
24
+
25
+ function getButtonTitle(value: MutationFilter) {
26
+ if (value.textFilter === '' && value.annotationNameFilter.size === 0) {
27
+ return `Filter mutations`;
28
+ }
29
+
30
+ return [value.textFilter, ...value.annotationNameFilter].filter((it) => it !== '').join(', ');
31
+ }
32
+
33
+ const TextInput: FunctionComponent<MutationsOverTimeMutationsFilterProps> = ({ setFilterValue, value }) => {
34
+ const onInput = useCallback(
35
+ (newValue: string) => {
36
+ setFilterValue((previousFilter) => ({
37
+ ...previousFilter,
38
+ textFilter: newValue,
39
+ }));
40
+ },
41
+ [setFilterValue],
42
+ );
43
+
44
+ const onDeleteClick = () => {
45
+ setFilterValue((previousFilter) => ({
46
+ ...previousFilter,
47
+ textFilter: '',
48
+ }));
49
+ };
50
+
51
+ return (
52
+ <div>
53
+ <label className='flex gap-1 input input-xs'>
54
+ <DebouncedInput placeholder={'Filter'} onInput={onInput} value={value.textFilter} type='text' />
55
+ {value.textFilter !== '' && (
56
+ <button className={'cursor-pointer'} onClick={onDeleteClick}>
57
+ <DeleteIcon />
58
+ </button>
59
+ )}
60
+ </label>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ function DebouncedInput({
66
+ value: initialValue,
67
+ onInput,
68
+ debounce = 500,
69
+ ...props
70
+ }: {
71
+ onInput: (value: string) => void;
72
+ debounce?: number;
73
+ value?: string;
74
+ } & Omit<h.JSX.IntrinsicElements['input'], 'onInput'>) {
75
+ const [value, setValue] = useState<string | undefined>(initialValue);
76
+
77
+ useEffect(() => {
78
+ setValue(initialValue);
79
+ }, [initialValue]);
80
+
81
+ useEffect(() => {
82
+ const timeout = setTimeout(() => {
83
+ onInput(value ?? '');
84
+ }, debounce);
85
+
86
+ return () => clearTimeout(timeout);
87
+ }, [value, debounce, onInput]);
88
+
89
+ const onChangeInput = useCallback((event: h.JSX.TargetedEvent<HTMLInputElement>) => {
90
+ setValue(event.currentTarget.value);
91
+ }, []);
92
+
93
+ return <input {...props} value={value} onInput={onChangeInput} />;
94
+ }
95
+
96
+ const AnnotationCheckboxes: FunctionComponent<MutationsOverTimeMutationsFilterProps> = ({ value, setFilterValue }) => {
97
+ const mutationAnnotations = useRawMutationAnnotations();
98
+
99
+ if (mutationAnnotations.length === 0) {
100
+ return null;
101
+ }
102
+
103
+ return (
104
+ <>
105
+ <div className='divider mt-0.5 mb-0' />
106
+ <div className='text-sm'>
107
+ <div className='font-bold mb-1'>Filter by annotations</div>
108
+ {mutationAnnotations.map((annotation, index) => (
109
+ <li className='flex flex-row items-center' key={annotation.name}>
110
+ <label>
111
+ <input
112
+ className={'mr-2'}
113
+ type='checkbox'
114
+ id={`item-${index}`}
115
+ checked={value.annotationNameFilter.has(annotation.name)}
116
+ onChange={() => {
117
+ setFilterValue((previousFilter) => {
118
+ const newAnnotationFilter = previousFilter.annotationNameFilter.has(
119
+ annotation.name,
120
+ )
121
+ ? [...previousFilter.annotationNameFilter].filter(
122
+ (name) => name !== annotation.name,
123
+ )
124
+ : [...previousFilter.annotationNameFilter, annotation.name];
125
+ return {
126
+ ...previousFilter,
127
+ annotationNameFilter: new Set(newAnnotationFilter),
128
+ };
129
+ });
130
+ }}
131
+ />
132
+ {annotation.name} (<span className='text-red-600'>{annotation.symbol}</span>)
133
+ </label>
134
+ </li>
135
+ ))}
136
+ </div>
137
+ </>
138
+ );
139
+ };