@genspectrum/dashboard-components 0.16.4 → 0.17.1

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 (58) hide show
  1. package/custom-elements.json +130 -74
  2. package/dist/{LineageFilterChangedEvent-COWV-Y0k.js → LineageFilterChangedEvent-DkvWdq_G.js} +2 -2
  3. package/dist/LineageFilterChangedEvent-DkvWdq_G.js.map +1 -0
  4. package/dist/components.d.ts +64 -48
  5. package/dist/components.js +858 -242
  6. package/dist/components.js.map +1 -1
  7. package/dist/style.css +391 -12
  8. package/dist/util.d.ts +23 -25
  9. package/dist/util.js +1 -1
  10. package/package.json +2 -1
  11. package/src/preact/components/clearable-select.stories.tsx +75 -0
  12. package/src/preact/components/clearable-select.tsx +76 -0
  13. package/src/preact/components/downshift-combobox.tsx +9 -7
  14. package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +31 -33
  15. package/src/preact/dateRangeFilter/computeInitialValues.ts +2 -15
  16. package/src/preact/dateRangeFilter/date-picker.tsx +66 -0
  17. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +69 -31
  18. package/src/preact/dateRangeFilter/date-range-filter.tsx +136 -139
  19. package/src/preact/dateRangeFilter/dateRangeOption.ts +11 -11
  20. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +133 -84
  21. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +46 -16
  22. package/src/preact/mutationsOverTime/mutations-over-time.tsx +3 -0
  23. package/src/preact/shared/WithClassName/WithClassName.ts +1 -0
  24. package/src/preact/shared/icons/DeleteIcon.tsx +3 -0
  25. package/src/preact/shared/stories/expectOptionSelected.tsx +7 -0
  26. package/src/preact/shared/tanstackTable/pagination.tsx +132 -0
  27. package/src/preact/shared/tanstackTable/tanstackTable.tsx +43 -0
  28. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +2 -1
  29. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +3 -5
  30. package/src/utilEntrypoint.ts +1 -1
  31. package/src/web-components/MutationAnnotations.mdx +33 -0
  32. package/src/web-components/ResizeContainer.mdx +1 -1
  33. package/src/web-components/errorHandling.mdx +1 -1
  34. package/src/web-components/gs-app.ts +2 -2
  35. package/src/web-components/input/gs-date-range-filter.stories.ts +38 -32
  36. package/src/web-components/input/gs-date-range-filter.tsx +8 -2
  37. package/src/web-components/input/gs-lineage-filter.tsx +1 -1
  38. package/src/web-components/input/gs-location-filter.tsx +1 -1
  39. package/src/web-components/input/gs-mutation-filter.tsx +1 -1
  40. package/src/web-components/input/gs-text-filter.tsx +1 -1
  41. package/src/web-components/visualization/gs-aggregate.tsx +2 -2
  42. package/src/web-components/visualization/gs-mutation-comparison.tsx +5 -2
  43. package/src/web-components/visualization/gs-mutations-over-time.spec-d.ts +39 -0
  44. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +4 -0
  45. package/src/web-components/visualization/gs-mutations-over-time.tsx +13 -33
  46. package/src/web-components/visualization/gs-mutations.tsx +5 -2
  47. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +2 -2
  48. package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -2
  49. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +2 -2
  50. package/src/web-components/visualization/gs-sequences-by-location.tsx +2 -2
  51. package/src/web-components/visualization/gs-statistics.tsx +2 -2
  52. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.spec-d.ts +24 -0
  53. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +3 -3
  54. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +7 -38
  55. package/standalone-bundle/dashboard-components.js +18384 -16486
  56. package/standalone-bundle/dashboard-components.js.map +1 -1
  57. package/standalone-bundle/style.css +1 -1
  58. package/dist/LineageFilterChangedEvent-COWV-Y0k.js.map +0 -1
@@ -1,5 +1,6 @@
1
- import { Fragment, type FunctionComponent } from 'preact';
2
- import { useRef } from 'preact/hooks';
1
+ import { type PaginationState } from '@tanstack/table-core';
2
+ import { type FunctionComponent } from 'preact';
3
+ import { useMemo, useState } from 'preact/hooks';
3
4
 
4
5
  import { type MutationOverTimeDataMap } from './MutationOverTimeData';
5
6
  import { type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
@@ -10,108 +11,154 @@ import { AnnotatedMutation } from '../components/annotated-mutation';
10
11
  import { type ColorScale, getColorWithingScale, getTextColorForScale } from '../components/color-scale-selector';
11
12
  import Tooltip, { type TooltipPosition } from '../components/tooltip';
12
13
  import { formatProportion } from '../shared/table/formatProportion';
14
+ import { type PageSizes, Pagination } from '../shared/tanstackTable/pagination';
15
+ import {
16
+ createColumnHelper,
17
+ flexRender,
18
+ getCoreRowModel,
19
+ getPaginationRowModel,
20
+ usePreactTable,
21
+ } from '../shared/tanstackTable/tanstackTable';
13
22
 
14
23
  export interface MutationsOverTimeGridProps {
15
24
  data: MutationOverTimeDataMap;
16
25
  colorScale: ColorScale;
17
- maxNumberOfGridRows?: number;
18
26
  sequenceType: SequenceType;
27
+ pageSizes: PageSizes;
19
28
  }
20
29
 
21
- const MAX_NUMBER_OF_GRID_ROWS = 100;
22
- const MUTATION_CELL_WIDTH_REM = 8;
30
+ type RowType = { mutation: Substitution | Deletion; values: (MutationOverTimeMutationValue | undefined)[] };
23
31
 
24
32
  const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
25
33
  data,
26
34
  colorScale,
27
- maxNumberOfGridRows,
28
35
  sequenceType,
36
+ pageSizes,
29
37
  }) => {
30
- const currentMaxNumberOfGridRows = maxNumberOfGridRows ?? MAX_NUMBER_OF_GRID_ROWS;
31
- const allMutations = data.getFirstAxisKeys();
32
- const shownMutations = allMutations.slice(0, currentMaxNumberOfGridRows);
33
-
34
- const dates = data.getSecondAxisKeys();
35
-
36
- const gridRef = useRef<HTMLDivElement>(null);
38
+ const tableData = useMemo(() => {
39
+ const allMutations = data.getFirstAxisKeys();
40
+ return data.getAsArray().map((row, index) => {
41
+ return { mutation: allMutations[index], values: [...row] };
42
+ });
43
+ }, [data]);
44
+
45
+ const [pagination, setPagination] = useState<PaginationState>({
46
+ pageIndex: 0,
47
+ pageSize: typeof pageSizes === 'number' ? pageSizes : (pageSizes.at(0) ?? 10),
48
+ });
49
+
50
+ const columns = useMemo(() => {
51
+ const columnHelper = createColumnHelper<RowType>();
52
+ const dates = data.getSecondAxisKeys();
53
+
54
+ const mutationHeader = columnHelper.accessor((row) => row.mutation, {
55
+ id: 'mutation',
56
+ header: () => <span>Mutation</span>,
57
+ cell: ({ getValue }) => {
58
+ const value = getValue();
59
+ return (
60
+ <div className={'text-center'}>
61
+ <AnnotatedMutation mutation={value} sequenceType={sequenceType} />
62
+ </div>
63
+ );
64
+ },
65
+ });
66
+
67
+ const dateHeaders = dates.map((date, index) => {
68
+ return columnHelper.accessor((row) => row.values[index], {
69
+ id: `date-${index}`,
70
+ header: () => (
71
+ <div className='@container min-w-[0.05rem]'>
72
+ <p {...styleGridHeader(index, dates.length)}>{date.dateString}</p>
73
+ </div>
74
+ ),
75
+ cell: ({ getValue, row, column, table }) => {
76
+ const value = getValue();
77
+ const rowIndex = row.index;
78
+ const columnIndex = column.getIndex();
79
+ const numberOfRows = table.getRowModel().rows.length;
80
+ const numberOfColumns = table.getAllColumns().length;
81
+
82
+ return (
83
+ <div className={'text-center'}>
84
+ <ProportionCell
85
+ value={value ?? null}
86
+ date={date}
87
+ mutation={row.original.mutation}
88
+ tooltipPosition={getTooltipPosition(
89
+ rowIndex -
90
+ table.getState().pagination.pageIndex * table.getState().pagination.pageSize,
91
+ numberOfRows,
92
+ columnIndex,
93
+ numberOfColumns,
94
+ )}
95
+ colorScale={colorScale}
96
+ />
97
+ </div>
98
+ );
99
+ },
100
+ });
101
+ });
102
+
103
+ return [mutationHeader, ...dateHeaders];
104
+ }, [colorScale, data, sequenceType]);
105
+
106
+ const table = usePreactTable({
107
+ data: tableData,
108
+ columns,
109
+ getCoreRowModel: getCoreRowModel(),
110
+ getPaginationRowModel: getPaginationRowModel(),
111
+ debugTable: true,
112
+ onPaginationChange: setPagination,
113
+ state: {
114
+ pagination,
115
+ },
116
+ });
37
117
 
38
118
  return (
39
- <>
40
- {allMutations.length > currentMaxNumberOfGridRows && (
41
- <div className='pl-2'>
42
- Showing {currentMaxNumberOfGridRows} of {allMutations.length} mutations. You can narrow the filter
43
- to reduce the number of mutations.
44
- </div>
45
- )}
46
- {allMutations.length === 0 ? (
47
- <div className={'flex justify-center'}>No data available for your filters.</div>
48
- ) : (
49
- <div
50
- ref={gridRef}
51
- style={{
52
- display: 'grid',
53
- gridTemplateRows: `repeat(${shownMutations.length}, 24px)`,
54
- gridTemplateColumns: `${MUTATION_CELL_WIDTH_REM}rem repeat(${dates.length}, minmax(0.05rem, 1fr))`,
55
- }}
56
- className='text-center'
57
- >
58
- {dates.map((date, columnIndex) => (
59
- <div
60
- className='@container font-semibold'
61
- style={{ gridRowStart: 1, gridColumnStart: columnIndex + 2 }}
62
- key={date.dateString}
63
- >
64
- <p {...styleGridHeader(columnIndex, dates)}>{date.dateString}</p>
65
- </div>
119
+ <div className='w-full'>
120
+ <table className={'w-full'}>
121
+ <thead>
122
+ {table.getHeaderGroups().map((headerGroup) => (
123
+ <tr key={headerGroup.id}>
124
+ {headerGroup.headers.map((header) => (
125
+ <th key={header.id} colSpan={header.colSpan} style={{ width: `${header.getSize()}px` }}>
126
+ {header.isPlaceholder
127
+ ? null
128
+ : flexRender(header.column.columnDef.header, header.getContext())}
129
+ </th>
130
+ ))}
131
+ </tr>
66
132
  ))}
67
- {shownMutations.map((mutation, rowIndex) => {
68
- return (
69
- <Fragment key={`fragment-${mutation.code}`}>
70
- <div
71
- key={`mutation-${mutation.code}`}
72
- style={{ gridRowStart: rowIndex + 2, gridColumnStart: 1 }}
73
- className='flex items-center justify-center'
74
- >
75
- <AnnotatedMutation mutation={mutation} sequenceType={sequenceType} />
76
- </div>
77
- {dates.map((date, columnIndex) => {
78
- const value = data.get(mutation, date) ?? null;
79
- const tooltipPosition = getTooltipPosition(
80
- rowIndex,
81
- shownMutations.length,
82
- columnIndex,
83
- dates.length,
84
- );
85
- return (
86
- <div
87
- style={{ gridRowStart: rowIndex + 2, gridColumnStart: columnIndex + 2 }}
88
- key={`${mutation.code}-${date.dateString}`}
89
- >
90
- <ProportionCell
91
- value={value}
92
- date={date}
93
- mutation={mutation}
94
- tooltipPosition={tooltipPosition}
95
- colorScale={colorScale}
96
- />
97
- </div>
98
- );
99
- })}
100
- </Fragment>
101
- );
102
- })}
103
- </div>
104
- )}
105
- </>
133
+ </thead>
134
+ <tbody>
135
+ {table.getRowModel().rows.map((row) => (
136
+ <tr key={row.id}>
137
+ {row.getVisibleCells().map((cell) => (
138
+ <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
139
+ ))}
140
+ </tr>
141
+ ))}
142
+ {table.getRowModel().rows.length === 0 && (
143
+ <td colSpan={table.getFlatHeaders().length}>
144
+ <div className={'text-center'}>No data available for your filters.</div>
145
+ </td>
146
+ )}
147
+ </tbody>
148
+ </table>
149
+ <div className={'mt-2'}>
150
+ <Pagination table={table} pageSizes={pageSizes} />
151
+ </div>
152
+ </div>
106
153
  );
107
154
  };
108
155
 
109
- function styleGridHeader(columnIndex: number, dates: Temporal[]) {
156
+ function styleGridHeader(columnIndex: number, numDateColumns: number) {
110
157
  if (columnIndex === 0) {
111
158
  return { className: 'overflow-visible text-nowrap' };
112
159
  }
113
160
 
114
- if (columnIndex === dates.length - 1) {
161
+ if (columnIndex === numDateColumns - 1) {
115
162
  return { className: 'overflow-visible text-nowrap', style: { direction: 'rtl' } };
116
163
  }
117
164
 
@@ -168,9 +215,11 @@ const ProportionCell: FunctionComponent<{
168
215
  }}
169
216
  className={`w-full h-full hover:font-bold text-xs group @container`}
170
217
  >
171
- <span className='invisible @[2rem]:visible'>
172
- {value === null ? '' : formatProportion(value.proportion, 0)}
173
- </span>
218
+ {value === null ? (
219
+ <span className={'invisible'}>No data</span>
220
+ ) : (
221
+ <span className='invisible @[2rem]:visible'>{formatProportion(value.proportion, 0)}</span>
222
+ )}
174
223
  </div>
175
224
  </Tooltip>
176
225
  </div>
@@ -1,5 +1,6 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
2
  import { expect, userEvent, waitFor } from '@storybook/test';
3
+ import { type Canvas } from '@storybook/types';
3
4
 
4
5
  import { MutationsOverTime, type MutationsOverTimeProps } from './mutations-over-time';
5
6
  import { LAPIS_URL } from '../../constants';
@@ -32,6 +33,7 @@ const meta: Meta<MutationsOverTimeProps> = {
32
33
  lapisDateField: { control: 'text' },
33
34
  displayMutations: { control: 'object' },
34
35
  initialMeanProportionInterval: { control: 'object' },
36
+ pageSizes: { control: 'object' },
35
37
  },
36
38
  parameters: {
37
39
  fetchMock: {},
@@ -75,42 +77,67 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
75
77
  granularity: 'month',
76
78
  lapisDateField: 'date',
77
79
  initialMeanProportionInterval: { min: 0.05, max: 0.9 },
80
+ pageSizes: [10, 20, 30, 40, 50],
78
81
  },
79
82
  play: async ({ canvasElement }) => {
80
83
  await expectMutationAnnotation(canvasElement, 'C44T');
81
84
  },
82
85
  };
83
86
 
84
- // This test uses mock data: showMessagWhenTooManyMutations.ts (through mutationOverTimeWorker.mock.ts)
85
- export const ShowsMessageWhenTooManyMutations: StoryObj<MutationsOverTimeProps> = {
87
+ export const ShowsNoDataWhenNoMutationsAreInFilter: StoryObj<MutationsOverTimeProps> = {
86
88
  ...Default,
87
89
  args: {
88
90
  ...Default.args,
89
- lapisFilter: { dateFrom: '2023-01-01', dateTo: '2023-12-31' },
91
+ lapisFilter: { dateFrom: '1800-01-01', dateTo: '1800-01-02' },
92
+ height: '700px',
90
93
  granularity: 'year',
91
94
  },
92
95
  play: async ({ canvas }) => {
93
- await waitFor(() => expect(canvas.getByText('Showing 100 of 137 mutations.', { exact: false })).toBeVisible(), {
96
+ await waitFor(() => expect(canvas.getByText('No data available.', { exact: false })).toBeVisible(), {
94
97
  timeout: 10000,
95
98
  });
96
99
  },
97
100
  };
98
101
 
99
- export const ShowsNoDataWhenNoMutationsAreInFilter: StoryObj<MutationsOverTimeProps> = {
102
+ export const UsesPagination: StoryObj<MutationsOverTimeProps> = {
100
103
  ...Default,
101
- args: {
102
- ...Default.args,
103
- lapisFilter: { dateFrom: '1800-01-01', dateTo: '1800-01-02' },
104
- height: '700px',
105
- granularity: 'year',
106
- },
107
- play: async ({ canvas }) => {
108
- await waitFor(() => expect(canvas.getByText('No data available.', { exact: false })).toBeVisible(), {
109
- timeout: 10000,
104
+ play: async ({ canvas, step }) => {
105
+ const mutationOnFirstPage = 'C44T';
106
+ const mutationOnSecondPage = 'T21653-';
107
+ await expectMutationOnPage(canvas, mutationOnFirstPage);
108
+
109
+ await step('Navigate to next page', async () => {
110
+ canvas.getByRole('button', { name: 'Next page' }).click();
111
+
112
+ await expectMutationOnPage(canvas, mutationOnSecondPage);
113
+ });
114
+
115
+ await step('Use goto page input', async () => {
116
+ const gotoPageInput = canvas.getByRole('spinbutton', { name: 'Enter page number to go to' });
117
+ await userEvent.clear(gotoPageInput);
118
+ await userEvent.type(gotoPageInput, '1');
119
+ await userEvent.tab();
120
+
121
+ await expectMutationOnPage(canvas, mutationOnFirstPage);
122
+ });
123
+
124
+ await step('Change number of rows per page', async () => {
125
+ const pageSizeSelector = canvas.getByLabelText('Select number of rows per page');
126
+ await userEvent.selectOptions(pageSizeSelector, '20');
127
+
128
+ await expectMutationOnPage(canvas, mutationOnFirstPage);
129
+ await expectMutationOnPage(canvas, mutationOnSecondPage);
110
130
  });
111
131
  },
112
132
  };
113
133
 
134
+ async function expectMutationOnPage(canvas: Canvas, mutation: string) {
135
+ await waitFor(async () => {
136
+ const mutationOnFirstPage = canvas.getAllByText(mutation)[0];
137
+ await expect(mutationOnFirstPage).toBeVisible();
138
+ });
139
+ }
140
+
114
141
  export const ShowsNoDataMessageWhenThereAreNoDatesInFilter: StoryObj<MutationsOverTimeProps> = {
115
142
  ...Default,
116
143
  args: {
@@ -162,8 +189,11 @@ export const ShowsNoDataForStrictInitialProportionInterval: StoryObj<MutationsOv
162
189
  initialMeanProportionInterval: { min: 0.4, max: 0.41 },
163
190
  },
164
191
  play: async ({ canvas }) => {
165
- await waitFor(() =>
166
- expect(canvas.getByText('No data available for your filters.', { exact: false })).toBeVisible(),
192
+ await waitFor(
193
+ () => expect(canvas.getByText('No data available for your filters.', { exact: false })).toBeVisible(),
194
+ {
195
+ timeout: 10000,
196
+ },
167
197
  );
168
198
  },
169
199
  };
@@ -33,6 +33,7 @@ import { ProportionSelectorDropdown } from '../components/proportion-selector-dr
33
33
  import { ResizeContainer } from '../components/resize-container';
34
34
  import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/segment-selector';
35
35
  import Tabs from '../components/tabs';
36
+ import { pageSizesSchema } from '../shared/tanstackTable/pagination';
36
37
  import { useWebWorker } from '../webWorkers/useWebWorker';
37
38
 
38
39
  const mutationsOverTimeViewSchema = z.literal(views.grid);
@@ -51,6 +52,7 @@ const mutationOverTimeSchema = z.object({
51
52
  }),
52
53
  width: z.string(),
53
54
  height: z.string().optional(),
55
+ pageSizes: pageSizesSchema,
54
56
  });
55
57
  export type MutationsOverTimeProps = z.infer<typeof mutationOverTimeSchema>;
56
58
 
@@ -166,6 +168,7 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
166
168
  data={filteredData}
167
169
  colorScale={colorScale}
168
170
  sequenceType={originalComponentProps.sequenceType}
171
+ pageSizes={originalComponentProps.pageSizes}
169
172
  />
170
173
  ),
171
174
  };
@@ -0,0 +1 @@
1
+ export type WithClassName<T = object> = T & { className?: string };
@@ -0,0 +1,3 @@
1
+ export function DeleteIcon() {
2
+ return <>×</>;
3
+ }
@@ -0,0 +1,7 @@
1
+ import { expect, within } from '@storybook/test';
2
+
3
+ export const expectOptionSelected = async (canvasElement: HTMLElement, option: string) => {
4
+ const canvas = within(canvasElement);
5
+ const placeholderOption = canvas.getByRole('combobox').querySelector('option:checked');
6
+ await expect(placeholderOption).toHaveTextContent(option);
7
+ };
@@ -0,0 +1,132 @@
1
+ import type { Table } from '@tanstack/table-core';
2
+ import z from 'zod';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ type PaginationProps = { table: Table<any> };
6
+ export const pageSizesSchema = z.union([z.array(z.number()), z.number()]);
7
+ export type PageSizes = z.infer<typeof pageSizesSchema>;
8
+
9
+ export function Pagination({
10
+ table,
11
+ pageSizes,
12
+ }: PaginationProps & {
13
+ pageSizes: PageSizes;
14
+ }) {
15
+ 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} />
21
+ </div>
22
+ );
23
+ }
24
+
25
+ function PageIndicator({ table }: PaginationProps) {
26
+ if (table.getRowModel().rows.length <= 1) {
27
+ return null;
28
+ }
29
+
30
+ 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>
36
+ </span>
37
+ );
38
+ }
39
+
40
+ function PageSizeSelector({
41
+ table,
42
+ pageSizes,
43
+ }: PaginationProps & {
44
+ pageSizes: PageSizes;
45
+ }) {
46
+ if (typeof pageSizes === 'number' || pageSizes.length <= 1) {
47
+ return null;
48
+ }
49
+
50
+ return (
51
+ <label className='flex items-center gap-2'>
52
+ <div className={'text-nowrap'}>Rows per page:</div>
53
+ <select
54
+ className={'select'}
55
+ value={table.getState().pagination.pageSize}
56
+ onChange={(e) => {
57
+ table.setPageSize(Number(e.currentTarget?.value));
58
+ }}
59
+ aria-label='Select number of rows per page'
60
+ >
61
+ {pageSizes.map((pageSize) => (
62
+ <option key={pageSize} value={pageSize}>
63
+ {pageSize}
64
+ </option>
65
+ ))}
66
+ </select>
67
+ </label>
68
+ );
69
+ }
70
+
71
+ function GotoPageSelector({ table }: PaginationProps) {
72
+ if (table.getRowModel().rows.length === 0) {
73
+ return null;
74
+ }
75
+
76
+ return (
77
+ <label className='flex items-center'>
78
+ Go to page:
79
+ <input
80
+ type='number'
81
+ min='1'
82
+ max={table.getPageCount()}
83
+ defaultValue={table.getState().pagination.pageIndex + 1}
84
+ onChange={(e) => {
85
+ const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 : 0;
86
+ table.setPageIndex(page);
87
+ }}
88
+ className='input'
89
+ aria-label='Enter page number to go to'
90
+ />
91
+ </label>
92
+ );
93
+ }
94
+
95
+ function SelectPageButtons({ table }: PaginationProps) {
96
+ return (
97
+ <div className={'join'} role='group' aria-label='Pagination controls'>
98
+ <button
99
+ className='btn btn-outline join-item btn-sm'
100
+ onClick={() => table.firstPage()}
101
+ disabled={!table.getCanPreviousPage()}
102
+ aria-label='First page'
103
+ >
104
+ <div className='iconify mdi--chevron-left-first' />
105
+ </button>
106
+ <button
107
+ className='btn btn-outline join-item btn-sm'
108
+ onClick={() => table.previousPage()}
109
+ disabled={!table.getCanPreviousPage()}
110
+ aria-label='Previous page'
111
+ >
112
+ <div className='iconify mdi--chevron-left' />
113
+ </button>
114
+ <button
115
+ className='btn btn-outline join-item btn-sm'
116
+ onClick={() => table.nextPage()}
117
+ disabled={!table.getCanNextPage()}
118
+ aria-label='Next page'
119
+ >
120
+ <div className='iconify mdi--chevron-right' />
121
+ </button>
122
+ <button
123
+ className='btn btn-outline join-item btn-sm'
124
+ onClick={() => table.lastPage()}
125
+ disabled={!table.getCanNextPage()}
126
+ aria-label='Last page'
127
+ >
128
+ <div className='iconify mdi--chevron-right-last' />
129
+ </button>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,43 @@
1
+ import { createTable, type RowData, type TableOptions, type TableOptionsResolved } from '@tanstack/table-core';
2
+ import { type ComponentType, h, type VNode } from 'preact';
3
+ import { useState } from 'preact/hooks';
4
+
5
+ export * from '@tanstack/table-core';
6
+
7
+ // Adapted from https://github.com/TanStack/table/blob/55ea94863b6b6e6d17bd51ecda61c6a6a1262c88/packages/preact-table/src/FlexRender.tsx
8
+
9
+ export type Renderable<TProps> = VNode<TProps> | ComponentType<TProps> | undefined | null | string | number | boolean;
10
+
11
+ export function flexRender<TProps extends object>(Comp: Renderable<TProps>, props: TProps) {
12
+ return !Comp ? null : typeof Comp === 'function' ? <Comp {...props} /> : Comp;
13
+ }
14
+
15
+ export function usePreactTable<TData extends RowData>(options: TableOptions<TData>) {
16
+ const resolvedOptions: TableOptionsResolved<TData> = {
17
+ state: {},
18
+ onStateChange: () => {},
19
+ renderFallbackValue: null,
20
+ ...options,
21
+ };
22
+
23
+ const [tableRef] = useState(() => ({
24
+ current: createTable<TData>(resolvedOptions),
25
+ }));
26
+
27
+ const [state, setState] = useState(() => tableRef.current.initialState);
28
+
29
+ tableRef.current.setOptions((prev) => ({
30
+ ...prev,
31
+ ...options,
32
+ state: {
33
+ ...state,
34
+ ...options.state,
35
+ },
36
+ onStateChange: (updater) => {
37
+ setState(updater);
38
+ options.onStateChange?.(updater);
39
+ },
40
+ }));
41
+
42
+ return tableRef.current;
43
+ }
@@ -18,6 +18,7 @@ const meta: Meta<WastewaterMutationsOverTimeProps> = {
18
18
  options: ['nucleotide', 'amino acid'],
19
19
  control: { type: 'radio' },
20
20
  },
21
+ pageSizes: { control: 'object' },
21
22
  },
22
23
  parameters: {
23
24
  fetchMock: {},
@@ -42,7 +43,7 @@ export const Default: StoryObj<WastewaterMutationsOverTimeProps> = {
42
43
  width: '100%',
43
44
  lapisFilter: {},
44
45
  sequenceType: 'nucleotide',
45
- maxNumberOfGridRows: 100,
46
+ pageSizes: [10, 20, 30, 40, 50],
46
47
  },
47
48
  parameters: {
48
49
  fetchMock: {
@@ -16,6 +16,7 @@ import { ResizeContainer } from '../../components/resize-container';
16
16
  import Tabs from '../../components/tabs';
17
17
  import { type MutationOverTimeDataMap } from '../../mutationsOverTime/MutationOverTimeData';
18
18
  import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid';
19
+ import { pageSizesSchema } from '../../shared/tanstackTable/pagination';
19
20
  import { useQuery } from '../../useQuery';
20
21
 
21
22
  const wastewaterMutationOverTimeSchema = z.object({
@@ -23,7 +24,7 @@ const wastewaterMutationOverTimeSchema = z.object({
23
24
  sequenceType: sequenceTypeSchema,
24
25
  width: z.string(),
25
26
  height: z.string().optional(),
26
- maxNumberOfGridRows: z.number(),
27
+ pageSizes: pageSizesSchema,
27
28
  });
28
29
 
29
30
  export type WastewaterMutationsOverTimeProps = z.infer<typeof wastewaterMutationOverTimeSchema>;
@@ -76,7 +77,6 @@ export const WastewaterMutationsOverTimeInner: FunctionComponent<WastewaterMutat
76
77
  <MutationsOverTimeTabs
77
78
  mutationOverTimeDataPerLocation={mutationOverTimeDataPerLocation}
78
79
  originalComponentProps={componentProps}
79
- maxNumberOfGridRows={componentProps.maxNumberOfGridRows}
80
80
  />
81
81
  );
82
82
  };
@@ -89,13 +89,11 @@ type MutationOverTimeDataPerLocation = {
89
89
  type MutationOverTimeTabsProps = {
90
90
  mutationOverTimeDataPerLocation: MutationOverTimeDataPerLocation;
91
91
  originalComponentProps: WastewaterMutationsOverTimeProps;
92
- maxNumberOfGridRows?: number;
93
92
  };
94
93
 
95
94
  const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
96
95
  mutationOverTimeDataPerLocation,
97
96
  originalComponentProps,
98
- maxNumberOfGridRows,
99
97
  }) => {
100
98
  const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
101
99
 
@@ -105,7 +103,7 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
105
103
  <MutationsOverTimeGrid
106
104
  data={data}
107
105
  colorScale={colorScale}
108
- maxNumberOfGridRows={maxNumberOfGridRows}
106
+ pageSizes={originalComponentProps.pageSizes}
109
107
  sequenceType={originalComponentProps.sequenceType}
110
108
  />
111
109
  ),
@@ -1,6 +1,6 @@
1
1
  export {
2
2
  type DateRangeOption,
3
- type DateRangeSelectOption,
3
+ type DateRangeValue,
4
4
  dateRangeOptionPresets,
5
5
  DateRangeOptionChangedEvent,
6
6
  } from './preact/dateRangeFilter/dateRangeOption';