@genspectrum/dashboard-components 0.4.2 → 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.
@@ -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/dist/style.css CHANGED
@@ -376,7 +376,7 @@ input[type="range"] {
376
376
  background-color: #C6C6C6;
377
377
  pointer-events: none;
378
378
  }/*
379
- ! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
379
+ ! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com
380
380
  *//*
381
381
  1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
382
382
  2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
@@ -835,6 +835,10 @@ html {
835
835
  -webkit-tap-highlight-color: transparent;
836
836
  }
837
837
 
838
+ * {
839
+ scrollbar-color: currentColor transparent;
840
+ }
841
+
838
842
  :root {
839
843
  color-scheme: light;
840
844
  --in: 72.06% 0.191 231.6;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -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 fields={fields} filter={filter} views={views} />
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> = ({ fields, views, filter }) => {
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) {
@@ -11,7 +11,6 @@ import {
11
11
  type PresetOptionValues,
12
12
  } from './selectableOptions';
13
13
  import { ErrorBoundary } from '../components/error-boundary';
14
- import { ResizeContainer } from '../components/resize-container';
15
14
  import { Select } from '../components/select';
16
15
  import type { ScaleType } from '../shared/charts/getYAxisScale';
17
16
 
@@ -41,7 +40,7 @@ export const DateRangeSelector = <CustomLabel extends string>({
41
40
 
42
41
  return (
43
42
  <ErrorBoundary size={size}>
44
- <ResizeContainer size={size}>
43
+ <div style={{ width }}>
45
44
  <DateRangeSelectorInner
46
45
  customSelectOptions={customSelectOptions}
47
46
  earliestDate={earliestDate}
@@ -50,7 +49,7 @@ export const DateRangeSelector = <CustomLabel extends string>({
50
49
  initialDateFrom={initialDateFrom}
51
50
  initialDateTo={initialDateTo}
52
51
  />
53
- </ResizeContainer>
52
+ </div>
54
53
  </ErrorBoundary>
55
54
  );
56
55
  };
@@ -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
- export async function queryAggregateData(variant: LapisFilter, fields: string[], lapis: string, signal?: AbortSignal) {
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 data = (await fetchData.evaluate(lapis, signal)).content;
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
  }