@genspectrum/dashboard-components 0.18.4 → 0.18.6

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 (37) hide show
  1. package/README.md +12 -0
  2. package/custom-elements.json +1 -1
  3. package/dist/components.d.ts +44 -44
  4. package/dist/components.js +826 -343
  5. package/dist/components.js.map +1 -1
  6. package/dist/style.css +2 -2
  7. package/dist/util.d.ts +44 -44
  8. package/package.json +2 -2
  9. package/src/preact/MutationAnnotationsContext.tsx +34 -27
  10. package/src/preact/components/dropdown.tsx +1 -1
  11. package/src/preact/components/info.tsx +1 -1
  12. package/src/preact/components/mutations-over-time-text-filter.stories.tsx +57 -0
  13. package/src/preact/components/mutations-over-time-text-filter.tsx +63 -0
  14. package/src/preact/components/segment-selector.stories.tsx +12 -5
  15. package/src/preact/components/segment-selector.tsx +11 -7
  16. package/src/preact/mutationComparison/mutation-comparison.tsx +5 -1
  17. package/src/preact/mutationFilter/mutation-filter.stories.tsx +169 -50
  18. package/src/preact/mutationFilter/mutation-filter.tsx +239 -234
  19. package/src/preact/mutationFilter/parseAndValidateMutation.ts +62 -10
  20. package/src/preact/mutationFilter/parseMutation.spec.ts +62 -47
  21. package/src/preact/mutations/mutations.tsx +5 -1
  22. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +128 -0
  23. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +39 -2
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +9 -12
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +31 -6
  27. package/src/preact/sequencesByLocation/__mockData__/worldAtlas.json +1 -1
  28. package/src/preact/shared/tanstackTable/pagination-context.tsx +30 -0
  29. package/src/preact/shared/tanstackTable/pagination.tsx +41 -21
  30. package/src/preact/shared/tanstackTable/tanstackTable.tsx +17 -3
  31. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +22 -4
  32. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +11 -2
  33. package/src/web-components/input/gs-mutation-filter.stories.ts +4 -4
  34. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
  35. package/standalone-bundle/dashboard-components.js +12896 -13334
  36. package/standalone-bundle/dashboard-components.js.map +1 -1
  37. package/standalone-bundle/style.css +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.18.4",
3
+ "version": "0.18.6",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -132,7 +132,7 @@
132
132
  "eslint-config-preact": "^1.3.0",
133
133
  "eslint-plugin-import": "^2.29.1",
134
134
  "eslint-plugin-jest": "^28.2.0",
135
- "eslint-plugin-storybook": "^0.11.0",
135
+ "eslint-plugin-storybook": "^0.12.0",
136
136
  "happy-dom": "^17.1.1",
137
137
  "http-server": "^14.1.1",
138
138
  "lit-analyzer": "^2.0.3",
@@ -37,33 +37,7 @@ export const MutationAnnotationsContextProvider: FunctionalComponent<
37
37
  return parseResult;
38
38
  }
39
39
 
40
- const nucleotideMap = new Map<string, MutationAnnotations>();
41
- const nucleotidePositions = new Map<string, MutationAnnotations>();
42
- const aminoAcidMap = new Map<string, MutationAnnotations>();
43
- const aminoAcidPositions = new Map<string, MutationAnnotations>();
44
-
45
- value.forEach((annotation) => {
46
- new Set(annotation.nucleotideMutations).forEach((code) => {
47
- addAnnotationToMap(nucleotideMap, code, annotation);
48
- });
49
- new Set(annotation.aminoAcidMutations).forEach((code) => {
50
- addAnnotationToMap(aminoAcidMap, code, annotation);
51
- });
52
- new Set(annotation.nucleotidePositions).forEach((position) => {
53
- addAnnotationToMap(nucleotidePositions, position, annotation);
54
- });
55
- new Set(annotation.aminoAcidPositions).forEach((position) => {
56
- addAnnotationToMap(aminoAcidPositions, position, annotation);
57
- });
58
- });
59
-
60
- return {
61
- success: true as const,
62
- value: {
63
- nucleotide: { mutation: nucleotideMap, position: nucleotidePositions },
64
- 'amino acid': { mutation: aminoAcidMap, position: aminoAcidPositions },
65
- },
66
- };
40
+ return { success: true as const, value: getMutationAnnotationsContext(value) };
67
41
  }, [value]);
68
42
 
69
43
  if (!parseResult.success) {
@@ -79,6 +53,33 @@ export const MutationAnnotationsContextProvider: FunctionalComponent<
79
53
  );
80
54
  };
81
55
 
56
+ export function getMutationAnnotationsContext(value: MutationAnnotations) {
57
+ const nucleotideMap = new Map<string, MutationAnnotations>();
58
+ const nucleotidePositions = new Map<string, MutationAnnotations>();
59
+ const aminoAcidMap = new Map<string, MutationAnnotations>();
60
+ const aminoAcidPositions = new Map<string, MutationAnnotations>();
61
+
62
+ value.forEach((annotation) => {
63
+ new Set(annotation.nucleotideMutations).forEach((code) => {
64
+ addAnnotationToMap(nucleotideMap, code, annotation);
65
+ });
66
+ new Set(annotation.aminoAcidMutations).forEach((code) => {
67
+ addAnnotationToMap(aminoAcidMap, code, annotation);
68
+ });
69
+ new Set(annotation.nucleotidePositions).forEach((position) => {
70
+ addAnnotationToMap(nucleotidePositions, position, annotation);
71
+ });
72
+ new Set(annotation.aminoAcidPositions).forEach((position) => {
73
+ addAnnotationToMap(aminoAcidPositions, position, annotation);
74
+ });
75
+ });
76
+
77
+ return {
78
+ nucleotide: { mutation: nucleotideMap, position: nucleotidePositions },
79
+ 'amino acid': { mutation: aminoAcidMap, position: aminoAcidPositions },
80
+ };
81
+ }
82
+
82
83
  function addAnnotationToMap(map: Map<string, MutationAnnotations>, code: string, annotation: MutationAnnotation) {
83
84
  const oldAnnotations = map.get(code.toUpperCase()) ?? [];
84
85
  map.set(code.toUpperCase(), [...oldAnnotations, annotation]);
@@ -87,6 +88,12 @@ function addAnnotationToMap(map: Map<string, MutationAnnotations>, code: string,
87
88
  export function useMutationAnnotationsProvider() {
88
89
  const mutationAnnotations = useContext(MutationAnnotationsContext);
89
90
 
91
+ return getMutationAnnotationsProvider(mutationAnnotations);
92
+ }
93
+
94
+ export function getMutationAnnotationsProvider(
95
+ mutationAnnotations: Record<SequenceType, MutationAnnotationPerSequenceType>,
96
+ ) {
90
97
  return (mutation: Mutation, sequenceType: SequenceType) => {
91
98
  const position =
92
99
  mutation.segment === undefined
@@ -30,7 +30,7 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({ children, buttonTit
30
30
  return (
31
31
  <>
32
32
  <button type='button' className='btn btn-xs whitespace-nowrap w-full' onClick={toggle} ref={referenceRef}>
33
- {buttonTitle}
33
+ <span className={'w-full truncate'}>{buttonTitle}</span>
34
34
  </button>
35
35
  <div ref={floatingRef} className={`${dropdownClass} ${showContent ? '' : 'hidden'}`}>
36
36
  {children}
@@ -21,7 +21,7 @@ export const InfoHeadline2: FunctionComponent = ({ children }) => {
21
21
  };
22
22
 
23
23
  export const InfoParagraph: FunctionComponent = ({ children }) => {
24
- return <p className='text-justify text-base font-normal my-1'>{children}</p>;
24
+ return <p className='text-justify text-base font-normal my-1 text-wrap'>{children}</p>;
25
25
  };
26
26
 
27
27
  export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
@@ -0,0 +1,57 @@
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 } from 'preact/hooks';
5
+
6
+ import { MutationsOverTimeTextFilter, type TextFilterProps } from './mutations-over-time-text-filter';
7
+
8
+ const meta: Meta = {
9
+ title: 'Component/Mutations over time text filter',
10
+ component: 'MutationsOverTimeTextFilter',
11
+ parameters: { fetchMock: {} },
12
+ };
13
+
14
+ export default meta;
15
+
16
+ const WrapperWithState = ({ setFilterValue, value }: { setFilterValue: (value: string) => void; value: string }) => {
17
+ const [state, setState] = useState(value);
18
+
19
+ return (
20
+ <MutationsOverTimeTextFilter
21
+ setFilterValue={(value) => {
22
+ setFilterValue(value);
23
+ setState(value);
24
+ }}
25
+ value={state}
26
+ />
27
+ );
28
+ };
29
+
30
+ export const MutationsOverTimeTextFilterStory: StoryObj<TextFilterProps> = {
31
+ render: (args) => {
32
+ return <WrapperWithState setFilterValue={args.setFilterValue} value={args.value} />;
33
+ },
34
+ args: {
35
+ setFilterValue: fn(),
36
+ value: 'Test',
37
+ },
38
+ play: async ({ canvasElement, step }) => {
39
+ const canvas = within(canvasElement);
40
+
41
+ await step('Expect initial value to show on the button', async () => {
42
+ const button = canvas.getByRole('button');
43
+ await expect(button).toHaveTextContent('Test');
44
+ });
45
+
46
+ await step('Change filter and expect it to show on the button', async () => {
47
+ const button = canvas.getByRole('button');
48
+ await userEvent.click(button);
49
+
50
+ const inputField = canvas.getByRole('textbox');
51
+ await userEvent.clear(inputField);
52
+ await userEvent.type(inputField, 'OtherText');
53
+
54
+ await waitFor(() => expect(button).toHaveTextContent('OtherText'));
55
+ });
56
+ },
57
+ };
@@ -0,0 +1,63 @@
1
+ import type { h } from 'preact';
2
+ import { useCallback, useEffect, useState } from 'preact/hooks';
3
+
4
+ import { Dropdown } from './dropdown';
5
+ import { DeleteIcon } from '../shared/icons/DeleteIcon';
6
+
7
+ export type TextFilterProps = { setFilterValue: (newValue: string) => void; value: string };
8
+
9
+ export function MutationsOverTimeTextFilter({ setFilterValue, value }: TextFilterProps) {
10
+ const onInput = (newValue: string) => {
11
+ setFilterValue(newValue);
12
+ };
13
+
14
+ const onDeleteClick = () => setFilterValue('');
15
+
16
+ return (
17
+ <div className={'w-28 inline-flex'}>
18
+ <Dropdown buttonTitle={value === '' ? `Filter mutations` : value} placement={'bottom-start'}>
19
+ <div>
20
+ <label className='flex gap-1 input input-xs'>
21
+ <DebouncedInput placeholder={'Filter'} onInput={onInput} value={value} type='text' />
22
+ {value !== undefined && value !== '' && (
23
+ <button className={'cursor-pointer'} onClick={onDeleteClick}>
24
+ <DeleteIcon />
25
+ </button>
26
+ )}
27
+ </label>
28
+ </div>
29
+ </Dropdown>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ function DebouncedInput({
35
+ value: initialValue,
36
+ onInput,
37
+ debounce = 500,
38
+ ...props
39
+ }: {
40
+ onInput: (value: string) => void;
41
+ debounce?: number;
42
+ value?: string;
43
+ } & Omit<h.JSX.IntrinsicElements['input'], 'onInput'>) {
44
+ const [value, setValue] = useState<string | undefined>(initialValue);
45
+
46
+ useEffect(() => {
47
+ setValue(initialValue);
48
+ }, [initialValue]);
49
+
50
+ useEffect(() => {
51
+ const timeout = setTimeout(() => {
52
+ onInput(value ?? '');
53
+ }, debounce);
54
+
55
+ return () => clearTimeout(timeout);
56
+ }, [value, debounce, onInput]);
57
+
58
+ const onChangeInput = useCallback((event: h.JSX.TargetedEvent<HTMLInputElement>) => {
59
+ setValue(event.currentTarget.value);
60
+ }, []);
61
+
62
+ return <input {...props} value={value} onInput={onChangeInput} />;
63
+ }
@@ -4,6 +4,7 @@ import { type FunctionComponent } from 'preact';
4
4
  import { useState } from 'preact/hooks';
5
5
 
6
6
  import { type DisplayedSegment, SegmentSelector, type SegmentSelectorProps } from './segment-selector';
7
+ import type { SequenceType } from '../../types';
7
8
 
8
9
  const meta: Meta<SegmentSelectorProps> = {
9
10
  title: 'Component/Segment selector',
@@ -15,7 +16,8 @@ export default meta;
15
16
 
16
17
  const WrapperWithState: FunctionComponent<{
17
18
  displayedSegments: DisplayedSegment[];
18
- }> = ({ displayedSegments: initialDisplayedSegments }) => {
19
+ sequenceType: SequenceType;
20
+ }> = ({ displayedSegments: initialDisplayedSegments, sequenceType }) => {
19
21
  const [displayedSegments, setDisplayedSegments] = useState<DisplayedSegment[]>(initialDisplayedSegments);
20
22
 
21
23
  return (
@@ -24,6 +26,7 @@ const WrapperWithState: FunctionComponent<{
24
26
  setDisplayedSegments={(items: DisplayedSegment[]) => {
25
27
  setDisplayedSegments(items);
26
28
  }}
29
+ sequenceType={sequenceType}
27
30
  />
28
31
  );
29
32
  };
@@ -50,12 +53,13 @@ export const AllSegmentsSelected: StoryObj<SegmentSelectorProps> = {
50
53
  checked: true,
51
54
  },
52
55
  ],
56
+ sequenceType: 'amino acid',
53
57
  },
54
58
  play: async ({ canvasElement, step }) => {
55
59
  const canvas = within(canvasElement);
56
60
 
57
- await step("Show 'All segments' as label", async () => {
58
- await expect(canvas.getByText('All segments')).toBeInTheDocument();
61
+ await step("Show 'All genes' as label", async () => {
62
+ await expect(canvas.getByText('All genes')).toBeInTheDocument();
59
63
  });
60
64
  },
61
65
  };
@@ -80,6 +84,7 @@ export const NoSegmentsSelected: StoryObj<SegmentSelectorProps> = {
80
84
  checked: false,
81
85
  },
82
86
  ],
87
+ sequenceType: 'nucleotide',
83
88
  },
84
89
  play: async ({ canvasElement, step }) => {
85
90
  const canvas = within(canvasElement);
@@ -110,12 +115,13 @@ export const LongSegmentsSelected: StoryObj<SegmentSelectorProps> = {
110
115
  checked: true,
111
116
  },
112
117
  ],
118
+ sequenceType: 'amino acid',
113
119
  },
114
120
  play: async ({ canvasElement, step }) => {
115
121
  const canvas = within(canvasElement);
116
122
 
117
- await step('Show number of active segments as label', async () => {
118
- await expect(canvas.getByText('2 segments')).toBeInTheDocument();
123
+ await step('Show number of active genes as label', async () => {
124
+ await expect(canvas.getByText('2 genes')).toBeInTheDocument();
119
125
  });
120
126
  },
121
127
  };
@@ -140,6 +146,7 @@ export const ShortSegmentsSelected: StoryObj<SegmentSelectorProps> = {
140
146
  checked: false,
141
147
  },
142
148
  ],
149
+ sequenceType: 'amino acid',
143
150
  },
144
151
  play: async ({ canvasElement, step }) => {
145
152
  const canvas = within(canvasElement);
@@ -13,40 +13,44 @@ export type DisplayedSegment = CheckboxItem & {
13
13
  export type SegmentSelectorProps = {
14
14
  displayedSegments: DisplayedSegment[];
15
15
  setDisplayedSegments: (items: DisplayedSegment[]) => void;
16
+ sequenceType: SequenceType;
16
17
  };
17
18
 
18
19
  export const SegmentSelector: FunctionComponent<SegmentSelectorProps> = ({
19
20
  displayedSegments,
20
21
  setDisplayedSegments,
22
+ sequenceType,
21
23
  }) => {
22
24
  if (displayedSegments.length <= 1) {
23
25
  return null;
24
26
  }
25
27
 
26
28
  return (
27
- <div className='w-24'>
29
+ <div className='w-24 inline-flex'>
28
30
  <CheckboxSelector
29
31
  items={displayedSegments}
30
- label={getSegmentSelectorLabel(displayedSegments)}
32
+ label={getSegmentSelectorLabel(displayedSegments, sequenceType)}
31
33
  setItems={(items) => setDisplayedSegments(items)}
32
34
  />
33
35
  </div>
34
36
  );
35
37
  };
36
38
 
37
- const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[]) => {
39
+ const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[], sequenceType: SequenceType) => {
38
40
  const allSelectedSelected = displayedSegments
39
41
  .filter((segment) => segment.checked)
40
42
  .map((segment) => segment.segment);
41
43
 
44
+ const label = sequenceType === 'amino acid' ? 'gene' : 'segment';
45
+
42
46
  if (allSelectedSelected.length === 0) {
43
- return `No segments`;
47
+ return `No ${label}s`;
44
48
  }
45
49
  if (displayedSegments.length === allSelectedSelected.length) {
46
- return `All segments`;
50
+ return `All ${label}s`;
47
51
  }
48
52
 
49
- const longestDisplayString = `All segments`;
53
+ const longestDisplayString = `All ${label}s`;
50
54
 
51
55
  const allSelectedSelectedString = allSelectedSelected.join(', ');
52
56
 
@@ -54,7 +58,7 @@ const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[]) => {
54
58
  return allSelectedSelectedString;
55
59
  }
56
60
 
57
- return `${allSelectedSelected.length} ${allSelectedSelected.length === 1 ? 'segment' : 'segments'}`;
61
+ return `${allSelectedSelected.length} ${allSelectedSelected.length === 1 ? label : `${label}s`}`;
58
62
  };
59
63
 
60
64
  export function useDisplayedSegments(sequenceType: SequenceType) {
@@ -172,7 +172,11 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
172
172
  setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
173
173
  setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
174
174
  />
175
- <SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
175
+ <SegmentSelector
176
+ displayedSegments={displayedSegments}
177
+ setDisplayedSegments={setDisplayedSegments}
178
+ sequenceType={originalComponentProps.sequenceType}
179
+ />
176
180
  <MutationTypeSelector
177
181
  displayedMutationTypes={displayedMutationTypes}
178
182
  setDisplayedMutationTypes={setDisplayedMutationTypes}