@genspectrum/dashboard-components 1.16.0 → 1.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 +7 -7
- package/dist/components.d.ts +145 -45
- package/dist/components.js +524 -291
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +215 -55
- package/package.json +1 -1
- package/src/preact/MutationAnnotationsContext.spec.tsx +82 -10
- package/src/preact/MutationAnnotationsContext.tsx +93 -45
- package/src/preact/components/annotated-mutation.stories.tsx +31 -0
- package/src/preact/components/annotated-mutation.tsx +5 -5
- 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} +34 -49
- 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/gs-app.ts +8 -4
- package/src/web-components/mutation-annotations-context.ts +13 -5
- package/src/web-components/mutationAnnotations.mdx +29 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +78 -22
- package/standalone-bundle/dashboard-components.js +6914 -6701
- 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
|
@@ -11,12 +11,18 @@ import { ErrorDisplay } from './components/error-display';
|
|
|
11
11
|
import { ResizeContainer } from './components/resize-container';
|
|
12
12
|
import { type Mutation } from '../utils/mutations';
|
|
13
13
|
|
|
14
|
-
type
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
export type ResolvedMutationAnnotation = {
|
|
15
|
+
annotation: MutationAnnotation;
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
type
|
|
20
|
+
type AnnotationLookup = {
|
|
21
|
+
mutation: Map<string, ResolvedMutationAnnotation[]>;
|
|
22
|
+
position: Map<string, ResolvedMutationAnnotation[]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type MutationAnnotationsContextValue = Record<SequenceType, AnnotationLookup> & {
|
|
20
26
|
rawAnnotations: MutationAnnotations;
|
|
21
27
|
};
|
|
22
28
|
|
|
@@ -32,73 +38,117 @@ const MutationAnnotationsContext = createContext<MutationAnnotationsContextValue
|
|
|
32
38
|
},
|
|
33
39
|
});
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Validates and provides mutation annotations to all descendant components.
|
|
43
|
+
* Accepts the raw MutationAnnotations config, builds the internal lookup index, and stores it in context.
|
|
44
|
+
* Renders an error message if the provided annotations fail schema validation.
|
|
45
|
+
*/
|
|
35
46
|
export const MutationAnnotationsContextProvider: FunctionalComponent<
|
|
36
47
|
Omit<ComponentProps<typeof MutationAnnotationsContext.Provider>, 'value'> & { value: MutationAnnotations }
|
|
37
48
|
> = ({ value, children }) => {
|
|
38
|
-
const parseResult = useMemo(() =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}, [value]);
|
|
49
|
+
const parseResult = useMemo(() => mutationAnnotationsSchema.safeParse(value), [value]);
|
|
50
|
+
const contextValue = useMemo(
|
|
51
|
+
() =>
|
|
52
|
+
parseResult.success
|
|
53
|
+
? { success: true as const, value: buildAnnotationIndex(parseResult.data) }
|
|
54
|
+
: { success: false as const, error: parseResult.error },
|
|
55
|
+
[parseResult],
|
|
56
|
+
);
|
|
47
57
|
|
|
48
|
-
if (!
|
|
58
|
+
if (!contextValue.success) {
|
|
49
59
|
return (
|
|
50
60
|
<ResizeContainer size={{ width: '100%' }}>
|
|
51
|
-
<ErrorDisplay error={
|
|
61
|
+
<ErrorDisplay error={contextValue.error} layout='vertical' />
|
|
52
62
|
</ResizeContainer>
|
|
53
63
|
);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
return (
|
|
57
|
-
<MutationAnnotationsContext.Provider value={
|
|
67
|
+
<MutationAnnotationsContext.Provider value={contextValue.value}>{children}</MutationAnnotationsContext.Provider>
|
|
58
68
|
);
|
|
59
69
|
};
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Indexes a flat list of MutationAnnotations into fast lookup maps, resolving per-entry name/description overrides
|
|
73
|
+
* eagerly. Called once (memoized) when the annotations config is set on the provider.
|
|
74
|
+
*
|
|
75
|
+
* Returns two maps per sequence type — one keyed by exact mutation code, one by position string — each mapping to
|
|
76
|
+
* the list of ResolvedMutationAnnotations that apply to that key.
|
|
77
|
+
*/
|
|
78
|
+
export function buildAnnotationIndex(value: MutationAnnotations): MutationAnnotationsContextValue {
|
|
79
|
+
const nucleotideMutationMap = new Map<string, ResolvedMutationAnnotation[]>();
|
|
80
|
+
const nucleotidePositionMap = new Map<string, ResolvedMutationAnnotation[]>();
|
|
81
|
+
const aminoAcidMutationMap = new Map<string, ResolvedMutationAnnotation[]>();
|
|
82
|
+
const aminoAcidPositionMap = new Map<string, ResolvedMutationAnnotation[]>();
|
|
66
83
|
|
|
67
84
|
value.forEach((annotation) => {
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
annotation.nucleotideMutations?.forEach((entry) => {
|
|
86
|
+
addToMap(
|
|
87
|
+
nucleotideMutationMap,
|
|
88
|
+
typeof entry === 'string' ? entry : entry.mutation,
|
|
89
|
+
resolve(annotation, entry),
|
|
90
|
+
);
|
|
70
91
|
});
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
annotation.aminoAcidMutations?.forEach((entry) => {
|
|
93
|
+
addToMap(
|
|
94
|
+
aminoAcidMutationMap,
|
|
95
|
+
typeof entry === 'string' ? entry : entry.mutation,
|
|
96
|
+
resolve(annotation, entry),
|
|
97
|
+
);
|
|
73
98
|
});
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
annotation.nucleotidePositions?.forEach((entry) => {
|
|
100
|
+
addToMap(
|
|
101
|
+
nucleotidePositionMap,
|
|
102
|
+
typeof entry === 'string' ? entry : entry.position,
|
|
103
|
+
resolve(annotation, entry),
|
|
104
|
+
);
|
|
76
105
|
});
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
annotation.aminoAcidPositions?.forEach((entry) => {
|
|
107
|
+
addToMap(
|
|
108
|
+
aminoAcidPositionMap,
|
|
109
|
+
typeof entry === 'string' ? entry : entry.position,
|
|
110
|
+
resolve(annotation, entry),
|
|
111
|
+
);
|
|
79
112
|
});
|
|
80
113
|
});
|
|
81
114
|
|
|
82
115
|
return {
|
|
83
116
|
rawAnnotations: value,
|
|
84
|
-
nucleotide: { mutation:
|
|
85
|
-
'amino acid': { mutation:
|
|
117
|
+
nucleotide: { mutation: nucleotideMutationMap, position: nucleotidePositionMap },
|
|
118
|
+
'amino acid': { mutation: aminoAcidMutationMap, position: aminoAcidPositionMap },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolve(
|
|
123
|
+
annotation: MutationAnnotation,
|
|
124
|
+
entry: string | { name?: string; description?: string },
|
|
125
|
+
): ResolvedMutationAnnotation {
|
|
126
|
+
const overrides = typeof entry === 'object' ? entry : undefined;
|
|
127
|
+
return {
|
|
128
|
+
annotation,
|
|
129
|
+
name: overrides?.name ?? annotation.name,
|
|
130
|
+
description: overrides?.description ?? annotation.description,
|
|
86
131
|
};
|
|
87
132
|
}
|
|
88
133
|
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
map.set(code.toUpperCase(), [...
|
|
134
|
+
function addToMap(map: Map<string, ResolvedMutationAnnotation[]>, code: string, resolved: ResolvedMutationAnnotation) {
|
|
135
|
+
const existing = map.get(code.toUpperCase()) ?? [];
|
|
136
|
+
map.set(code.toUpperCase(), [...existing, resolved]);
|
|
92
137
|
}
|
|
93
138
|
|
|
94
139
|
export function useRawMutationAnnotations() {
|
|
95
140
|
return useContext(MutationAnnotationsContext).rawAnnotations;
|
|
96
141
|
}
|
|
97
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Returns a lookup function `(mutation, sequenceType) => ResolvedMutationAnnotation[] | undefined` that, given a
|
|
145
|
+
* specific mutation, returns all annotations that apply to it with name and description already resolved.
|
|
146
|
+
* Returns undefined if no annotations match.
|
|
147
|
+
*/
|
|
98
148
|
export function useMutationAnnotationsProvider() {
|
|
99
149
|
const mutationAnnotations = useContext(MutationAnnotationsContext);
|
|
100
150
|
|
|
101
|
-
return getMutationAnnotationsProvider(mutationAnnotations);
|
|
151
|
+
return useMemo(() => getMutationAnnotationsProvider(mutationAnnotations), [mutationAnnotations]);
|
|
102
152
|
}
|
|
103
153
|
|
|
104
154
|
export function getMutationAnnotationsProvider(mutationAnnotations: MutationAnnotationsContextValue) {
|
|
@@ -108,21 +158,19 @@ export function getMutationAnnotationsProvider(mutationAnnotations: MutationAnno
|
|
|
108
158
|
? `${mutation.position}`
|
|
109
159
|
: `${mutation.segment.toUpperCase()}:${mutation.position}`;
|
|
110
160
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
161
|
+
const exactMatches = mutationAnnotations[sequenceType].mutation.get(mutation.code.toUpperCase());
|
|
162
|
+
const positionMatches = mutationAnnotations[sequenceType].position.get(position);
|
|
113
163
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
? [...possiblePositionAnnotations, ...possibleExactAnnotations]
|
|
117
|
-
: (possiblePositionAnnotations ?? possibleExactAnnotations);
|
|
164
|
+
const combined =
|
|
165
|
+
exactMatches && positionMatches ? [...exactMatches, ...positionMatches] : (exactMatches ?? positionMatches);
|
|
118
166
|
|
|
119
|
-
const
|
|
167
|
+
const seenNames = new Set<string>();
|
|
120
168
|
|
|
121
|
-
return
|
|
122
|
-
if (
|
|
169
|
+
return combined?.filter((resolved) => {
|
|
170
|
+
if (seenNames.has(resolved.annotation.name)) {
|
|
123
171
|
return false;
|
|
124
172
|
}
|
|
125
|
-
|
|
173
|
+
seenNames.add(resolved.annotation.name);
|
|
126
174
|
return true;
|
|
127
175
|
});
|
|
128
176
|
};
|
|
@@ -128,6 +128,37 @@ export const MutationWithMultipleAnnotationEntries: StoryObj<StoryProps> = {
|
|
|
128
128
|
},
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
+
export const MutationWithPerMutationInfoOverride: StoryObj<StoryProps> = {
|
|
132
|
+
...MutationWithoutAnnotationEntry,
|
|
133
|
+
args: {
|
|
134
|
+
...MutationWithoutAnnotationEntry.args,
|
|
135
|
+
annotations: [
|
|
136
|
+
{
|
|
137
|
+
name: 'Group annotation',
|
|
138
|
+
description: 'Group-level description',
|
|
139
|
+
symbol: 'c',
|
|
140
|
+
nucleotideMutations: [
|
|
141
|
+
{
|
|
142
|
+
mutation: 'A23403G',
|
|
143
|
+
name: '3CLpro:T31C',
|
|
144
|
+
description: 'Per-mutation description for 3CLpro:T31C',
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
play: async ({ canvasElement }) => {
|
|
151
|
+
const canvas = within(canvasElement);
|
|
152
|
+
|
|
153
|
+
await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
|
|
154
|
+
await expect(getAnnotationIndicator(canvas)).toBeVisible();
|
|
155
|
+
|
|
156
|
+
await userEvent.click(canvas.getByText('c'));
|
|
157
|
+
await waitFor(() => expect(canvas.queryByText('3CLpro:T31C')).toBeVisible());
|
|
158
|
+
await expect(canvas.queryByText('Per-mutation description for 3CLpro:T31C')).toBeVisible();
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
131
162
|
export const AminoAcidMutationWithAnnotationEntry: StoryObj<StoryProps> = {
|
|
132
163
|
...MutationWithoutAnnotationEntry,
|
|
133
164
|
args: {
|
|
@@ -78,11 +78,11 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
|
|
|
78
78
|
const modalContent = (
|
|
79
79
|
<div className='block'>
|
|
80
80
|
<InfoHeadline1>Annotations for {mutation.code}</InfoHeadline1>
|
|
81
|
-
{mutationAnnotations.map((
|
|
82
|
-
<Fragment key={annotation.name}>
|
|
83
|
-
<InfoHeadline2>{
|
|
81
|
+
{mutationAnnotations.map((resolved) => (
|
|
82
|
+
<Fragment key={resolved.annotation.name}>
|
|
83
|
+
<InfoHeadline2>{resolved.name}</InfoHeadline2>
|
|
84
84
|
<InfoParagraph>
|
|
85
|
-
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(
|
|
85
|
+
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(resolved.description) }} />
|
|
86
86
|
</InfoParagraph>
|
|
87
87
|
</Fragment>
|
|
88
88
|
))}
|
|
@@ -99,7 +99,7 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
|
|
|
99
99
|
>
|
|
100
100
|
<sup className='hover:underline focus-visible:underline decoration-red-600'>
|
|
101
101
|
{mutationAnnotations
|
|
102
|
-
.map((
|
|
102
|
+
.map((resolved) => resolved.annotation.symbol)
|
|
103
103
|
.map((symbol, index) => (
|
|
104
104
|
<Fragment key={symbol}>
|
|
105
105
|
<span className='text-red-600'>{symbol}</span>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useState } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
type ToStringable = {
|
|
4
5
|
toString: () => string;
|
|
@@ -9,7 +10,7 @@ export type DataValue = string | number | boolean | null | undefined | ToStringa
|
|
|
9
10
|
export interface CsvDownloadButtonProps {
|
|
10
11
|
label?: string;
|
|
11
12
|
filename?: string;
|
|
12
|
-
getData: () => Record<string, DataValue>[]
|
|
13
|
+
getData: () => Record<string, DataValue>[] | Promise<Record<string, DataValue>[]>;
|
|
13
14
|
className?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -19,19 +20,26 @@ export const CsvDownloadButton: FunctionComponent<CsvDownloadButtonProps> = ({
|
|
|
19
20
|
getData,
|
|
20
21
|
className,
|
|
21
22
|
}) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
24
|
+
|
|
25
|
+
const download = async () => {
|
|
26
|
+
setIsDownloading(true);
|
|
27
|
+
try {
|
|
28
|
+
const content = await getDownloadContent();
|
|
29
|
+
const blob = new Blob([content], { type: 'text/csv' });
|
|
30
|
+
const url = URL.createObjectURL(blob);
|
|
31
|
+
const a = document.createElement('a');
|
|
32
|
+
a.href = url;
|
|
33
|
+
a.download = filename;
|
|
34
|
+
a.click();
|
|
35
|
+
URL.revokeObjectURL(url);
|
|
36
|
+
} finally {
|
|
37
|
+
setIsDownloading(false);
|
|
38
|
+
}
|
|
31
39
|
};
|
|
32
40
|
|
|
33
|
-
const getDownloadContent = () => {
|
|
34
|
-
const data = getData();
|
|
41
|
+
const getDownloadContent = async () => {
|
|
42
|
+
const data = await getData();
|
|
35
43
|
const keys = getDataKeys(data);
|
|
36
44
|
const header = keys.join(',');
|
|
37
45
|
const rows = data.map((row) => keys.map((key) => row[key]).join(',')).join('\n');
|
|
@@ -50,8 +58,8 @@ export const CsvDownloadButton: FunctionComponent<CsvDownloadButtonProps> = ({
|
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
return (
|
|
53
|
-
<button className={className} onClick={download}>
|
|
54
|
-
{label}
|
|
61
|
+
<button className={className} onClick={() => void download()} disabled={isDownloading}>
|
|
62
|
+
{isDownloading ? 'Downloading...' : label}
|
|
55
63
|
</button>
|
|
56
64
|
);
|
|
57
65
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createColumnHelper, getCoreRowModel, getPaginationRowModel } from '@tanstack/table-core';
|
|
2
|
+
import type { Table } from '@tanstack/table-core';
|
|
1
3
|
import { type FunctionComponent, type JSX } from 'preact';
|
|
2
|
-
import { useMemo } from 'preact/hooks';
|
|
4
|
+
import { type Dispatch, type StateUpdater, useMemo } from 'preact/hooks';
|
|
3
5
|
import z from 'zod';
|
|
4
6
|
|
|
5
7
|
import { type ColorScale, getColorWithinScale, getTextColorForScale } from './color-scale-selector';
|
|
@@ -11,13 +13,7 @@ import { type TemporalDataMap } from '../mutationsOverTime/MutationOverTimeData'
|
|
|
11
13
|
import { formatProportion } from '../shared/table/formatProportion';
|
|
12
14
|
import { type PageSizes, Pagination } from '../shared/tanstackTable/pagination';
|
|
13
15
|
import { usePageSizeContext } from '../shared/tanstackTable/pagination-context';
|
|
14
|
-
import {
|
|
15
|
-
createColumnHelper,
|
|
16
|
-
flexRender,
|
|
17
|
-
getCoreRowModel,
|
|
18
|
-
getPaginationRowModel,
|
|
19
|
-
usePreactTable,
|
|
20
|
-
} from '../shared/tanstackTable/tanstackTable';
|
|
16
|
+
import { flexRender, usePreactTable } from '../shared/tanstackTable/tanstackTable';
|
|
21
17
|
|
|
22
18
|
const NON_BREAKING_SPACE = '\u00A0';
|
|
23
19
|
|
|
@@ -60,18 +56,118 @@ function FeaturesOverTimeGrid<F>({
|
|
|
60
56
|
featureRenderer,
|
|
61
57
|
tooltipPortalTarget,
|
|
62
58
|
}: FeaturesOverTimeGridProps<F>) {
|
|
63
|
-
const tableData =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
const tableData = useGridTableData(data, customColumns, featureRenderer);
|
|
60
|
+
const columns = useGridColumns(
|
|
61
|
+
data.getSecondAxisKeys(),
|
|
62
|
+
rowLabelHeader,
|
|
63
|
+
customColumns,
|
|
64
|
+
colorScale,
|
|
65
|
+
tooltipPortalTarget,
|
|
66
|
+
featureRenderer,
|
|
67
|
+
);
|
|
68
|
+
const { pageSize } = usePageSizeContext();
|
|
69
|
+
|
|
70
|
+
const table = usePreactTable({
|
|
71
|
+
data: tableData,
|
|
72
|
+
columns,
|
|
73
|
+
getCoreRowModel: getCoreRowModel(),
|
|
74
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
75
|
+
initialState: {
|
|
76
|
+
pagination: { pageIndex: 0, pageSize },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return <FeaturesOverTimeGridDisplay table={table} pageSizes={pageSizes} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface FeaturesOverTimeGridServerPaginatedProps<F> {
|
|
84
|
+
rowLabelHeader: string;
|
|
85
|
+
data: TemporalDataMap<F> | null;
|
|
86
|
+
isLoading: boolean;
|
|
87
|
+
/** Labels to show in the row label column while the page data is loading. */
|
|
88
|
+
loadingRowLabels: string[];
|
|
89
|
+
/** Date columns to show in the header while loading */
|
|
90
|
+
requestedDateRanges: Temporal[];
|
|
91
|
+
colorScale: ColorScale;
|
|
92
|
+
pageSizes: PageSizes;
|
|
93
|
+
/** Controlled page index (0-based). */
|
|
94
|
+
pageIndex: number;
|
|
95
|
+
/** Total number of rows across all pages. */
|
|
96
|
+
totalRows: number;
|
|
97
|
+
onPageChange: Dispatch<StateUpdater<number>>;
|
|
98
|
+
customColumns?: CustomColumn[];
|
|
99
|
+
featureRenderer: FeatureRenderer<F>;
|
|
100
|
+
tooltipPortalTarget: HTMLElement | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function FeaturesOverTimeGridServerPaginated<F>({
|
|
104
|
+
rowLabelHeader,
|
|
105
|
+
data,
|
|
106
|
+
isLoading,
|
|
107
|
+
loadingRowLabels,
|
|
108
|
+
requestedDateRanges,
|
|
109
|
+
colorScale,
|
|
110
|
+
pageSizes,
|
|
111
|
+
pageIndex,
|
|
112
|
+
totalRows,
|
|
113
|
+
onPageChange,
|
|
114
|
+
customColumns = EMPTY_COLUMNS,
|
|
115
|
+
featureRenderer,
|
|
116
|
+
tooltipPortalTarget,
|
|
117
|
+
}: FeaturesOverTimeGridServerPaginatedProps<F>) {
|
|
118
|
+
const tableData = useGridTableData(data, customColumns, featureRenderer);
|
|
119
|
+
const columns = useGridColumns(
|
|
120
|
+
data?.getSecondAxisKeys() ?? requestedDateRanges,
|
|
121
|
+
rowLabelHeader,
|
|
122
|
+
customColumns,
|
|
123
|
+
colorScale,
|
|
124
|
+
tooltipPortalTarget,
|
|
125
|
+
featureRenderer,
|
|
126
|
+
);
|
|
127
|
+
const { pageSize, setPageSize } = usePageSizeContext();
|
|
128
|
+
|
|
129
|
+
const table = usePreactTable({
|
|
130
|
+
data: tableData,
|
|
131
|
+
columns,
|
|
132
|
+
getCoreRowModel: getCoreRowModel(),
|
|
133
|
+
// getPaginationRowModel not needed with manualPagination: true
|
|
134
|
+
manualPagination: true,
|
|
135
|
+
pageCount: Math.ceil(totalRows / pageSize),
|
|
136
|
+
state: {
|
|
137
|
+
pagination: { pageIndex, pageSize },
|
|
138
|
+
},
|
|
139
|
+
onPaginationChange: (updater) => {
|
|
140
|
+
const current = { pageIndex, pageSize };
|
|
141
|
+
const next = typeof updater === 'function' ? updater(current) : updater;
|
|
142
|
+
if (next.pageIndex !== current.pageIndex) {
|
|
143
|
+
onPageChange(next.pageIndex);
|
|
144
|
+
}
|
|
145
|
+
if (next.pageSize !== current.pageSize) {
|
|
146
|
+
setPageSize(next.pageSize);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
});
|
|
71
150
|
|
|
72
|
-
|
|
151
|
+
return (
|
|
152
|
+
<FeaturesOverTimeGridDisplay
|
|
153
|
+
table={table}
|
|
154
|
+
pageSizes={pageSizes}
|
|
155
|
+
loadingState={{ isLoading, loadingRowLabels }}
|
|
156
|
+
totalRows={totalRows}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function useGridColumns<F>(
|
|
162
|
+
dates: Temporal[],
|
|
163
|
+
rowLabelHeader: string,
|
|
164
|
+
customColumns: CustomColumn[],
|
|
165
|
+
colorScale: ColorScale,
|
|
166
|
+
tooltipPortalTarget: HTMLElement | null,
|
|
167
|
+
featureRenderer: FeatureRenderer<F>,
|
|
168
|
+
) {
|
|
169
|
+
return useMemo(() => {
|
|
73
170
|
const columnHelper = createColumnHelper<RowType<F>>();
|
|
74
|
-
const dates = data.getSecondAxisKeys();
|
|
75
171
|
|
|
76
172
|
const featureHeader = columnHelper.accessor((row) => row.feature, {
|
|
77
173
|
id: 'feature',
|
|
@@ -137,21 +233,48 @@ function FeaturesOverTimeGrid<F>({
|
|
|
137
233
|
});
|
|
138
234
|
|
|
139
235
|
return [featureHeader, ...customColumnHeaders, ...dateHeaders];
|
|
140
|
-
}, [colorScale,
|
|
236
|
+
}, [colorScale, dates, customColumns, tooltipPortalTarget, featureRenderer, rowLabelHeader]);
|
|
237
|
+
}
|
|
141
238
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
239
|
+
function useGridTableData<F>(
|
|
240
|
+
data: TemporalDataMap<F> | null | undefined,
|
|
241
|
+
customColumns: CustomColumn[],
|
|
242
|
+
featureRenderer: FeatureRenderer<F>,
|
|
243
|
+
) {
|
|
244
|
+
return useMemo(() => {
|
|
245
|
+
if (!data) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
const firstAxisKeys = data.getFirstAxisKeys();
|
|
249
|
+
return data.getAsArray().map((row, index): RowType<F> => {
|
|
250
|
+
const firstAxisKey = firstAxisKeys[index];
|
|
251
|
+
const customValues = customColumns.map((col) => col.values[featureRenderer.asString(firstAxisKey)]);
|
|
252
|
+
return { feature: firstAxisKey, values: [...row], customValues };
|
|
253
|
+
});
|
|
254
|
+
}, [data, customColumns, featureRenderer]);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
type FeaturesOverTimeGridDisplayProps<F> = {
|
|
258
|
+
table: Table<RowType<F>>;
|
|
259
|
+
pageSizes: PageSizes;
|
|
260
|
+
/** Override for the pagination row count (server-driven pagination). */
|
|
261
|
+
totalRows?: number;
|
|
262
|
+
loadingState?:
|
|
263
|
+
| {
|
|
264
|
+
isLoading: boolean;
|
|
265
|
+
/** Labels to render in the row label column while loading. One skeleton row is shown per label. */
|
|
266
|
+
loadingRowLabels: string[];
|
|
267
|
+
}
|
|
268
|
+
| { isLoading: false; loadingRowLabels?: never };
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
function FeaturesOverTimeGridDisplay<F>({
|
|
272
|
+
table,
|
|
273
|
+
pageSizes,
|
|
274
|
+
loadingState,
|
|
275
|
+
totalRows,
|
|
276
|
+
}: FeaturesOverTimeGridDisplayProps<F>) {
|
|
277
|
+
const displayedTotalRows = totalRows ?? table.getCoreRowModel().rows.length;
|
|
155
278
|
|
|
156
279
|
return (
|
|
157
280
|
<div className='w-full'>
|
|
@@ -170,22 +293,45 @@ function FeaturesOverTimeGrid<F>({
|
|
|
170
293
|
))}
|
|
171
294
|
</thead>
|
|
172
295
|
<tbody>
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
{
|
|
176
|
-
<td
|
|
296
|
+
{loadingState?.isLoading ? (
|
|
297
|
+
loadingState.loadingRowLabels.map((label, rowIndex) => (
|
|
298
|
+
<tr key={label}>
|
|
299
|
+
<td className='text-center'>{label}</td>
|
|
300
|
+
{rowIndex === 0 && (
|
|
301
|
+
<td
|
|
302
|
+
rowSpan={loadingState.loadingRowLabels.length}
|
|
303
|
+
colSpan={table.getFlatHeaders().length - 1}
|
|
304
|
+
className='text-center'
|
|
305
|
+
>
|
|
306
|
+
<span className='loading loading-spinner loading-sm' />
|
|
307
|
+
</td>
|
|
308
|
+
)}
|
|
309
|
+
</tr>
|
|
310
|
+
))
|
|
311
|
+
) : (
|
|
312
|
+
<>
|
|
313
|
+
{table.getRowModel().rows.map((row) => (
|
|
314
|
+
<tr key={row.id}>
|
|
315
|
+
{row.getVisibleCells().map((cell) => (
|
|
316
|
+
<td key={cell.id}>
|
|
317
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
318
|
+
</td>
|
|
319
|
+
))}
|
|
320
|
+
</tr>
|
|
177
321
|
))}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
322
|
+
{table.getRowModel().rows.length === 0 && (
|
|
323
|
+
<tr>
|
|
324
|
+
<td colSpan={table.getFlatHeaders().length}>
|
|
325
|
+
<div className={'text-center'}>No data available for your filters.</div>
|
|
326
|
+
</td>
|
|
327
|
+
</tr>
|
|
328
|
+
)}
|
|
329
|
+
</>
|
|
184
330
|
)}
|
|
185
331
|
</tbody>
|
|
186
332
|
</table>
|
|
187
333
|
<div className={'mt-2'}>
|
|
188
|
-
<Pagination table={table} pageSizes={pageSizes} />
|
|
334
|
+
<Pagination table={table} pageSizes={pageSizes} totalRows={displayedTotalRows} />
|
|
189
335
|
</div>
|
|
190
336
|
</div>
|
|
191
337
|
);
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from './mutations-over-time-mutations-filter';
|
|
10
10
|
import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
|
|
11
11
|
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
12
|
-
import { type MutationFilter } from '../mutationsOverTime/
|
|
12
|
+
import { type MutationFilter } from '../mutationsOverTime/getFilteredMutationCodes';
|
|
13
13
|
|
|
14
14
|
const meta: Meta = {
|
|
15
15
|
title: 'Component/Mutations over time mutations filter',
|
|
@@ -3,7 +3,7 @@ import { type Dispatch, type StateUpdater, useCallback, useEffect, useState } fr
|
|
|
3
3
|
|
|
4
4
|
import { Dropdown } from './dropdown';
|
|
5
5
|
import { useRawMutationAnnotations } from '../MutationAnnotationsContext';
|
|
6
|
-
import { type MutationFilter } from '../mutationsOverTime/
|
|
6
|
+
import { type MutationFilter } from '../mutationsOverTime/getFilteredMutationCodes';
|
|
7
7
|
import { DeleteIcon } from '../shared/icons/DeleteIcon';
|
|
8
8
|
|
|
9
9
|
export type MutationsOverTimeMutationsFilterProps = {
|