@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.
Files changed (36) hide show
  1. package/custom-elements.json +3 -3
  2. package/dist/components.d.ts +4 -4
  3. package/dist/components.js +449 -246
  4. package/dist/components.js.map +1 -1
  5. package/dist/util.d.ts +6 -6
  6. package/package.json +1 -1
  7. package/src/preact/MutationAnnotationsContext.tsx +1 -1
  8. package/src/preact/components/csv-download-button.tsx +22 -14
  9. package/src/preact/components/features-over-time-grid.tsx +189 -43
  10. package/src/preact/components/mutations-over-time-mutations-filter.stories.tsx +1 -1
  11. package/src/preact/components/mutations-over-time-mutations-filter.tsx +1 -1
  12. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutationsOverTimePage1.json +52 -0
  13. package/src/preact/mutationsOverTime/__mockData__/byWeek/mutationsOverTimePage1.json +76 -0
  14. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mockDefaultMutationsOverTimeWithFilter.json +43 -0
  15. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePage1.json +126 -0
  16. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePage2.json +116 -0
  17. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePageSize20.json +216 -0
  18. package/src/preact/mutationsOverTime/getFilteredMutationCodes.spec.ts +236 -0
  19. package/src/preact/mutationsOverTime/{getFilteredMutationsOverTimeData.ts → getFilteredMutationCodes.ts} +29 -44
  20. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +128 -23
  21. package/src/preact/mutationsOverTime/mutations-over-time.tsx +139 -74
  22. package/src/preact/mutationsOverTime/useMutationsOverTimePageData.ts +111 -0
  23. package/src/preact/shared/tanstackTable/pagination-context.tsx +5 -2
  24. package/src/preact/shared/tanstackTable/pagination.tsx +11 -9
  25. package/src/preact/shared/tanstackTable/tanstackTable.tsx +7 -4
  26. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +1 -1
  27. package/src/query/queryMutationsOverTime.spec.ts +187 -662
  28. package/src/query/queryMutationsOverTime.ts +46 -33
  29. package/src/utils/useControlledState.ts +15 -0
  30. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +78 -22
  31. package/standalone-bundle/dashboard-components.js +6872 -6690
  32. package/standalone-bundle/dashboard-components.js.map +1 -1
  33. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutationsOverTime.json +0 -5496
  34. package/src/preact/mutationsOverTime/__mockData__/byWeek/mutationsOverTime.json +0 -7100
  35. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTime.json +0 -12646
  36. 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, useMemo, useState, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
2
+ import { type Dispatch, type StateUpdater, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
3
3
  import z from 'zod';
4
4
 
5
- import { type BaseMutationOverTimeDataMap, type MutationOverTimeDataMap } from './MutationOverTimeData';
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
- lapisFilterSchema,
15
- sequenceTypeSchema,
16
- type SubstitutionOrDeletionEntry,
17
- temporalGranularitySchema,
18
- views,
19
- } from '../../types';
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 FeaturesOverTimeGrid, { type FeatureRenderer, customColumnSchema } from '../components/features-over-time-grid';
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 { data, error, isLoading } = useQuery(
89
- () =>
90
- queryMutationsOverTimeData(lapisFilter, sequenceType, lapis, lapisDateField, granularity, displayMutations),
91
- [granularity, lapis, lapisDateField, lapisFilter, sequenceType, displayMutations],
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 (isLoading) {
107
+ if (metadataLoading) {
95
108
  return <LoadingDisplay />;
96
109
  }
97
110
 
98
- if (error !== null) {
99
- throw error;
111
+ if (metadataError) {
112
+ throw metadataError;
100
113
  }
101
114
 
102
- if (data.overallMutationData.length === 0) {
115
+ if (metadata.overallMutationData.length === 0) {
103
116
  return <NoDataDisplay />;
104
117
  }
105
118
 
106
- const { overallMutationData, mutationOverTimeData } = data;
107
119
  return (
108
- <MutationsOverTimeTabs
109
- overallMutationData={overallMutationData}
110
- mutationOverTimeData={mutationOverTimeData}
111
- originalComponentProps={componentProps}
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
- mutationOverTimeData: BaseMutationOverTimeDataMap;
132
+ metadata: MutationsOverTimeMetadata;
118
133
  originalComponentProps: MutationsOverTimeProps;
119
- overallMutationData: SubstitutionOrDeletionEntry<Substitution, Deletion>[];
134
+ pageIndex: number;
135
+ setPageIndex: Dispatch<StateUpdater<number>>;
120
136
  };
121
137
 
122
138
  const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
123
- mutationOverTimeData,
139
+ metadata,
124
140
  originalComponentProps,
125
- overallMutationData,
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 filteredData = useMemo(() => {
155
- return getFilteredMutationOverTimeData({
156
- data: mutationOverTimeData,
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
- hideGaps,
191
+ originalComponentProps.sequenceType,
162
192
  mutationFilterValue,
163
- sequenceType: originalComponentProps.sequenceType,
164
193
  annotationProvider,
165
- });
166
- }, [
167
- mutationOverTimeData,
168
- overallMutationData,
169
- displayedSegments,
170
- displayedMutationTypes,
171
- proportionInterval,
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
- originalComponentProps.sequenceType,
174
- mutationFilterValue,
175
- annotationProvider,
176
- ]);
216
+ );
177
217
 
178
- const mutationRenderer: FeatureRenderer<Substitution | Deletion> = {
179
- asString: (value: Substitution | Deletion) => value.code,
180
- renderRowLabel: (value: Substitution | Deletion) => (
181
- <div className={'text-center'}>
182
- <AnnotatedMutation mutation={value} sequenceType={originalComponentProps.sequenceType} />
183
- </div>
184
- ),
185
- renderTooltip: (value: Substitution | Deletion, temporal: Temporal, proportionValue: ProportionValue) => (
186
- <MutationsOverTimeGridTooltip mutation={value} date={temporal} value={proportionValue} />
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
- <FeaturesOverTimeGrid
240
+ <FeaturesOverTimeGridServerPaginated
198
241
  rowLabelHeader='Mutation'
199
- data={filteredData}
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
- <PageSizeContextProvider pageSizes={originalComponentProps.pageSizes}>
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={() => getDownloadData(filteredData)}
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, useState } from 'preact/hooks';
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] = useState(typeof pageSizes === 'number' ? pageSizes : (pageSizes.at(0) ?? 10));
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.getRowModel().rows.length <= 1) {
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.getRowModel().rows.length - 1;
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 {numRows}
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
- tableRef.current.setPageSize(pageSize);
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/getFilteredMutationsOverTimeData';
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';