@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.
@@ -582,11 +582,11 @@ function GsAppError(error) {
582
582
  var f$1 = 0;
583
583
  function u$1(e2, t2, n3, o2, i2, u2) {
584
584
  t2 || (t2 = {});
585
- var a2, c2, l2 = t2;
586
- "ref" in t2 && (a2 = t2.ref, delete t2.ref);
587
- var p2 = { type: e2, props: l2, key: n3, ref: a2, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: --f$1, __i: -1, __u: 0, __source: i2, __self: u2 };
588
- if ("function" == typeof e2 && (a2 = e2.defaultProps)) for (c2 in a2) void 0 === l2[c2] && (l2[c2] = a2[c2]);
589
- return options.vnode && options.vnode(p2), p2;
585
+ var a2, c2, p2 = t2;
586
+ if ("ref" in p2) for (c2 in p2 = {}, t2) "ref" == c2 ? a2 = t2[c2] : p2[c2] = t2[c2];
587
+ var l2 = { type: e2, props: p2, key: n3, ref: a2, __k: null, __: null, __b: 0, __e: null, __c: null, constructor: void 0, __v: --f$1, __i: -1, __u: 0, __source: i2, __self: u2 };
588
+ if ("function" == typeof e2 && (a2 = e2.defaultProps)) for (c2 in a2) void 0 === p2[c2] && (p2[c2] = a2[c2]);
589
+ return options.vnode && options.vnode(l2), l2;
590
590
  }
591
591
  var t, r, u, i, o = 0, f = [], c = options, e = c.__b, a = c.__r, v = c.diffed, l = c.__c, m = c.unmount, s = c.__;
592
592
  function d(n3, t2) {
@@ -2037,7 +2037,7 @@ function useQuery(fetchDataCallback, dependencies) {
2037
2037
  setIsLoading(false);
2038
2038
  }
2039
2039
  };
2040
- fetchData();
2040
+ void fetchData();
2041
2041
  }, [JSON.stringify(dependencies)]);
2042
2042
  if (isLoading) {
2043
2043
  return { isLoading: true };
@@ -2293,7 +2293,7 @@ const tailwindStyle = `*, ::before, ::after {
2293
2293
  --tw-contain-paint: ;
2294
2294
  --tw-contain-style: ;
2295
2295
  }/*
2296
- ! tailwindcss v3.4.16 | MIT License | https://tailwindcss.com
2296
+ ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
2297
2297
  *//*
2298
2298
  1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2299
2299
  2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
@@ -2965,6 +2965,7 @@ html {
2965
2965
  display: grid;
2966
2966
  width: 100%;
2967
2967
  overflow: hidden;
2968
+ direction: ltr;
2968
2969
  container-type: inline-size;
2969
2970
  grid-template-columns: auto 1fr;
2970
2971
  }
@@ -8326,7 +8327,7 @@ const AggregateInner = (componentProps) => {
8326
8327
  field: initialSortField,
8327
8328
  direction: initialSortDirection
8328
8329
  });
8329
- }, [lapisFilter, fields, lapis]);
8330
+ }, [lapisFilter, fields, lapis, initialSortField, initialSortDirection]);
8330
8331
  if (isLoading) {
8331
8332
  return /* @__PURE__ */ u$1(LoadingDisplay, {});
8332
8333
  }
@@ -10100,62 +10101,12 @@ svg.leaflet-image-layer.leaflet-interactive path {\r
10100
10101
  }\r
10101
10102
  `;
10102
10103
  const leafletStyleModifications = ".leaflet-container {\n background: transparent;\n}\n";
10103
- const mapSourceSchema = z$1.object({
10104
- type: z$1.literal("topojson"),
10105
- url: z$1.string().min(1),
10106
- topologyObjectsKey: z$1.string().min(1)
10107
- });
10108
- function useGeoJsonMap(mapSource) {
10109
- const {
10110
- data: geojsonData,
10111
- error,
10112
- isLoading
10113
- } = useQuery(async () => {
10114
- switch (mapSource.type) {
10115
- case "topojson":
10116
- return await loadTopojsonMap(mapSource);
10117
- }
10118
- }, [mapSource]);
10119
- if (isLoading) {
10120
- return { isLoading };
10121
- }
10122
- if (error) {
10123
- throw error;
10124
- }
10125
- return { geojsonData, isLoading: false };
10126
- }
10127
- async function loadTopojsonMap(mapSource) {
10128
- var _a;
10129
- const response = await fetch(mapSource.url);
10130
- const topology = await response.json();
10131
- if ((topology == null ? void 0 : topology.type) !== "Topology") {
10132
- throw new UserFacingError(
10133
- "Invalid map source",
10134
- `JSON downloaded from ${mapSource.url} does not look like a topojson Topology definition: missing 'type: "Topology"', got '${JSON.stringify(topology).substring(0, 100)}'`
10135
- );
10136
- }
10137
- const object = topology == null ? void 0 : topology.objects[mapSource.topologyObjectsKey];
10138
- if ((object == null ? void 0 : object.type) !== "GeometryCollection") {
10139
- throw new UserFacingError(
10140
- "Invalid map source",
10141
- `JSON downloaded from ${mapSource.url} does not have a GeometryCollection at key objects.${mapSource.topologyObjectsKey}, got '${(_a = JSON.stringify(topology)) == null ? void 0 : _a.substring(0, 100)}'`
10142
- );
10143
- }
10144
- return topojson.feature(topology, object);
10145
- }
10146
10104
  const SequencesByLocationMap = ({
10147
- mapSource,
10148
- ...otherProps
10149
- }) => {
10150
- const { isLoading: isLoadingMap, geojsonData } = useGeoJsonMap(mapSource);
10151
- if (isLoadingMap) {
10152
- return /* @__PURE__ */ u$1(LoadingDisplay, {});
10153
- }
10154
- return /* @__PURE__ */ u$1(SequencesByLocationMapInner$1, { geojsonData, ...otherProps });
10155
- };
10156
- const SequencesByLocationMapInner$1 = ({
10157
- geojsonData,
10158
- locationData,
10105
+ locations,
10106
+ totalCount,
10107
+ countOfMatchedLocationData,
10108
+ nullCount,
10109
+ unmatchedLocations,
10159
10110
  enableMapNavigation,
10160
10111
  lapisLocationField,
10161
10112
  zoom,
@@ -10163,22 +10114,7 @@ const SequencesByLocationMapInner$1 = ({
10163
10114
  offsetY,
10164
10115
  hasTableView
10165
10116
  }) => {
10166
- var _a;
10167
10117
  const ref = A(null);
10168
- const { locations, totalCount, countOfMatchedLocationData, unmatchedLocations } = T(() => {
10169
- const countAndProportionByCountry = buildLookupByLocationField(locationData, lapisLocationField);
10170
- const { locations: locations2, unmatchedLocations: unmatchedLocations2 } = matchLocationDataAndGeoJsonFeatures(
10171
- geojsonData,
10172
- countAndProportionByCountry,
10173
- lapisLocationField
10174
- );
10175
- const totalCount2 = locationData.map((value) => value.count).reduce((sum, b3) => sum + b3, 0);
10176
- const countOfMatchedLocationData2 = locations2.map((location) => {
10177
- var _a2;
10178
- return ((_a2 = location.properties.data) == null ? void 0 : _a2.count) ?? 0;
10179
- }).reduce((sum, b3) => sum + b3, 0);
10180
- return { locations: locations2, totalCount: totalCount2, countOfMatchedLocationData: countOfMatchedLocationData2, unmatchedLocations: unmatchedLocations2 };
10181
- }, [geojsonData, locationData, lapisLocationField]);
10182
10118
  y(() => {
10183
10119
  if (!ref.current) {
10184
10120
  return;
@@ -10194,9 +10130,9 @@ const SequencesByLocationMapInner$1 = ({
10194
10130
  });
10195
10131
  Leaflet.geoJson(locations, {
10196
10132
  style: (feature) => {
10197
- var _a2;
10133
+ var _a;
10198
10134
  return {
10199
- fillColor: getColor((_a2 = feature == null ? void 0 : feature.properties.data) == null ? void 0 : _a2.proportion),
10135
+ fillColor: getColor((_a = feature == null ? void 0 : feature.properties.data) == null ? void 0 : _a.proportion),
10200
10136
  fillOpacity: 1,
10201
10137
  color: "#666666",
10202
10138
  weight: 1
@@ -10207,7 +10143,6 @@ const SequencesByLocationMapInner$1 = ({
10207
10143
  leafletMap.remove();
10208
10144
  };
10209
10145
  }, [ref, locations, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
10210
- const nullCount = ((_a = locationData.find((row) => row[lapisLocationField] === null)) == null ? void 0 : _a.count) ?? 0;
10211
10146
  return /* @__PURE__ */ u$1("div", { className: "h-full", children: [
10212
10147
  /* @__PURE__ */ u$1("div", { ref, className: "h-full" }),
10213
10148
  /* @__PURE__ */ u$1("div", { className: "relative", children: /* @__PURE__ */ u$1(
@@ -10273,44 +10208,6 @@ const DataMatchInformation = ({
10273
10208
  ] })
10274
10209
  ] });
10275
10210
  };
10276
- function buildLookupByLocationField(locationData, lapisLocationField) {
10277
- return new Map(
10278
- locationData.filter((row) => typeof row[lapisLocationField] === "string").map((row) => [row[lapisLocationField], row])
10279
- );
10280
- }
10281
- function matchLocationDataAndGeoJsonFeatures(geojsonData, countAndProportionByCountry, lapisLocationField) {
10282
- const matchedLocations = [];
10283
- const locations = geojsonData.features.map(
10284
- (feature) => {
10285
- var _a;
10286
- const name = (_a = feature == null ? void 0 : feature.properties) == null ? void 0 : _a.name;
10287
- if (typeof name !== "string") {
10288
- throw new Error(
10289
- `GeoJSON feature with id '${feature.id}' does not have 'properties.name' of type string, was: '${name}'`
10290
- );
10291
- }
10292
- const data = countAndProportionByCountry.get(name) ?? null;
10293
- if (data !== null) {
10294
- matchedLocations.push(name);
10295
- }
10296
- return {
10297
- ...feature,
10298
- properties: {
10299
- ...feature.properties,
10300
- data
10301
- }
10302
- };
10303
- }
10304
- );
10305
- const unmatchedLocations = [...countAndProportionByCountry.keys()].filter(
10306
- (name) => !matchedLocations.includes(name)
10307
- );
10308
- if (unmatchedLocations.length > 0) {
10309
- 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(", ")}`;
10310
- console.warn(unmatchedLocationsWarning);
10311
- }
10312
- return { locations, unmatchedLocations };
10313
- }
10314
10211
  function getColor(value) {
10315
10212
  if (value === void 0) {
10316
10213
  return "#DDDDDD";
@@ -10364,12 +10261,150 @@ function p({ innerText, className = "" }) {
10364
10261
  return headline;
10365
10262
  }
10366
10263
  const SequencesByLocationTable = ({
10367
- locationData,
10264
+ tableData,
10368
10265
  lapisLocationField,
10369
10266
  pageSize
10370
10267
  }) => {
10371
- return /* @__PURE__ */ u$1(AggregateTable, { data: locationData, fields: [lapisLocationField], pageSize });
10268
+ const headers = [
10269
+ {
10270
+ name: lapisLocationField,
10271
+ sort: {
10272
+ compare: compareAscending
10273
+ }
10274
+ },
10275
+ {
10276
+ name: "count",
10277
+ sort: true
10278
+ },
10279
+ {
10280
+ name: "proportion",
10281
+ sort: true,
10282
+ formatter: (cell) => formatProportion(cell)
10283
+ },
10284
+ ..."isShownOnMap" in tableData[0] ? [{ id: "isShownOnMap", name: "shown on map", sort: true, width: "20%" }] : []
10285
+ ];
10286
+ return /* @__PURE__ */ u$1(Table, { data: tableData, columns: headers, pageSize });
10287
+ };
10288
+ const MapLocationDataType = {
10289
+ tableDataOnly: "tableDataOnly",
10290
+ tableAndMapData: "tableAndMapData"
10372
10291
  };
10292
+ function computeMapLocationData(locationData, geojsonData, lapisLocationField) {
10293
+ var _a;
10294
+ if (geojsonData === void 0) {
10295
+ return { type: MapLocationDataType.tableDataOnly, tableData: locationData };
10296
+ }
10297
+ const countAndProportionByCountry = buildLookupByLocationField(locationData, lapisLocationField);
10298
+ const { locations, unmatchedLocations } = matchLocationDataAndGeoJsonFeatures(
10299
+ geojsonData,
10300
+ countAndProportionByCountry,
10301
+ lapisLocationField
10302
+ );
10303
+ const totalCount = locationData.map((value) => value.count).reduce((sum, b3) => sum + b3, 0);
10304
+ const countOfMatchedLocationData = locations.map((location) => {
10305
+ var _a2;
10306
+ return ((_a2 = location.properties.data) == null ? void 0 : _a2.count) ?? 0;
10307
+ }).reduce((sum, b3) => sum + b3, 0);
10308
+ const nullCount = ((_a = locationData.find((row) => row[lapisLocationField] === null)) == null ? void 0 : _a.count) ?? 0;
10309
+ const tableData = getSequencesByLocationTableData(locationData, unmatchedLocations, lapisLocationField);
10310
+ return {
10311
+ type: MapLocationDataType.tableAndMapData,
10312
+ locations,
10313
+ tableData,
10314
+ totalCount,
10315
+ countOfMatchedLocationData,
10316
+ unmatchedLocations,
10317
+ nullCount
10318
+ };
10319
+ }
10320
+ function buildLookupByLocationField(locationData, lapisLocationField) {
10321
+ return new Map(
10322
+ locationData.filter((row) => typeof row[lapisLocationField] === "string").map((row) => [row[lapisLocationField], row])
10323
+ );
10324
+ }
10325
+ function matchLocationDataAndGeoJsonFeatures(geojsonData, countAndProportionByCountry, lapisLocationField) {
10326
+ const matchedLocations = [];
10327
+ const locations = geojsonData.features.map(
10328
+ (feature) => {
10329
+ var _a;
10330
+ const name = (_a = feature == null ? void 0 : feature.properties) == null ? void 0 : _a.name;
10331
+ if (typeof name !== "string") {
10332
+ throw new Error(
10333
+ `GeoJSON feature with id '${feature.id}' does not have 'properties.name' of type string, was: '${name}'`
10334
+ );
10335
+ }
10336
+ const data = countAndProportionByCountry.get(name) ?? null;
10337
+ if (data !== null) {
10338
+ matchedLocations.push(name);
10339
+ }
10340
+ return {
10341
+ ...feature,
10342
+ properties: {
10343
+ ...feature.properties,
10344
+ data
10345
+ }
10346
+ };
10347
+ }
10348
+ );
10349
+ const unmatchedLocations = [...countAndProportionByCountry.keys()].filter(
10350
+ (name) => !matchedLocations.includes(name)
10351
+ );
10352
+ if (unmatchedLocations.length > 0) {
10353
+ 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(", ")}`;
10354
+ console.warn(unmatchedLocationsWarning);
10355
+ }
10356
+ return { locations, unmatchedLocations };
10357
+ }
10358
+ function getSequencesByLocationTableData(locationData, unmatchedLocations, lapisLocationField) {
10359
+ return locationData.map((row) => ({
10360
+ ...row,
10361
+ isShownOnMap: `${isShownOnMap(row, unmatchedLocations, lapisLocationField)}`
10362
+ }));
10363
+ }
10364
+ function isShownOnMap(row, unmatchedLocations, lapisLocationField) {
10365
+ const locationValue = row[lapisLocationField];
10366
+ if (locationValue === null) {
10367
+ return false;
10368
+ }
10369
+ return !unmatchedLocations.includes(locationValue);
10370
+ }
10371
+ const mapSourceSchema = z$1.object({
10372
+ type: z$1.literal("topojson"),
10373
+ url: z$1.string().min(1),
10374
+ topologyObjectsKey: z$1.string().min(1)
10375
+ });
10376
+ async function loadMapSource(mapSource) {
10377
+ switch (mapSource.type) {
10378
+ case "topojson":
10379
+ return await loadTopojsonMap(mapSource);
10380
+ }
10381
+ }
10382
+ async function loadTopojsonMap(mapSource) {
10383
+ var _a;
10384
+ const response = await fetch(mapSource.url);
10385
+ const topology = await response.json();
10386
+ if ((topology == null ? void 0 : topology.type) !== "Topology") {
10387
+ throw new UserFacingError(
10388
+ "Invalid map source",
10389
+ `JSON downloaded from ${mapSource.url} does not look like a topojson Topology definition: missing 'type: "Topology"', got '${JSON.stringify(topology).substring(0, 100)}'`
10390
+ );
10391
+ }
10392
+ const object = topology == null ? void 0 : topology.objects[mapSource.topologyObjectsKey];
10393
+ if ((object == null ? void 0 : object.type) !== "GeometryCollection") {
10394
+ throw new UserFacingError(
10395
+ "Invalid map source",
10396
+ `JSON downloaded from ${mapSource.url} does not have a GeometryCollection at key objects.${mapSource.topologyObjectsKey}, got '${(_a = JSON.stringify(topology)) == null ? void 0 : _a.substring(0, 100)}'`
10397
+ );
10398
+ }
10399
+ return topojson.feature(topology, object);
10400
+ }
10401
+ async function querySequencesByLocationData(lapisFilter, lapisLocationField, lapis, mapSource) {
10402
+ const [locationData, geojsonData] = await Promise.all([
10403
+ queryAggregateData(lapisFilter, [lapisLocationField], lapis),
10404
+ mapSource !== void 0 ? loadMapSource(mapSource) : void 0
10405
+ ]);
10406
+ return computeMapLocationData(locationData, geojsonData, lapisLocationField);
10407
+ }
10373
10408
  const sequencesByLocationViewSchema = z$1.union([z$1.literal(views.map), z$1.literal(views.table)]);
10374
10409
  const sequencesByLocationPropsSchema = z$1.object({
10375
10410
  lapisFilter: lapisFilterSchema,
@@ -10390,15 +10425,15 @@ const SequencesByLocation = (componentProps) => {
10390
10425
  return /* @__PURE__ */ u$1(ErrorBoundary, { size, componentProps, schema: sequencesByLocationPropsSchema, children: /* @__PURE__ */ u$1(ResizeContainer, { size, children: /* @__PURE__ */ u$1(SequencesByLocationMapInner, { ...componentProps }) }) });
10391
10426
  };
10392
10427
  const SequencesByLocationMapInner = (props) => {
10393
- const { lapisFilter, lapisLocationField } = props;
10428
+ const { lapisFilter, lapisLocationField, mapSource } = props;
10394
10429
  const lapis = x(LapisUrlContext);
10395
10430
  const {
10396
10431
  data,
10397
10432
  error,
10398
10433
  isLoading: isLoadingLapisData
10399
10434
  } = useQuery(
10400
- async () => queryAggregateData(lapisFilter, [lapisLocationField], lapis),
10401
- [lapisFilter, lapisLocationField, lapis]
10435
+ async () => querySequencesByLocationData(lapisFilter, lapisLocationField, lapis, mapSource),
10436
+ [lapisFilter, lapisLocationField, lapis, mapSource]
10402
10437
  );
10403
10438
  if (isLoadingLapisData) {
10404
10439
  return /* @__PURE__ */ u$1(LoadingDisplay, {});
@@ -10414,17 +10449,17 @@ const SequencesByLocationMapTabs = ({
10414
10449
  }) => {
10415
10450
  const getTab = (view) => {
10416
10451
  switch (view) {
10417
- case views.map:
10418
- if (originalComponentProps.mapSource === void 0) {
10452
+ case views.map: {
10453
+ if (data.type !== MapLocationDataType.tableAndMapData) {
10419
10454
  throw new Error("mapSource is required when using the map view");
10420
10455
  }
10456
+ const { type: _type, tableData: _tableData, ...dataForMap } = data;
10421
10457
  return {
10422
10458
  title: "Map",
10423
10459
  content: /* @__PURE__ */ u$1(
10424
10460
  SequencesByLocationMap,
10425
10461
  {
10426
- locationData: data,
10427
- mapSource: originalComponentProps.mapSource,
10462
+ ...dataForMap,
10428
10463
  enableMapNavigation: originalComponentProps.enableMapNavigation,
10429
10464
  lapisLocationField: originalComponentProps.lapisLocationField,
10430
10465
  zoom: originalComponentProps.zoom,
@@ -10434,13 +10469,14 @@ const SequencesByLocationMapTabs = ({
10434
10469
  }
10435
10470
  )
10436
10471
  };
10472
+ }
10437
10473
  case views.table:
10438
10474
  return {
10439
10475
  title: "Table",
10440
10476
  content: /* @__PURE__ */ u$1(
10441
10477
  SequencesByLocationTable,
10442
10478
  {
10443
- locationData: data,
10479
+ tableData: data.tableData,
10444
10480
  lapisLocationField: originalComponentProps.lapisLocationField,
10445
10481
  pageSize: originalComponentProps.pageSize
10446
10482
  }
@@ -10449,10 +10485,24 @@ const SequencesByLocationMapTabs = ({
10449
10485
  }
10450
10486
  };
10451
10487
  const tabs = originalComponentProps.views.map((view) => getTab(view));
10452
- return /* @__PURE__ */ u$1(Tabs, { tabs, toolbar: /* @__PURE__ */ u$1(Toolbar, { originalComponentProps }) });
10488
+ return /* @__PURE__ */ u$1(
10489
+ Tabs,
10490
+ {
10491
+ tabs,
10492
+ toolbar: /* @__PURE__ */ u$1(Toolbar, { originalComponentProps, tableData: data.tableData })
10493
+ }
10494
+ );
10453
10495
  };
10454
- const Toolbar = ({ originalComponentProps }) => {
10496
+ const Toolbar = ({ originalComponentProps, tableData }) => {
10455
10497
  return /* @__PURE__ */ u$1("div", { class: "flex flex-row", children: [
10498
+ /* @__PURE__ */ u$1(
10499
+ CsvDownloadButton,
10500
+ {
10501
+ className: "mx-1 btn btn-xs",
10502
+ getData: () => tableData,
10503
+ filename: "sequences_by_location.csv"
10504
+ }
10505
+ ),
10456
10506
  /* @__PURE__ */ u$1(SequencesByLocationMapInfo, { originalComponentProps }),
10457
10507
  /* @__PURE__ */ u$1(Fullscreen, {})
10458
10508
  ] });