@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
@@ -0,0 +1,177 @@
1
+ import { BarController, Chart, type ChartConfiguration, type ChartDataset, registerables } from 'chart.js';
2
+ import { type FunctionComponent } from 'preact';
3
+ import { useMemo } from 'preact/hooks';
4
+
5
+ import type { AggregateData } from '../../query/queryAggregateData';
6
+ import GsChart from '../components/chart';
7
+ import { UserFacingError } from '../components/error-display';
8
+ import { NoDataDisplay } from '../components/no-data-display';
9
+ import { singleGraphColorRGBAById } from '../shared/charts/colors';
10
+ import { formatProportion } from '../shared/table/formatProportion';
11
+
12
+ interface AggregateBarChartProps {
13
+ data: AggregateData;
14
+ fields: string[];
15
+ maxNumberOfBars: number;
16
+ }
17
+
18
+ Chart.register(...registerables, BarController);
19
+
20
+ type DataPoint = {
21
+ y: string;
22
+ x: number;
23
+ proportion: number;
24
+ };
25
+
26
+ export const AggregateBarChart: FunctionComponent<AggregateBarChartProps> = ({ data, fields, maxNumberOfBars }) => {
27
+ if (data.length === 0) {
28
+ return <NoDataDisplay />;
29
+ }
30
+
31
+ if (fields.length === 0) {
32
+ throw new UserFacingError(
33
+ 'No fields given',
34
+ 'Cannot display a bar chart when the "fields" attribute of this component is empty, i.e. there are no fields given that the data should be stratified by. This must be fixed by the administrator of this page.',
35
+ );
36
+ }
37
+
38
+ if (fields.length > 2) {
39
+ throw new UserFacingError(
40
+ 'Too many fields given',
41
+ `Cannot display a bar chart when the "fields" attribute of this component contains more than two values. Got the fields: ${fields.join(', ')}. This must be fixed by the administrator of this page.`,
42
+ );
43
+ }
44
+
45
+ return <AggregateBarChartInner data={data} fields={fields} maxNumberOfBars={maxNumberOfBars} />;
46
+ };
47
+
48
+ const AggregateBarChartInner: FunctionComponent<AggregateBarChartProps> = ({ data, fields, maxNumberOfBars }) => {
49
+ const config = useMemo((): ChartConfiguration<'bar', DataPoint[]> => {
50
+ const { datasets, countsOfEachBar } = getDatasets(fields, maxNumberOfBars, data);
51
+
52
+ return {
53
+ type: 'bar',
54
+ data: {
55
+ datasets,
56
+ },
57
+ options: {
58
+ maintainAspectRatio: false,
59
+ animation: false,
60
+ indexAxis: 'y',
61
+ scales: {
62
+ x: {
63
+ stacked: true,
64
+ },
65
+ y: {
66
+ stacked: true,
67
+ },
68
+ },
69
+ plugins: {
70
+ legend: {
71
+ display: false,
72
+ },
73
+ tooltip: {
74
+ mode: 'y',
75
+ callbacks: {
76
+ afterTitle:
77
+ countsOfEachBar === undefined
78
+ ? undefined
79
+ : (tooltipItems) => {
80
+ return `Total: ${countsOfEachBar.get(tooltipItems[0].label)}`;
81
+ },
82
+ label: (context) => {
83
+ const { x, proportion } = context.dataset.data[
84
+ context.dataIndex
85
+ ] as unknown as DataPoint;
86
+ return fields.length === 1
87
+ ? `${x} (${formatProportion(proportion)})`
88
+ : `${context.dataset.label}: ${x} (${formatProportion(proportion)})`;
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ };
95
+ }, [data, fields, maxNumberOfBars]);
96
+
97
+ return <GsChart configuration={config} />;
98
+ };
99
+
100
+ function getDatasets(
101
+ fields: string[],
102
+ maxNumberOfBars: number,
103
+ data: (Record<string, string | number | boolean | null> & {
104
+ count: number;
105
+ proportion: number;
106
+ })[],
107
+ ) {
108
+ const sortedData = data.sort((a, b) => b.count - a.count);
109
+
110
+ if (fields.length === 1) {
111
+ return {
112
+ datasets: [
113
+ {
114
+ borderWidth: 1,
115
+ backgroundColor: singleGraphColorRGBAById(0, 0.3),
116
+ borderColor: singleGraphColorRGBAById(0),
117
+ data: sortedData.slice(0, maxNumberOfBars).map((row) => ({
118
+ y: row[fields[0]] as string,
119
+ x: row.count,
120
+ proportion: row.proportion,
121
+ })),
122
+ },
123
+ ],
124
+ };
125
+ }
126
+
127
+ const map = new Map<string, DataPoint[]>();
128
+ const countsOfEachBar = new Map<string, number>();
129
+
130
+ for (const row of sortedData) {
131
+ const yValue = row[fields[0]];
132
+ const secondaryValue = row[fields[1]];
133
+ if (yValue === null || secondaryValue === null) {
134
+ continue;
135
+ }
136
+ const yAxisKey = String(yValue);
137
+ const secondaryKey = String(secondaryValue);
138
+
139
+ if (!map.has(secondaryKey)) {
140
+ map.set(secondaryKey, []);
141
+ }
142
+ map.get(secondaryKey)?.push({
143
+ y: yAxisKey,
144
+ x: row.count,
145
+ proportion: row.proportion,
146
+ });
147
+ countsOfEachBar.set(yAxisKey, (countsOfEachBar.get(yAxisKey) ?? 0) + row.count);
148
+ }
149
+
150
+ const datasets: ChartDataset<'bar', DataPoint[]>[] = Array.from(map.entries())
151
+ .map(sortAndTruncateYAxisKeys(countsOfEachBar, maxNumberOfBars))
152
+ .map(([key, value], index) => ({
153
+ borderWidth: 1,
154
+ backgroundColor: singleGraphColorRGBAById(index, 0.3),
155
+ borderColor: singleGraphColorRGBAById(index),
156
+ label: key,
157
+ data: value,
158
+ }));
159
+ return { datasets, countsOfEachBar };
160
+ }
161
+
162
+ function sortAndTruncateYAxisKeys(countsOfEachBar: Map<string, number>, maxNumberOfBars: number) {
163
+ const yAxisKeysToConsider = new Set(
164
+ Array.from(countsOfEachBar.entries())
165
+ .sort((a, b) => b[1] - a[1])
166
+ .slice(0, maxNumberOfBars)
167
+ .map(([key]) => key),
168
+ );
169
+
170
+ return ([key, value]: [string, DataPoint[]]): [string, DataPoint[]] => {
171
+ const sortedValues = value.sort((a, b) => (countsOfEachBar.get(b.y) ?? 0) - (countsOfEachBar.get(a.y) ?? 0));
172
+ const valuesWithLargestBars = sortedValues
173
+ .slice(0, maxNumberOfBars)
174
+ .filter((v) => yAxisKeysToConsider.has(v.y));
175
+ return [key, valuesWithLargestBars];
176
+ };
177
+ }
@@ -1,4 +1,5 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { useMemo } from 'preact/hooks';
2
3
 
3
4
  import { type AggregateData, compareAscending } from '../../query/queryAggregateData';
4
5
  import { Table } from '../components/table';
@@ -8,9 +9,17 @@ type AggregateTableProps = {
8
9
  fields: string[];
9
10
  data: AggregateData;
10
11
  pageSize: boolean | number;
12
+ initialSortField: string;
13
+ initialSortDirection: 'ascending' | 'descending';
11
14
  };
12
15
 
13
- export const AggregateTable: FunctionComponent<AggregateTableProps> = ({ data, fields, pageSize }) => {
16
+ export const AggregateTable: FunctionComponent<AggregateTableProps> = ({
17
+ data,
18
+ fields,
19
+ pageSize,
20
+ initialSortField,
21
+ initialSortDirection,
22
+ }) => {
14
23
  const headers = [
15
24
  ...fields.map((field) => {
16
25
  return {
@@ -31,5 +40,18 @@ export const AggregateTable: FunctionComponent<AggregateTableProps> = ({ data, f
31
40
  },
32
41
  ];
33
42
 
34
- return <Table data={data} columns={headers} pageSize={pageSize} />;
43
+ const sortedData = useMemo(() => {
44
+ const validSortFields = ['count', 'proportion', ...fields];
45
+ if (!validSortFields.includes(initialSortField)) {
46
+ throw new Error(`InitialSort field not in fields. Valid fields are: ${validSortFields.join(', ')}`);
47
+ }
48
+
49
+ return data.sort((a, b) =>
50
+ initialSortDirection === 'ascending'
51
+ ? compareAscending(a[initialSortField], b[initialSortField])
52
+ : compareAscending(b[initialSortField], a[initialSortField]),
53
+ );
54
+ }, [data, initialSortField, initialSortDirection, fields]);
55
+
56
+ return <Table data={sortedData} columns={headers} pageSize={pageSize} />;
35
57
  };
@@ -5,7 +5,7 @@ import aggregatedData from './__mockData__/aggregated.json';
5
5
  import { Aggregate, type AggregateProps } from './aggregate';
6
6
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
7
7
  import { LapisUrlContext } from '../LapisUrlContext';
8
- import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
8
+ import { expectInvalidAttributesErrorMessage, playThatExpectsErrorMessage } from '../shared/stories/expectErrorMessage';
9
9
 
10
10
  const meta: Meta<AggregateProps> = {
11
11
  title: 'Visualization/Aggregate',
@@ -50,7 +50,7 @@ export const Default: StoryObj<AggregateProps> = {
50
50
  ),
51
51
  args: {
52
52
  fields: ['division', 'host'],
53
- views: ['table'],
53
+ views: ['table', 'bar'],
54
54
  lapisFilter: {
55
55
  country: 'USA',
56
56
  },
@@ -59,6 +59,7 @@ export const Default: StoryObj<AggregateProps> = {
59
59
  initialSortField: 'count',
60
60
  initialSortDirection: 'descending',
61
61
  pageSize: 10,
62
+ maxNumberOfBars: 20,
62
63
  },
63
64
  };
64
65
 
@@ -108,3 +109,61 @@ export const WithEmptyFieldString: StoryObj<AggregateProps> = {
108
109
  });
109
110
  },
110
111
  };
112
+
113
+ export const BarChartWithNoFields: StoryObj<AggregateProps> = {
114
+ ...Default,
115
+ args: {
116
+ ...Default.args,
117
+ views: ['bar', 'table'],
118
+ fields: [],
119
+ },
120
+ parameters: {
121
+ fetchMock: {
122
+ mocks: [
123
+ {
124
+ matcher: {
125
+ name: 'aggregatedData',
126
+ url: AGGREGATED_ENDPOINT,
127
+ },
128
+ response: {
129
+ status: 200,
130
+ body: aggregatedData,
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ },
136
+ play: playThatExpectsErrorMessage(
137
+ 'Error - No fields given',
138
+ `Cannot display a bar chart when the "fields" attribute of this component is empty`,
139
+ ),
140
+ };
141
+
142
+ export const BarChartWithMoreThan2Fields: StoryObj<AggregateProps> = {
143
+ ...Default,
144
+ args: {
145
+ ...Default.args,
146
+ views: ['bar', 'table'],
147
+ fields: ['division', 'host', 'country'],
148
+ },
149
+ parameters: {
150
+ fetchMock: {
151
+ mocks: [
152
+ {
153
+ matcher: {
154
+ name: 'aggregatedData',
155
+ url: AGGREGATED_ENDPOINT,
156
+ },
157
+ response: {
158
+ status: 200,
159
+ body: aggregatedData,
160
+ },
161
+ },
162
+ ],
163
+ },
164
+ },
165
+ play: playThatExpectsErrorMessage(
166
+ 'Error - Too many fields given',
167
+ 'Cannot display a bar chart when the "fields" attribute of this component contains more than two values.',
168
+ ),
169
+ };
@@ -15,8 +15,9 @@ import { NoDataDisplay } from '../components/no-data-display';
15
15
  import { ResizeContainer } from '../components/resize-container';
16
16
  import Tabs from '../components/tabs';
17
17
  import { useQuery } from '../useQuery';
18
+ import { AggregateBarChart } from './aggregate-bar-chart';
18
19
 
19
- const aggregateViewSchema = z.literal(views.table);
20
+ const aggregateViewSchema = z.union([z.literal(views.table), z.literal(views.bar)]);
20
21
  export type AggregateView = z.infer<typeof aggregateViewSchema>;
21
22
 
22
23
  const aggregatePropsSchema = z.object({
@@ -28,6 +29,7 @@ const aggregatePropsSchema = z.object({
28
29
  pageSize: z.union([z.boolean(), z.number()]),
29
30
  width: z.string(),
30
31
  height: z.string(),
32
+ maxNumberOfBars: z.number(),
31
33
  });
32
34
  export type AggregateProps = z.infer<typeof aggregatePropsSchema>;
33
35
 
@@ -49,10 +51,7 @@ export const AggregateInner: FunctionComponent<AggregateProps> = (componentProps
49
51
  const lapis = useContext(LapisUrlContext);
50
52
 
51
53
  const { data, error, isLoading } = useQuery(async () => {
52
- return queryAggregateData(lapisFilter, fields, lapis, {
53
- field: initialSortField,
54
- direction: initialSortDirection,
55
- });
54
+ return queryAggregateData(lapisFilter, fields, lapis);
56
55
  }, [lapisFilter, fields, lapis, initialSortField, initialSortDirection]);
57
56
 
58
57
  if (isLoading) {
@@ -78,7 +77,7 @@ type AggregatedDataTabsProps = {
78
77
  const AggregatedDataTabs: FunctionComponent<AggregatedDataTabsProps> = ({ data, originalComponentProps }) => {
79
78
  const getTab = (view: AggregateView) => {
80
79
  switch (view) {
81
- case 'table':
80
+ case views.table:
82
81
  return {
83
82
  title: 'Table',
84
83
  content: (
@@ -86,6 +85,19 @@ const AggregatedDataTabs: FunctionComponent<AggregatedDataTabsProps> = ({ data,
86
85
  data={data}
87
86
  fields={originalComponentProps.fields}
88
87
  pageSize={originalComponentProps.pageSize}
88
+ initialSortField={originalComponentProps.initialSortField}
89
+ initialSortDirection={originalComponentProps.initialSortDirection}
90
+ />
91
+ ),
92
+ };
93
+ case views.bar:
94
+ return {
95
+ title: 'Bar',
96
+ content: (
97
+ <AggregateBarChart
98
+ data={data}
99
+ fields={originalComponentProps.fields}
100
+ maxNumberOfBars={originalComponentProps.maxNumberOfBars}
89
101
  />
90
102
  ),
91
103
  };