@genspectrum/dashboard-components 0.11.3 → 0.11.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.
- package/custom-elements.json +16 -0
- package/dist/components.d.ts +44 -44
- package/dist/components.js +181 -131
- package/dist/components.js.map +1 -1
- package/dist/style.css +2 -1
- package/dist/util.d.ts +44 -44
- package/package.json +2 -2
- package/src/preact/aggregatedData/aggregate.tsx +1 -1
- package/src/preact/map/__mockData__/aggregatedGermany.json +5 -1
- package/src/preact/map/{useGeoJsonMap.tsx → loadMapSource.tsx} +7 -24
- package/src/preact/map/sequences-by-location-map.tsx +13 -99
- package/src/preact/map/sequences-by-location-table.tsx +28 -5
- package/src/preact/map/sequences-by-location.tsx +32 -13
- package/src/preact/useQuery.ts +1 -1
- package/src/query/computeMapLocationData.spec.ts +103 -0
- package/src/query/computeMapLocationData.ts +136 -0
- package/src/query/querySequencesByLocationData.ts +18 -0
- package/src/utilEntrypoint.ts +1 -1
- package/src/web-components/visualization/gs-sequences-by-location.stories.ts +17 -0
- package/standalone-bundle/dashboard-components.js +2861 -2811
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
|
@@ -4,15 +4,22 @@ import z from 'zod';
|
|
|
4
4
|
|
|
5
5
|
import { SequencesByLocationMap } from './sequences-by-location-map';
|
|
6
6
|
import { SequencesByLocationTable } from './sequences-by-location-table';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type EnhancedLocationsTableData,
|
|
9
|
+
type MapLocationData,
|
|
10
|
+
MapLocationDataType,
|
|
11
|
+
} from '../../query/computeMapLocationData';
|
|
12
|
+
import { type AggregateData } from '../../query/queryAggregateData';
|
|
13
|
+
import { querySequencesByLocationData } from '../../query/querySequencesByLocationData';
|
|
8
14
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
15
|
+
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
9
16
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
10
17
|
import { Fullscreen } from '../components/fullscreen';
|
|
11
18
|
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
|
|
12
19
|
import { LoadingDisplay } from '../components/loading-display';
|
|
13
20
|
import { ResizeContainer } from '../components/resize-container';
|
|
14
21
|
import { useQuery } from '../useQuery';
|
|
15
|
-
import { mapSourceSchema } from './
|
|
22
|
+
import { mapSourceSchema } from './loadMapSource';
|
|
16
23
|
import { lapisFilterSchema, views } from '../../types';
|
|
17
24
|
import Tabs from '../components/tabs';
|
|
18
25
|
|
|
@@ -49,7 +56,7 @@ export const SequencesByLocation: FunctionComponent<SequencesByLocationProps> =
|
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationProps> = (props) => {
|
|
52
|
-
const { lapisFilter, lapisLocationField } = props;
|
|
59
|
+
const { lapisFilter, lapisLocationField, mapSource } = props;
|
|
53
60
|
|
|
54
61
|
const lapis = useContext(LapisUrlContext);
|
|
55
62
|
const {
|
|
@@ -57,8 +64,8 @@ const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationProps> =
|
|
|
57
64
|
error,
|
|
58
65
|
isLoading: isLoadingLapisData,
|
|
59
66
|
} = useQuery(
|
|
60
|
-
async () =>
|
|
61
|
-
[lapisFilter, lapisLocationField, lapis],
|
|
67
|
+
async () => querySequencesByLocationData(lapisFilter, lapisLocationField, lapis, mapSource),
|
|
68
|
+
[lapisFilter, lapisLocationField, lapis, mapSource],
|
|
62
69
|
);
|
|
63
70
|
|
|
64
71
|
if (isLoadingLapisData) {
|
|
@@ -74,7 +81,7 @@ const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationProps> =
|
|
|
74
81
|
|
|
75
82
|
type SequencesByLocationMapTabsProps = {
|
|
76
83
|
originalComponentProps: SequencesByLocationProps;
|
|
77
|
-
data:
|
|
84
|
+
data: MapLocationData;
|
|
78
85
|
};
|
|
79
86
|
|
|
80
87
|
const SequencesByLocationMapTabs: FunctionComponent<SequencesByLocationMapTabsProps> = ({
|
|
@@ -83,16 +90,16 @@ const SequencesByLocationMapTabs: FunctionComponent<SequencesByLocationMapTabsPr
|
|
|
83
90
|
}) => {
|
|
84
91
|
const getTab = (view: SequencesByLocationMapView) => {
|
|
85
92
|
switch (view) {
|
|
86
|
-
case views.map:
|
|
87
|
-
if (
|
|
93
|
+
case views.map: {
|
|
94
|
+
if (data.type !== MapLocationDataType.tableAndMapData) {
|
|
88
95
|
throw new Error('mapSource is required when using the map view');
|
|
89
96
|
}
|
|
97
|
+
const { type: _type, tableData: _tableData, ...dataForMap } = data;
|
|
90
98
|
return {
|
|
91
99
|
title: 'Map',
|
|
92
100
|
content: (
|
|
93
101
|
<SequencesByLocationMap
|
|
94
|
-
|
|
95
|
-
mapSource={originalComponentProps.mapSource}
|
|
102
|
+
{...dataForMap}
|
|
96
103
|
enableMapNavigation={originalComponentProps.enableMapNavigation}
|
|
97
104
|
lapisLocationField={originalComponentProps.lapisLocationField}
|
|
98
105
|
zoom={originalComponentProps.zoom}
|
|
@@ -102,12 +109,13 @@ const SequencesByLocationMapTabs: FunctionComponent<SequencesByLocationMapTabsPr
|
|
|
102
109
|
/>
|
|
103
110
|
),
|
|
104
111
|
};
|
|
112
|
+
}
|
|
105
113
|
case views.table:
|
|
106
114
|
return {
|
|
107
115
|
title: 'Table',
|
|
108
116
|
content: (
|
|
109
117
|
<SequencesByLocationTable
|
|
110
|
-
|
|
118
|
+
tableData={data.tableData}
|
|
111
119
|
lapisLocationField={originalComponentProps.lapisLocationField}
|
|
112
120
|
pageSize={originalComponentProps.pageSize}
|
|
113
121
|
/>
|
|
@@ -118,16 +126,27 @@ const SequencesByLocationMapTabs: FunctionComponent<SequencesByLocationMapTabsPr
|
|
|
118
126
|
|
|
119
127
|
const tabs = originalComponentProps.views.map((view) => getTab(view));
|
|
120
128
|
|
|
121
|
-
return
|
|
129
|
+
return (
|
|
130
|
+
<Tabs
|
|
131
|
+
tabs={tabs}
|
|
132
|
+
toolbar={<Toolbar originalComponentProps={originalComponentProps} tableData={data.tableData} />}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
122
135
|
};
|
|
123
136
|
|
|
124
137
|
type ToolbarProps = {
|
|
125
138
|
originalComponentProps: SequencesByLocationProps;
|
|
139
|
+
tableData: AggregateData | EnhancedLocationsTableData;
|
|
126
140
|
};
|
|
127
141
|
|
|
128
|
-
const Toolbar: FunctionComponent<ToolbarProps> = ({ originalComponentProps }) => {
|
|
142
|
+
const Toolbar: FunctionComponent<ToolbarProps> = ({ originalComponentProps, tableData }) => {
|
|
129
143
|
return (
|
|
130
144
|
<div class='flex flex-row'>
|
|
145
|
+
<CsvDownloadButton
|
|
146
|
+
className='mx-1 btn btn-xs'
|
|
147
|
+
getData={() => tableData}
|
|
148
|
+
filename='sequences_by_location.csv'
|
|
149
|
+
/>
|
|
131
150
|
<SequencesByLocationMapInfo originalComponentProps={originalComponentProps} />
|
|
132
151
|
<Fullscreen />
|
|
133
152
|
</div>
|
package/src/preact/useQuery.ts
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { FeatureCollection, GeometryObject } from 'geojson';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { computeMapLocationData } from './computeMapLocationData';
|
|
5
|
+
import type { GeoJsonFeatureProperties } from '../preact/map/loadMapSource';
|
|
6
|
+
|
|
7
|
+
const lapisLocationField = 'locationField';
|
|
8
|
+
|
|
9
|
+
const country1 = 'country1';
|
|
10
|
+
const country2 = 'country2';
|
|
11
|
+
|
|
12
|
+
const locationData = [
|
|
13
|
+
{
|
|
14
|
+
[lapisLocationField]: country1,
|
|
15
|
+
count: 1,
|
|
16
|
+
proportion: 0.1,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
[lapisLocationField]: country2,
|
|
20
|
+
count: 2,
|
|
21
|
+
proportion: 0.2,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
[lapisLocationField]: null,
|
|
25
|
+
count: 3,
|
|
26
|
+
proportion: 0.3,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const geometryObject = {
|
|
31
|
+
type: 'GeometryCollection',
|
|
32
|
+
geometries: [],
|
|
33
|
+
} satisfies GeometryObject;
|
|
34
|
+
|
|
35
|
+
const geojsonData = {
|
|
36
|
+
type: 'FeatureCollection',
|
|
37
|
+
features: [
|
|
38
|
+
{
|
|
39
|
+
type: 'Feature',
|
|
40
|
+
properties: {
|
|
41
|
+
name: country1,
|
|
42
|
+
},
|
|
43
|
+
geometry: geometryObject,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
} satisfies FeatureCollection<GeometryObject, GeoJsonFeatureProperties>;
|
|
47
|
+
|
|
48
|
+
describe('computeMapLocationData', () => {
|
|
49
|
+
test('should return tableDataOnly when geojsonData is undefined', () => {
|
|
50
|
+
const actual = computeMapLocationData(locationData, undefined, lapisLocationField);
|
|
51
|
+
|
|
52
|
+
expect(actual).to.deep.equal({
|
|
53
|
+
type: 'tableDataOnly',
|
|
54
|
+
tableData: locationData,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should return tableAndMapData when geojsonData is defined', () => {
|
|
59
|
+
const actual = computeMapLocationData(locationData, geojsonData, lapisLocationField);
|
|
60
|
+
|
|
61
|
+
expect(actual).to.deep.equal({
|
|
62
|
+
type: 'tableAndMapData',
|
|
63
|
+
locations: [
|
|
64
|
+
{
|
|
65
|
+
type: 'Feature',
|
|
66
|
+
properties: {
|
|
67
|
+
name: country1,
|
|
68
|
+
data: {
|
|
69
|
+
count: 1,
|
|
70
|
+
proportion: 0.1,
|
|
71
|
+
[lapisLocationField]: country1,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
geometry: geometryObject,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
tableData: [
|
|
78
|
+
{
|
|
79
|
+
[lapisLocationField]: country1,
|
|
80
|
+
count: 1,
|
|
81
|
+
proportion: 0.1,
|
|
82
|
+
isShownOnMap: 'true',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
[lapisLocationField]: country2,
|
|
86
|
+
count: 2,
|
|
87
|
+
proportion: 0.2,
|
|
88
|
+
isShownOnMap: 'false',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
[lapisLocationField]: null,
|
|
92
|
+
count: 3,
|
|
93
|
+
proportion: 0.3,
|
|
94
|
+
isShownOnMap: 'false',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
totalCount: 6,
|
|
98
|
+
countOfMatchedLocationData: 1,
|
|
99
|
+
unmatchedLocations: [country2],
|
|
100
|
+
nullCount: 3,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection, GeometryObject } from 'geojson';
|
|
2
|
+
|
|
3
|
+
import type { AggregateData } from './queryAggregateData';
|
|
4
|
+
import type { GeoJsonFeatureProperties } from '../preact/map/loadMapSource';
|
|
5
|
+
|
|
6
|
+
export type FeatureData = { proportion: number; count: number };
|
|
7
|
+
|
|
8
|
+
export type EnhancedGeoJsonFeatureProperties = GeoJsonFeatureProperties & {
|
|
9
|
+
data: FeatureData | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type EnhancedLocationsTableData = (AggregateData[number] & { isShownOnMap: string })[];
|
|
13
|
+
|
|
14
|
+
export const MapLocationDataType = {
|
|
15
|
+
tableDataOnly: 'tableDataOnly',
|
|
16
|
+
tableAndMapData: 'tableAndMapData',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type MapLocationData =
|
|
20
|
+
| {
|
|
21
|
+
type: typeof MapLocationDataType.tableDataOnly;
|
|
22
|
+
tableData: AggregateData;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: typeof MapLocationDataType.tableAndMapData;
|
|
26
|
+
locations: Feature<GeometryObject, EnhancedGeoJsonFeatureProperties>[];
|
|
27
|
+
tableData: EnhancedLocationsTableData;
|
|
28
|
+
totalCount: number;
|
|
29
|
+
countOfMatchedLocationData: number;
|
|
30
|
+
unmatchedLocations: string[];
|
|
31
|
+
nullCount: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function computeMapLocationData(
|
|
35
|
+
locationData: AggregateData,
|
|
36
|
+
geojsonData: FeatureCollection<GeometryObject, GeoJsonFeatureProperties> | undefined,
|
|
37
|
+
lapisLocationField: string,
|
|
38
|
+
): MapLocationData {
|
|
39
|
+
if (geojsonData === undefined) {
|
|
40
|
+
return { type: MapLocationDataType.tableDataOnly, tableData: locationData };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const countAndProportionByCountry = buildLookupByLocationField(locationData, lapisLocationField);
|
|
44
|
+
const { locations, unmatchedLocations } = matchLocationDataAndGeoJsonFeatures(
|
|
45
|
+
geojsonData,
|
|
46
|
+
countAndProportionByCountry,
|
|
47
|
+
lapisLocationField,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const totalCount = locationData.map((value) => value.count).reduce((sum, b) => sum + b, 0);
|
|
51
|
+
const countOfMatchedLocationData = locations
|
|
52
|
+
.map((location) => location.properties.data?.count ?? 0)
|
|
53
|
+
.reduce((sum, b) => sum + b, 0);
|
|
54
|
+
const nullCount = locationData.find((row) => row[lapisLocationField] === null)?.count ?? 0;
|
|
55
|
+
|
|
56
|
+
const tableData = getSequencesByLocationTableData(locationData, unmatchedLocations, lapisLocationField);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
type: MapLocationDataType.tableAndMapData,
|
|
60
|
+
locations,
|
|
61
|
+
tableData,
|
|
62
|
+
totalCount,
|
|
63
|
+
countOfMatchedLocationData,
|
|
64
|
+
unmatchedLocations,
|
|
65
|
+
nullCount,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildLookupByLocationField(locationData: AggregateData, lapisLocationField: string) {
|
|
70
|
+
return new Map<string, FeatureData>(
|
|
71
|
+
locationData
|
|
72
|
+
.filter((row) => typeof row[lapisLocationField] === 'string')
|
|
73
|
+
.map((row) => [row[lapisLocationField] as string, row]),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function matchLocationDataAndGeoJsonFeatures(
|
|
78
|
+
geojsonData: FeatureCollection<GeometryObject, GeoJsonFeatureProperties>,
|
|
79
|
+
countAndProportionByCountry: Map<string, FeatureData>,
|
|
80
|
+
lapisLocationField: string,
|
|
81
|
+
) {
|
|
82
|
+
const matchedLocations: string[] = [];
|
|
83
|
+
|
|
84
|
+
const locations: Feature<GeometryObject, EnhancedGeoJsonFeatureProperties>[] = geojsonData.features.map(
|
|
85
|
+
(feature) => {
|
|
86
|
+
const name = feature?.properties?.name;
|
|
87
|
+
if (typeof name !== 'string') {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`GeoJSON feature with id '${feature.id}' does not have 'properties.name' of type string, was: '${name}'`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = countAndProportionByCountry.get(name) ?? null;
|
|
94
|
+
if (data !== null) {
|
|
95
|
+
matchedLocations.push(name);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
...feature,
|
|
99
|
+
properties: {
|
|
100
|
+
...feature.properties,
|
|
101
|
+
data,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const unmatchedLocations = [...countAndProportionByCountry.keys()].filter(
|
|
108
|
+
(name) => !matchedLocations.includes(name),
|
|
109
|
+
);
|
|
110
|
+
if (unmatchedLocations.length > 0) {
|
|
111
|
+
const unmatchedLocationsWarning = `gs-map: Found location data from LAPIS (aggregated by "${lapisLocationField}") that could not be matched on locations on the given map. Unmatched location names are: ${unmatchedLocations.map((it) => `"${it}"`).join(', ')}`;
|
|
112
|
+
console.warn(unmatchedLocationsWarning); // eslint-disable-line no-console -- We should give some feedback about unmatched location data.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { locations, unmatchedLocations };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getSequencesByLocationTableData(
|
|
119
|
+
locationData: AggregateData,
|
|
120
|
+
unmatchedLocations: string[],
|
|
121
|
+
lapisLocationField: string,
|
|
122
|
+
): EnhancedLocationsTableData {
|
|
123
|
+
return locationData.map((row) => ({
|
|
124
|
+
...row,
|
|
125
|
+
isShownOnMap: `${isShownOnMap(row, unmatchedLocations, lapisLocationField)}`,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isShownOnMap(row: AggregateData[number], unmatchedLocations: string[], lapisLocationField: string) {
|
|
130
|
+
const locationValue = row[lapisLocationField];
|
|
131
|
+
if (locationValue === null) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return !unmatchedLocations.includes(locationValue as string);
|
|
136
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { computeMapLocationData } from './computeMapLocationData';
|
|
2
|
+
import { queryAggregateData } from './queryAggregateData';
|
|
3
|
+
import { loadMapSource, type MapSource } from '../preact/map/loadMapSource';
|
|
4
|
+
import type { LapisFilter } from '../types';
|
|
5
|
+
|
|
6
|
+
export async function querySequencesByLocationData(
|
|
7
|
+
lapisFilter: LapisFilter,
|
|
8
|
+
lapisLocationField: string,
|
|
9
|
+
lapis: string,
|
|
10
|
+
mapSource: MapSource | undefined,
|
|
11
|
+
) {
|
|
12
|
+
const [locationData, geojsonData] = await Promise.all([
|
|
13
|
+
queryAggregateData(lapisFilter, [lapisLocationField], lapis),
|
|
14
|
+
mapSource !== undefined ? loadMapSource(mapSource) : undefined,
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
return computeMapLocationData(locationData, geojsonData, lapisLocationField);
|
|
18
|
+
}
|
package/src/utilEntrypoint.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type {
|
|
|
27
27
|
RelativeGrowthAdvantageProps,
|
|
28
28
|
} from './preact/relativeGrowthAdvantage/relative-growth-advantage';
|
|
29
29
|
export type { StatisticsProps } from './preact/statistic/statistics';
|
|
30
|
-
export type { MapSource } from './preact/map/
|
|
30
|
+
export type { MapSource } from './preact/map/loadMapSource';
|
|
31
31
|
|
|
32
32
|
export type { ConfidenceIntervalMethod } from './preact/shared/charts/confideceInterval';
|
|
33
33
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expect, userEvent, waitFor } from '@storybook/test';
|
|
1
2
|
import { type Meta, type StoryObj } from '@storybook/web-components';
|
|
2
3
|
import { html } from 'lit';
|
|
3
4
|
|
|
@@ -9,6 +10,7 @@ import aggregatedWorld from '../../preact/map/__mockData__/aggregatedWorld.json'
|
|
|
9
10
|
import mapOfGermany from '../../preact/map/__mockData__/germanyMap.json';
|
|
10
11
|
import worldAtlas from '../../preact/map/__mockData__/worldAtlas.json';
|
|
11
12
|
import { type SequencesByLocationProps } from '../../preact/map/sequences-by-location';
|
|
13
|
+
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
12
14
|
|
|
13
15
|
import './gs-sequences-by-location';
|
|
14
16
|
import '../app';
|
|
@@ -188,6 +190,21 @@ export const Germany: StoryObj<SequencesByLocationProps> = {
|
|
|
188
190
|
},
|
|
189
191
|
};
|
|
190
192
|
|
|
193
|
+
export const GermanyOnTableTab: StoryObj<SequencesByLocationProps> = {
|
|
194
|
+
...Germany,
|
|
195
|
+
play: async ({ canvasElement, step }) => {
|
|
196
|
+
const canvas = await withinShadowRoot(canvasElement, 'gs-sequences-by-location');
|
|
197
|
+
|
|
198
|
+
await waitFor(() => expect(canvas.getByRole('button', { name: 'Table' })).toBeInTheDocument());
|
|
199
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Table' }));
|
|
200
|
+
|
|
201
|
+
await step('Sort by division', async () => {
|
|
202
|
+
await waitFor(() => expect(canvas.getByText('division')).toBeInTheDocument());
|
|
203
|
+
await userEvent.click(canvas.getByText('division'));
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
191
208
|
export const GermanyTableOnly: StoryObj<SequencesByLocationProps> = {
|
|
192
209
|
render: (args) => html`
|
|
193
210
|
<gs-app lapis="${LAPIS_URL}">
|