@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
|
@@ -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> = ({
|
|
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
|
-
|
|
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
|
|
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
|
};
|