@genspectrum/dashboard-components 0.19.3 → 0.19.5

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 (29) hide show
  1. package/custom-elements.json +223 -0
  2. package/dist/{LineageFilterChangedEvent-b0iuroUL.js → LineageFilterChangedEvent-GgkxoF3X.js} +4 -2
  3. package/dist/{LineageFilterChangedEvent-b0iuroUL.js.map → LineageFilterChangedEvent-GgkxoF3X.js.map} +1 -1
  4. package/dist/components.d.ts +137 -20
  5. package/dist/components.js +589 -241
  6. package/dist/components.js.map +1 -1
  7. package/dist/util.d.ts +55 -20
  8. package/dist/util.js +1 -1
  9. package/package.json +1 -1
  10. package/src/preact/components/min-max-range-slider.tsx +19 -4
  11. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +4 -1
  12. package/src/preact/genomeViewer/loadGff3.spec.ts +1 -1
  13. package/src/preact/genomeViewer/loadGff3.ts +12 -6
  14. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +4 -2
  15. package/src/preact/numberRangeFilter/NumberRangeFilterChangedEvent.ts +31 -0
  16. package/src/preact/numberRangeFilter/number-range-filter.stories.tsx +383 -0
  17. package/src/preact/numberRangeFilter/number-range-filter.tsx +159 -0
  18. package/src/preact/numberRangeFilter/useSelectedRangeReducer.ts +137 -0
  19. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +35 -1
  20. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +40 -3
  21. package/src/utilEntrypoint.ts +2 -0
  22. package/src/utils/gsEventNames.ts +2 -0
  23. package/src/web-components/input/gs-number-range-filter.spec.ts +27 -0
  24. package/src/web-components/input/gs-number-range-filter.stories.ts +96 -0
  25. package/src/web-components/input/gs-number-range-filter.tsx +148 -0
  26. package/src/web-components/input/gs-text-filter.stories.ts +2 -2
  27. package/src/web-components/input/index.ts +1 -0
  28. package/standalone-bundle/dashboard-components.js +6991 -6688
  29. package/standalone-bundle/dashboard-components.js.map +1 -1
@@ -1,5 +1,6 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect, userEvent } from '@storybook/test';
2
+ import { expect, userEvent, waitFor } from '@storybook/test';
3
+ import type { Canvas } from '@storybook/types';
3
4
 
4
5
  import { WastewaterMutationsOverTime, type WastewaterMutationsOverTimeProps } from './wastewater-mutations-over-time';
5
6
  import { WISE_DETAILS_ENDPOINT, WISE_LAPIS_URL } from '../../../constants';
@@ -7,6 +8,7 @@ import referenceGenome from '../../../lapisApi/__mockData__/referenceGenome.json
7
8
  import { LapisUrlContextProvider } from '../../LapisUrlContext';
8
9
  import { ReferenceGenomeContext } from '../../ReferenceGenomeContext';
9
10
  import details from './__mockData__/details.json';
11
+ import type { MutationsOverTimeProps } from '../../mutationsOverTime/mutations-over-time';
10
12
 
11
13
  const meta: Meta<WastewaterMutationsOverTimeProps> = {
12
14
  title: 'Wastewater visualization/Wastewater mutations over time',
@@ -108,3 +110,35 @@ export const AminoAcids: StoryObj<WastewaterMutationsOverTimeProps> = {
108
110
  });
109
111
  },
110
112
  };
113
+
114
+ export const UsesMutationFilter: StoryObj<MutationsOverTimeProps> = {
115
+ ...Default,
116
+ play: async ({ canvas, step }) => {
117
+ await expectMutationOnPage(canvas, 'A966C');
118
+
119
+ await step('input filter', async () => {
120
+ const filterButton = canvas.getByRole('button', { name: 'Filter mutations' });
121
+ await userEvent.click(filterButton);
122
+
123
+ const filterInput = canvas.getByPlaceholderText('Filter');
124
+ await userEvent.type(filterInput, '26');
125
+ });
126
+
127
+ await step('should show only matching filter', async () => {
128
+ await expectMutationOnPage(canvas, 'T4026G');
129
+ await expectMutationOnPage(canvas, 'T5260C');
130
+
131
+ await waitFor(async () => {
132
+ const filteredMutation = canvas.queryByText('A966C');
133
+ await expect(filteredMutation).not.toBeInTheDocument();
134
+ });
135
+ });
136
+ },
137
+ };
138
+
139
+ async function expectMutationOnPage(canvas: Canvas, mutation: string) {
140
+ await waitFor(async () => {
141
+ const mutationOnFirstPage = canvas.getAllByText(mutation)[0];
142
+ await expect(mutationOnFirstPage).toBeVisible();
143
+ });
144
+ }
@@ -3,20 +3,23 @@ import { type Dispatch, type StateUpdater, useMemo, useState } from 'preact/hook
3
3
  import z from 'zod';
4
4
 
5
5
  import { computeWastewaterMutationsOverTimeDataPerLocation } from './computeWastewaterMutationsOverTimeDataPerLocation';
6
- import { lapisFilterSchema, sequenceTypeSchema } from '../../../types';
6
+ import { lapisFilterSchema, type SequenceType, sequenceTypeSchema } from '../../../types';
7
7
  import { Map2dView } from '../../../utils/map2d';
8
8
  import { useLapisUrl } from '../../LapisUrlContext';
9
+ import { useMutationAnnotationsProvider } from '../../MutationAnnotationsContext';
9
10
  import { type ColorScale } from '../../components/color-scale-selector';
10
11
  import { ColorScaleSelectorDropdown } from '../../components/color-scale-selector-dropdown';
11
12
  import { ErrorBoundary } from '../../components/error-boundary';
12
13
  import { Fullscreen } from '../../components/fullscreen';
13
14
  import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../../components/info';
14
15
  import { LoadingDisplay } from '../../components/loading-display';
16
+ import { MutationsOverTimeTextFilter } from '../../components/mutations-over-time-text-filter';
15
17
  import { NoDataDisplay } from '../../components/no-data-display';
16
18
  import { ResizeContainer } from '../../components/resize-container';
17
19
  import { type DisplayedSegment, SegmentSelector } from '../../components/segment-selector';
18
20
  import Tabs from '../../components/tabs';
19
21
  import { type MutationOverTimeDataMap } from '../../mutationsOverTime/MutationOverTimeData';
22
+ import { mutationOrAnnotationDoNotMatchFilter } from '../../mutationsOverTime/getFilteredMutationsOverTimeData';
20
23
  import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid';
21
24
  import { pageSizesSchema } from '../../shared/tanstackTable/pagination';
22
25
  import { PageSizeContextProvider } from '../../shared/tanstackTable/pagination-context';
@@ -111,13 +114,23 @@ type MutationOverTimeTabsProps = {
111
114
  function getFilteredMutationOverTimeData({
112
115
  data,
113
116
  displayedSegments,
117
+ mutationFilterValue,
118
+ annotationProvider,
119
+ sequenceType,
114
120
  }: {
115
121
  data: MutationOverTimeDataMap;
116
122
  displayedSegments: DisplayedSegment[];
123
+ mutationFilterValue: string;
124
+ sequenceType: SequenceType;
125
+ annotationProvider: ReturnType<typeof useMutationAnnotationsProvider>;
117
126
  }): MutationOverTimeDataMap {
118
127
  const filteredData = new Map2dView(data);
119
128
 
120
129
  const mutationsToFilterOut = data.getFirstAxisKeys().filter((entry) => {
130
+ if (mutationOrAnnotationDoNotMatchFilter(entry, sequenceType, mutationFilterValue, annotationProvider)) {
131
+ return true;
132
+ }
133
+
121
134
  return displayedSegments.some((segment) => segment.segment === entry.segment && !segment.checked);
122
135
  });
123
136
 
@@ -132,6 +145,9 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
132
145
  mutationOverTimeDataPerLocation,
133
146
  originalComponentProps,
134
147
  }) => {
148
+ const [mutationFilterValue, setMutationFilterValue] = useState('');
149
+ const annotationProvider = useMutationAnnotationsProvider();
150
+
135
151
  const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
136
152
  const [displayedSegments, setDisplayedSegments] = useDisplayedSegments(mutationOverTimeDataPerLocation);
137
153
 
@@ -141,14 +157,28 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
141
157
  title: location,
142
158
  content: (
143
159
  <MutationsOverTimeGrid
144
- data={getFilteredMutationOverTimeData({ data, displayedSegments })}
160
+ data={getFilteredMutationOverTimeData({
161
+ data,
162
+ displayedSegments,
163
+ mutationFilterValue,
164
+ annotationProvider,
165
+ sequenceType: originalComponentProps.sequenceType,
166
+ })}
145
167
  colorScale={colorScale}
146
168
  pageSizes={originalComponentProps.pageSizes}
147
169
  sequenceType={originalComponentProps.sequenceType}
148
170
  />
149
171
  ),
150
172
  })),
151
- [mutationOverTimeDataPerLocation, displayedSegments, colorScale, originalComponentProps],
173
+ [
174
+ mutationOverTimeDataPerLocation,
175
+ displayedSegments,
176
+ mutationFilterValue,
177
+ annotationProvider,
178
+ colorScale,
179
+ originalComponentProps.pageSizes,
180
+ originalComponentProps.sequenceType,
181
+ ],
152
182
  );
153
183
 
154
184
  const toolbar = (
@@ -159,6 +189,8 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
159
189
  data={mutationOverTimeDataPerLocation}
160
190
  displayedSegments={displayedSegments}
161
191
  setDisplayedSegments={setDisplayedSegments}
192
+ setFilterValue={setMutationFilterValue}
193
+ mutationFilterValue={mutationFilterValue}
162
194
  />
163
195
  );
164
196
 
@@ -176,6 +208,8 @@ type ToolbarProps = {
176
208
  data: MutationOverTimeDataPerLocation;
177
209
  displayedSegments: DisplayedSegment[];
178
210
  setDisplayedSegments: (segments: DisplayedSegment[]) => void;
211
+ mutationFilterValue: string;
212
+ setFilterValue: (filterValue: string) => void;
179
213
  };
180
214
 
181
215
  const Toolbar: FunctionComponent<ToolbarProps> = ({
@@ -184,9 +218,12 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
184
218
  originalComponentProps,
185
219
  displayedSegments,
186
220
  setDisplayedSegments,
221
+ setFilterValue,
222
+ mutationFilterValue,
187
223
  }) => {
188
224
  return (
189
225
  <>
226
+ <MutationsOverTimeTextFilter setFilterValue={setFilterValue} value={mutationFilterValue} />
190
227
  <ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
191
228
  <SegmentSelector
192
229
  displayedSegments={displayedSegments}
@@ -40,3 +40,5 @@ export { TextFilterChangedEvent } from './preact/textFilter/TextFilterChangedEve
40
40
  export type { MutationAnnotations, MutationAnnotation } from './web-components/mutation-annotations-context';
41
41
 
42
42
  export { gsEventNames } from './utils/gsEventNames';
43
+
44
+ export { type NumberRange } from './preact/numberRangeFilter/NumberRangeFilterChangedEvent';
@@ -6,4 +6,6 @@ export const gsEventNames = {
6
6
  lineageFilterChanged: 'gs-lineage-filter-changed',
7
7
  locationChanged: 'gs-location-changed',
8
8
  textFilterChanged: 'gs-text-filter-changed',
9
+ numberRangeFilterChanged: 'gs-number-range-filter-changed',
10
+ numberRangeValueChanged: 'gs-number-range-value-changed',
9
11
  } as const;
@@ -0,0 +1,27 @@
1
+ import { describe, expectTypeOf, test } from 'vitest';
2
+
3
+ import { NumberRangeFilterComponent } from './gs-number-range-filter';
4
+ import { type NumberRangeFilterProps } from '../../preact/numberRangeFilter/number-range-filter';
5
+
6
+ describe('gs-number-range-filter types', () => {
7
+ test('should match', ({}) => {
8
+ expectTypeOf(NumberRangeFilterComponent.prototype)
9
+ .toHaveProperty('value')
10
+ .toEqualTypeOf<NumberRangeFilterProps['value']>();
11
+ expectTypeOf(NumberRangeFilterComponent.prototype)
12
+ .toHaveProperty('lapisField')
13
+ .toEqualTypeOf<NumberRangeFilterProps['lapisField']>();
14
+ expectTypeOf(NumberRangeFilterComponent.prototype)
15
+ .toHaveProperty('sliderMin')
16
+ .toEqualTypeOf<NumberRangeFilterProps['sliderMin']>();
17
+ expectTypeOf(NumberRangeFilterComponent.prototype)
18
+ .toHaveProperty('sliderMax')
19
+ .toEqualTypeOf<NumberRangeFilterProps['sliderMax']>();
20
+ expectTypeOf(NumberRangeFilterComponent.prototype)
21
+ .toHaveProperty('sliderStep')
22
+ .toEqualTypeOf<NumberRangeFilterProps['sliderStep']>();
23
+ expectTypeOf(NumberRangeFilterComponent.prototype)
24
+ .toHaveProperty('width')
25
+ .toEqualTypeOf<NumberRangeFilterProps['width']>();
26
+ });
27
+ });
@@ -0,0 +1,96 @@
1
+ import '../gs-app';
2
+ import './gs-number-range-filter';
3
+
4
+ import { type Meta, type StoryObj } from '@storybook/web-components';
5
+ import { html } from 'lit';
6
+
7
+ import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
8
+ import { LAPIS_URL } from '../../constants';
9
+ import { type NumberRangeFilterProps } from '../../preact/numberRangeFilter/number-range-filter';
10
+ import { gsEventNames } from '../../utils/gsEventNames';
11
+
12
+ const codeExample = String.raw`
13
+ <gs-number-range-filter
14
+ value='{"ageFrom": 10, "ageTo": 90}'
15
+ lapisField="age"
16
+ sliderMin="0"
17
+ sliderMax="100"
18
+ sliderStep="1"
19
+ width="50%"
20
+ ></gs-number-range-filter>`;
21
+
22
+ const meta: Meta<NumberRangeFilterProps> = {
23
+ title: 'Input/Number range filter',
24
+ component: 'gs-number-range-filter',
25
+ parameters: withComponentDocs({
26
+ actions: {
27
+ handles: [gsEventNames.numberRangeFilterChanged, gsEventNames.numberRangeValueChanged],
28
+ },
29
+ componentDocs: {
30
+ opensShadowDom: true,
31
+ expectsChildren: false,
32
+ codeExample,
33
+ },
34
+ }),
35
+ tags: ['autodocs'],
36
+ argTypes: {
37
+ value: {
38
+ control: {
39
+ type: 'object',
40
+ },
41
+ },
42
+ lapisField: {
43
+ control: {
44
+ type: 'text',
45
+ },
46
+ },
47
+ sliderMin: {
48
+ control: {
49
+ type: 'number',
50
+ },
51
+ },
52
+ sliderMax: {
53
+ control: {
54
+ type: 'number',
55
+ },
56
+ },
57
+ sliderStep: {
58
+ control: {
59
+ type: 'number',
60
+ },
61
+ },
62
+ width: {
63
+ control: {
64
+ type: 'text',
65
+ },
66
+ },
67
+ },
68
+ };
69
+
70
+ export default meta;
71
+
72
+ export const Default: StoryObj<NumberRangeFilterProps> = {
73
+ render: (args) => {
74
+ return html`
75
+ <gs-app lapis="${LAPIS_URL}">
76
+ <gs-number-range-filter
77
+ .value=${args.value}
78
+ .lapisField=${args.lapisField}
79
+ .sliderMin=${args.sliderMin}
80
+ .sliderMax=${args.sliderMax}
81
+ .sliderStep=${args.sliderStep}
82
+ .width=${args.width}
83
+ >
84
+ </gs-number-range-filter>
85
+ </gs-app>
86
+ `;
87
+ },
88
+ args: {
89
+ lapisField: 'age',
90
+ value: { min: 10, max: 90 },
91
+ sliderMin: 0,
92
+ sliderMax: 100,
93
+ sliderStep: 0.1,
94
+ width: '100%',
95
+ },
96
+ };
@@ -0,0 +1,148 @@
1
+ import { customElement, property } from 'lit/decorators.js';
2
+ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
+
4
+ import {
5
+ type NumberRangeFilterChangedEvent,
6
+ type NumberRangeValueChangedEvent,
7
+ } from '../../preact/numberRangeFilter/NumberRangeFilterChangedEvent';
8
+ import { NumberRangeFilter } from '../../preact/numberRangeFilter/number-range-filter';
9
+ import { type gsEventNames } from '../../utils/gsEventNames';
10
+ import { PreactLitAdapter } from '../PreactLitAdapter';
11
+
12
+ /**
13
+ *
14
+ * ## Context
15
+ *
16
+ * This component lets the user specify filters for a `float` or `int` field in LAPIS.
17
+ * It consists of two text input fields and a slider.
18
+ *
19
+ * The slider must be restricted to a certain range.
20
+ * Users can however still enter values outside of this range in the text input fields.
21
+ *
22
+ * @fires {CustomEvent<Record<string, string | undefined>>} gs-number-range-filter-changed
23
+ * Fired when the slider is released,
24
+ * `onBlur` on the input fields after the user has typed a valid range in the input fields,
25
+ * or when one of the input fields is cleared.
26
+ * The `details` of this event contain an object with `${lapisField}From` and `${lapisField}To` as keys.
27
+ * The values are the numbers from the input fields or `undefined` if the input field is empty:
28
+ * ```
29
+ * {
30
+ * [`${lapisField}From`]: number | undefined
31
+ * [`${lapisField}To`]: number | undefined
32
+ * }
33
+ * ```
34
+ * Example:
35
+ * ```
36
+ * {
37
+ * ageFrom: 18,
38
+ * ageTo: undefined
39
+ * }
40
+ * ```
41
+ *
42
+ * @fires {CustomEvent<Record<string, string | undefined>>} gs-number-range-value-changed
43
+ * Similar to the `gs-number-range-filter-changed` event,
44
+ * but contains an `event.detail` that has a fixed format:
45
+ * ```
46
+ * {
47
+ * min: number | undefined
48
+ * max: number | undefined
49
+ * }
50
+ * ```
51
+ * This event should be used when you want to control this component externally.
52
+ * The `event.detail` can be used as the value of the component.
53
+ * Example:
54
+ * ```
55
+ * {
56
+ * min: 18,
57
+ * max: undefined
58
+ * }
59
+ * ```
60
+ */
61
+ @customElement('gs-number-range-filter')
62
+ export class NumberRangeFilterComponent extends PreactLitAdapter {
63
+ /**
64
+ * The value to use for this number filter.
65
+ *
66
+ * Must be of the form:
67
+ * ```
68
+ * {
69
+ * [`${lapisField}From`]: number | undefined
70
+ * [`${lapisField}To`]: number | undefined
71
+ * }
72
+ * ```
73
+ *
74
+ * This is the same format that the `gs-number-value-changed` event will emit.
75
+ */
76
+ @property({ type: Object })
77
+ value: { min?: number; max?: number } = {};
78
+
79
+ /**
80
+ * Required.
81
+ *
82
+ * The LAPIS field name to use for this text filter.
83
+ * The field must exist on this LAPIS instance.
84
+ */
85
+ @property()
86
+ lapisField = '';
87
+
88
+ /**
89
+ * The `min` value to use for the slider.
90
+ */
91
+ @property({ type: Number })
92
+ sliderMin = 0;
93
+
94
+ /**
95
+ * The `max` value to use for the slider.
96
+ */
97
+ @property({ type: Number })
98
+ sliderMax = 100;
99
+
100
+ /**
101
+ * The `step` value to use for the slider.
102
+ *
103
+ * This attribute has no effect on the text input.
104
+ */
105
+ @property({ type: Number })
106
+ sliderStep = 1;
107
+
108
+ /**
109
+ * The width of the component.
110
+ *
111
+ * Visit https://genspectrum.github.io/dashboard-components/?path=/docs/concepts-size-of-components--docs for more information.
112
+ */
113
+ @property({ type: String })
114
+ width: string = '100%';
115
+
116
+ override render() {
117
+ return (
118
+ <NumberRangeFilter
119
+ value={this.value}
120
+ lapisField={this.lapisField}
121
+ sliderMin={this.sliderMin}
122
+ sliderMax={this.sliderMax}
123
+ sliderStep={this.sliderStep}
124
+ width={this.width}
125
+ />
126
+ );
127
+ }
128
+ }
129
+
130
+ declare global {
131
+ interface HTMLElementTagNameMap {
132
+ 'gs-number-range-filter': NumberRangeFilterComponent;
133
+ }
134
+
135
+ interface HTMLElementEventMap {
136
+ [gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
137
+ [gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
138
+ }
139
+ }
140
+
141
+ declare global {
142
+ // eslint-disable-next-line @typescript-eslint/no-namespace
143
+ namespace JSX {
144
+ interface IntrinsicElements {
145
+ 'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
146
+ }
147
+ }
148
+ }
@@ -18,8 +18,8 @@ const codeExample = String.raw`
18
18
  lapisFilter='{"country": "Germany"}'
19
19
  placeholderText="Enter host name"
20
20
  value="Homo sapiens"
21
- width="50%">
22
- </gs-text-filter>`;
21
+ width="50%"
22
+ ></gs-text-filter>`;
23
23
 
24
24
  const meta: Meta<Required<TextFilterProps>> = {
25
25
  title: 'Input/Text filter',
@@ -3,3 +3,4 @@ export { LocationFilterComponent } from './gs-location-filter';
3
3
  export { TextFilterComponent } from './gs-text-filter';
4
4
  export { MutationFilterComponent } from './gs-mutation-filter';
5
5
  export { LineageFilterComponent } from './gs-lineage-filter';
6
+ export { NumberRangeFilterComponent } from './gs-number-range-filter';