@genspectrum/dashboard-components 0.18.5 → 0.19.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.
Files changed (46) hide show
  1. package/README.md +12 -2
  2. package/custom-elements.json +3 -3
  3. package/dist/assets/{mutationOverTimeWorker--b8ZHlji.js.map → mutationOverTimeWorker-ChQTFL68.js.map} +1 -1
  4. package/dist/components.d.ts +15 -14
  5. package/dist/components.js +1602 -332
  6. package/dist/components.js.map +1 -1
  7. package/dist/util.d.ts +14 -14
  8. package/package.json +3 -4
  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 -2
  12. package/src/preact/components/min-max-range-slider.tsx +0 -2
  13. package/src/preact/components/mutations-over-time-text-filter.stories.tsx +57 -0
  14. package/src/preact/components/mutations-over-time-text-filter.tsx +63 -0
  15. package/src/preact/components/segment-selector.tsx +1 -1
  16. package/src/preact/components/table.tsx +0 -2
  17. package/src/preact/dateRangeFilter/date-picker.tsx +15 -10
  18. package/src/preact/mutationFilter/mutation-filter.stories.tsx +169 -50
  19. package/src/preact/mutationFilter/mutation-filter.tsx +239 -234
  20. package/src/preact/mutationFilter/parseAndValidateMutation.ts +62 -10
  21. package/src/preact/mutationFilter/parseMutation.spec.ts +62 -47
  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 +8 -11
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +26 -5
  27. package/src/preact/shared/tanstackTable/pagination-context.tsx +30 -0
  28. package/src/preact/shared/tanstackTable/pagination.tsx +19 -6
  29. package/src/preact/shared/tanstackTable/tanstackTable.tsx +17 -3
  30. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +19 -1
  31. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
  32. package/src/styles/replaceCssProperties.stories.tsx +49 -0
  33. package/src/styles/replaceCssProperties.ts +25 -0
  34. package/src/styles/tailwind.css +1 -0
  35. package/src/web-components/PreactLitAdapter.tsx +6 -3
  36. package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +0 -2
  37. package/src/web-components/gs-app.stories.ts +6 -2
  38. package/src/web-components/gs-app.ts +4 -1
  39. package/src/web-components/input/gs-date-range-filter.tsx +6 -0
  40. package/src/web-components/input/gs-mutation-filter.stories.ts +4 -4
  41. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
  42. package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
  43. package/standalone-bundle/dashboard-components.js +10836 -11289
  44. package/standalone-bundle/dashboard-components.js.map +1 -1
  45. package/dist/style.css +0 -392
  46. package/standalone-bundle/style.css +0 -1
package/dist/util.d.ts CHANGED
@@ -918,7 +918,7 @@ declare global {
918
918
 
919
919
  declare global {
920
920
  interface HTMLElementTagNameMap {
921
- 'gs-mutations-component': MutationsComponent;
921
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
922
922
  }
923
923
  }
924
924
 
@@ -926,7 +926,7 @@ declare global {
926
926
  declare global {
927
927
  namespace JSX {
928
928
  interface IntrinsicElements {
929
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
929
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
930
930
  }
931
931
  }
932
932
  }
@@ -934,7 +934,7 @@ declare global {
934
934
 
935
935
  declare global {
936
936
  interface HTMLElementTagNameMap {
937
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
937
+ 'gs-mutations-component': MutationsComponent;
938
938
  }
939
939
  }
940
940
 
@@ -942,7 +942,7 @@ declare global {
942
942
  declare global {
943
943
  namespace JSX {
944
944
  interface IntrinsicElements {
945
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
945
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
946
946
  }
947
947
  }
948
948
  }
@@ -1062,11 +1062,10 @@ declare global {
1062
1062
 
1063
1063
  declare global {
1064
1064
  interface HTMLElementTagNameMap {
1065
- 'gs-date-range-filter': DateRangeFilterComponent;
1065
+ 'gs-location-filter': LocationFilterComponent;
1066
1066
  }
1067
1067
  interface HTMLElementEventMap {
1068
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1069
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1068
+ 'gs-location-changed': LocationChangedEvent;
1070
1069
  }
1071
1070
  }
1072
1071
 
@@ -1074,7 +1073,7 @@ declare global {
1074
1073
  declare global {
1075
1074
  namespace JSX {
1076
1075
  interface IntrinsicElements {
1077
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1076
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1078
1077
  }
1079
1078
  }
1080
1079
  }
@@ -1082,10 +1081,10 @@ declare global {
1082
1081
 
1083
1082
  declare global {
1084
1083
  interface HTMLElementTagNameMap {
1085
- 'gs-location-filter': LocationFilterComponent;
1084
+ 'gs-text-filter': TextFilterComponent;
1086
1085
  }
1087
1086
  interface HTMLElementEventMap {
1088
- 'gs-location-changed': LocationChangedEvent;
1087
+ 'gs-text-filter-changed': TextFilterChangedEvent;
1089
1088
  }
1090
1089
  }
1091
1090
 
@@ -1093,7 +1092,7 @@ declare global {
1093
1092
  declare global {
1094
1093
  namespace JSX {
1095
1094
  interface IntrinsicElements {
1096
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1095
+ 'gs-text-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1097
1096
  }
1098
1097
  }
1099
1098
  }
@@ -1101,10 +1100,11 @@ declare global {
1101
1100
 
1102
1101
  declare global {
1103
1102
  interface HTMLElementTagNameMap {
1104
- 'gs-text-filter': TextFilterComponent;
1103
+ 'gs-date-range-filter': DateRangeFilterComponent;
1105
1104
  }
1106
1105
  interface HTMLElementEventMap {
1107
- 'gs-text-filter-changed': TextFilterChangedEvent;
1106
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1107
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1108
1108
  }
1109
1109
  }
1110
1110
 
@@ -1112,7 +1112,7 @@ declare global {
1112
1112
  declare global {
1113
1113
  namespace JSX {
1114
1114
  interface IntrinsicElements {
1115
- 'gs-text-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1115
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1116
1116
  }
1117
1117
  }
1118
1118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.18.5",
3
+ "version": "0.19.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -38,8 +38,7 @@
38
38
  "require": "./dist/util.js"
39
39
  },
40
40
  "./custom-elements.json": "./custom-elements.json",
41
- "./package.json": "./package.json",
42
- "./style.css": "./dist/style.css"
41
+ "./package.json": "./package.json"
43
42
  },
44
43
  "files": [
45
44
  "dist",
@@ -132,7 +131,7 @@
132
131
  "eslint-config-preact": "^1.3.0",
133
132
  "eslint-plugin-import": "^2.29.1",
134
133
  "eslint-plugin-jest": "^28.2.0",
135
- "eslint-plugin-storybook": "^0.11.0",
134
+ "eslint-plugin-storybook": "^0.12.0",
136
135
  "happy-dom": "^17.1.1",
137
136
  "http-server": "^14.1.1",
138
137
  "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 }) => {
@@ -103,7 +103,6 @@ function generateFullExampleCode(componentCode: string, componentName: string) {
103
103
  return `<html>
104
104
  <head>
105
105
  <script type="module" src="https://unpkg.com/@genspectrum/dashboard-components@latest/standalone-bundle/dashboard-components.js"></script>
106
- <link rel="stylesheet" href="https://unpkg.com/@genspectrum/dashboard-components@latest/dist/style.css" />
107
106
  </head>
108
107
 
109
108
  <body>
@@ -1,8 +1,6 @@
1
1
  import { type FunctionComponent, type JSX } from 'preact';
2
2
  import { useState } from 'preact/hooks';
3
3
 
4
- import './min-max-percent-slider.css';
5
-
6
4
  export interface MinMaxPercentSliderProps {
7
5
  min: number;
8
6
  max: number;
@@ -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
+ }
@@ -26,7 +26,7 @@ export const SegmentSelector: FunctionComponent<SegmentSelectorProps> = ({
26
26
  }
27
27
 
28
28
  return (
29
- <div className='w-20 inline-flex'>
29
+ <div className='w-24 inline-flex'>
30
30
  <CheckboxSelector
31
31
  items={displayedSegments}
32
32
  label={getSegmentSelectorLabel(displayedSegments, sequenceType)}
@@ -3,8 +3,6 @@ import { type OneDArray, type TColumn, type TData } from 'gridjs/dist/src/types'
3
3
  import { type ComponentChild } from 'preact';
4
4
  import { useEffect, useRef } from 'preact/hooks';
5
5
 
6
- import 'gridjs/dist/theme/mermaid.css';
7
-
8
6
  export const tableStyle = {
9
7
  table: {
10
8
  fontSize: '12px',
@@ -1,4 +1,3 @@
1
- import 'flatpickr/dist/flatpickr.min.css';
2
1
  import flatpickr from 'flatpickr';
3
2
  import { useEffect, useRef, useState } from 'preact/hooks';
4
3
 
@@ -22,8 +21,10 @@ export function DatePicker({
22
21
 
23
22
  const [datePicker, setDatePicker] = useState<flatpickr.Instance | null>(null);
24
23
 
24
+ const calendarRef = useRef<HTMLDivElement>(null);
25
+
25
26
  useEffect(() => {
26
- if (!inputRef.current) {
27
+ if (!inputRef.current || !calendarRef.current) {
27
28
  return;
28
29
  }
29
30
 
@@ -33,6 +34,7 @@ export function DatePicker({
33
34
  defaultDate: value,
34
35
  minDate,
35
36
  maxDate,
37
+ appendTo: calendarRef.current,
36
38
  });
37
39
 
38
40
  setDatePicker(instance);
@@ -54,13 +56,16 @@ export function DatePicker({
54
56
  };
55
57
 
56
58
  return (
57
- <input
58
- className={`input w-full ${className}`}
59
- type='text'
60
- placeholder={placeholderText}
61
- ref={inputRef}
62
- onChange={handleChange}
63
- onBlur={handleChange}
64
- />
59
+ <div className={'w-full'}>
60
+ <input
61
+ className={`input w-full ${className}`}
62
+ type='text'
63
+ placeholder={placeholderText}
64
+ ref={inputRef}
65
+ onChange={handleChange}
66
+ onBlur={handleChange}
67
+ />
68
+ <div ref={calendarRef} />
69
+ </div>
65
70
  );
66
71
  }