@genspectrum/dashboard-components 0.17.0 → 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.
- package/custom-elements.json +44 -13
- package/dist/components.d.ts +18 -14
- package/dist/components.js +557 -99
- package/dist/components.js.map +1 -1
- package/dist/style.css +318 -6
- package/dist/util.d.ts +12 -12
- package/package.json +2 -1
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +133 -84
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +46 -16
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +3 -0
- package/src/preact/shared/tanstackTable/pagination.tsx +132 -0
- package/src/preact/shared/tanstackTable/tanstackTable.tsx +43 -0
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +2 -1
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +3 -5
- package/src/web-components/visualization/gs-mutations-over-time.spec-d.ts +39 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +4 -0
- package/src/web-components/visualization/gs-mutations-over-time.tsx +8 -31
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.spec-d.ts +24 -0
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +3 -3
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +5 -36
- package/standalone-bundle/dashboard-components.js +17733 -15921
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
{
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
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 ===
|
|
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
|
-
|
|
172
|
-
{
|
|
173
|
-
|
|
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
|
-
|
|
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: '
|
|
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('
|
|
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
|
|
102
|
+
export const UsesPagination: StoryObj<MutationsOverTimeProps> = {
|
|
100
103
|
...Default,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
pageSizes={originalComponentProps.pageSizes}
|
|
109
107
|
sequenceType={originalComponentProps.sequenceType}
|
|
110
108
|
/>
|
|
111
109
|
),
|