@genspectrum/dashboard-components 0.17.0 → 0.18.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.
- package/custom-elements.json +47 -16
- package/dist/components.d.ts +21 -17
- package/dist/components.js +426 -3654
- package/dist/components.js.map +1 -1
- package/dist/style.css +2 -3375
- package/dist/util.d.ts +15 -15
- package/package.json +6 -5
- package/src/preact/aggregatedData/aggregate.tsx +3 -3
- package/src/preact/components/clearable-select.tsx +1 -1
- package/src/preact/components/color-scale-selector-dropdown.tsx +1 -1
- package/src/preact/components/confidence-interval-selector.tsx +1 -1
- package/src/preact/components/downshift-combobox.tsx +3 -3
- package/src/preact/components/fullscreen.tsx +6 -2
- package/src/preact/components/info.tsx +1 -1
- package/src/preact/components/mutation-type-selector.tsx +1 -1
- package/src/preact/components/percent-intput.tsx +3 -3
- package/src/preact/components/proportion-selector-dropdown.tsx +1 -1
- package/src/preact/components/scaling-selector.tsx +1 -1
- package/src/preact/components/select.tsx +1 -1
- package/src/preact/components/tabs.tsx +1 -1
- package/src/preact/dateRangeFilter/date-picker.tsx +1 -1
- package/src/preact/dateRangeFilter/date-range-filter.tsx +4 -4
- package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
- package/src/preact/mutations/mutations.tsx +2 -2
- 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 +4 -1
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +1 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -1
- package/src/preact/sequencesByLocation/sequences-by-location-map.tsx +1 -1
- package/src/preact/sequencesByLocation/sequences-by-location.tsx +3 -7
- 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/styles/tailwind.css +14 -3
- package/src/web-components/input/gs-date-range-filter.stories.ts +2 -2
- package/src/web-components/input/gs-lineage-filter.stories.ts +1 -1
- package/src/web-components/input/gs-location-filter.stories.ts +1 -1
- package/src/web-components/input/gs-mutation-filter.stories.ts +1 -1
- package/src/web-components/input/gs-text-filter.stories.ts +1 -1
- 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 +17318 -15507
- 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
|
};
|
|
@@ -237,7 +240,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
237
240
|
labelPrefix='Mean proportion'
|
|
238
241
|
/>
|
|
239
242
|
<CsvDownloadButton
|
|
240
|
-
className='
|
|
243
|
+
className='btn btn-xs'
|
|
241
244
|
getData={() => getDownloadData(filteredData)}
|
|
242
245
|
filename='mutations_over_time.csv'
|
|
243
246
|
/>
|
|
@@ -167,7 +167,7 @@ const Toolbar = ({ activeTab, data, yAxisScaleType, setYAxisScaleType, originalC
|
|
|
167
167
|
/>
|
|
168
168
|
)}
|
|
169
169
|
<CsvDownloadButton
|
|
170
|
-
className='
|
|
170
|
+
className='btn btn-xs'
|
|
171
171
|
getData={() => getNumberOfSequencesOverTimeTableData(data, originalComponentProps.granularity)}
|
|
172
172
|
filename='number_of_sequences_over_time.csv'
|
|
173
173
|
/>
|
|
@@ -218,7 +218,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
218
218
|
/>
|
|
219
219
|
)}
|
|
220
220
|
<CsvDownloadButton
|
|
221
|
-
className='
|
|
221
|
+
className='btn btn-xs'
|
|
222
222
|
getData={() => getPrevalenceOverTimeTableData(data, granularity)}
|
|
223
223
|
filename='prevalence_over_time.csv'
|
|
224
224
|
/>
|
|
@@ -106,7 +106,7 @@ const DataMatchInformation: FunctionComponent<DataMatchInformationProps> = ({
|
|
|
106
106
|
|
|
107
107
|
return (
|
|
108
108
|
<Modal
|
|
109
|
-
buttonClassName='text-sm absolute bottom-0 px-1 z-
|
|
109
|
+
buttonClassName='text-sm absolute bottom-0 px-1 z-1001 bg-white rounded-sm border border-gray-200'
|
|
110
110
|
modalContent={
|
|
111
111
|
<>
|
|
112
112
|
<InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
|
|
@@ -149,15 +149,11 @@ type ToolbarProps = {
|
|
|
149
149
|
|
|
150
150
|
const Toolbar: FunctionComponent<ToolbarProps> = ({ originalComponentProps, tableData }) => {
|
|
151
151
|
return (
|
|
152
|
-
|
|
153
|
-
<CsvDownloadButton
|
|
154
|
-
className='mx-1 btn btn-xs'
|
|
155
|
-
getData={() => tableData}
|
|
156
|
-
filename='sequences_by_location.csv'
|
|
157
|
-
/>
|
|
152
|
+
<>
|
|
153
|
+
<CsvDownloadButton className='btn btn-xs' getData={() => tableData} filename='sequences_by_location.csv' />
|
|
158
154
|
<SequencesByLocationMapInfo originalComponentProps={originalComponentProps} />
|
|
159
155
|
<Fullscreen />
|
|
160
|
-
|
|
156
|
+
</>
|
|
161
157
|
);
|
|
162
158
|
};
|
|
163
159
|
|
|
@@ -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: {
|