@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.
Files changed (41) hide show
  1. package/custom-elements.json +92 -6
  2. package/dist/assets/{mutationOverTimeWorker-CWneD7i5.js.map → mutationOverTimeWorker-DTv93Ere.js.map} +1 -1
  3. package/dist/components.d.ts +67 -19
  4. package/dist/components.js +301 -90
  5. package/dist/components.js.map +1 -1
  6. package/dist/style.css +3 -0
  7. package/dist/util.d.ts +53 -22
  8. package/package.json +1 -1
  9. package/src/preact/aggregatedData/__mockData__/aggregatedWith1Field.json +399 -0
  10. package/src/preact/aggregatedData/__mockData__/aggregatedWith2Fields.json +1771 -0
  11. package/src/preact/aggregatedData/aggregate-bar-chart.tsx +177 -0
  12. package/src/preact/aggregatedData/aggregate-table.tsx +24 -2
  13. package/src/preact/aggregatedData/aggregate.stories.tsx +61 -2
  14. package/src/preact/aggregatedData/aggregate.tsx +18 -6
  15. package/src/preact/mutations/__mockData__/baselineNucleotideMutations.json +337412 -0
  16. package/src/preact/mutations/__mockData__/overallVariantCount.json +14 -0
  17. package/src/preact/mutations/getMutationsTableData.spec.ts +20 -3
  18. package/src/preact/mutations/getMutationsTableData.ts +37 -2
  19. package/src/preact/mutations/mutations-table.tsx +47 -27
  20. package/src/preact/mutations/mutations.stories.tsx +41 -9
  21. package/src/preact/mutations/mutations.tsx +22 -6
  22. package/src/preact/mutations/queryMutations.ts +28 -8
  23. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +11077 -3062
  24. package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +3883 -6606
  25. package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +17624 -2203
  26. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +1 -1
  27. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
  28. package/src/preact/shared/charts/colors.ts +1 -1
  29. package/src/query/queryAggregateData.spec.ts +16 -109
  30. package/src/query/queryAggregateData.ts +2 -12
  31. package/src/query/queryGeneralStatistics.ts +2 -2
  32. package/src/query/queryMutationsOverTime.spec.ts +144 -4
  33. package/src/query/queryMutationsOverTime.ts +17 -1
  34. package/src/web-components/visualization/gs-aggregate.stories.ts +90 -20
  35. package/src/web-components/visualization/gs-aggregate.tsx +20 -0
  36. package/src/web-components/visualization/gs-mutations.stories.ts +62 -4
  37. package/src/web-components/visualization/gs-mutations.tsx +44 -0
  38. package/standalone-bundle/assets/{mutationOverTimeWorker-x1ipPFL0.js.map → mutationOverTimeWorker-DEybsZ5r.js.map} +1 -1
  39. package/standalone-bundle/dashboard-components.js +4136 -3956
  40. package/standalone-bundle/dashboard-components.js.map +1 -1
  41. 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 ?? 0;
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 `rgba(${ColorsRGB[key].join(',')},${alpha})`;
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
- 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
- ]);
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 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
- ]);
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 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');
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, undefined, signal);
11
- const denominatorCount = await queryAggregateData(denominatorFilter, [], lapis, undefined, signal);
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
- [{ proportion: 0.4, count: 4, totalCount: 11 }, null, null],
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: 12 }] },
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, 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
- [{ proportion: 0.4, count: 4, totalCount: 11 }, null],
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 */