@genspectrum/dashboard-components 1.16.0 → 1.17.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 +3 -3
- package/dist/components.d.ts +4 -4
- package/dist/components.js +449 -246
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +6 -6
- package/package.json +1 -1
- package/src/preact/MutationAnnotationsContext.tsx +1 -1
- package/src/preact/components/csv-download-button.tsx +22 -14
- package/src/preact/components/features-over-time-grid.tsx +189 -43
- package/src/preact/components/mutations-over-time-mutations-filter.stories.tsx +1 -1
- package/src/preact/components/mutations-over-time-mutations-filter.tsx +1 -1
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutationsOverTimePage1.json +52 -0
- package/src/preact/mutationsOverTime/__mockData__/byWeek/mutationsOverTimePage1.json +76 -0
- package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mockDefaultMutationsOverTimeWithFilter.json +43 -0
- package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePage1.json +126 -0
- package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePage2.json +116 -0
- package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePageSize20.json +216 -0
- package/src/preact/mutationsOverTime/getFilteredMutationCodes.spec.ts +236 -0
- package/src/preact/mutationsOverTime/{getFilteredMutationsOverTimeData.ts → getFilteredMutationCodes.ts} +29 -44
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +128 -23
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +139 -74
- package/src/preact/mutationsOverTime/useMutationsOverTimePageData.ts +111 -0
- package/src/preact/shared/tanstackTable/pagination-context.tsx +5 -2
- package/src/preact/shared/tanstackTable/pagination.tsx +11 -9
- package/src/preact/shared/tanstackTable/tanstackTable.tsx +7 -4
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +1 -1
- package/src/query/queryMutationsOverTime.spec.ts +187 -662
- package/src/query/queryMutationsOverTime.ts +46 -33
- package/src/utils/useControlledState.ts +15 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +78 -22
- package/standalone-bundle/dashboard-components.js +6872 -6690
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutationsOverTime.json +0 -5496
- package/src/preact/mutationsOverTime/__mockData__/byWeek/mutationsOverTime.json +0 -7100
- package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTime.json +0 -12646
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +0 -417
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { type Dispatch, type StateUpdater,
|
|
2
|
+
import { type Dispatch, type StateUpdater, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
displayMutationsSchema,
|
|
8
|
-
getFilteredMutationOverTimeData,
|
|
9
|
-
type MutationFilter,
|
|
10
|
-
} from './getFilteredMutationsOverTimeData';
|
|
5
|
+
import { displayMutationsSchema, getFilteredMutationCodes, type MutationFilter } from './getFilteredMutationCodes';
|
|
11
6
|
import { MutationsOverTimeGridTooltip } from './mutations-over-time-grid-tooltip';
|
|
12
|
-
import { type ProportionValue, getProportion, queryMutationsOverTimeData } from '../../query/queryMutationsOverTime';
|
|
13
7
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from '../../
|
|
8
|
+
getProportion,
|
|
9
|
+
type MutationsOverTimeMetadata,
|
|
10
|
+
type ProportionValue,
|
|
11
|
+
queryMutationsOverTimeMetadata,
|
|
12
|
+
queryMutationsOverTimePage,
|
|
13
|
+
} from '../../query/queryMutationsOverTime';
|
|
14
|
+
import { lapisFilterSchema, sequenceTypeSchema, temporalGranularitySchema, views } from '../../types';
|
|
20
15
|
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
21
16
|
import { type Temporal, toTemporalClass } from '../../utils/temporalClass';
|
|
22
17
|
import { useDispatchFinishedLoadingEvent } from '../../utils/useDispatchFinishedLoadingEvent';
|
|
23
18
|
import { useLapisUrl } from '../LapisUrlContext';
|
|
24
19
|
import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
|
|
20
|
+
import { type MutationOverTimeDataMap } from './MutationOverTimeData';
|
|
25
21
|
import { AnnotatedMutation } from '../components/annotated-mutation';
|
|
26
22
|
import { type ColorScale } from '../components/color-scale-selector';
|
|
27
23
|
import { ColorScaleSelectorDropdown } from '../components/color-scale-selector-dropdown';
|
|
28
24
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
29
25
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
30
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
customColumnSchema,
|
|
28
|
+
type FeatureRenderer,
|
|
29
|
+
FeaturesOverTimeGridServerPaginated,
|
|
30
|
+
} from '../components/features-over-time-grid';
|
|
31
31
|
import { Fullscreen } from '../components/fullscreen';
|
|
32
32
|
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
|
|
33
33
|
import { LoadingDisplay } from '../components/loading-display';
|
|
@@ -40,8 +40,9 @@ import { ResizeContainer } from '../components/resize-container';
|
|
|
40
40
|
import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/segment-selector';
|
|
41
41
|
import Tabs from '../components/tabs';
|
|
42
42
|
import { pageSizesSchema } from '../shared/tanstackTable/pagination';
|
|
43
|
-
import { PageSizeContextProvider } from '../shared/tanstackTable/pagination-context';
|
|
43
|
+
import { PageSizeContextProvider, usePageSizeContext } from '../shared/tanstackTable/pagination-context';
|
|
44
44
|
import { useQuery } from '../useQuery';
|
|
45
|
+
import { handleHideGaps, useMutationsOverTimePageData } from './useMutationsOverTimePageData';
|
|
45
46
|
|
|
46
47
|
const mutationsOverTimeViewSchema = z.literal(views.grid);
|
|
47
48
|
export type MutationsOverTimeView = z.infer<typeof mutationsOverTimeViewSchema>;
|
|
@@ -83,47 +84,68 @@ export const MutationsOverTime: FunctionComponent<MutationsOverTimeProps> = (com
|
|
|
83
84
|
|
|
84
85
|
export const MutationsOverTimeInner: FunctionComponent<MutationsOverTimeProps> = ({ ...componentProps }) => {
|
|
85
86
|
const lapis = useLapisUrl();
|
|
86
|
-
const { lapisFilter, sequenceType, granularity, lapisDateField, displayMutations } = componentProps;
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const { lapisFilter, sequenceType, granularity, lapisDateField, displayMutations, pageSizes } = componentProps;
|
|
88
|
+
|
|
89
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
90
|
+
|
|
91
|
+
const {
|
|
92
|
+
data: metadata,
|
|
93
|
+
error: metadataError,
|
|
94
|
+
isLoading: metadataLoading,
|
|
95
|
+
} = useQuery(() => {
|
|
96
|
+
setPageIndex(0);
|
|
97
|
+
return queryMutationsOverTimeMetadata(
|
|
98
|
+
lapisFilter,
|
|
99
|
+
sequenceType,
|
|
100
|
+
lapis,
|
|
101
|
+
lapisDateField,
|
|
102
|
+
granularity,
|
|
103
|
+
displayMutations,
|
|
104
|
+
);
|
|
105
|
+
}, [granularity, lapis, lapisDateField, lapisFilter, sequenceType, displayMutations]);
|
|
93
106
|
|
|
94
|
-
if (
|
|
107
|
+
if (metadataLoading) {
|
|
95
108
|
return <LoadingDisplay />;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
if (
|
|
99
|
-
throw
|
|
111
|
+
if (metadataError) {
|
|
112
|
+
throw metadataError;
|
|
100
113
|
}
|
|
101
114
|
|
|
102
|
-
if (
|
|
115
|
+
if (metadata.overallMutationData.length === 0) {
|
|
103
116
|
return <NoDataDisplay />;
|
|
104
117
|
}
|
|
105
118
|
|
|
106
|
-
const { overallMutationData, mutationOverTimeData } = data;
|
|
107
119
|
return (
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
<PageSizeContextProvider pageSizes={pageSizes}>
|
|
121
|
+
<MutationsOverTimeTabs
|
|
122
|
+
metadata={metadata}
|
|
123
|
+
originalComponentProps={componentProps}
|
|
124
|
+
pageIndex={pageIndex}
|
|
125
|
+
setPageIndex={setPageIndex}
|
|
126
|
+
/>
|
|
127
|
+
</PageSizeContextProvider>
|
|
113
128
|
);
|
|
114
129
|
};
|
|
115
130
|
|
|
116
131
|
type MutationOverTimeTabsProps = {
|
|
117
|
-
|
|
132
|
+
metadata: MutationsOverTimeMetadata;
|
|
118
133
|
originalComponentProps: MutationsOverTimeProps;
|
|
119
|
-
|
|
134
|
+
pageIndex: number;
|
|
135
|
+
setPageIndex: Dispatch<StateUpdater<number>>;
|
|
120
136
|
};
|
|
121
137
|
|
|
122
138
|
const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
123
|
-
|
|
139
|
+
metadata,
|
|
124
140
|
originalComponentProps,
|
|
125
|
-
|
|
141
|
+
pageIndex,
|
|
142
|
+
setPageIndex,
|
|
126
143
|
}) => {
|
|
144
|
+
const lapis = useLapisUrl();
|
|
145
|
+
const { lapisFilter, sequenceType, lapisDateField } = originalComponentProps;
|
|
146
|
+
const { overallMutationData, requestedDateRanges } = metadata;
|
|
147
|
+
const { pageSize } = usePageSizeContext();
|
|
148
|
+
|
|
127
149
|
const tabsRef = useDispatchFinishedLoadingEvent();
|
|
128
150
|
const tooltipPortalTargetRef = useRef<HTMLDivElement>(null);
|
|
129
151
|
const [tooltipPortalTarget, setTooltipPortalTarget] = useState<HTMLDivElement | null>(null);
|
|
@@ -148,44 +170,65 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
148
170
|
]);
|
|
149
171
|
|
|
150
172
|
const [hideGaps, setHideGaps] = useState<boolean>(originalComponentProps.hideGaps ?? false);
|
|
151
|
-
|
|
152
173
|
useEffect(() => setHideGaps(originalComponentProps.hideGaps ?? false), [originalComponentProps.hideGaps]);
|
|
153
174
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
const filteredMutationCodes = useMemo(
|
|
176
|
+
() =>
|
|
177
|
+
getFilteredMutationCodes({
|
|
178
|
+
overallMutationData,
|
|
179
|
+
displayedSegments,
|
|
180
|
+
displayedMutationTypes,
|
|
181
|
+
proportionInterval,
|
|
182
|
+
mutationFilterValue,
|
|
183
|
+
sequenceType: originalComponentProps.sequenceType,
|
|
184
|
+
annotationProvider,
|
|
185
|
+
}),
|
|
186
|
+
[
|
|
157
187
|
overallMutationData,
|
|
158
188
|
displayedSegments,
|
|
159
189
|
displayedMutationTypes,
|
|
160
190
|
proportionInterval,
|
|
161
|
-
|
|
191
|
+
originalComponentProps.sequenceType,
|
|
162
192
|
mutationFilterValue,
|
|
163
|
-
sequenceType: originalComponentProps.sequenceType,
|
|
164
193
|
annotationProvider,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
194
|
+
],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
setPageIndex(0);
|
|
199
|
+
}, [filteredMutationCodes, setPageIndex]);
|
|
200
|
+
|
|
201
|
+
const totalFilteredRows = filteredMutationCodes.length;
|
|
202
|
+
const {
|
|
203
|
+
isLoading: isPageLoading,
|
|
204
|
+
data: pageData,
|
|
205
|
+
pageMutationCodes,
|
|
206
|
+
} = useMutationsOverTimePageData(
|
|
207
|
+
filteredMutationCodes,
|
|
208
|
+
pageIndex,
|
|
209
|
+
pageSize,
|
|
210
|
+
lapisFilter,
|
|
211
|
+
lapis,
|
|
212
|
+
lapisDateField,
|
|
213
|
+
sequenceType,
|
|
214
|
+
requestedDateRanges,
|
|
172
215
|
hideGaps,
|
|
173
|
-
|
|
174
|
-
mutationFilterValue,
|
|
175
|
-
annotationProvider,
|
|
176
|
-
]);
|
|
216
|
+
);
|
|
177
217
|
|
|
178
|
-
const mutationRenderer: FeatureRenderer<Substitution | Deletion> =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
218
|
+
const mutationRenderer: FeatureRenderer<Substitution | Deletion> = useMemo(
|
|
219
|
+
() => ({
|
|
220
|
+
asString: (value: Substitution | Deletion) => value.code,
|
|
221
|
+
renderRowLabel: (value: Substitution | Deletion) => (
|
|
222
|
+
<div className={'text-center'}>
|
|
223
|
+
<AnnotatedMutation mutation={value} sequenceType={originalComponentProps.sequenceType} />
|
|
224
|
+
</div>
|
|
225
|
+
),
|
|
226
|
+
renderTooltip: (value: Substitution | Deletion, temporal: Temporal, proportionValue: ProportionValue) => (
|
|
227
|
+
<MutationsOverTimeGridTooltip mutation={value} date={temporal} value={proportionValue} />
|
|
228
|
+
),
|
|
229
|
+
}),
|
|
230
|
+
[originalComponentProps.sequenceType],
|
|
231
|
+
);
|
|
189
232
|
|
|
190
233
|
const getTab = (view: MutationsOverTimeView) => {
|
|
191
234
|
switch (view) {
|
|
@@ -194,11 +237,17 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
194
237
|
return {
|
|
195
238
|
title: 'Grid',
|
|
196
239
|
content: (
|
|
197
|
-
<
|
|
240
|
+
<FeaturesOverTimeGridServerPaginated
|
|
198
241
|
rowLabelHeader='Mutation'
|
|
199
|
-
data={
|
|
242
|
+
data={pageData}
|
|
243
|
+
isLoading={isPageLoading}
|
|
244
|
+
loadingRowLabels={pageMutationCodes}
|
|
245
|
+
requestedDateRanges={requestedDateRanges}
|
|
200
246
|
colorScale={colorScale}
|
|
201
247
|
pageSizes={originalComponentProps.pageSizes}
|
|
248
|
+
pageIndex={pageIndex}
|
|
249
|
+
totalRows={totalFilteredRows}
|
|
250
|
+
onPageChange={setPageIndex}
|
|
202
251
|
customColumns={originalComponentProps.customColumns}
|
|
203
252
|
featureRenderer={mutationRenderer}
|
|
204
253
|
tooltipPortalTarget={tooltipPortalTarget}
|
|
@@ -221,20 +270,19 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
221
270
|
setProportionInterval={setProportionInterval}
|
|
222
271
|
hideGaps={hideGaps}
|
|
223
272
|
setHideGaps={setHideGaps}
|
|
224
|
-
filteredData={filteredData}
|
|
225
273
|
colorScale={colorScale}
|
|
226
274
|
setColorScale={setColorScale}
|
|
227
275
|
originalComponentProps={originalComponentProps}
|
|
228
276
|
setFilterValue={setMutationFilterValue}
|
|
229
277
|
mutationFilterValue={mutationFilterValue}
|
|
278
|
+
filteredMutationCodes={filteredMutationCodes}
|
|
279
|
+
metadata={metadata}
|
|
230
280
|
/>
|
|
231
281
|
);
|
|
232
282
|
|
|
233
283
|
return (
|
|
234
284
|
<div ref={tooltipPortalTargetRef}>
|
|
235
|
-
<
|
|
236
|
-
<Tabs ref={tabsRef} tabs={tabs} toolbar={toolbar} />
|
|
237
|
-
</PageSizeContextProvider>
|
|
285
|
+
<Tabs ref={tabsRef} tabs={tabs} toolbar={toolbar} />
|
|
238
286
|
</div>
|
|
239
287
|
);
|
|
240
288
|
};
|
|
@@ -249,12 +297,13 @@ type ToolbarProps = {
|
|
|
249
297
|
setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
|
|
250
298
|
hideGaps: boolean;
|
|
251
299
|
setHideGaps: Dispatch<StateUpdater<boolean>>;
|
|
252
|
-
filteredData: MutationOverTimeDataMap;
|
|
253
300
|
colorScale: ColorScale;
|
|
254
301
|
setColorScale: Dispatch<StateUpdater<ColorScale>>;
|
|
255
302
|
originalComponentProps: MutationsOverTimeProps;
|
|
256
303
|
mutationFilterValue: MutationFilter;
|
|
257
304
|
setFilterValue: Dispatch<StateUpdater<MutationFilter>>;
|
|
305
|
+
filteredMutationCodes: string[];
|
|
306
|
+
metadata: MutationsOverTimeMetadata;
|
|
258
307
|
};
|
|
259
308
|
|
|
260
309
|
const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
@@ -267,13 +316,29 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
267
316
|
setProportionInterval,
|
|
268
317
|
hideGaps,
|
|
269
318
|
setHideGaps,
|
|
270
|
-
filteredData,
|
|
271
319
|
colorScale,
|
|
272
320
|
setColorScale,
|
|
273
321
|
originalComponentProps,
|
|
274
322
|
setFilterValue,
|
|
275
323
|
mutationFilterValue,
|
|
324
|
+
filteredMutationCodes,
|
|
325
|
+
metadata,
|
|
276
326
|
}) => {
|
|
327
|
+
const lapis = useLapisUrl();
|
|
328
|
+
const { lapisFilter, sequenceType, lapisDateField } = originalComponentProps;
|
|
329
|
+
|
|
330
|
+
const getDownloadDataAsync = async (): Promise<Record<string, string | number>[]> => {
|
|
331
|
+
const pageData = await queryMutationsOverTimePage(
|
|
332
|
+
lapisFilter,
|
|
333
|
+
lapis,
|
|
334
|
+
lapisDateField,
|
|
335
|
+
sequenceType,
|
|
336
|
+
metadata.requestedDateRanges,
|
|
337
|
+
filteredMutationCodes,
|
|
338
|
+
);
|
|
339
|
+
return getDownloadData(handleHideGaps(pageData, hideGaps));
|
|
340
|
+
};
|
|
341
|
+
|
|
277
342
|
return (
|
|
278
343
|
<>
|
|
279
344
|
<MutationsOverTimeMutationsFilter setFilterValue={setFilterValue} value={mutationFilterValue} />
|
|
@@ -308,7 +373,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
308
373
|
)}
|
|
309
374
|
<CsvDownloadButton
|
|
310
375
|
className='btn btn-xs'
|
|
311
|
-
getData={
|
|
376
|
+
getData={getDownloadDataAsync}
|
|
312
377
|
filename='mutations_over_time.csv'
|
|
313
378
|
/>
|
|
314
379
|
<MutationsOverTimeInfo originalComponentProps={originalComponentProps} />
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
import { type MutationOverTimeDataMap } from './MutationOverTimeData';
|
|
4
|
+
import { type MutationsOverTimeMetadata, queryMutationsOverTimePage } from '../../query/queryMutationsOverTime';
|
|
5
|
+
import { type LapisFilter } from '../../types';
|
|
6
|
+
import { Map2dView } from '../../utils/map2d';
|
|
7
|
+
import { useQuery } from '../useQuery';
|
|
8
|
+
|
|
9
|
+
type MutationsOverTimePageQuery =
|
|
10
|
+
| { isLoading: true; data: null; pageMutationCodes: string[] }
|
|
11
|
+
| { isLoading: false; data: MutationOverTimeDataMap; pageMutationCodes: string[] };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetches the data for a single page of the mutations-over-time grid.
|
|
15
|
+
*
|
|
16
|
+
* Rather than loading all mutation data at once, this hook fetches only the rows (mutation codes)
|
|
17
|
+
* that are visible on the current page. The set of visible mutations is determined by
|
|
18
|
+
* `filteredMutationCodes` (all mutations that pass the current client-side filters), sliced to
|
|
19
|
+
* the current page according to `pageIndex` and `pageSize`.
|
|
20
|
+
*
|
|
21
|
+
* Results are cached in a `useRef`-backed Map keyed by the full query context (filter, date
|
|
22
|
+
* ranges, sequence type, and the specific mutation codes on the page), so revisiting a page does
|
|
23
|
+
* not trigger a new network request. The cache is cleared automatically whenever the underlying
|
|
24
|
+
* query inputs change (i.e. `lapisFilter`, `lapis`, `lapisDateField`, `sequenceType`, or
|
|
25
|
+
* `requestedDateRanges`), ensuring stale data is never served after the dataset changes.
|
|
26
|
+
*
|
|
27
|
+
* When `hideGaps` is true, date-range columns that contain no data are removed from the returned
|
|
28
|
+
* view before it is passed to the grid.
|
|
29
|
+
*/
|
|
30
|
+
export function useMutationsOverTimePageData(
|
|
31
|
+
filteredMutationCodes: string[],
|
|
32
|
+
pageIndex: number,
|
|
33
|
+
pageSize: number,
|
|
34
|
+
lapisFilter: LapisFilter,
|
|
35
|
+
lapis: string,
|
|
36
|
+
lapisDateField: string,
|
|
37
|
+
sequenceType: 'nucleotide' | 'amino acid',
|
|
38
|
+
requestedDateRanges: MutationsOverTimeMetadata['requestedDateRanges'],
|
|
39
|
+
hideGaps: boolean,
|
|
40
|
+
): MutationsOverTimePageQuery {
|
|
41
|
+
const pageMutationCodes = useMemo(
|
|
42
|
+
() => filteredMutationCodes.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize),
|
|
43
|
+
[filteredMutationCodes, pageIndex, pageSize],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const cache = useRef<Map<string, MutationOverTimeDataMap>>(new Map());
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
cache.current = new Map(); // make sure the cache doesn't grow indefinitely
|
|
49
|
+
}, [lapisFilter, lapis, lapisDateField, sequenceType, requestedDateRanges]);
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
data: pageData,
|
|
53
|
+
error,
|
|
54
|
+
isLoading,
|
|
55
|
+
} = useQuery(async () => {
|
|
56
|
+
const cacheKey = JSON.stringify({
|
|
57
|
+
lapisFilter,
|
|
58
|
+
lapis,
|
|
59
|
+
lapisDateField,
|
|
60
|
+
sequenceType,
|
|
61
|
+
requestedDateRanges,
|
|
62
|
+
pageMutationCodes,
|
|
63
|
+
});
|
|
64
|
+
const cachedData = cache.current.get(cacheKey);
|
|
65
|
+
if (cachedData !== undefined) {
|
|
66
|
+
return cachedData;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const newData = await queryMutationsOverTimePage(
|
|
70
|
+
lapisFilter,
|
|
71
|
+
lapis,
|
|
72
|
+
lapisDateField,
|
|
73
|
+
sequenceType,
|
|
74
|
+
requestedDateRanges,
|
|
75
|
+
pageMutationCodes,
|
|
76
|
+
);
|
|
77
|
+
cache.current.set(cacheKey, newData);
|
|
78
|
+
return newData;
|
|
79
|
+
}, [lapisFilter, lapis, lapisDateField, sequenceType, requestedDateRanges, pageMutationCodes]);
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return useMemo(() => {
|
|
86
|
+
if (isLoading) {
|
|
87
|
+
return { isLoading: true, data: null, pageMutationCodes };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { isLoading: false, data: handleHideGaps(pageData, hideGaps), pageMutationCodes };
|
|
91
|
+
}, [pageData, hideGaps, isLoading, pageMutationCodes]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns a new view on `data` where columns (date ranges) that do not contain any data are
|
|
96
|
+
* filtered out. When `hideGaps` is false the original data is returned unchanged.
|
|
97
|
+
*/
|
|
98
|
+
export function handleHideGaps(data: MutationOverTimeDataMap, hideGaps: boolean) {
|
|
99
|
+
if (!hideGaps) {
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const view = new Map2dView(data);
|
|
104
|
+
view.getSecondAxisKeys()
|
|
105
|
+
.filter((dateRange) => {
|
|
106
|
+
const vals = view.getColumn(dateRange);
|
|
107
|
+
return !vals.some((v) => (v?.type === 'value' || v?.type === 'valueWithCoverage') && v.totalCount > 0);
|
|
108
|
+
})
|
|
109
|
+
.forEach((dateRange) => view.deleteColumn(dateRange));
|
|
110
|
+
return view;
|
|
111
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createContext, type FunctionComponent } from 'preact';
|
|
2
|
-
import { type Dispatch, type StateUpdater, useContext
|
|
2
|
+
import { type Dispatch, type StateUpdater, useContext } from 'preact/hooks';
|
|
3
3
|
|
|
4
4
|
import type { PageSizes } from './pagination';
|
|
5
|
+
import { useControlledState } from '../../../utils/useControlledState';
|
|
5
6
|
|
|
6
7
|
type PageSizeContext = {
|
|
7
8
|
pageSize: number;
|
|
@@ -24,7 +25,9 @@ export type PageSizeContextProviderProps = {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export const PageSizeContextProvider: FunctionComponent<PageSizeContextProviderProps> = ({ children, pageSizes }) => {
|
|
27
|
-
const [pageSize, setPageSize] =
|
|
28
|
+
const [pageSize, setPageSize] = useControlledState(
|
|
29
|
+
typeof pageSizes === 'number' ? pageSizes : (pageSizes.at(0) ?? 10),
|
|
30
|
+
);
|
|
28
31
|
|
|
29
32
|
return <pageSizeContext.Provider value={{ pageSize, setPageSize }}>{children}</pageSizeContext.Provider>;
|
|
30
33
|
};
|
|
@@ -13,16 +13,19 @@ export type PageSizes = z.infer<typeof pageSizesSchema>;
|
|
|
13
13
|
export function Pagination({
|
|
14
14
|
table,
|
|
15
15
|
pageSizes,
|
|
16
|
+
totalRows,
|
|
16
17
|
}: PaginationProps & {
|
|
17
18
|
pageSizes: PageSizes;
|
|
19
|
+
/** Override the total row count (for server-driven pagination). */
|
|
20
|
+
totalRows: number;
|
|
18
21
|
}) {
|
|
19
22
|
return (
|
|
20
23
|
<div className='@container'>
|
|
21
24
|
<div className='flex items-center gap-x-6 gap-y-2 flex-wrap @xl:justify-end justify-center'>
|
|
22
25
|
<PageSizeSelector table={table} pageSizes={pageSizes} />
|
|
23
|
-
<PageIndicator table={table} />
|
|
26
|
+
<PageIndicator table={table} totalRows={totalRows} />
|
|
24
27
|
<div className='@xl:block hidden'>
|
|
25
|
-
<GotoPageSelector table={table} />
|
|
28
|
+
<GotoPageSelector table={table} totalRows={totalRows} />
|
|
26
29
|
</div>
|
|
27
30
|
<SelectPageButtons table={table} />
|
|
28
31
|
</div>
|
|
@@ -30,18 +33,17 @@ export function Pagination({
|
|
|
30
33
|
);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
function PageIndicator({ table }: PaginationProps) {
|
|
34
|
-
if (table.
|
|
36
|
+
function PageIndicator({ table, totalRows }: PaginationProps & { totalRows: number }) {
|
|
37
|
+
if (table.getPaginationRowModel().rows.length <= 1 && totalRows <= 1) {
|
|
35
38
|
return null;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const minRow = table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1;
|
|
39
|
-
const maxRow = minRow + table.
|
|
40
|
-
const numRows = table.getCoreRowModel().rows.length;
|
|
42
|
+
const maxRow = minRow + table.getPaginationRowModel().rows.length - 1;
|
|
41
43
|
|
|
42
44
|
return (
|
|
43
45
|
<span className='text-sm'>
|
|
44
|
-
{minRow} - {maxRow} of {
|
|
46
|
+
{minRow} - {maxRow} of {totalRows}
|
|
45
47
|
</span>
|
|
46
48
|
);
|
|
47
49
|
}
|
|
@@ -88,8 +90,8 @@ function PageSizeSelector({
|
|
|
88
90
|
);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
function GotoPageSelector({ table }: PaginationProps) {
|
|
92
|
-
if (table.getRowModel().rows.length === 0) {
|
|
93
|
+
function GotoPageSelector({ table, totalRows }: PaginationProps & { totalRows: number }) {
|
|
94
|
+
if (table.getRowModel().rows.length === 0 && totalRows === 0) {
|
|
93
95
|
return null;
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -4,8 +4,6 @@ import { useEffect, useState } from 'preact/hooks';
|
|
|
4
4
|
|
|
5
5
|
import { usePageSizeContext } from './pagination-context';
|
|
6
6
|
|
|
7
|
-
export * from '@tanstack/table-core';
|
|
8
|
-
|
|
9
7
|
export type Renderable<TProps> = VNode<TProps> | ComponentType<TProps> | undefined | null | string | number | boolean;
|
|
10
8
|
|
|
11
9
|
/*
|
|
@@ -45,12 +43,17 @@ export function usePreactTable<TData extends RowData>(options: TableOptions<TDat
|
|
|
45
43
|
},
|
|
46
44
|
}));
|
|
47
45
|
|
|
46
|
+
// When pagination is controlled externally (manualPagination: true with a state override),
|
|
47
|
+
// the caller manages pageSize themselves — skip the context sync to avoid conflicts.
|
|
48
|
+
const isControlled = options.manualPagination === true && options.state?.pagination !== undefined;
|
|
48
49
|
const { pageSize } = usePageSizeContext();
|
|
49
50
|
useEffect(
|
|
50
51
|
() => {
|
|
51
|
-
|
|
52
|
+
if (!isControlled) {
|
|
53
|
+
tableRef.current.setPageSize(pageSize);
|
|
54
|
+
}
|
|
52
55
|
},
|
|
53
|
-
[pageSize], // eslint-disable-line react-hooks/exhaustive-deps -- only run this when the pageSize changes
|
|
56
|
+
[pageSize, isControlled], // eslint-disable-line react-hooks/exhaustive-deps -- only run this when the pageSize changes
|
|
54
57
|
);
|
|
55
58
|
|
|
56
59
|
return tableRef.current;
|
|
@@ -28,7 +28,7 @@ import { type MutationOverTimeDataMap } from '../../mutationsOverTime/MutationOv
|
|
|
28
28
|
import {
|
|
29
29
|
type MutationFilter,
|
|
30
30
|
mutationOrAnnotationDoNotMatchFilter,
|
|
31
|
-
} from '../../mutationsOverTime/
|
|
31
|
+
} from '../../mutationsOverTime/getFilteredMutationCodes';
|
|
32
32
|
import { MutationsOverTimeGridTooltip } from '../../mutationsOverTime/mutations-over-time-grid-tooltip';
|
|
33
33
|
import { pageSizesSchema } from '../../shared/tanstackTable/pagination';
|
|
34
34
|
import { PageSizeContextProvider } from '../../shared/tanstackTable/pagination-context';
|