@genspectrum/dashboard-components 0.11.2 → 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) {
@@ -1242,6 +1242,109 @@ const CsvDownloadButton = ({
1242
1242
  };
1243
1243
  return /* @__PURE__ */ u$1("button", { className, onClick: download, children: label });
1244
1244
  };
1245
+ function useModalRef() {
1246
+ return A(null);
1247
+ }
1248
+ const Modal = ({ children, modalRef }) => {
1249
+ return /* @__PURE__ */ u$1("dialog", { ref: modalRef, className: "modal modal-bottom sm:modal-middle", children: [
1250
+ /* @__PURE__ */ u$1("div", { className: "modal-box sm:max-w-5xl", children: [
1251
+ /* @__PURE__ */ u$1("form", { method: "dialog", children: /* @__PURE__ */ u$1("button", { className: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2", children: "✕" }) }),
1252
+ /* @__PURE__ */ u$1("div", { className: "flex flex-col", children }),
1253
+ /* @__PURE__ */ u$1("div", { className: "modal-action", children: /* @__PURE__ */ u$1("form", { method: "dialog", children: /* @__PURE__ */ u$1("button", { className: "float-right underline text-sm hover:text-blue-700 mr-2", children: "Close" }) }) })
1254
+ ] }),
1255
+ /* @__PURE__ */ u$1("form", { method: "dialog", className: "modal-backdrop", children: /* @__PURE__ */ u$1("button", { children: "Helper to close when clicked outside" }) })
1256
+ ] });
1257
+ };
1258
+ const Info = ({ children }) => {
1259
+ const modalRef = useModalRef();
1260
+ const toggleHelp = () => {
1261
+ var _a;
1262
+ (_a = modalRef.current) == null ? void 0 : _a.showModal();
1263
+ };
1264
+ return /* @__PURE__ */ u$1("div", { className: "relative", children: [
1265
+ /* @__PURE__ */ u$1("button", { type: "button", className: "btn btn-xs", onClick: toggleHelp, children: "?" }),
1266
+ /* @__PURE__ */ u$1(Modal, { modalRef, children })
1267
+ ] });
1268
+ };
1269
+ const InfoHeadline1 = ({ children }) => {
1270
+ return /* @__PURE__ */ u$1("h1", { className: "text-lg font-bold", children });
1271
+ };
1272
+ const InfoHeadline2 = ({ children }) => {
1273
+ return /* @__PURE__ */ u$1("h2", { className: "text-base font-bold mt-4", children });
1274
+ };
1275
+ const InfoParagraph = ({ children }) => {
1276
+ return /* @__PURE__ */ u$1("p", { className: "text-justify my-1", children });
1277
+ };
1278
+ const InfoLink = ({ children, href }) => {
1279
+ return /* @__PURE__ */ u$1("a", { className: "text-blue-600 hover:text-blue-800", href, target: "_blank", rel: "noopener noreferrer", children });
1280
+ };
1281
+ const InfoComponentCode = ({ componentName, params, lapisUrl }) => {
1282
+ const componentCode = componentParametersToCode(componentName, params, lapisUrl);
1283
+ const codePenData = {
1284
+ title: "GenSpectrum dashboard component",
1285
+ html: generateFullExampleCode(componentCode, componentName),
1286
+ layout: "left",
1287
+ editors: "100"
1288
+ };
1289
+ return /* @__PURE__ */ u$1(Fragment, { children: [
1290
+ /* @__PURE__ */ u$1(InfoHeadline2, { children: "Use this component yourself" }),
1291
+ /* @__PURE__ */ u$1(InfoParagraph, { children: [
1292
+ "This component was created using the following parameters:",
1293
+ /* @__PURE__ */ u$1("div", { className: "p-4 border border-gray-200 rounded-lg overflow-x-auto", children: /* @__PURE__ */ u$1("pre", { children: /* @__PURE__ */ u$1("code", { children: componentCode }) }) })
1294
+ ] }),
1295
+ /* @__PURE__ */ u$1(InfoParagraph, { children: [
1296
+ "You can add this component to your own website using the",
1297
+ " ",
1298
+ /* @__PURE__ */ u$1(InfoLink, { href: "https://github.com/GenSpectrum/dashboard-components", children: "GenSpectrum dashboard components library" }),
1299
+ " ",
1300
+ "and the code from above."
1301
+ ] }),
1302
+ /* @__PURE__ */ u$1(InfoParagraph, { children: /* @__PURE__ */ u$1("form", { action: "https://codepen.io/pen/define", method: "POST", target: "_blank", children: [
1303
+ /* @__PURE__ */ u$1(
1304
+ "input",
1305
+ {
1306
+ type: "hidden",
1307
+ name: "data",
1308
+ value: JSON.stringify(codePenData).replace(/"/g, """).replace(/'/g, "'")
1309
+ }
1310
+ ),
1311
+ /* @__PURE__ */ u$1("button", { className: "text-blue-600 hover:text-blue-800", type: "submit", children: "Click here to try it out on CodePen." })
1312
+ ] }) })
1313
+ ] });
1314
+ };
1315
+ function componentParametersToCode(componentName, params, lapisUrl) {
1316
+ const stringifyIfNeeded = (value) => {
1317
+ return typeof value === "object" ? JSON.stringify(value) : value;
1318
+ };
1319
+ const attributes = indentLines(
1320
+ Object.entries(params).map(([key, value]) => `${key}='${stringifyIfNeeded(value)}'`).join("\n"),
1321
+ 4
1322
+ );
1323
+ return `<gs-app lapis="${lapisUrl}">
1324
+ <gs-${componentName}
1325
+ ${attributes}
1326
+ />
1327
+ </gs-app>`;
1328
+ }
1329
+ function generateFullExampleCode(componentCode, componentName) {
1330
+ const storyBookPath = `/docs/visualization-${componentName}--docs`;
1331
+ return `<html>
1332
+ <head>
1333
+ <script type="module" src="https://unpkg.com/@genspectrum/dashboard-components@latest/standalone-bundle/dashboard-components.js"><\/script>
1334
+ <link rel="stylesheet" href="https://unpkg.com/@genspectrum/dashboard-components@latest/dist/style.css" />
1335
+ </head>
1336
+
1337
+ <body>
1338
+ <!-- Component documentation: https://genspectrum.github.io/dashboard-components/?path=${storyBookPath} -->
1339
+ ${indentLines(componentCode, 2)}
1340
+ </body>
1341
+ </html>
1342
+ `;
1343
+ }
1344
+ function indentLines(text, numberSpaces) {
1345
+ const spaces = " ".repeat(numberSpaces);
1346
+ return text.split("\n").map((line) => spaces + line).join("\n");
1347
+ }
1245
1348
  const GS_ERROR_EVENT_TYPE = "gs-error";
1246
1349
  class ErrorEvent extends Event {
1247
1350
  constructor(error) {
@@ -1270,7 +1373,7 @@ class InvalidPropsError extends Error {
1270
1373
  const ErrorDisplay = ({ error, resetError, layout }) => {
1271
1374
  console.error(error);
1272
1375
  const containerRef = A(null);
1273
- const ref = A(null);
1376
+ const modalRef = useModalRef();
1274
1377
  y(() => {
1275
1378
  var _a;
1276
1379
  (_a = containerRef.current) == null ? void 0 : _a.dispatchEvent(new ErrorEvent(error));
@@ -1288,17 +1391,20 @@ const ErrorDisplay = ({ error, resetError, layout }) => {
1288
1391
  "Oops! Something went wrong.",
1289
1392
  details !== void 0 && /* @__PURE__ */ u$1(Fragment, { children: [
1290
1393
  " ",
1291
- /* @__PURE__ */ u$1("button", { className: "underline hover:text-gray-400", onClick: () => {
1292
- var _a;
1293
- return (_a = ref.current) == null ? void 0 : _a.showModal();
1294
- }, children: "Show details." }),
1295
- /* @__PURE__ */ u$1("dialog", { ref, class: "modal", children: [
1296
- /* @__PURE__ */ u$1("div", { class: "modal-box", children: [
1297
- /* @__PURE__ */ u$1("form", { method: "dialog", children: /* @__PURE__ */ u$1("button", { className: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2", children: "✕" }) }),
1298
- /* @__PURE__ */ u$1("h1", { class: "text-lg", children: details.headline }),
1299
- /* @__PURE__ */ u$1("div", { class: "py-4", children: details.message })
1300
- ] }),
1301
- /* @__PURE__ */ u$1("form", { method: "dialog", class: "modal-backdrop", children: /* @__PURE__ */ u$1("button", { children: "close" }) })
1394
+ /* @__PURE__ */ u$1(
1395
+ "button",
1396
+ {
1397
+ className: "underline hover:text-gray-400",
1398
+ onClick: () => {
1399
+ var _a;
1400
+ return (_a = modalRef.current) == null ? void 0 : _a.showModal();
1401
+ },
1402
+ children: "Show details."
1403
+ }
1404
+ ),
1405
+ /* @__PURE__ */ u$1(Modal, { modalRef, children: [
1406
+ /* @__PURE__ */ u$1(InfoHeadline1, { children: details.headline }),
1407
+ /* @__PURE__ */ u$1(InfoParagraph, { children: details.message })
1302
1408
  ] })
1303
1409
  ] })
1304
1410
  ] })
@@ -1465,103 +1571,6 @@ function useFullscreenStatus() {
1465
1571
  }, []);
1466
1572
  return isFullscreen;
1467
1573
  }
1468
- const Info = ({ children }) => {
1469
- const dialogRef = A(null);
1470
- const toggleHelp = () => {
1471
- var _a;
1472
- (_a = dialogRef.current) == null ? void 0 : _a.showModal();
1473
- };
1474
- return /* @__PURE__ */ u$1("div", { className: "relative", children: [
1475
- /* @__PURE__ */ u$1("button", { type: "button", className: "btn btn-xs", onClick: toggleHelp, children: "?" }),
1476
- /* @__PURE__ */ u$1("dialog", { ref: dialogRef, className: "modal modal-bottom sm:modal-middle", children: [
1477
- /* @__PURE__ */ u$1("div", { className: "modal-box sm:max-w-5xl", children: [
1478
- /* @__PURE__ */ u$1("form", { method: "dialog", children: /* @__PURE__ */ u$1("button", { className: "btn btn-sm btn-circle btn-ghost absolute right-2 top-2", children: "✕" }) }),
1479
- /* @__PURE__ */ u$1("div", { className: "flex flex-col", children }),
1480
- /* @__PURE__ */ u$1("div", { className: "modal-action", children: /* @__PURE__ */ u$1("form", { method: "dialog", children: /* @__PURE__ */ u$1("button", { className: "float-right underline text-sm hover:text-blue-700 mr-2", children: "Close" }) }) })
1481
- ] }),
1482
- /* @__PURE__ */ u$1("form", { method: "dialog", className: "modal-backdrop", children: /* @__PURE__ */ u$1("button", { children: "Helper to close when clicked outside" }) })
1483
- ] })
1484
- ] });
1485
- };
1486
- const InfoHeadline1 = ({ children }) => {
1487
- return /* @__PURE__ */ u$1("h1", { className: "text-lg font-bold", children });
1488
- };
1489
- const InfoHeadline2 = ({ children }) => {
1490
- return /* @__PURE__ */ u$1("h2", { className: "text-base font-bold mt-4", children });
1491
- };
1492
- const InfoParagraph = ({ children }) => {
1493
- return /* @__PURE__ */ u$1("p", { className: "text-justify my-1", children });
1494
- };
1495
- const InfoLink = ({ children, href }) => {
1496
- return /* @__PURE__ */ u$1("a", { className: "text-blue-600 hover:text-blue-800", href, target: "_blank", rel: "noopener noreferrer", children });
1497
- };
1498
- const InfoComponentCode = ({ componentName, params, lapisUrl }) => {
1499
- const componentCode = componentParametersToCode(componentName, params, lapisUrl);
1500
- const codePenData = {
1501
- title: "GenSpectrum dashboard component",
1502
- html: generateFullExampleCode(componentCode, componentName),
1503
- layout: "left",
1504
- editors: "100"
1505
- };
1506
- return /* @__PURE__ */ u$1(Fragment, { children: [
1507
- /* @__PURE__ */ u$1(InfoHeadline2, { children: "Use this component yourself" }),
1508
- /* @__PURE__ */ u$1(InfoParagraph, { children: [
1509
- "This component was created using the following parameters:",
1510
- /* @__PURE__ */ u$1("div", { className: "p-4 border border-gray-200 rounded-lg overflow-x-auto", children: /* @__PURE__ */ u$1("pre", { children: /* @__PURE__ */ u$1("code", { children: componentCode }) }) })
1511
- ] }),
1512
- /* @__PURE__ */ u$1(InfoParagraph, { children: [
1513
- "You can add this component to your own website using the",
1514
- " ",
1515
- /* @__PURE__ */ u$1(InfoLink, { href: "https://github.com/GenSpectrum/dashboard-components", children: "GenSpectrum dashboard components library" }),
1516
- " ",
1517
- "and the code from above."
1518
- ] }),
1519
- /* @__PURE__ */ u$1(InfoParagraph, { children: /* @__PURE__ */ u$1("form", { action: "https://codepen.io/pen/define", method: "POST", target: "_blank", children: [
1520
- /* @__PURE__ */ u$1(
1521
- "input",
1522
- {
1523
- type: "hidden",
1524
- name: "data",
1525
- value: JSON.stringify(codePenData).replace(/"/g, "&quot;").replace(/'/g, "&apos;")
1526
- }
1527
- ),
1528
- /* @__PURE__ */ u$1("button", { className: "text-blue-600 hover:text-blue-800", type: "submit", children: "Click here to try it out on CodePen." })
1529
- ] }) })
1530
- ] });
1531
- };
1532
- function componentParametersToCode(componentName, params, lapisUrl) {
1533
- const stringifyIfNeeded = (value) => {
1534
- return typeof value === "object" ? JSON.stringify(value) : value;
1535
- };
1536
- const attributes = indentLines(
1537
- Object.entries(params).map(([key, value]) => `${key}='${stringifyIfNeeded(value)}'`).join("\n"),
1538
- 4
1539
- );
1540
- return `<gs-app lapis="${lapisUrl}">
1541
- <gs-${componentName}
1542
- ${attributes}
1543
- />
1544
- </gs-app>`;
1545
- }
1546
- function generateFullExampleCode(componentCode, componentName) {
1547
- const storyBookPath = `/docs/visualization-${componentName}--docs`;
1548
- return `<html>
1549
- <head>
1550
- <script type="module" src="https://unpkg.com/@genspectrum/dashboard-components@latest/standalone-bundle/dashboard-components.js"><\/script>
1551
- <link rel="stylesheet" href="https://unpkg.com/@genspectrum/dashboard-components@latest/dist/style.css" />
1552
- </head>
1553
-
1554
- <body>
1555
- <!-- Component documentation: https://genspectrum.github.io/dashboard-components/?path=${storyBookPath} -->
1556
- ${indentLines(componentCode, 2)}
1557
- </body>
1558
- </html>
1559
- `;
1560
- }
1561
- function indentLines(text, numberSpaces) {
1562
- const spaces = " ".repeat(numberSpaces);
1563
- return text.split("\n").map((line) => spaces + line).join("\n");
1564
- }
1565
1574
  const LoadingDisplay = () => {
1566
1575
  return /* @__PURE__ */ u$1(
1567
1576
  "div",
@@ -2028,7 +2037,7 @@ function useQuery(fetchDataCallback, dependencies) {
2028
2037
  setIsLoading(false);
2029
2038
  }
2030
2039
  };
2031
- fetchData();
2040
+ void fetchData();
2032
2041
  }, [JSON.stringify(dependencies)]);
2033
2042
  if (isLoading) {
2034
2043
  return { isLoading: true };
@@ -2284,7 +2293,7 @@ const tailwindStyle = `*, ::before, ::after {
2284
2293
  --tw-contain-paint: ;
2285
2294
  --tw-contain-style: ;
2286
2295
  }/*
2287
- ! tailwindcss v3.4.16 | MIT License | https://tailwindcss.com
2296
+ ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
2288
2297
  *//*
2289
2298
  1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2290
2299
  2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
@@ -2956,6 +2965,7 @@ html {
2956
2965
  display: grid;
2957
2966
  width: 100%;
2958
2967
  overflow: hidden;
2968
+ direction: ltr;
2959
2969
  container-type: inline-size;
2960
2970
  grid-template-columns: auto 1fr;
2961
2971
  }
@@ -4486,9 +4496,6 @@ input.tab:checked + .tab-content,
4486
4496
  border-end-end-radius: inherit;
4487
4497
  border-start-end-radius: inherit;
4488
4498
  }
4489
- .modal-middle {
4490
- place-items: center;
4491
- }
4492
4499
  .modal-bottom {
4493
4500
  place-items: end;
4494
4501
  }
@@ -4933,9 +4940,6 @@ input.tab:checked + .tab-content,
4933
4940
  .min-w-\\[7\\.5rem\\] {
4934
4941
  min-width: 7.5rem;
4935
4942
  }
4936
- .max-w-3xl {
4937
- max-width: 48rem;
4938
- }
4939
4943
  .max-w-screen-lg {
4940
4944
  max-width: 1024px;
4941
4945
  }
@@ -5109,10 +5113,6 @@ input.tab:checked + .tab-content,
5109
5113
  padding-top: 0.5rem;
5110
5114
  padding-bottom: 0.5rem;
5111
5115
  }
5112
- .py-4 {
5113
- padding-top: 1rem;
5114
- padding-bottom: 1rem;
5115
- }
5116
5116
  .pl-2 {
5117
5117
  padding-left: 0.5rem;
5118
5118
  }
@@ -8327,7 +8327,7 @@ const AggregateInner = (componentProps) => {
8327
8327
  field: initialSortField,
8328
8328
  direction: initialSortDirection
8329
8329
  });
8330
- }, [lapisFilter, fields, lapis]);
8330
+ }, [lapisFilter, fields, lapis, initialSortField, initialSortDirection]);
8331
8331
  if (isLoading) {
8332
8332
  return /* @__PURE__ */ u$1(LoadingDisplay, {});
8333
8333
  }
@@ -10101,62 +10101,12 @@ svg.leaflet-image-layer.leaflet-interactive path {\r
10101
10101
  }\r
10102
10102
  `;
10103
10103
  const leafletStyleModifications = ".leaflet-container {\n background: transparent;\n}\n";
10104
- const mapSourceSchema = z$1.object({
10105
- type: z$1.literal("topojson"),
10106
- url: z$1.string().min(1),
10107
- topologyObjectsKey: z$1.string().min(1)
10108
- });
10109
- function useGeoJsonMap(mapSource) {
10110
- const {
10111
- data: geojsonData,
10112
- error,
10113
- isLoading
10114
- } = useQuery(async () => {
10115
- switch (mapSource.type) {
10116
- case "topojson":
10117
- return await loadTopojsonMap(mapSource);
10118
- }
10119
- }, [mapSource]);
10120
- if (isLoading) {
10121
- return { isLoading };
10122
- }
10123
- if (error) {
10124
- throw error;
10125
- }
10126
- return { geojsonData, isLoading: false };
10127
- }
10128
- async function loadTopojsonMap(mapSource) {
10129
- var _a;
10130
- const response = await fetch(mapSource.url);
10131
- const topology = await response.json();
10132
- if ((topology == null ? void 0 : topology.type) !== "Topology") {
10133
- throw new UserFacingError(
10134
- "Invalid map source",
10135
- `JSON downloaded from ${mapSource.url} does not look like a topojson Topology definition: missing 'type: "Topology"', got '${JSON.stringify(topology).substring(0, 100)}'`
10136
- );
10137
- }
10138
- const object = topology == null ? void 0 : topology.objects[mapSource.topologyObjectsKey];
10139
- if ((object == null ? void 0 : object.type) !== "GeometryCollection") {
10140
- throw new UserFacingError(
10141
- "Invalid map source",
10142
- `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)}'`
10143
- );
10144
- }
10145
- return topojson.feature(topology, object);
10146
- }
10147
10104
  const SequencesByLocationMap = ({
10148
- mapSource,
10149
- ...otherProps
10150
- }) => {
10151
- const { isLoading: isLoadingMap, geojsonData } = useGeoJsonMap(mapSource);
10152
- if (isLoadingMap) {
10153
- return /* @__PURE__ */ u$1(LoadingDisplay, {});
10154
- }
10155
- return /* @__PURE__ */ u$1(SequencesByLocationMapInner$1, { geojsonData, ...otherProps });
10156
- };
10157
- const SequencesByLocationMapInner$1 = ({
10158
- geojsonData,
10159
- locationData,
10105
+ locations,
10106
+ totalCount,
10107
+ countOfMatchedLocationData,
10108
+ nullCount,
10109
+ unmatchedLocations,
10160
10110
  enableMapNavigation,
10161
10111
  lapisLocationField,
10162
10112
  zoom,
@@ -10164,22 +10114,7 @@ const SequencesByLocationMapInner$1 = ({
10164
10114
  offsetY,
10165
10115
  hasTableView
10166
10116
  }) => {
10167
- var _a;
10168
10117
  const ref = A(null);
10169
- const { locations, totalCount, countOfMatchedLocationData, unmatchedLocations } = T(() => {
10170
- const countAndProportionByCountry = buildLookupByLocationField(locationData, lapisLocationField);
10171
- const { locations: locations2, unmatchedLocations: unmatchedLocations2 } = matchLocationDataAndGeoJsonFeatures(
10172
- geojsonData,
10173
- countAndProportionByCountry,
10174
- lapisLocationField
10175
- );
10176
- const totalCount2 = locationData.map((value) => value.count).reduce((sum, b3) => sum + b3, 0);
10177
- const countOfMatchedLocationData2 = locations2.map((location) => {
10178
- var _a2;
10179
- return ((_a2 = location.properties.data) == null ? void 0 : _a2.count) ?? 0;
10180
- }).reduce((sum, b3) => sum + b3, 0);
10181
- return { locations: locations2, totalCount: totalCount2, countOfMatchedLocationData: countOfMatchedLocationData2, unmatchedLocations: unmatchedLocations2 };
10182
- }, [geojsonData, locationData, lapisLocationField]);
10183
10118
  y(() => {
10184
10119
  if (!ref.current) {
10185
10120
  return;
@@ -10195,11 +10130,11 @@ const SequencesByLocationMapInner$1 = ({
10195
10130
  });
10196
10131
  Leaflet.geoJson(locations, {
10197
10132
  style: (feature) => {
10198
- var _a2;
10133
+ var _a;
10199
10134
  return {
10200
- 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),
10201
10136
  fillOpacity: 1,
10202
- color: "grey",
10137
+ color: "#666666",
10203
10138
  weight: 1
10204
10139
  };
10205
10140
  }
@@ -10208,7 +10143,6 @@ const SequencesByLocationMapInner$1 = ({
10208
10143
  leafletMap.remove();
10209
10144
  };
10210
10145
  }, [ref, locations, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
10211
- const nullCount = ((_a = locationData.find((row) => row[lapisLocationField] === null)) == null ? void 0 : _a.count) ?? 0;
10212
10146
  return /* @__PURE__ */ u$1("div", { className: "h-full", children: [
10213
10147
  /* @__PURE__ */ u$1("div", { ref, className: "h-full" }),
10214
10148
  /* @__PURE__ */ u$1("div", { className: "relative", children: /* @__PURE__ */ u$1(
@@ -10230,7 +10164,7 @@ const DataMatchInformation = ({
10230
10164
  nullCount,
10231
10165
  hasTableView
10232
10166
  }) => {
10233
- const dialogRef = A(null);
10167
+ const modalRef = useModalRef();
10234
10168
  const proportion = formatProportion(countOfMatchedLocationData / totalCount);
10235
10169
  return /* @__PURE__ */ u$1(Fragment, { children: [
10236
10170
  /* @__PURE__ */ u$1(
@@ -10238,7 +10172,7 @@ const DataMatchInformation = ({
10238
10172
  {
10239
10173
  onClick: () => {
10240
10174
  var _a;
10241
- return (_a = dialogRef.current) == null ? void 0 : _a.showModal();
10175
+ return (_a = modalRef.current) == null ? void 0 : _a.showModal();
10242
10176
  },
10243
10177
  className: "text-sm absolute bottom-0 px-1 z-[1001] bg-white rounded border cursor-pointer tooltip",
10244
10178
  "data-tip": "Click for detailed information",
@@ -10249,76 +10183,34 @@ const DataMatchInformation = ({
10249
10183
  ]
10250
10184
  }
10251
10185
  ),
10252
- /* @__PURE__ */ u$1("dialog", { ref: dialogRef, className: "modal modal-middle", children: [
10253
- /* @__PURE__ */ u$1("div", { className: "modal-box max-w-3xl", children: [
10254
- /* @__PURE__ */ u$1(InfoHeadline1, { children: "Sequences By Location - Map View" }),
10255
- /* @__PURE__ */ u$1(InfoParagraph, { children: [
10256
- "The current filter has matched ",
10257
- totalCount.toLocaleString("en-us"),
10258
- " sequences. From these sequences, we were able to match ",
10259
- countOfMatchedLocationData.toLocaleString("en-us"),
10260
- " (",
10261
- proportion,
10262
- ") on locations on the map."
10263
- ] }),
10264
- /* @__PURE__ */ u$1(InfoParagraph, { children: [
10265
- unmatchedLocations.length > 0 && /* @__PURE__ */ u$1(Fragment, { children: [
10266
- "The following locations from the data could not be matched on the map:",
10267
- " ",
10268
- unmatchedLocations.map((it) => `"${it}"`).join(", "),
10269
- ".",
10270
- " "
10271
- ] }),
10272
- nullCount > 0 && `${nullCount.toLocaleString("en-us")} matching sequences have no location information. `,
10273
- hasTableView && "You can check the table view for more detailed information."
10274
- ] }),
10275
- /* @__PURE__ */ u$1("div", { className: "modal-action", children: /* @__PURE__ */ u$1("form", { method: "dialog", children: /* @__PURE__ */ u$1("button", { className: "float-right underline text-sm hover:text-blue-700 mr-2", children: "Close" }) }) })
10186
+ /* @__PURE__ */ u$1(Modal, { modalRef, children: [
10187
+ /* @__PURE__ */ u$1(InfoHeadline1, { children: "Sequences By Location - Map View" }),
10188
+ /* @__PURE__ */ u$1(InfoParagraph, { children: [
10189
+ "The current filter has matched ",
10190
+ totalCount.toLocaleString("en-us"),
10191
+ " sequences. From these sequences, we were able to match ",
10192
+ countOfMatchedLocationData.toLocaleString("en-us"),
10193
+ " (",
10194
+ proportion,
10195
+ ") on locations on the map."
10276
10196
  ] }),
10277
- /* @__PURE__ */ u$1("form", { method: "dialog", className: "modal-backdrop", children: /* @__PURE__ */ u$1("button", { children: "Helper to close when clicked outside" }) })
10197
+ /* @__PURE__ */ u$1(InfoParagraph, { children: [
10198
+ unmatchedLocations.length > 0 && /* @__PURE__ */ u$1(Fragment, { children: [
10199
+ "The following locations from the data could not be matched on the map:",
10200
+ " ",
10201
+ unmatchedLocations.map((it) => `"${it}"`).join(", "),
10202
+ ".",
10203
+ " "
10204
+ ] }),
10205
+ nullCount > 0 && `${nullCount.toLocaleString("en-us")} matching sequences have no location information. `,
10206
+ hasTableView && "You can check the table view for more detailed information."
10207
+ ] })
10278
10208
  ] })
10279
10209
  ] });
10280
10210
  };
10281
- function buildLookupByLocationField(locationData, lapisLocationField) {
10282
- return new Map(
10283
- locationData.filter((row) => typeof row[lapisLocationField] === "string").map((row) => [row[lapisLocationField], row])
10284
- );
10285
- }
10286
- function matchLocationDataAndGeoJsonFeatures(geojsonData, countAndProportionByCountry, lapisLocationField) {
10287
- const matchedLocations = [];
10288
- const locations = geojsonData.features.map(
10289
- (feature) => {
10290
- var _a;
10291
- const name = (_a = feature == null ? void 0 : feature.properties) == null ? void 0 : _a.name;
10292
- if (typeof name !== "string") {
10293
- throw new Error(
10294
- `GeoJSON feature with id '${feature.id}' does not have 'properties.name' of type string, was: '${name}'`
10295
- );
10296
- }
10297
- const data = countAndProportionByCountry.get(name) ?? null;
10298
- if (data !== null) {
10299
- matchedLocations.push(name);
10300
- }
10301
- return {
10302
- ...feature,
10303
- properties: {
10304
- ...feature.properties,
10305
- data
10306
- }
10307
- };
10308
- }
10309
- );
10310
- const unmatchedLocations = [...countAndProportionByCountry.keys()].filter(
10311
- (name) => !matchedLocations.includes(name)
10312
- );
10313
- if (unmatchedLocations.length > 0) {
10314
- 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(", ")}`;
10315
- console.warn(unmatchedLocationsWarning);
10316
- }
10317
- return { locations, unmatchedLocations };
10318
- }
10319
10211
  function getColor(value) {
10320
10212
  if (value === void 0) {
10321
- return "#888888";
10213
+ return "#DDDDDD";
10322
10214
  }
10323
10215
  const thresholds = [
10324
10216
  { limit: 0.4, color: "#662506" },
@@ -10369,12 +10261,150 @@ function p({ innerText, className = "" }) {
10369
10261
  return headline;
10370
10262
  }
10371
10263
  const SequencesByLocationTable = ({
10372
- locationData,
10264
+ tableData,
10373
10265
  lapisLocationField,
10374
10266
  pageSize
10375
10267
  }) => {
10376
- 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"
10377
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
+ }
10378
10408
  const sequencesByLocationViewSchema = z$1.union([z$1.literal(views.map), z$1.literal(views.table)]);
10379
10409
  const sequencesByLocationPropsSchema = z$1.object({
10380
10410
  lapisFilter: lapisFilterSchema,
@@ -10395,15 +10425,15 @@ const SequencesByLocation = (componentProps) => {
10395
10425
  return /* @__PURE__ */ u$1(ErrorBoundary, { size, componentProps, schema: sequencesByLocationPropsSchema, children: /* @__PURE__ */ u$1(ResizeContainer, { size, children: /* @__PURE__ */ u$1(SequencesByLocationMapInner, { ...componentProps }) }) });
10396
10426
  };
10397
10427
  const SequencesByLocationMapInner = (props) => {
10398
- const { lapisFilter, lapisLocationField } = props;
10428
+ const { lapisFilter, lapisLocationField, mapSource } = props;
10399
10429
  const lapis = x(LapisUrlContext);
10400
10430
  const {
10401
10431
  data,
10402
10432
  error,
10403
10433
  isLoading: isLoadingLapisData
10404
10434
  } = useQuery(
10405
- async () => queryAggregateData(lapisFilter, [lapisLocationField], lapis),
10406
- [lapisFilter, lapisLocationField, lapis]
10435
+ async () => querySequencesByLocationData(lapisFilter, lapisLocationField, lapis, mapSource),
10436
+ [lapisFilter, lapisLocationField, lapis, mapSource]
10407
10437
  );
10408
10438
  if (isLoadingLapisData) {
10409
10439
  return /* @__PURE__ */ u$1(LoadingDisplay, {});
@@ -10419,17 +10449,17 @@ const SequencesByLocationMapTabs = ({
10419
10449
  }) => {
10420
10450
  const getTab = (view) => {
10421
10451
  switch (view) {
10422
- case views.map:
10423
- if (originalComponentProps.mapSource === void 0) {
10452
+ case views.map: {
10453
+ if (data.type !== MapLocationDataType.tableAndMapData) {
10424
10454
  throw new Error("mapSource is required when using the map view");
10425
10455
  }
10456
+ const { type: _type, tableData: _tableData, ...dataForMap } = data;
10426
10457
  return {
10427
10458
  title: "Map",
10428
10459
  content: /* @__PURE__ */ u$1(
10429
10460
  SequencesByLocationMap,
10430
10461
  {
10431
- locationData: data,
10432
- mapSource: originalComponentProps.mapSource,
10462
+ ...dataForMap,
10433
10463
  enableMapNavigation: originalComponentProps.enableMapNavigation,
10434
10464
  lapisLocationField: originalComponentProps.lapisLocationField,
10435
10465
  zoom: originalComponentProps.zoom,
@@ -10439,13 +10469,14 @@ const SequencesByLocationMapTabs = ({
10439
10469
  }
10440
10470
  )
10441
10471
  };
10472
+ }
10442
10473
  case views.table:
10443
10474
  return {
10444
10475
  title: "Table",
10445
10476
  content: /* @__PURE__ */ u$1(
10446
10477
  SequencesByLocationTable,
10447
10478
  {
10448
- locationData: data,
10479
+ tableData: data.tableData,
10449
10480
  lapisLocationField: originalComponentProps.lapisLocationField,
10450
10481
  pageSize: originalComponentProps.pageSize
10451
10482
  }
@@ -10454,10 +10485,24 @@ const SequencesByLocationMapTabs = ({
10454
10485
  }
10455
10486
  };
10456
10487
  const tabs = originalComponentProps.views.map((view) => getTab(view));
10457
- 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
+ );
10458
10495
  };
10459
- const Toolbar = ({ originalComponentProps }) => {
10496
+ const Toolbar = ({ originalComponentProps, tableData }) => {
10460
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
+ ),
10461
10506
  /* @__PURE__ */ u$1(SequencesByLocationMapInfo, { originalComponentProps }),
10462
10507
  /* @__PURE__ */ u$1(Fullscreen, {})
10463
10508
  ] });