@genspectrum/dashboard-components 0.11.5 → 0.11.7
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 +92 -6
- package/dist/assets/{mutationOverTimeWorker-CWneD7i5.js.map → mutationOverTimeWorker-DTv93Ere.js.map} +1 -1
- package/dist/components.d.ts +67 -19
- package/dist/components.js +301 -90
- package/dist/components.js.map +1 -1
- package/dist/style.css +3 -0
- package/dist/util.d.ts +53 -22
- package/package.json +1 -1
- package/src/preact/aggregatedData/__mockData__/aggregatedWith1Field.json +399 -0
- package/src/preact/aggregatedData/__mockData__/aggregatedWith2Fields.json +1771 -0
- package/src/preact/aggregatedData/aggregate-bar-chart.tsx +177 -0
- package/src/preact/aggregatedData/aggregate-table.tsx +24 -2
- package/src/preact/aggregatedData/aggregate.stories.tsx +61 -2
- package/src/preact/aggregatedData/aggregate.tsx +18 -6
- package/src/preact/mutations/__mockData__/baselineNucleotideMutations.json +337412 -0
- package/src/preact/mutations/__mockData__/overallVariantCount.json +14 -0
- package/src/preact/mutations/getMutationsTableData.spec.ts +20 -3
- package/src/preact/mutations/getMutationsTableData.ts +37 -2
- package/src/preact/mutations/mutations-table.tsx +47 -27
- package/src/preact/mutations/mutations.stories.tsx +41 -9
- package/src/preact/mutations/mutations.tsx +22 -6
- package/src/preact/mutations/queryMutations.ts +28 -8
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +11077 -3062
- package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +3883 -6606
- package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +17624 -2203
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +1 -1
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
- package/src/preact/shared/charts/colors.ts +1 -1
- package/src/query/queryAggregateData.spec.ts +16 -109
- package/src/query/queryAggregateData.ts +2 -12
- package/src/query/queryGeneralStatistics.ts +2 -2
- package/src/query/queryMutationsOverTime.spec.ts +144 -4
- package/src/query/queryMutationsOverTime.ts +17 -1
- package/src/web-components/visualization/gs-aggregate.stories.ts +90 -20
- package/src/web-components/visualization/gs-aggregate.tsx +20 -0
- package/src/web-components/visualization/gs-mutations.stories.ts +62 -4
- package/src/web-components/visualization/gs-mutations.tsx +44 -0
- package/standalone-bundle/assets/{mutationOverTimeWorker-x1ipPFL0.js.map → mutationOverTimeWorker-DEybsZ5r.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +4136 -3956
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
|
@@ -85,7 +85,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number) {
|
|
88
|
-
const tooltipX = rowIndex < rows / 2 ? 'bottom' : 'top';
|
|
88
|
+
const tooltipX = rowIndex < rows / 2 || rowIndex < 6 ? 'bottom' : 'top';
|
|
89
89
|
const tooltipY = columnIndex < columns / 2 ? 'start' : 'end';
|
|
90
90
|
return `${tooltipX}-${tooltipY}` as const;
|
|
91
91
|
}
|
|
@@ -252,7 +252,7 @@ function getDownloadData(filteredData: MutationOverTimeDataMap) {
|
|
|
252
252
|
return filteredData.getFirstAxisKeys().map((mutation) => {
|
|
253
253
|
return dates.reduce(
|
|
254
254
|
(accumulated, date) => {
|
|
255
|
-
const proportion = filteredData.get(mutation, date)?.proportion ??
|
|
255
|
+
const proportion = filteredData.get(mutation, date)?.proportion ?? '';
|
|
256
256
|
return {
|
|
257
257
|
...accumulated,
|
|
258
258
|
[date.dateString]: proportion,
|
|
@@ -18,7 +18,7 @@ export const singleGraphColorRGBAById = (id: number, alpha = 1) => {
|
|
|
18
18
|
const keys = Object.keys(ColorsRGB) as GraphColor[];
|
|
19
19
|
const key = keys[id % keys.length];
|
|
20
20
|
|
|
21
|
-
return
|
|
21
|
+
return singleGraphColorRGBByName(key, alpha);
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export const singleGraphColorRGBByName = (name: GraphColor, alpha = 1) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { queryAggregateData } from './queryAggregateData';
|
|
3
|
+
import { compareAscending, queryAggregateData } from './queryAggregateData';
|
|
4
4
|
import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
|
|
5
5
|
|
|
6
6
|
describe('queryAggregateData', () => {
|
|
@@ -29,118 +29,25 @@ describe('queryAggregateData', () => {
|
|
|
29
29
|
{ proportion: 0.125, count: 4, region: 'region1', host: 'host2' },
|
|
30
30
|
]);
|
|
31
31
|
});
|
|
32
|
+
});
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
]);
|
|
34
|
+
describe('compareAscending', () => {
|
|
35
|
+
test('should compare numbers', () => {
|
|
36
|
+
expect(compareAscending(1, 2)).to.equal(-1);
|
|
37
|
+
expect(compareAscending(2, 1)).to.equal(1);
|
|
38
|
+
expect(compareAscending(2, 2)).to.equal(0);
|
|
93
39
|
});
|
|
94
40
|
|
|
95
|
-
test('should
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
]);
|
|
41
|
+
test('should compare strings', () => {
|
|
42
|
+
expect(compareAscending('a', 'b')).to.equal(-1);
|
|
43
|
+
expect(compareAscending('b', 'a')).to.equal(1);
|
|
44
|
+
expect(compareAscending('a', 'a')).to.equal(0);
|
|
124
45
|
});
|
|
125
46
|
|
|
126
|
-
test('should
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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');
|
|
47
|
+
test('should compare boolean', () => {
|
|
48
|
+
expect(compareAscending(true, false)).to.equal(1);
|
|
49
|
+
expect(compareAscending(false, true)).to.equal(-1);
|
|
50
|
+
expect(compareAscending(true, true)).to.equal(0);
|
|
51
|
+
expect(compareAscending(false, false)).to.equal(0);
|
|
145
52
|
});
|
|
146
53
|
});
|
|
@@ -9,7 +9,7 @@ export type AggregateData = (Record<string, string | null | number | boolean> &
|
|
|
9
9
|
proportion: number;
|
|
10
10
|
})[];
|
|
11
11
|
|
|
12
|
-
export const compareAscending = (a: string | null | number, b: string | null | number) => {
|
|
12
|
+
export const compareAscending = (a: string | null | number | boolean, b: string | null | number | boolean) => {
|
|
13
13
|
if (typeof a === 'number' && typeof b === 'number') {
|
|
14
14
|
return a - b;
|
|
15
15
|
}
|
|
@@ -24,20 +24,10 @@ export async function queryAggregateData(
|
|
|
24
24
|
lapisFilter: LapisFilter,
|
|
25
25
|
fields: string[],
|
|
26
26
|
lapis: string,
|
|
27
|
-
initialSort: InitialSort = { field: 'count', direction: 'descending' },
|
|
28
27
|
signal?: AbortSignal,
|
|
29
28
|
) {
|
|
30
|
-
const validSortFields = ['count', 'proportion', ...fields];
|
|
31
|
-
if (!validSortFields.includes(initialSort.field)) {
|
|
32
|
-
throw new Error(`InitialSort field not in fields. Valid fields are: ${validSortFields.join(', ')}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
29
|
const fetchData = new FetchAggregatedOperator<Record<string, string | null | number>>(lapisFilter, fields);
|
|
36
|
-
const sortData = new SortOperator(fetchData, (a, b) =>
|
|
37
|
-
return initialSort.direction === 'ascending'
|
|
38
|
-
? compareAscending(a[initialSort.field], b[initialSort.field])
|
|
39
|
-
: compareAscending(b[initialSort.field], a[initialSort.field]);
|
|
40
|
-
});
|
|
30
|
+
const sortData = new SortOperator(fetchData, (a, b) => compareAscending(b.count, a.count));
|
|
41
31
|
const data = (await sortData.evaluate(lapis, signal)).content;
|
|
42
32
|
|
|
43
33
|
const total = data.reduce((acc, row) => acc + row.count, 0);
|
|
@@ -7,8 +7,8 @@ export async function queryGeneralStatistics(
|
|
|
7
7
|
lapis: string,
|
|
8
8
|
signal?: AbortSignal,
|
|
9
9
|
) {
|
|
10
|
-
const numeratorCount = await queryAggregateData(numeratorFilter, [], lapis,
|
|
11
|
-
const denominatorCount = await queryAggregateData(denominatorFilter, [], lapis,
|
|
10
|
+
const numeratorCount = await queryAggregateData(numeratorFilter, [], lapis, signal);
|
|
11
|
+
const denominatorCount = await queryAggregateData(denominatorFilter, [], lapis, signal);
|
|
12
12
|
|
|
13
13
|
if (numeratorCount.length === 0 || denominatorCount.length === 0) {
|
|
14
14
|
throw new Error('No data found for the given filters');
|
|
@@ -100,7 +100,11 @@ describe('queryMutationsOverTime', () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
expect(mutationOverTimeData.getAsArray()).to.deep.equal([
|
|
103
|
-
[
|
|
103
|
+
[
|
|
104
|
+
{ proportion: 0.4, count: 4, totalCount: 11 },
|
|
105
|
+
{ proportion: 0, count: 0, totalCount: 12 },
|
|
106
|
+
{ proportion: 0, count: 0, totalCount: 13 },
|
|
107
|
+
],
|
|
104
108
|
[
|
|
105
109
|
{ proportion: 0.1, count: 1, totalCount: 11 },
|
|
106
110
|
{ proportion: 0.2, count: 2, totalCount: 12 },
|
|
@@ -177,7 +181,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
177
181
|
dateFieldTo: '2023-01-02',
|
|
178
182
|
fields: [],
|
|
179
183
|
},
|
|
180
|
-
response: { data: [{ count:
|
|
184
|
+
response: { data: [{ count: 0 }] },
|
|
181
185
|
},
|
|
182
186
|
{
|
|
183
187
|
body: {
|
|
@@ -247,7 +251,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
247
251
|
});
|
|
248
252
|
|
|
249
253
|
expect(mutationOverTimeData.getAsArray()).to.deep.equal([
|
|
250
|
-
[{ proportion: 0.4, count: 4, totalCount: 11 }, null,
|
|
254
|
+
[{ proportion: 0.4, count: 4, totalCount: 11 }, null, { proportion: 0, count: 0, totalCount: 13 }],
|
|
251
255
|
[{ proportion: 0.1, count: 1, totalCount: 11 }, null, { proportion: 0.3, count: 3, totalCount: 13 }],
|
|
252
256
|
]);
|
|
253
257
|
|
|
@@ -688,7 +692,10 @@ describe('queryMutationsOverTime', () => {
|
|
|
688
692
|
});
|
|
689
693
|
|
|
690
694
|
expect(mutationOverTimeData.getAsArray()).to.deep.equal([
|
|
691
|
-
[
|
|
695
|
+
[
|
|
696
|
+
{ proportion: 0.4, count: 4, totalCount: 11 },
|
|
697
|
+
{ proportion: 0, count: 0, totalCount: 12 },
|
|
698
|
+
],
|
|
692
699
|
[
|
|
693
700
|
{ proportion: 0.1, count: 1, totalCount: 11 },
|
|
694
701
|
{ proportion: 0.2, count: 2, totalCount: 12 },
|
|
@@ -734,6 +741,139 @@ describe('queryMutationsOverTime', () => {
|
|
|
734
741
|
expect(dates.length).toBe(0);
|
|
735
742
|
});
|
|
736
743
|
|
|
744
|
+
it('should fill with 0 if the mutation does not exist in a date range but count > 0', async () => {
|
|
745
|
+
const lapisFilter = { field1: 'value1', field2: 'value2' };
|
|
746
|
+
const dateField = 'dateField';
|
|
747
|
+
|
|
748
|
+
lapisRequestMocks.multipleAggregated([
|
|
749
|
+
{
|
|
750
|
+
body: { ...lapisFilter, fields: [dateField] },
|
|
751
|
+
response: {
|
|
752
|
+
data: [
|
|
753
|
+
{ count: 1, [dateField]: '2023-01-01' },
|
|
754
|
+
{ count: 1, [dateField]: '2023-01-02' },
|
|
755
|
+
],
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
body: {
|
|
760
|
+
...lapisFilter,
|
|
761
|
+
dateFieldFrom: '2023-01-01',
|
|
762
|
+
dateFieldTo: '2023-01-01',
|
|
763
|
+
fields: [],
|
|
764
|
+
},
|
|
765
|
+
response: { data: [{ count: 11 }] },
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
body: {
|
|
769
|
+
...lapisFilter,
|
|
770
|
+
dateFieldFrom: '2023-01-02',
|
|
771
|
+
dateFieldTo: '2023-01-02',
|
|
772
|
+
fields: [],
|
|
773
|
+
},
|
|
774
|
+
response: { data: [{ count: 11 }] },
|
|
775
|
+
},
|
|
776
|
+
]);
|
|
777
|
+
|
|
778
|
+
lapisRequestMocks.multipleMutations(
|
|
779
|
+
[
|
|
780
|
+
{
|
|
781
|
+
body: {
|
|
782
|
+
...lapisFilter,
|
|
783
|
+
dateFieldFrom: '2023-01-01',
|
|
784
|
+
dateFieldTo: '2023-01-01',
|
|
785
|
+
minProportion: 0.001,
|
|
786
|
+
},
|
|
787
|
+
response: { data: [getSomeTestMutation(0.1, 1)] },
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
body: {
|
|
791
|
+
...lapisFilter,
|
|
792
|
+
dateFieldFrom: '2023-01-02',
|
|
793
|
+
dateFieldTo: '2023-01-02',
|
|
794
|
+
minProportion: 0.001,
|
|
795
|
+
},
|
|
796
|
+
response: { data: [] },
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
body: {
|
|
800
|
+
...lapisFilter,
|
|
801
|
+
dateFieldFrom: '2023-01-01',
|
|
802
|
+
dateFieldTo: '2023-01-02',
|
|
803
|
+
minProportion: 0.001,
|
|
804
|
+
},
|
|
805
|
+
response: {
|
|
806
|
+
data: [getSomeTestMutation(0.21, 6)],
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
'nucleotide',
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
const { mutationOverTimeData } = await queryMutationsOverTimeData({
|
|
814
|
+
lapisFilter,
|
|
815
|
+
sequenceType: 'nucleotide',
|
|
816
|
+
lapis: DUMMY_LAPIS_URL,
|
|
817
|
+
lapisDateField: dateField,
|
|
818
|
+
granularity: 'day',
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
expect(mutationOverTimeData.getAsArray()).to.deep.equal([
|
|
822
|
+
[
|
|
823
|
+
{ proportion: 0.1, count: 1, totalCount: 11 },
|
|
824
|
+
{ proportion: 0, count: 0, totalCount: 11 },
|
|
825
|
+
],
|
|
826
|
+
]);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('should return null if count in a date range is 0', async () => {
|
|
830
|
+
const lapisFilter = { field1: 'value1', field2: 'value2' };
|
|
831
|
+
const dateField = 'dateField';
|
|
832
|
+
|
|
833
|
+
lapisRequestMocks.multipleAggregated([
|
|
834
|
+
{
|
|
835
|
+
body: { ...lapisFilter, fields: [dateField] },
|
|
836
|
+
response: {
|
|
837
|
+
data: [{ count: 0, [dateField]: '2023-01-01' }],
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
body: {
|
|
842
|
+
...lapisFilter,
|
|
843
|
+
dateFieldFrom: '2023-01-01',
|
|
844
|
+
dateFieldTo: '2023-01-01',
|
|
845
|
+
fields: [],
|
|
846
|
+
},
|
|
847
|
+
response: { data: [{ count: 0 }] },
|
|
848
|
+
},
|
|
849
|
+
]);
|
|
850
|
+
|
|
851
|
+
lapisRequestMocks.multipleMutations(
|
|
852
|
+
[
|
|
853
|
+
{
|
|
854
|
+
body: {
|
|
855
|
+
...lapisFilter,
|
|
856
|
+
dateFieldFrom: '2023-01-01',
|
|
857
|
+
dateFieldTo: '2023-01-01',
|
|
858
|
+
minProportion: 0.001,
|
|
859
|
+
},
|
|
860
|
+
response: { data: [getSomeTestMutation(0.1, 1)] },
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
'nucleotide',
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
const { mutationOverTimeData } = await queryMutationsOverTimeData({
|
|
867
|
+
lapisFilter,
|
|
868
|
+
sequenceType: 'nucleotide',
|
|
869
|
+
lapis: DUMMY_LAPIS_URL,
|
|
870
|
+
lapisDateField: dateField,
|
|
871
|
+
granularity: 'day',
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
expect(mutationOverTimeData.getAsArray()).to.deep.equal([[null]]);
|
|
875
|
+
});
|
|
876
|
+
|
|
737
877
|
function getSomeTestMutation(proportion: number, count: number) {
|
|
738
878
|
return {
|
|
739
879
|
mutation: 'sequenceName:A123T',
|
|
@@ -122,6 +122,7 @@ export async function queryMutationsOverTimeData({
|
|
|
122
122
|
|
|
123
123
|
const data = await fetchAndPrepareSubstitutionsOrDeletions(filter, sequenceType).evaluate(lapis, signal);
|
|
124
124
|
const totalCountQuery = await getTotalNumberOfSequencesInDateRange(filter).evaluate(lapis, signal);
|
|
125
|
+
|
|
125
126
|
return {
|
|
126
127
|
date,
|
|
127
128
|
mutations: data.content,
|
|
@@ -244,9 +245,14 @@ export function groupByMutation(
|
|
|
244
245
|
});
|
|
245
246
|
|
|
246
247
|
data.forEach((mutationData) => {
|
|
248
|
+
if (mutationData.totalCount == 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const date = toTemporal(mutationData.date);
|
|
253
|
+
|
|
247
254
|
mutationData.mutations.forEach((mutationEntry) => {
|
|
248
255
|
const mutation = toSubstitutionOrDeletion(mutationEntry.mutation);
|
|
249
|
-
const date = toTemporal(mutationData.date);
|
|
250
256
|
|
|
251
257
|
if (dataArray.get(mutation, date) !== undefined) {
|
|
252
258
|
dataArray.set(mutation, date, {
|
|
@@ -256,6 +262,16 @@ export function groupByMutation(
|
|
|
256
262
|
});
|
|
257
263
|
}
|
|
258
264
|
});
|
|
265
|
+
|
|
266
|
+
for (const firstAxisKey of dataArray.getFirstAxisKeys()) {
|
|
267
|
+
if (dataArray.get(firstAxisKey, date) === null) {
|
|
268
|
+
dataArray.set(firstAxisKey, date, {
|
|
269
|
+
count: 0,
|
|
270
|
+
proportion: 0,
|
|
271
|
+
totalCount: mutationData.totalCount,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
259
275
|
});
|
|
260
276
|
|
|
261
277
|
return dataArray;
|
|
@@ -4,6 +4,8 @@ import { html } from 'lit';
|
|
|
4
4
|
import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
|
|
5
5
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
6
6
|
import aggregatedData from '../../preact/aggregatedData/__mockData__/aggregated.json';
|
|
7
|
+
import aggregatedDataWith1Field from '../../preact/aggregatedData/__mockData__/aggregatedWith1Field.json';
|
|
8
|
+
import aggregatedDataWith2Fields from '../../preact/aggregatedData/__mockData__/aggregatedWith2Fields.json';
|
|
7
9
|
import type { AggregateProps } from '../../preact/aggregatedData/aggregate';
|
|
8
10
|
|
|
9
11
|
import './gs-aggregate';
|
|
@@ -19,6 +21,7 @@ const codeExample = `
|
|
|
19
21
|
initialSortField="count"
|
|
20
22
|
initialSortDirection="descending"
|
|
21
23
|
pageSize="10"
|
|
24
|
+
maxNumberOfBars="50"
|
|
22
25
|
></gs-aggregate>`;
|
|
23
26
|
|
|
24
27
|
const meta: Meta<Required<AggregateProps>> = {
|
|
@@ -27,7 +30,7 @@ const meta: Meta<Required<AggregateProps>> = {
|
|
|
27
30
|
argTypes: {
|
|
28
31
|
fields: [{ control: 'object' }],
|
|
29
32
|
views: {
|
|
30
|
-
options: ['table'],
|
|
33
|
+
options: ['table', 'bar'],
|
|
31
34
|
control: { type: 'check' },
|
|
32
35
|
},
|
|
33
36
|
width: { control: 'text' },
|
|
@@ -40,24 +43,6 @@ const meta: Meta<Required<AggregateProps>> = {
|
|
|
40
43
|
},
|
|
41
44
|
},
|
|
42
45
|
parameters: withComponentDocs({
|
|
43
|
-
fetchMock: {
|
|
44
|
-
mocks: [
|
|
45
|
-
{
|
|
46
|
-
matcher: {
|
|
47
|
-
name: 'aggregatedData',
|
|
48
|
-
url: AGGREGATED_ENDPOINT,
|
|
49
|
-
body: {
|
|
50
|
-
fields: ['division', 'host'],
|
|
51
|
-
country: 'USA',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
response: {
|
|
55
|
-
status: 200,
|
|
56
|
-
body: aggregatedData,
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
46
|
componentDocs: {
|
|
62
47
|
opensShadowDom: true,
|
|
63
48
|
expectsChildren: false,
|
|
@@ -81,12 +66,33 @@ export const Table: StoryObj<Required<AggregateProps>> = {
|
|
|
81
66
|
.initialSortField=${args.initialSortField}
|
|
82
67
|
.initialSortDirection=${args.initialSortDirection}
|
|
83
68
|
.pageSize=${args.pageSize}
|
|
69
|
+
.maxNumberOfBars=${args.maxNumberOfBars}
|
|
84
70
|
></gs-aggregate>
|
|
85
71
|
</gs-app>
|
|
86
72
|
`,
|
|
73
|
+
parameters: {
|
|
74
|
+
fetchMock: {
|
|
75
|
+
mocks: [
|
|
76
|
+
{
|
|
77
|
+
matcher: {
|
|
78
|
+
name: 'aggregatedData',
|
|
79
|
+
url: AGGREGATED_ENDPOINT,
|
|
80
|
+
body: {
|
|
81
|
+
fields: ['division', 'host'],
|
|
82
|
+
country: 'USA',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
response: {
|
|
86
|
+
status: 200,
|
|
87
|
+
body: aggregatedData,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
87
93
|
args: {
|
|
88
94
|
fields: ['division', 'host'],
|
|
89
|
-
views: ['table'],
|
|
95
|
+
views: ['table', 'bar'],
|
|
90
96
|
lapisFilter: {
|
|
91
97
|
country: 'USA',
|
|
92
98
|
},
|
|
@@ -95,5 +101,69 @@ export const Table: StoryObj<Required<AggregateProps>> = {
|
|
|
95
101
|
initialSortField: 'count',
|
|
96
102
|
initialSortDirection: 'descending',
|
|
97
103
|
pageSize: 10,
|
|
104
|
+
maxNumberOfBars: 10,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const BarChartWithOneField: StoryObj<Required<AggregateProps>> = {
|
|
109
|
+
...Table,
|
|
110
|
+
args: {
|
|
111
|
+
...Table.args,
|
|
112
|
+
fields: ['division'],
|
|
113
|
+
views: ['bar', 'table'],
|
|
114
|
+
},
|
|
115
|
+
parameters: {
|
|
116
|
+
fetchMock: {
|
|
117
|
+
mocks: [
|
|
118
|
+
{
|
|
119
|
+
matcher: {
|
|
120
|
+
name: 'aggregatedData',
|
|
121
|
+
url: AGGREGATED_ENDPOINT,
|
|
122
|
+
body: {
|
|
123
|
+
fields: ['division'],
|
|
124
|
+
country: 'USA',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
response: {
|
|
128
|
+
status: 200,
|
|
129
|
+
body: aggregatedDataWith1Field,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const BarChartWithTwoFields: StoryObj<Required<AggregateProps>> = {
|
|
138
|
+
...Table,
|
|
139
|
+
args: {
|
|
140
|
+
...Table.args,
|
|
141
|
+
fields: ['division', 'nextstrainClade'],
|
|
142
|
+
lapisFilter: {
|
|
143
|
+
country: 'Germany',
|
|
144
|
+
dateTo: '2022-02-01',
|
|
145
|
+
},
|
|
146
|
+
views: ['bar', 'table'],
|
|
147
|
+
},
|
|
148
|
+
parameters: {
|
|
149
|
+
fetchMock: {
|
|
150
|
+
mocks: [
|
|
151
|
+
{
|
|
152
|
+
matcher: {
|
|
153
|
+
name: 'aggregatedData',
|
|
154
|
+
url: AGGREGATED_ENDPOINT,
|
|
155
|
+
body: {
|
|
156
|
+
fields: ['division', 'nextstrainClade'],
|
|
157
|
+
country: 'Germany',
|
|
158
|
+
dateTo: '2022-02-01',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
response: {
|
|
162
|
+
status: 200,
|
|
163
|
+
body: aggregatedDataWith2Fields,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
98
168
|
},
|
|
99
169
|
};
|
|
@@ -20,6 +20,16 @@ import { PreactLitAdapterWithGridJsStyles } from '../PreactLitAdapterWithGridJsS
|
|
|
20
20
|
* along with the aggregated value and its proportion.
|
|
21
21
|
* The proportion represents the ratio of the aggregated value to the total count of the data
|
|
22
22
|
* (considering the applied filter).
|
|
23
|
+
*
|
|
24
|
+
* ### Bar Chart View
|
|
25
|
+
*
|
|
26
|
+
* In the bar chart view, the data is presented in vertical bars.
|
|
27
|
+
* The bar chart is supported when `fields` contains one or two entries.
|
|
28
|
+
* The first field will be used as the y-axis.
|
|
29
|
+
* If a second field is provided, it's values will be stacked along the x-axis for each key on the y-axis.
|
|
30
|
+
*
|
|
31
|
+
* The chart shows the bars with the highest aggregated `count`.
|
|
32
|
+
* The number of bars can be adjusted with the `maxNumberOfBars` property.
|
|
23
33
|
*/
|
|
24
34
|
@customElement('gs-aggregate')
|
|
25
35
|
export class AggregateComponent extends PreactLitAdapterWithGridJsStyles {
|
|
@@ -87,6 +97,12 @@ export class AggregateComponent extends PreactLitAdapterWithGridJsStyles {
|
|
|
87
97
|
@property({ type: Object })
|
|
88
98
|
pageSize: boolean | number = false;
|
|
89
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The maximum number of bars to display in the bar chart view.
|
|
102
|
+
*/
|
|
103
|
+
@property({ type: Object })
|
|
104
|
+
maxNumberOfBars: number = 20;
|
|
105
|
+
|
|
90
106
|
override render() {
|
|
91
107
|
return (
|
|
92
108
|
<Aggregate
|
|
@@ -98,6 +114,7 @@ export class AggregateComponent extends PreactLitAdapterWithGridJsStyles {
|
|
|
98
114
|
initialSortField={this.initialSortField}
|
|
99
115
|
initialSortDirection={this.initialSortDirection}
|
|
100
116
|
pageSize={this.pageSize}
|
|
117
|
+
maxNumberOfBars={this.maxNumberOfBars}
|
|
101
118
|
/>
|
|
102
119
|
);
|
|
103
120
|
}
|
|
@@ -131,4 +148,7 @@ type InitialSortDirectionMatches = Expect<
|
|
|
131
148
|
Equals<typeof AggregateComponent.prototype.initialSortDirection, AggregateProps['initialSortDirection']>
|
|
132
149
|
>;
|
|
133
150
|
type PageSizeMatches = Expect<Equals<typeof AggregateComponent.prototype.pageSize, AggregateProps['pageSize']>>;
|
|
151
|
+
type MaxNumberOfBarsMatches = Expect<
|
|
152
|
+
Equals<typeof AggregateComponent.prototype.maxNumberOfBars, AggregateProps['maxNumberOfBars']>
|
|
153
|
+
>;
|
|
134
154
|
/* eslint-enable @typescript-eslint/no-unused-vars, no-unused-vars */
|