@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
@@ -0,0 +1,30 @@
1
+ import { createContext, type FunctionComponent } from 'preact';
2
+ import { type Dispatch, type StateUpdater, useContext, useState } from 'preact/hooks';
3
+
4
+ import type { PageSizes } from './pagination';
5
+
6
+ type PageSizeContext = {
7
+ pageSize: number;
8
+ setPageSize: Dispatch<StateUpdater<number>>;
9
+ };
10
+
11
+ const pageSizeContext = createContext<PageSizeContext>({
12
+ pageSize: -1,
13
+ setPageSize: () => {
14
+ throw new Error('pageSizeContext not initialized');
15
+ },
16
+ });
17
+
18
+ export function usePageSizeContext() {
19
+ return useContext(pageSizeContext);
20
+ }
21
+
22
+ export type PageSizeContextProviderProps = {
23
+ pageSizes: PageSizes;
24
+ };
25
+
26
+ export const PageSizeContextProvider: FunctionComponent<PageSizeContextProviderProps> = ({ children, pageSizes }) => {
27
+ const [pageSize, setPageSize] = useState(typeof pageSizes === 'number' ? pageSizes : (pageSizes.at(0) ?? 10));
28
+
29
+ return <pageSizeContext.Provider value={{ pageSize, setPageSize }}>{children}</pageSizeContext.Provider>;
30
+ };
@@ -1,8 +1,12 @@
1
1
  import type { Table } from '@tanstack/table-core';
2
2
  import z from 'zod';
3
3
 
4
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
- type PaginationProps = { table: Table<any> };
4
+ import { usePageSizeContext } from './pagination-context';
5
+
6
+ type PaginationProps = {
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ table: Table<any>;
9
+ };
6
10
  export const pageSizesSchema = z.union([z.array(z.number()), z.number()]);
7
11
  export type PageSizes = z.infer<typeof pageSizesSchema>;
8
12
 
@@ -13,11 +17,15 @@ export function Pagination({
13
17
  pageSizes: PageSizes;
14
18
  }) {
15
19
  return (
16
- <div className='flex items-center gap-4 justify-end flex-wrap'>
17
- <PageSizeSelector table={table} pageSizes={pageSizes} />
18
- <PageIndicator table={table} />
19
- <GotoPageSelector table={table} />
20
- <SelectPageButtons table={table} />
20
+ <div className='@container'>
21
+ <div className='flex items-center gap-x-6 gap-y-2 flex-wrap @xl:justify-end justify-center'>
22
+ <PageSizeSelector table={table} pageSizes={pageSizes} />
23
+ <PageIndicator table={table} />
24
+ <div className='@xl:block hidden'>
25
+ <GotoPageSelector table={table} />
26
+ </div>
27
+ <SelectPageButtons table={table} />
28
+ </div>
21
29
  </div>
22
30
  );
23
31
  }
@@ -27,34 +35,46 @@ function PageIndicator({ table }: PaginationProps) {
27
35
  return null;
28
36
  }
29
37
 
38
+ const minRow = table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1;
39
+ const maxRow = minRow + table.getRowModel().rows.length - 1;
40
+ const numRows = table.getCoreRowModel().rows.length;
41
+
30
42
  return (
31
- <span className='flex items-center gap-1'>
32
- <div>Page</div>
33
- <strong>
34
- {table.getState().pagination.pageIndex + 1} of {table.getPageCount().toLocaleString()}
35
- </strong>
43
+ <span className='text-sm'>
44
+ {minRow} - {maxRow} of {numRows}
36
45
  </span>
37
46
  );
38
47
  }
39
48
 
49
+ const heightForSmallerLines = 'h-[calc(var(--size)*0.7)]';
50
+
40
51
  function PageSizeSelector({
41
52
  table,
42
53
  pageSizes,
43
54
  }: PaginationProps & {
44
55
  pageSizes: PageSizes;
45
56
  }) {
57
+ const { pageSize, setPageSize } = usePageSizeContext();
58
+
46
59
  if (typeof pageSizes === 'number' || pageSizes.length <= 1) {
47
60
  return null;
48
61
  }
49
62
 
50
63
  return (
51
- <label className='flex items-center gap-2'>
52
- <div className={'text-nowrap'}>Rows per page:</div>
64
+ <label className='flex items-center'>
65
+ <div className={'text-nowrap text-sm'}>Rows per page:</div>
53
66
  <select
54
- className={'select'}
55
- value={table.getState().pagination.pageSize}
67
+ className={`select select-ghost select-sm ${heightForSmallerLines}`}
68
+ value={pageSize}
56
69
  onChange={(e) => {
57
- table.setPageSize(Number(e.currentTarget?.value));
70
+ const pageSize = Number(e.currentTarget?.value);
71
+ if (Number.isNaN(pageSize)) {
72
+ throw new Error(
73
+ `Invalid page size selected: The value ${e.currentTarget?.value} could not be parsed as a number.`,
74
+ );
75
+ }
76
+ setPageSize(pageSize);
77
+ table.setPageSize(pageSize);
58
78
  }}
59
79
  aria-label='Select number of rows per page'
60
80
  >
@@ -74,8 +94,8 @@ function GotoPageSelector({ table }: PaginationProps) {
74
94
  }
75
95
 
76
96
  return (
77
- <label className='flex items-center'>
78
- Go to page:
97
+ <label className='items-center flex'>
98
+ <span className='text-nowrap text-sm'>Go to page:</span>
79
99
  <input
80
100
  type='number'
81
101
  min='1'
@@ -83,9 +103,9 @@ function GotoPageSelector({ table }: PaginationProps) {
83
103
  defaultValue={table.getState().pagination.pageIndex + 1}
84
104
  onChange={(e) => {
85
105
  const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 : 0;
86
- table.setPageIndex(page);
106
+ table.setPageIndex(Math.min(page, table.getPageCount() - 1));
87
107
  }}
88
- className='input'
108
+ className={`input input-ghost input-sm ${heightForSmallerLines}`}
89
109
  aria-label='Enter page number to go to'
90
110
  />
91
111
  </label>
@@ -1,17 +1,23 @@
1
1
  import { createTable, type RowData, type TableOptions, type TableOptionsResolved } from '@tanstack/table-core';
2
2
  import { type ComponentType, h, type VNode } from 'preact';
3
- import { useState } from 'preact/hooks';
3
+ import { useEffect, useState } from 'preact/hooks';
4
4
 
5
- export * from '@tanstack/table-core';
5
+ import { usePageSizeContext } from './pagination-context';
6
6
 
7
- // Adapted from https://github.com/TanStack/table/blob/55ea94863b6b6e6d17bd51ecda61c6a6a1262c88/packages/preact-table/src/FlexRender.tsx
7
+ export * from '@tanstack/table-core';
8
8
 
9
9
  export type Renderable<TProps> = VNode<TProps> | ComponentType<TProps> | undefined | null | string | number | boolean;
10
10
 
11
+ /*
12
+ * Adapted from https://github.com/TanStack/table/blob/55ea94863b6b6e6d17bd51ecda61c6a6a1262c88/packages/preact-table/src/FlexRender.tsx
13
+ */
11
14
  export function flexRender<TProps extends object>(Comp: Renderable<TProps>, props: TProps) {
12
15
  return !Comp ? null : typeof Comp === 'function' ? <Comp {...props} /> : Comp;
13
16
  }
14
17
 
18
+ /*
19
+ * Taken from https://github.com/TanStack/table/blob/f7bf6f1adfa4f8b28b9968b29745f2452d4be9d8/packages/react-table/src/index.tsx
20
+ */
15
21
  export function usePreactTable<TData extends RowData>(options: TableOptions<TData>) {
16
22
  const resolvedOptions: TableOptionsResolved<TData> = {
17
23
  state: {},
@@ -39,5 +45,13 @@ export function usePreactTable<TData extends RowData>(options: TableOptions<TDat
39
45
  },
40
46
  }));
41
47
 
48
+ const { pageSize } = usePageSizeContext();
49
+ useEffect(
50
+ () => {
51
+ tableRef.current.setPageSize(pageSize);
52
+ },
53
+ [pageSize], // eslint-disable-line react-hooks/exhaustive-deps -- only run this when the pageSize changes
54
+ );
55
+
42
56
  return tableRef.current;
43
57
  }
@@ -1,5 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect } from '@storybook/test';
2
+ import { expect, userEvent } from '@storybook/test';
3
3
 
4
4
  import { WastewaterMutationsOverTime, type WastewaterMutationsOverTimeProps } from './wastewater-mutations-over-time';
5
5
  import { WISE_DETAILS_ENDPOINT, WISE_LAPIS_URL } from '../../../constants';
@@ -67,6 +67,24 @@ export const Default: StoryObj<WastewaterMutationsOverTimeProps> = {
67
67
  },
68
68
  };
69
69
 
70
+ export const ChangingRowsPerPageChangesItForEveryTag: StoryObj<WastewaterMutationsOverTimeProps> = {
71
+ ...Default,
72
+ play: async ({ canvas, step }) => {
73
+ await step('Wait for component to render', async () => {
74
+ await canvas.findByText('Lugano');
75
+ });
76
+
77
+ const getRowsPerPageSelectors = async () => await canvas.findAllByLabelText('Rows per page', { exact: false });
78
+
79
+ await step('change rows per page', async () => {
80
+ await expect((await getRowsPerPageSelectors())[0]).toHaveValue('10');
81
+ await expect((await getRowsPerPageSelectors())[1]).toHaveValue('10');
82
+ await userEvent.selectOptions((await getRowsPerPageSelectors())[0], '20');
83
+ await expect((await getRowsPerPageSelectors())[1]).toHaveValue('20');
84
+ });
85
+ },
86
+ };
87
+
70
88
  export const AminoAcids: StoryObj<WastewaterMutationsOverTimeProps> = {
71
89
  ...Default,
72
90
  args: {
@@ -75,11 +93,11 @@ export const AminoAcids: StoryObj<WastewaterMutationsOverTimeProps> = {
75
93
  },
76
94
  play: async ({ canvas, step }) => {
77
95
  await step('Wait for component to render', async () => {
78
- await canvas.findByText('All segments');
96
+ await canvas.findByText('All genes');
79
97
  });
80
98
 
81
- await step("Click 'All segments' button", async () => {
82
- canvas.getByRole('button', { name: 'All segments' }).click();
99
+ await step("Click 'All genes' button", async () => {
100
+ canvas.getByRole('button', { name: 'All genes' }).click();
83
101
  await expect(canvas.getByText('Select none')).toBeInTheDocument();
84
102
  canvas.getByRole('button', { name: 'Select none' }).click();
85
103
  await canvas.findAllByText('No data available for your filters.');
@@ -19,6 +19,7 @@ import Tabs from '../../components/tabs';
19
19
  import { type MutationOverTimeDataMap } from '../../mutationsOverTime/MutationOverTimeData';
20
20
  import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid';
21
21
  import { pageSizesSchema } from '../../shared/tanstackTable/pagination';
22
+ import { PageSizeContextProvider } from '../../shared/tanstackTable/pagination-context';
22
23
  import { useQuery } from '../../useQuery';
23
24
 
24
25
  const wastewaterMutationOverTimeSchema = z.object({
@@ -161,7 +162,11 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
161
162
  />
162
163
  );
163
164
 
164
- return <Tabs tabs={tabs} toolbar={toolbar} />;
165
+ return (
166
+ <PageSizeContextProvider pageSizes={originalComponentProps.pageSizes}>
167
+ <Tabs tabs={tabs} toolbar={toolbar} />
168
+ </PageSizeContextProvider>
169
+ );
165
170
  };
166
171
 
167
172
  type ToolbarProps = {
@@ -183,7 +188,11 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
183
188
  return (
184
189
  <>
185
190
  <ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
186
- <SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
191
+ <SegmentSelector
192
+ displayedSegments={displayedSegments}
193
+ setDisplayedSegments={setDisplayedSegments}
194
+ sequenceType={originalComponentProps.sequenceType}
195
+ />
187
196
  <WastewaterMutationsOverTimeInfo originalComponentProps={originalComponentProps} />
188
197
  <Fullscreen />
189
198
  </>
@@ -107,7 +107,7 @@ export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
107
107
  ...Template,
108
108
  args: {
109
109
  ...Template.args,
110
- initialValue: ['seg1:123T', 'gene2:56', 'ins_seg2:78:AAA'],
110
+ initialValue: ['seg1:3T', 'gene2:4', 'ins_seg2:4:AAA'],
111
111
  },
112
112
  parameters: {
113
113
  fetchMock: {
@@ -163,9 +163,9 @@ export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
163
163
  });
164
164
 
165
165
  await waitFor(async () => {
166
- await expect(canvas.getByText('seg1:123T')).toBeVisible();
167
- await expect(canvas.getByText('gene2:56')).toBeVisible();
168
- await expect(canvas.getByText('ins_seg2:78:AAA')).toBeVisible();
166
+ await expect(canvas.getByText('seg1:3T')).toBeVisible();
167
+ await expect(canvas.getByText('gene2:4')).toBeVisible();
168
+ await expect(canvas.getByText('ins_seg2:4:AAA')).toBeVisible();
169
169
  });
170
170
  },
171
171
  };
@@ -16,7 +16,7 @@ import { withinShadowRoot } from '../withinShadowRoot.story';
16
16
 
17
17
  const codeExample = String.raw`
18
18
  <gs-prevalence-over-time
19
- numeratorFilter='[{ "displayName": "EG", "lapisFilter": { "country": "USA", "pangoLineage": "EG*" }}, { "displayName": "JN.1", "lapisFilter": { "country": "USA", "pangoLineage": "JN.1*" }}]'
19
+ numeratorFilters='[{ "displayName": "EG", "lapisFilter": { "country": "USA", "pangoLineage": "EG*" }}, { "displayName": "JN.1", "lapisFilter": { "country": "USA", "pangoLineage": "JN.1*" }}]'
20
20
  denominatorFilter='{ "country": "USA"}'
21
21
  granularity="month"
22
22
  smoothingWindow="0"