@genspectrum/dashboard-components 0.4.3 → 0.4.4
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 +40 -2
- package/dist/dashboard-components.js +49 -7
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +9 -0
- package/package.json +1 -1
- package/src/preact/aggregatedData/aggregate.stories.tsx +4 -0
- package/src/preact/aggregatedData/aggregate.tsx +20 -3
- package/src/query/queryAggregateData.spec.ts +117 -3
- package/src/query/queryAggregateData.ts +31 -2
- package/src/web-components/visualization/gs-aggregate.stories.ts +11 -0
- package/src/web-components/visualization/gs-aggregate.tsx +15 -0
|
@@ -57,6 +57,15 @@ export declare class AggregateComponent extends PreactLitAdapterWithGridJsStyles
|
|
|
57
57
|
* The headline of the component. Set to an empty string to hide the headline.
|
|
58
58
|
*/
|
|
59
59
|
headline: string;
|
|
60
|
+
/**
|
|
61
|
+
* The field by which the table is initially sorted.
|
|
62
|
+
* Must be one of the fields specified in the fields property, 'count', or 'proportion'.
|
|
63
|
+
*/
|
|
64
|
+
initialSortField: string;
|
|
65
|
+
/**
|
|
66
|
+
* The initial sort direction of the table.
|
|
67
|
+
*/
|
|
68
|
+
initialSortDirection: 'ascending' | 'descending';
|
|
60
69
|
render(): JSX_2.Element;
|
|
61
70
|
}
|
|
62
71
|
|
package/package.json
CHANGED
|
@@ -13,6 +13,8 @@ const meta: Meta<AggregateProps> = {
|
|
|
13
13
|
width: { control: 'text' },
|
|
14
14
|
height: { control: 'text' },
|
|
15
15
|
headline: { control: 'text' },
|
|
16
|
+
initialSortField: { control: 'text' },
|
|
17
|
+
initialSortDirection: { control: 'radio', options: ['ascending', 'descending'] },
|
|
16
18
|
},
|
|
17
19
|
parameters: {
|
|
18
20
|
fetchMock: {
|
|
@@ -53,5 +55,7 @@ export const Default: StoryObj<AggregateProps> = {
|
|
|
53
55
|
width: '100%',
|
|
54
56
|
height: '700px',
|
|
55
57
|
headline: 'Aggregate',
|
|
58
|
+
initialSortField: 'count',
|
|
59
|
+
initialSortDirection: 'descending',
|
|
56
60
|
},
|
|
57
61
|
};
|
|
@@ -17,6 +17,7 @@ import Tabs from '../components/tabs';
|
|
|
17
17
|
import { useQuery } from '../useQuery';
|
|
18
18
|
|
|
19
19
|
export type View = 'table';
|
|
20
|
+
export type InitialSort = { field: string; direction: 'ascending' | 'descending' };
|
|
20
21
|
|
|
21
22
|
export type AggregateProps = {
|
|
22
23
|
width: string;
|
|
@@ -28,6 +29,8 @@ export interface AggregateInnerProps {
|
|
|
28
29
|
filter: LapisFilter;
|
|
29
30
|
fields: string[];
|
|
30
31
|
views: View[];
|
|
32
|
+
initialSortField: string;
|
|
33
|
+
initialSortDirection: 'ascending' | 'descending';
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
@@ -37,6 +40,8 @@ export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
|
37
40
|
headline = 'Mutations',
|
|
38
41
|
filter,
|
|
39
42
|
fields,
|
|
43
|
+
initialSortField,
|
|
44
|
+
initialSortDirection,
|
|
40
45
|
}) => {
|
|
41
46
|
const size = { height, width };
|
|
42
47
|
|
|
@@ -44,18 +49,30 @@ export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
|
44
49
|
<ErrorBoundary size={size} headline={headline}>
|
|
45
50
|
<ResizeContainer size={size}>
|
|
46
51
|
<Headline heading={headline}>
|
|
47
|
-
<AggregateInner
|
|
52
|
+
<AggregateInner
|
|
53
|
+
fields={fields}
|
|
54
|
+
filter={filter}
|
|
55
|
+
views={views}
|
|
56
|
+
initialSortField={initialSortField}
|
|
57
|
+
initialSortDirection={initialSortDirection}
|
|
58
|
+
/>
|
|
48
59
|
</Headline>
|
|
49
60
|
</ResizeContainer>
|
|
50
61
|
</ErrorBoundary>
|
|
51
62
|
);
|
|
52
63
|
};
|
|
53
64
|
|
|
54
|
-
export const AggregateInner: FunctionComponent<AggregateInnerProps> = ({
|
|
65
|
+
export const AggregateInner: FunctionComponent<AggregateInnerProps> = ({
|
|
66
|
+
fields,
|
|
67
|
+
views,
|
|
68
|
+
filter,
|
|
69
|
+
initialSortField,
|
|
70
|
+
initialSortDirection,
|
|
71
|
+
}) => {
|
|
55
72
|
const lapis = useContext(LapisUrlContext);
|
|
56
73
|
|
|
57
74
|
const { data, error, isLoading } = useQuery(async () => {
|
|
58
|
-
return queryAggregateData(filter, fields, lapis);
|
|
75
|
+
return queryAggregateData(filter, fields, lapis, { field: initialSortField, direction: initialSortDirection });
|
|
59
76
|
}, [filter, fields, lapis]);
|
|
60
77
|
|
|
61
78
|
if (isLoading) {
|
|
@@ -4,7 +4,7 @@ import { queryAggregateData } from './queryAggregateData';
|
|
|
4
4
|
import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
|
|
5
5
|
|
|
6
6
|
describe('queryAggregateData', () => {
|
|
7
|
-
test('should fetch aggregate data', async () => {
|
|
7
|
+
test('should fetch aggregate data and sort initially by count descending when no initialSort is provided', async () => {
|
|
8
8
|
const fields = ['division', 'host'];
|
|
9
9
|
const filter = { country: 'USA' };
|
|
10
10
|
|
|
@@ -23,10 +23,124 @@ describe('queryAggregateData', () => {
|
|
|
23
23
|
const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL);
|
|
24
24
|
|
|
25
25
|
expect(result).to.deep.equal([
|
|
26
|
+
{ proportion: 0.5, count: 16, region: 'region2', host: 'host2' },
|
|
27
|
+
{ proportion: 0.25, count: 8, region: 'region2', host: 'host1' },
|
|
26
28
|
{ proportion: 0.125, count: 4, region: 'region1', host: 'host1' },
|
|
27
29
|
{ proportion: 0.125, count: 4, region: 'region1', host: 'host2' },
|
|
28
|
-
{ proportion: 0.25, count: 8, region: 'region2', host: 'host1' },
|
|
29
|
-
{ proportion: 0.5, count: 16, region: 'region2', host: 'host2' },
|
|
30
30
|
]);
|
|
31
31
|
});
|
|
32
|
+
|
|
33
|
+
test('should sort by initialSort field ascending', async () => {
|
|
34
|
+
const fields = ['division', 'host'];
|
|
35
|
+
const filter = { country: 'USA' };
|
|
36
|
+
const initialSortField = 'host';
|
|
37
|
+
const initialSortDirection = 'ascending';
|
|
38
|
+
|
|
39
|
+
lapisRequestMocks.aggregated(
|
|
40
|
+
{ fields, ...filter },
|
|
41
|
+
{
|
|
42
|
+
data: [
|
|
43
|
+
{ count: 4, region: 'region1', host: 'A_host' },
|
|
44
|
+
{ count: 4, region: 'region1', host: 'B_host' },
|
|
45
|
+
{ count: 8, region: 'region2', host: 'A_host1' },
|
|
46
|
+
{ count: 16, region: 'region2', host: 'C_host' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL, {
|
|
52
|
+
field: initialSortField,
|
|
53
|
+
direction: initialSortDirection,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result).to.deep.equal([
|
|
57
|
+
{ proportion: 0.125, count: 4, region: 'region1', host: 'A_host' },
|
|
58
|
+
{ proportion: 0.25, count: 8, region: 'region2', host: 'A_host1' },
|
|
59
|
+
{ proportion: 0.125, count: 4, region: 'region1', host: 'B_host' },
|
|
60
|
+
{ proportion: 0.5, count: 16, region: 'region2', host: 'C_host' },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should sort by initialSort field descending', async () => {
|
|
65
|
+
const fields = ['division', 'host'];
|
|
66
|
+
const filter = { country: 'USA' };
|
|
67
|
+
const initialSortField = 'host';
|
|
68
|
+
const initialSortDirection = 'descending';
|
|
69
|
+
|
|
70
|
+
lapisRequestMocks.aggregated(
|
|
71
|
+
{ fields, ...filter },
|
|
72
|
+
{
|
|
73
|
+
data: [
|
|
74
|
+
{ count: 4, region: 'region1', host: 'A_host' },
|
|
75
|
+
{ count: 4, region: 'region1', host: 'B_host' },
|
|
76
|
+
{ count: 8, region: 'region2', host: 'A_host1' },
|
|
77
|
+
{ count: 16, region: 'region2', host: 'C_host' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL, {
|
|
83
|
+
field: initialSortField,
|
|
84
|
+
direction: initialSortDirection,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result).to.deep.equal([
|
|
88
|
+
{ proportion: 0.5, count: 16, region: 'region2', host: 'C_host' },
|
|
89
|
+
{ proportion: 0.125, count: 4, region: 'region1', host: 'B_host' },
|
|
90
|
+
{ proportion: 0.25, count: 8, region: 'region2', host: 'A_host1' },
|
|
91
|
+
{ proportion: 0.125, count: 4, region: 'region1', host: 'A_host' },
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should sort by initialSort number field', async () => {
|
|
96
|
+
const fields = ['division', 'host'];
|
|
97
|
+
const filter = { country: 'USA' };
|
|
98
|
+
const initialSortField = 'proportion';
|
|
99
|
+
const initialSortDirection = 'descending';
|
|
100
|
+
|
|
101
|
+
lapisRequestMocks.aggregated(
|
|
102
|
+
{ fields, ...filter },
|
|
103
|
+
{
|
|
104
|
+
data: [
|
|
105
|
+
{ count: 4, region: 'region1', host: 'A_host' },
|
|
106
|
+
{ count: 4, region: 'region1', host: 'B_host' },
|
|
107
|
+
{ count: 8, region: 'region2', host: 'A_host1' },
|
|
108
|
+
{ count: 16, region: 'region2', host: 'C_host' },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL, {
|
|
114
|
+
field: initialSortField,
|
|
115
|
+
direction: initialSortDirection,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result).to.deep.equal([
|
|
119
|
+
{ proportion: 0.125, count: 4, region: 'region1', host: 'A_host' },
|
|
120
|
+
{ proportion: 0.125, count: 4, region: 'region1', host: 'B_host' },
|
|
121
|
+
{ proportion: 0.25, count: 8, region: 'region2', host: 'A_host1' },
|
|
122
|
+
{ proportion: 0.5, count: 16, region: 'region2', host: 'C_host' },
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should throw if initialSortField is not in fields', async () => {
|
|
127
|
+
const fields = ['division', 'host'];
|
|
128
|
+
const filter = { country: 'USA' };
|
|
129
|
+
const initialSortField = 'not_in_fields';
|
|
130
|
+
const initialSortDirection = 'descending';
|
|
131
|
+
|
|
132
|
+
lapisRequestMocks.aggregated(
|
|
133
|
+
{ fields, ...filter },
|
|
134
|
+
{
|
|
135
|
+
data: [{ count: 4, region: 'region1', host: 'A_host' }],
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await expect(
|
|
140
|
+
queryAggregateData(filter, fields, DUMMY_LAPIS_URL, {
|
|
141
|
+
field: initialSortField,
|
|
142
|
+
direction: initialSortDirection,
|
|
143
|
+
}),
|
|
144
|
+
).rejects.toThrowError('InitialSort field not in fields. Valid fields are: count, proportion, division, host');
|
|
145
|
+
});
|
|
32
146
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { FetchAggregatedOperator } from '../operator/FetchAggregatedOperator';
|
|
2
|
+
import { SortOperator } from '../operator/SortOperator';
|
|
3
|
+
import { type InitialSort } from '../preact/aggregatedData/aggregate';
|
|
2
4
|
import { type LapisFilter } from '../types';
|
|
3
5
|
|
|
4
6
|
export type AggregateData = (Record<string, string | null | number | boolean> & {
|
|
@@ -6,9 +8,36 @@ export type AggregateData = (Record<string, string | null | number | boolean> &
|
|
|
6
8
|
proportion: number;
|
|
7
9
|
})[];
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
const compareAscending = (a: string | null | number, b: string | null | number) => {
|
|
12
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
13
|
+
return a - b;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const strA = a != null ? String(a) : '';
|
|
17
|
+
const strB = b != null ? String(b) : '';
|
|
18
|
+
|
|
19
|
+
return strA.localeCompare(strB);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function queryAggregateData(
|
|
23
|
+
variant: LapisFilter,
|
|
24
|
+
fields: string[],
|
|
25
|
+
lapis: string,
|
|
26
|
+
initialSort: InitialSort = { field: 'count', direction: 'descending' },
|
|
27
|
+
signal?: AbortSignal,
|
|
28
|
+
) {
|
|
29
|
+
const validSortFields = ['count', 'proportion', ...fields];
|
|
30
|
+
if (!validSortFields.includes(initialSort.field)) {
|
|
31
|
+
throw new Error(`InitialSort field not in fields. Valid fields are: ${validSortFields.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
const fetchData = new FetchAggregatedOperator<Record<string, string | null | number>>(variant, fields);
|
|
11
|
-
const
|
|
35
|
+
const sortData = new SortOperator(fetchData, (a, b) => {
|
|
36
|
+
return initialSort.direction === 'ascending'
|
|
37
|
+
? compareAscending(a[initialSort.field], b[initialSort.field])
|
|
38
|
+
: compareAscending(b[initialSort.field], a[initialSort.field]);
|
|
39
|
+
});
|
|
40
|
+
const data = (await sortData.evaluate(lapis, signal)).content;
|
|
12
41
|
|
|
13
42
|
const total = data.reduce((acc, row) => acc + row.count, 0);
|
|
14
43
|
|
|
@@ -17,6 +17,8 @@ const codeExample = `
|
|
|
17
17
|
headline="Aggregate"
|
|
18
18
|
width='100%'
|
|
19
19
|
height='700px'
|
|
20
|
+
initialSortField="count"
|
|
21
|
+
initialSortDirection="descending"
|
|
20
22
|
></gs-aggregate>`;
|
|
21
23
|
|
|
22
24
|
const meta: Meta<Required<AggregateProps>> = {
|
|
@@ -31,6 +33,11 @@ const meta: Meta<Required<AggregateProps>> = {
|
|
|
31
33
|
width: { control: 'text' },
|
|
32
34
|
height: { control: 'text' },
|
|
33
35
|
headline: { control: 'text' },
|
|
36
|
+
initialSortField: { control: 'text' },
|
|
37
|
+
initialSortDirection: {
|
|
38
|
+
options: ['ascending', 'descending'],
|
|
39
|
+
control: { type: 'radio' },
|
|
40
|
+
},
|
|
34
41
|
},
|
|
35
42
|
parameters: withComponentDocs({
|
|
36
43
|
fetchMock: {
|
|
@@ -72,6 +79,8 @@ export const Table: StoryObj<Required<AggregateProps>> = {
|
|
|
72
79
|
.width=${args.width}
|
|
73
80
|
.height=${args.height}
|
|
74
81
|
.headline=${args.headline}
|
|
82
|
+
.initialSortField=${args.initialSortField}
|
|
83
|
+
.initialSortDirection=${args.initialSortDirection}
|
|
75
84
|
></gs-aggregate>
|
|
76
85
|
</gs-app>
|
|
77
86
|
`,
|
|
@@ -84,5 +93,7 @@ export const Table: StoryObj<Required<AggregateProps>> = {
|
|
|
84
93
|
width: '100%',
|
|
85
94
|
height: '700px',
|
|
86
95
|
headline: 'Aggregate',
|
|
96
|
+
initialSortField: 'count',
|
|
97
|
+
initialSortDirection: 'descending',
|
|
87
98
|
},
|
|
88
99
|
};
|
|
@@ -67,6 +67,19 @@ export class AggregateComponent extends PreactLitAdapterWithGridJsStyles {
|
|
|
67
67
|
@property({ type: String })
|
|
68
68
|
headline: string = 'Aggregate';
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The field by which the table is initially sorted.
|
|
72
|
+
* Must be one of the fields specified in the fields property, 'count', or 'proportion'.
|
|
73
|
+
*/
|
|
74
|
+
@property({ type: String })
|
|
75
|
+
initialSortField: string = 'count';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The initial sort direction of the table.
|
|
79
|
+
*/
|
|
80
|
+
@property({ type: String })
|
|
81
|
+
initialSortDirection: 'ascending' | 'descending' = 'descending';
|
|
82
|
+
|
|
70
83
|
override render() {
|
|
71
84
|
return (
|
|
72
85
|
<Aggregate
|
|
@@ -76,6 +89,8 @@ export class AggregateComponent extends PreactLitAdapterWithGridJsStyles {
|
|
|
76
89
|
width={this.width}
|
|
77
90
|
height={this.height}
|
|
78
91
|
headline={this.headline}
|
|
92
|
+
initialSortField={this.initialSortField}
|
|
93
|
+
initialSortDirection={this.initialSortDirection}
|
|
79
94
|
/>
|
|
80
95
|
);
|
|
81
96
|
}
|