You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ya...@apache.org on 2026/06/15 01:28:22 UTC

(airavata-portals) branch main updated: feat(admin): graph-first Experiment Statistics page (shadcn-vue AreaChart) (#230)

This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new 4cf44321d feat(admin): graph-first Experiment Statistics page (shadcn-vue AreaChart) (#230)
4cf44321d is described below

commit 4cf44321d381c07ed54164cf5aba19b7a885201b
Author: Yasith Jayawardana <ya...@gatech.edu>
AuthorDate: Sun Jun 14 21:28:16 2026 -0400

    feat(admin): graph-first Experiment Statistics page (shadcn-vue AreaChart) (#230)
    
    * Revamp Experiment Statistics admin page as a graph-first interface
    
    Replace the cluttered Experiment Statistics page with a single cohesive
    "filter-and-find" interface led by a multi-line time-series graph of
    gateway experiment activity.
    
    - Add a dependency-free, hand-rolled SVG multi-line chart
      (ExperimentStatisticsChart.vue): responsive viewBox, axes/gridlines,
      one polyline per status series in the series color, a hover tooltip
      showing the bucket time and per-series counts, and a legend that
      doubles as on/off toggles. Series colors use the shadcn design tokens
      (--primary, --chart-3, --warning, --success, --muted-foreground,
      --destructive) for all/created/running/completed/cancelled/failed.
    - Bucket the selected date range client-side and issue one aggregate
      ExperimentStatistics request per bucket concurrently (Promise.all),
      since there is no time-series endpoint. Granularity adapts to the range
      (hourly / daily / weekly) and the bucket count is capped at 30. A
      loading overlay covers the chart while requests are in flight.
    - Provide a date-range picker plus Last 24 hours / Last week /
      Last 30 days shortcuts, and inline optional scoping filters
      (username / application / hostname) that scope both the graph and the
      drill-down list.
    - Remove the separate "Load experiment details" lookup card and the row
      of six stat cards (delete the now-unused ExperimentStatisticsCard).
      Keep the unique by-experiment-id / by-job-id lookup as a slim "Find an
      experiment" affordance and a compact per-status experiments list with
      the existing experiment-detail tabs/drill-down below the graph.
    - Update the unit tests to match the new layout.
    
    Keeps the /experiment-statistics route and name intact.
    
    * Use shadcn-vue AreaChart for Experiment Statistics graph
    
    Replace the hand-rolled SVG chart on the Experiment Statistics admin
    page with the shadcn-vue AreaChart family (built on @unovis), and make
    the chart compact and consistent with the design system.
    
    - Vendor the shadcn-vue chart components under common's ui/ set
      (ui/chart: ChartCrosshair, ChartLegend, ChartTooltip,
      ChartSingleTooltip; ui/chart-area: AreaChart), converted from
      TypeScript to the plain-JS <script setup> convention used by the rest
      of the ui/ components. Cross-component imports use relative paths so
      the components resolve under every consuming app's "@" alias
      (admin, common, and workspace, which mirrors only a subset of ui/).
    - Add @unovis/vue and @unovis/ts to the common package (the new
      sanctioned charting dependency). CurveType is re-exported from the
      chart-area barrel so consumers need no direct @unovis dependency.
    - Wire Unovis' CSS variables to the design tokens in app.css and size
      the axis/tooltip fonts to the design-system scale (text-xs), so the
      chart reads consistently with the rest of the UI.
    - Render the chart as a compact ~256px-tall (h-64), responsive-width
      area chart with a smooth monotone curve and translucent gradient area
      fills, down from the previous oversized chart.
    - Drive categories/data/colors from the existing client-side bucketing
      and keep the independent per-series on/off toggle row (in the series
      colors) and the date-range picker + shortcuts. Series colors stay on
      the chart tokens: all=primary, created=chart-3, running=warning,
      completed=success, cancelled=muted-foreground, failed=destructive.
    - Remove the now-unused hand-rolled ExperimentStatisticsChart.vue and
      stub the AreaChart in the unit test (jsdom cannot render @unovis).
    
    Keeps the /experiment-statistics route and name intact.
    
    * fix(admin): make the statistics AreaChart fill its container width
    
    Unovis' VisXYContainer falls back to a fixed ~300px width when no width CSS is
    applied, collapsing the area chart into a narrow box on the right. Add
    .unovis-xy-container { width/height: 100% } so the chart fills its h-64 wrapper.
    (Also de-duplicated the tooltip CSS vars.)
---
 .../statistics/ExperimentStatisticsCard.vue        |   90 --
 .../statistics/ExperimentStatisticsContainer.vue   | 1150 ++++++++++-------
 .../ExperimentStatisticsContainer.spec.js          |  119 +-
 .../django_airavata/static/common/css/app.css      |   33 +
 .../js/components/ui/chart-area/AreaChart.vue      |  173 +++
 .../common/js/components/ui/chart-area/index.js    |    4 +
 .../js/components/ui/chart/ChartCrosshair.vue      |   45 +
 .../common/js/components/ui/chart/ChartLegend.vue  |   65 +
 .../js/components/ui/chart/ChartSingleTooltip.vue  |   73 ++
 .../common/js/components/ui/chart/ChartTooltip.vue |   36 +
 .../static/common/js/components/ui/chart/index.js  |   24 +
 .../static/common/js/components/ui/index.js        |    2 +
 .../django_airavata/static/common/package.json     |    2 +
 .../django_airavata/static/common/yarn.lock        | 1337 +++++++++++++++++++-
 14 files changed, 2531 insertions(+), 622 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue b/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
deleted file mode 100644
index 1b2c42abb..000000000
--- a/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsCard.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<template>
-  <Card class="statistics-card overflow-hidden">
-    <CardHeader>
-      <div class="text-right">
-        <div class="statistic-count whitespace-nowrap">
-          <abbr :title="count">{{ displayedCount }}</abbr>
-        </div>
-        <div>{{ title }}</div>
-      </div>
-    </CardHeader>
-    <CardContent>
-      <a
-        href="#"
-        class="text-primary hover:no-underline"
-        @click.prevent="$emit('click')"
-      >
-        <slot name="link-text">
-          <div v-for="state in states" :key="state.value">
-            {{ shortName(state) }}
-          </div>
-        </slot>
-      </a>
-    </CardContent>
-  </Card>
-</template>
-
-<script>
-export default {
-  name: "experiment-statistics-card",
-  props: {
-    bgVariant: {
-      type: String,
-      default: "light",
-    },
-    headerTextVariant: {
-      type: String,
-      default: "dark",
-    },
-    linkVariant: {
-      type: String,
-      default: "primary",
-    },
-    count: {
-      type: Number,
-      required: true,
-    },
-    title: {
-      type: String,
-      required: true,
-    },
-    states: {
-      type: Array,
-      default: () => [],
-    },
-  },
-  computed: {
-    displayedCount() {
-      // Round large numbers and display m for 10^6 and k for 10^3
-      if (this.count >= Math.pow(10, 6)) {
-        return (this.count / Math.pow(10, 6)).toFixed(0) + "m";
-      } else if (this.count >= Math.pow(10, 3)) {
-        return (this.count / Math.pow(10, 3)).toFixed(0) + "k";
-      } else {
-        return this.count;
-      }
-    },
-  },
-  methods: {
-    // Render the prefix-stripped short alias (e.g. "COMPLETED") instead of the
-    // raw proto member name ("EXPERIMENT_STATE_COMPLETED"), matching the rest of
-    // the portal (see ExperimentStatusBadge).
-    shortName(state) {
-      return state.constructor.shortAlias(state.name);
-    },
-  },
-};
-</script>
-
-<style scoped>
-.statistic-count {
-  font-size: 2.8rem;
-  overflow: hidden;
-}
-.statistics-card {
-  height: calc(100% - 30px);
-}
-abbr {
-  text-decoration: none;
-}
-</style>
diff --git a/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue b/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
index 14a9c8a95..c5751ce33 100644
--- a/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
+++ b/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/src/components/statistics/ExperimentStatisticsContainer.vue
@@ -1,365 +1,478 @@
 <template>
   <main-layout
     title="Experiment Statistics"
-    subtitle="Browse experiment activity and details across the gateway."
+    subtitle="Gateway experiment activity over time."
   >
-    <div class="space-y-4">
-      <Card>
-        <CardHeader>
-          <CardTitle>Load experiment details</CardTitle>
-        </CardHeader>
-        <CardContent>
-          <Tabs default-value="by-experiment-id">
-            <TabsList>
-              <TabsTrigger value="by-experiment-id"
-                >By Experiment ID</TabsTrigger
-              >
-              <TabsTrigger value="by-job-id">By Job ID</TabsTrigger>
-            </TabsList>
-            <TabsContent value="by-experiment-id" class="pt-4">
-              <div class="flex items-stretch gap-2">
-                <Input
-                  v-model.trim="experimentId"
-                  placeholder="Experiment ID"
-                  @keydown.enter="
-                    experimentId && showExperimentDetails(experimentId)
-                  "
+    <Tabs v-model="activeTab" ref="tabs" class="space-y-4">
+      <TabsList>
+        <TabsTrigger value="statistics">Statistics</TabsTrigger>
+        <TabsTrigger
+          v-for="experimentTab in experimentDetailTabs"
+          :key="experimentTab.experiment.experiment_id"
+          :value="experimentTab.experiment.experiment_id"
+        >
+          {{ experimentTab.tabTitle }}
+          <a
+            href="#"
+            @click.prevent="
+              removeExperimentDetailTab(experimentTab.experiment.experiment_id)
+            "
+            class="text-muted-foreground ml-1"
+          >
+            <X class="size-3.5" />
+            <span class="sr-only">Close experiment tab</span>
+          </a>
+        </TabsTrigger>
+      </TabsList>
+
+      <TabsContent value="statistics" class="space-y-4">
+        <!-- Primary deliverable: the time-series graph with an inline filter
+             bar (date range + shortcuts + optional scoping filters). -->
+        <Card>
+          <CardContent class="space-y-4 pt-6">
+            <div class="flex flex-wrap items-center justify-between gap-3">
+              <div class="flex flex-wrap items-center gap-2">
+                <span
+                  class="border-input flex items-center rounded-md border px-3"
+                >
+                  <CalendarDays class="size-4" aria-hidden="true" />
+                </span>
+                <flat-pickr
+                  v-model="dateRange"
+                  :config="dateConfig"
+                  @on-change="dateRangeChanged"
+                  placeholder="Select a date range"
+                  class="border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 w-56 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-3"
                 />
                 <Button
-                  :disabled="!experimentId"
-                  @click="showExperimentDetails(experimentId)"
-                  variant="default"
-                  >Load</Button
+                  v-for="shortcut in rangeShortcuts"
+                  :key="shortcut.key"
+                  :variant="
+                    activeShortcut === shortcut.key ? 'default' : 'outline'
+                  "
+                  size="sm"
+                  @click="applyShortcut(shortcut.key)"
+                  >{{ shortcut.label }}</Button
                 >
               </div>
-            </TabsContent>
-            <TabsContent value="by-job-id" class="pt-4">
-              <div class="flex items-stretch gap-2">
+              <div class="flex flex-wrap items-center gap-2">
+                <DropdownMenu>
+                  <DropdownMenuTrigger as-child>
+                    <Button variant="outline" size="sm">
+                      <FilterIcon class="size-4" />
+                      Add filter
+                    </Button>
+                  </DropdownMenuTrigger>
+                  <DropdownMenuContent>
+                    <DropdownMenuItem
+                      v-if="!usernameFilterEnabled"
+                      @click="usernameFilterEnabled = true"
+                      >Username</DropdownMenuItem
+                    >
+                    <DropdownMenuItem
+                      v-if="!applicationNameFilterEnabled"
+                      @click="applicationNameFilterEnabled = true"
+                      >Application Name</DropdownMenuItem
+                    >
+                    <DropdownMenuItem
+                      v-if="!hostnameFilterEnabled"
+                      @click="hostnameFilterEnabled = true"
+                      >Hostname</DropdownMenuItem
+                    >
+                    <DropdownMenuItem
+                      v-if="allFiltersEnabled"
+                      disabled
+                      class="text-muted-foreground"
+                      >No more filters</DropdownMenuItem
+                    >
+                  </DropdownMenuContent>
+                </DropdownMenu>
+              </div>
+            </div>
+
+            <!-- Active scoping filters, shown inline only when enabled. -->
+            <div
+              v-if="anyFilterEnabled"
+              class="flex flex-wrap items-center gap-2"
+            >
+              <div
+                v-if="usernameFilterEnabled"
+                class="flex items-stretch gap-1"
+              >
                 <Input
-                  v-model.trim="jobId"
-                  placeholder="Job ID"
-                  @keydown.enter="jobId && showExperimentDetailsForJobId(jobId)"
+                  v-model="usernameFilter"
+                  placeholder="Username"
+                  class="h-9 w-48"
+                  @keydown.enter="loadStatistics"
                 />
                 <Button
-                  :disabled="!jobId"
-                  @click="showExperimentDetailsForJobId(jobId)"
-                  variant="default"
-                  >Load</Button
+                  variant="outline"
+                  size="icon"
+                  @click="removeUsernameFilter"
                 >
+                  <X class="size-4" />
+                  <span class="sr-only">Remove username filter</span>
+                </Button>
               </div>
-            </TabsContent>
-          </Tabs>
-        </CardContent>
-      </Card>
-      <Card>
-        <CardContent>
-          <Tabs v-model="activeTab" ref="tabs">
-            <TabsList>
-              <TabsTrigger value="statistics">{{
-                selectedExperimentsTabTitle
-              }}</TabsTrigger>
-              <TabsTrigger
-                v-for="experimentTab in experimentDetailTabs"
-                :key="experimentTab.experiment.experiment_id"
-                :value="experimentTab.experiment.experiment_id"
+              <div
+                v-if="applicationNameFilterEnabled"
+                class="flex items-stretch gap-1"
               >
-                {{ experimentTab.tabTitle }}
-                <a
-                  href="#"
-                  @click.prevent="
-                    removeExperimentDetailTab(
-                      experimentTab.experiment.experiment_id,
-                    )
-                  "
-                  class="text-muted-foreground ml-1"
+                <select
+                  v-model="applicationNameFilter"
+                  @change="loadStatistics"
+                  class="border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 w-56 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-3"
                 >
-                  <X class="size-3.5" />
-                  <span class="sr-only">Close experiment tab</span>
-                </a>
-              </TabsTrigger>
-            </TabsList>
-            <TabsContent value="statistics" class="space-y-4 pt-4">
-              <Card>
-                <CardHeader>
-                  <CardTitle>Filter Options</CardTitle>
-                </CardHeader>
-                <CardContent>
-                  <div class="mb-2 flex w-full items-stretch gap-2">
-                    <span
-                      class="border-input flex items-center rounded-md border px-3"
-                    >
-                      <CalendarDays class="size-4" aria-hidden="true" />
-                    </span>
-                    <flat-pickr
-                      :value="dateRange"
-                      :config="dateConfig"
-                      @on-change="dateRangeChanged"
-                      class="border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 flex-1 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-3"
-                    />
-                    <Button @click="getPast24Hours" variant="outline"
-                      >Past 24 Hours</Button
-                    >
-                    <Button @click="getPastWeek" variant="outline"
-                      >Past Week</Button
-                    >
-                  </div>
-                  <DropdownMenu>
-                    <DropdownMenuTrigger as-child>
-                      <Button variant="outline" class="mb-2"
-                        >Add Filters</Button
-                      >
-                    </DropdownMenuTrigger>
-                    <DropdownMenuContent>
-                      <DropdownMenuItem
-                        v-if="!usernameFilterEnabled"
-                        @click="usernameFilterEnabled = true"
-                        >Username</DropdownMenuItem
-                      >
-                      <DropdownMenuItem
-                        v-if="!applicationNameFilterEnabled"
-                        @click="applicationNameFilterEnabled = true"
-                        >Application Name</DropdownMenuItem
-                      >
-                      <DropdownMenuItem
-                        v-if="!hostnameFilterEnabled"
-                        @click="hostnameFilterEnabled = true"
-                        >Hostname</DropdownMenuItem
-                      >
-                    </DropdownMenuContent>
-                  </DropdownMenu>
-                  <div
-                    v-if="usernameFilterEnabled"
-                    class="mb-2 flex items-stretch gap-2"
+                  <option :value="null" disabled>Application name</option>
+                  <option
+                    v-for="opt in applicationNameOptions"
+                    :key="opt.value"
+                    :value="opt.value"
                   >
+                    {{ opt.text }}
+                  </option>
+                </select>
+                <Button
+                  variant="outline"
+                  size="icon"
+                  @click="removeApplicationNameFilter"
+                >
+                  <X class="size-4" />
+                  <span class="sr-only">Remove application name filter</span>
+                </Button>
+              </div>
+              <div
+                v-if="hostnameFilterEnabled"
+                class="flex items-stretch gap-1"
+              >
+                <select
+                  v-model="hostnameFilter"
+                  @change="loadStatistics"
+                  class="border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 w-56 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-3"
+                >
+                  <option :value="null" disabled>Compute resource</option>
+                  <option
+                    v-for="opt in hostnameOptions"
+                    :key="opt.value"
+                    :value="opt.value"
+                  >
+                    {{ opt.text }}
+                  </option>
+                </select>
+                <Button
+                  variant="outline"
+                  size="icon"
+                  @click="removeHostnameFilter"
+                >
+                  <X class="size-4" />
+                  <span class="sr-only">Remove hostname filter</span>
+                </Button>
+              </div>
+            </div>
+
+            <!-- Compact, smooth-curved area chart (shadcn-vue AreaChart, built
+                 on @unovis). A loading overlay covers it while the per-bucket
+                 requests are in flight. The chart's own legend is disabled in
+                 favor of the independent on/off toggle row below. -->
+            <div class="relative h-64 w-full">
+              <div
+                v-if="loading"
+                class="absolute inset-0 z-10 flex items-center justify-center bg-background/60"
+              >
+                <Loader2 class="size-6 animate-spin text-muted-foreground" />
+              </div>
+              <area-chart
+                v-if="visibleCategories.length > 0 && chartData.length > 0"
+                :key="chartKey"
+                :data="chartData"
+                :categories="visibleCategories"
+                index="label"
+                :colors="visibleColors"
+                :curve-type="curveType"
+                :y-formatter="formatCount"
+                :show-legend="false"
+                :show-grid-line="true"
+                class="h-full"
+              />
+              <div
+                v-else
+                class="flex h-full items-center justify-center text-sm text-muted-foreground"
+              >
+                {{
+                  chartData.length === 0
+                    ? "No data for the selected range."
+                    : "No series selected."
+                }}
+              </div>
+            </div>
+            <!-- Legend doubling as independent on/off toggles, in the series
+                 colors, sized to the design-system text scale. -->
+            <div class="flex flex-wrap gap-x-4 gap-y-2">
+              <button
+                v-for="s in series"
+                :key="s.key"
+                type="button"
+                class="flex items-center gap-2 text-sm transition-opacity"
+                :class="s.visible ? 'opacity-100' : 'opacity-40'"
+                :aria-pressed="s.visible"
+                @click="toggleSeries(s.key)"
+              >
+                <span
+                  class="inline-block h-0.5 w-4 rounded-full"
+                  :style="{ backgroundColor: s.color }"
+                />
+                <span>{{ s.label }}</span>
+                <span class="text-muted-foreground tabular-nums">{{
+                  s.total
+                }}</span>
+              </button>
+            </div>
+            <p class="text-xs text-muted-foreground">
+              {{ rangeSummary }}
+            </p>
+          </CardContent>
+        </Card>
+
+        <!-- Secondary "find" affordances: a compact lookup and a drill-down
+             experiments list for the selected status. -->
+        <div class="grid gap-4 lg:grid-cols-3">
+          <Card class="lg:col-span-1">
+            <CardHeader>
+              <CardTitle class="text-base">Find an experiment</CardTitle>
+            </CardHeader>
+            <CardContent>
+              <Tabs default-value="by-experiment-id">
+                <TabsList>
+                  <TabsTrigger value="by-experiment-id"
+                    >By Experiment ID</TabsTrigger
+                  >
+                  <TabsTrigger value="by-job-id">By Job ID</TabsTrigger>
+                </TabsList>
+                <TabsContent value="by-experiment-id" class="pt-4">
+                  <div class="flex items-stretch gap-2">
                     <Input
-                      v-model="usernameFilter"
-                      placeholder="Username"
-                      @keydown.enter="loadStatistics"
+                      v-model.trim="experimentId"
+                      placeholder="Experiment ID"
+                      @keydown.enter="
+                        experimentId && showExperimentDetails(experimentId)
+                      "
                     />
-                    <Button variant="outline" @click="removeUsernameFilter">
-                      <X class="size-4" />
-                      <span class="sr-only">Remove username filter</span>
-                    </Button>
-                  </div>
-                  <div
-                    v-if="applicationNameFilterEnabled"
-                    class="mb-2 flex items-stretch gap-2"
-                  >
-                    <select
-                      v-model="applicationNameFilter"
-                      @change="loadStatistics"
-                      class="border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 flex-1 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-3"
-                    >
-                      <option :value="null" disabled>
-                        Select an application to filter on
-                      </option>
-                      <option
-                        v-for="opt in applicationNameOptions"
-                        :key="opt.value"
-                        :value="opt.value"
-                      >
-                        {{ opt.text }}
-                      </option>
-                    </select>
                     <Button
-                      variant="outline"
-                      @click="removeApplicationNameFilter"
+                      :disabled="!experimentId"
+                      @click="showExperimentDetails(experimentId)"
+                      variant="default"
+                      >Load</Button
                     >
-                      <X class="size-4" />
-                      <span class="sr-only"
-                        >Remove application name filter</span
-                      >
-                    </Button>
                   </div>
-                  <div
-                    v-if="hostnameFilterEnabled"
-                    class="mb-2 flex items-stretch gap-2"
-                  >
-                    <select
-                      v-model="hostnameFilter"
-                      @change="loadStatistics"
-                      class="border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 flex-1 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-3"
-                    >
-                      <option :value="null" disabled>
-                        Select compute resource to filter on
-                      </option>
-                      <option
-                        v-for="opt in hostnameOptions"
-                        :key="opt.value"
-                        :value="opt.value"
-                      >
-                        {{ opt.text }}
-                      </option>
-                    </select>
-                    <Button variant="outline" @click="removeHostnameFilter">
-                      <X class="size-4" />
-                      <span class="sr-only">Remove hostname filter</span>
-                    </Button>
-                  </div>
-                </CardContent>
-                <CardFooter>
-                  <div class="flex w-full justify-end">
-                    <Button @click="loadStatistics" class="ml-auto"
-                      >Get Statistics</Button
+                </TabsContent>
+                <TabsContent value="by-job-id" class="pt-4">
+                  <div class="flex items-stretch gap-2">
+                    <Input
+                      v-model.trim="jobId"
+                      placeholder="Job ID"
+                      @keydown.enter="
+                        jobId && showExperimentDetailsForJobId(jobId)
+                      "
+                    />
+                    <Button
+                      :disabled="!jobId"
+                      @click="showExperimentDetailsForJobId(jobId)"
+                      variant="default"
+                      >Load</Button
                     >
                   </div>
-                </CardFooter>
-              </Card>
-              <div>
-                <h2 class="mb-4 text-lg font-semibold">
-                  Experiment Statistics from {{ fromTimeDisplay }} to
-                  {{ toTimeDisplay }}
-                </h2>
-              </div>
-              <div class="grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-6">
-                <experiment-statistics-card
-                  :count="experimentStatistics.all_experiment_count || 0"
-                  title="Total Experiments"
-                  @click="selectExperiments('all_experiments')"
-                >
-                  <template v-slot:link-text>
-                    <span>All</span>
-                  </template>
-                </experiment-statistics-card>
-                <experiment-statistics-card
-                  :count="experimentStatistics.created_experiment_count || 0"
-                  :states="createdStates"
-                  title="Created Experiments"
-                  @click="selectExperiments('created_experiments')"
-                >
-                </experiment-statistics-card>
-                <experiment-statistics-card
-                  :count="experimentStatistics.running_experiment_count || 0"
-                  :states="runningStates"
-                  title="Running Experiments"
-                  @click="selectExperiments('running_experiments')"
-                >
-                </experiment-statistics-card>
-                <experiment-statistics-card
-                  :count="experimentStatistics.completed_experiment_count || 0"
-                  :states="completedStates"
-                  title="Completed Experiments"
-                  @click="selectExperiments('completed_experiments')"
-                >
-                </experiment-statistics-card>
-                <experiment-statistics-card
-                  :count="experimentStatistics.cancelled_experiment_count || 0"
-                  :states="canceledStates"
-                  title="Cancelled Experiments"
-                  @click="selectExperiments('cancelled_experiments')"
-                >
-                </experiment-statistics-card>
-                <experiment-statistics-card
-                  :count="experimentStatistics.failed_experiment_count || 0"
-                  :states="failedStates"
-                  title="Failed Experiments"
-                  @click="selectExperiments('failed_experiments')"
+                </TabsContent>
+              </Tabs>
+            </CardContent>
+          </Card>
+
+          <Card class="lg:col-span-2">
+            <CardHeader class="flex-row items-center justify-between space-y-0">
+              <CardTitle class="text-base">{{ selectedStatusLabel }}</CardTitle>
+              <select
+                v-model="selectedExperimentSummariesKey"
+                class="border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 w-48 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-3"
+              >
+                <option
+                  v-for="opt in summaryOptions"
+                  :key="opt.value"
+                  :value="opt.value"
                 >
-                </experiment-statistics-card>
-              </div>
-              <div v-if="items.length > 0">
-                <Card>
-                  <CardContent>
-                    <Table>
-                      <TableHeader>
-                        <TableRow>
-                          <TableHead v-for="field in fields" :key="field.key">
-                            {{ field.label }}
-                          </TableHead>
-                        </TableRow>
-                      </TableHeader>
-                      <TableBody>
-                        <TableRow
-                          v-for="item in items"
-                          :key="item.experiment_id"
-                        >
-                          <TableCell>{{ item.name }}</TableCell>
-                          <TableCell>{{ item.user_name }}</TableCell>
-                          <TableCell>
-                            <application-name
-                              :application-interface-id="item.execution_id"
-                            />
-                          </TableCell>
-                          <TableCell>
-                            <compute-resource-name
-                              :compute-resource-id="item.resource_host_id"
-                            />
-                          </TableCell>
-                          <TableCell
-                            ><human-date :date="item.creation_time"
-                          /></TableCell>
-                          <TableCell>
-                            <experiment-status-badge
-                              :status-name="item.experiment_status.name"
-                            />
-                          </TableCell>
-                          <TableCell>
-                            <a
-                              href="#"
-                              class="inline-flex items-center gap-1 text-primary hover:underline"
-                              @click.prevent="
-                                showExperimentDetails(item.experiment_id)
-                              "
-                            >
-                              View Details
-                              <BarChart3 class="size-4" aria-hidden="true" />
-                            </a>
-                          </TableCell>
-                        </TableRow>
-                      </TableBody>
-                    </Table>
-                  </CardContent>
-                </Card>
-                <pager
-                  v-if="experimentStatistics.all_experiment_count > 0"
-                  :paginator="experimentStatisticsPaginator"
-                  @next="experimentStatisticsPaginator.next()"
-                  @previous="experimentStatisticsPaginator.previous()"
-                ></pager>
-              </div>
-            </TabsContent>
-            <TabsContent
-              v-for="experimentTab in experimentDetailTabs"
-              :key="experimentTab.experiment.experiment_id"
-              :value="experimentTab.experiment.experiment_id"
-              class="pt-4"
-            >
-              <experiment-details-view :experiment="experimentTab.experiment" />
-            </TabsContent>
-          </Tabs>
-        </CardContent>
-      </Card>
-    </div>
+                  {{ opt.text }} ({{ totalForKey(opt.value) }})
+                </option>
+              </select>
+            </CardHeader>
+            <CardContent>
+              <Table v-if="items.length > 0">
+                <TableHeader>
+                  <TableRow>
+                    <TableHead v-for="field in fields" :key="field.key">
+                      {{ field.label }}
+                    </TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  <TableRow v-for="item in items" :key="item.experiment_id">
+                    <TableCell>{{ item.name }}</TableCell>
+                    <TableCell>{{ item.user_name }}</TableCell>
+                    <TableCell>
+                      <application-name
+                        :application-interface-id="item.execution_id"
+                      />
+                    </TableCell>
+                    <TableCell>
+                      <compute-resource-name
+                        :compute-resource-id="item.resource_host_id"
+                      />
+                    </TableCell>
+                    <TableCell
+                      ><human-date :date="item.creation_time"
+                    /></TableCell>
+                    <TableCell>
+                      <experiment-status-badge
+                        :status-name="item.experiment_status.name"
+                      />
+                    </TableCell>
+                    <TableCell>
+                      <a
+                        href="#"
+                        class="inline-flex items-center gap-1 text-primary hover:underline"
+                        @click.prevent="
+                          showExperimentDetails(item.experiment_id)
+                        "
+                      >
+                        View Details
+                        <BarChart3 class="size-4" aria-hidden="true" />
+                      </a>
+                    </TableCell>
+                  </TableRow>
+                </TableBody>
+              </Table>
+              <p v-else class="py-6 text-center text-sm text-muted-foreground">
+                No experiments in this range for the selected status.
+              </p>
+              <pager
+                v-if="experimentStatistics.all_experiment_count > 0"
+                :paginator="experimentStatisticsPaginator"
+                @next="experimentStatisticsPaginator.next()"
+                @previous="experimentStatisticsPaginator.previous()"
+              ></pager>
+            </CardContent>
+          </Card>
+        </div>
+      </TabsContent>
+
+      <TabsContent
+        v-for="experimentTab in experimentDetailTabs"
+        :key="experimentTab.experiment.experiment_id"
+        :value="experimentTab.experiment.experiment_id"
+        class="pt-4"
+      >
+        <experiment-details-view :experiment="experimentTab.experiment" />
+      </TabsContent>
+    </Tabs>
   </main-layout>
 </template>
 <script>
-import { BarChart3, CalendarDays, X } from "@lucide/vue";
+import {
+  BarChart3,
+  CalendarDays,
+  Filter as FilterIcon,
+  Loader2,
+  X,
+} from "@lucide/vue";
 import { errors, models, services, utils } from "django-airavata-api";
 import { components, notifications } from "django-airavata-common-ui";
-import ExperimentStatisticsCard from "./ExperimentStatisticsCard";
+import {
+  AreaChart,
+  CurveType,
+} from "django-airavata-common-ui/js/components/ui/chart-area";
 import ExperimentDetailsView from "./ExperimentDetailsView";
 
 import moment from "moment";
 
+const MS_PER_HOUR = 60 * 60 * 1000;
+const MS_PER_DAY = 24 * MS_PER_HOUR;
+// Cap on concurrent per-bucket requests so long ranges stay responsive.
+const MAX_BUCKETS = 30;
+
+// The six status series plotted on the chart. `countKey`/`summaryKey` map to the
+// ExperimentStatistics model's aggregate count + ExperimentSummary list fields.
+// Colors use the shadcn design tokens (see app.css): neutral/primary for "all",
+// then distinct accessible hues per status.
+const SERIES_DEFS = [
+  {
+    key: "all",
+    label: "All",
+    countKey: "all_experiment_count",
+    summaryKey: "all_experiments",
+    color: "var(--primary)",
+  },
+  {
+    key: "created",
+    label: "Created",
+    countKey: "created_experiment_count",
+    summaryKey: "created_experiments",
+    color: "var(--chart-3)",
+  },
+  {
+    key: "running",
+    label: "Running",
+    countKey: "running_experiment_count",
+    summaryKey: "running_experiments",
+    color: "var(--warning)",
+  },
+  {
+    key: "completed",
+    label: "Completed",
+    countKey: "completed_experiment_count",
+    summaryKey: "completed_experiments",
+    color: "var(--success)",
+  },
+  {
+    key: "cancelled",
+    label: "Cancelled",
+    countKey: "cancelled_experiment_count",
+    summaryKey: "cancelled_experiments",
+    color: "var(--muted-foreground)",
+  },
+  {
+    key: "failed",
+    label: "Failed",
+    countKey: "failed_experiment_count",
+    summaryKey: "failed_experiments",
+    color: "var(--destructive)",
+  },
+];
+
 export default {
   name: "experiment-statistics-container",
   data() {
-    //fp_incr sets the time of the date to midnight.
-    //Calculating from today midnight to tomorrow midnight.
-    const fromTime = new Date().fp_incr(0);
-    const toTime = new Date().fp_incr(1);
+    // Default range: the last 24 hours, ending now.
+    const toTime = new Date();
+    const fromTime = new Date(toTime.getTime() - MS_PER_DAY);
     return {
       experimentStatisticsPaginator: null,
-      selectedExperimentSummariesKey: null,
-      fromTime: fromTime,
-      toTime: toTime,
+      selectedExperimentSummariesKey: "all_experiments",
+      fromTime,
+      toTime,
       dateRange: [fromTime, toTime],
       dateConfig: {
         mode: "range",
-        wrap: true,
         dateFormat: "Y-m-d",
-        maxDate: new Date().fp_incr(1),
+        maxDate: new Date(),
       },
+      activeShortcut: "24h",
+      suppressDateChange: false,
+      loading: false,
+      // Per-bucket aggregate counts: buckets[i] holds the ExperimentStatistics
+      // results for the i-th time window.
+      bucketStats: [],
+      buckets: [],
+      seriesVisible: SERIES_DEFS.reduce((acc, def) => {
+        acc[def.key] = true;
+        return acc;
+      }, {}),
       usernameFilterEnabled: false,
       usernameFilter: null,
       applicationNameFilterEnabled: false,
@@ -384,9 +497,11 @@ export default {
   components: {
     BarChart3,
     CalendarDays,
+    FilterIcon,
+    Loader2,
     X,
     ExperimentDetailsView,
-    ExperimentStatisticsCard,
+    "area-chart": AreaChart,
     "application-name": components.ApplicationName,
     "compute-resource-name": components.ComputeResourceName,
     "human-date": components.HumanDate,
@@ -395,78 +510,119 @@ export default {
     pager: components.Pager,
   },
   computed: {
+    rangeShortcuts() {
+      return [
+        { key: "24h", label: "Last 24 hours" },
+        { key: "week", label: "Last week" },
+        { key: "30d", label: "Last 30 days" },
+      ];
+    },
+    anyFilterEnabled() {
+      return (
+        this.usernameFilterEnabled ||
+        this.applicationNameFilterEnabled ||
+        this.hostnameFilterEnabled
+      );
+    },
+    allFiltersEnabled() {
+      return (
+        this.usernameFilterEnabled &&
+        this.applicationNameFilterEnabled &&
+        this.hostnameFilterEnabled
+      );
+    },
+    // The aggregate ExperimentStatistics for the whole range (used by the
+    // drill-down list + counts). Returned by the final/full-range request.
     experimentStatistics() {
       return this.experimentStatisticsPaginator
         ? this.experimentStatisticsPaginator.results
         : {};
     },
-    createdStates() {
-      // TODO: moved to ExperimentStatistics model
-      return [models.ExperimentState.CREATED, models.ExperimentState.VALIDATED];
-    },
-    runningStates() {
-      return [
-        models.ExperimentState.SCHEDULED,
-        models.ExperimentState.LAUNCHED,
-        models.ExperimentState.EXECUTING,
-      ];
-    },
-    completedStates() {
-      return [models.ExperimentState.COMPLETED];
+    // Chart series: each status' per-bucket count, in the series color, with the
+    // current on/off visibility and a range total.
+    series() {
+      return SERIES_DEFS.map((def) => {
+        const values = this.bucketStats.map(
+          (stats) => (stats && stats[def.countKey]) || 0,
+        );
+        const total = values.reduce((sum, v) => sum + v, 0);
+        return {
+          key: def.key,
+          label: def.label,
+          color: def.color,
+          visible: this.seriesVisible[def.key],
+          values,
+          total,
+        };
+      });
     },
-    canceledStates() {
-      return [
-        models.ExperimentState.CANCELING,
-        models.ExperimentState.CANCELED,
-      ];
+    // Smooth (monotone) curve interpolation for the area/line series.
+    curveType() {
+      return CurveType.MonotoneX;
+    },
+    // The currently-visible series, used to build the chart categories/colors.
+    visibleSeries() {
+      return this.series.filter((s) => s.visible);
+    },
+    // Category keys (the series labels) for the AreaChart, in display order.
+    visibleCategories() {
+      return this.visibleSeries.map((s) => s.label);
+    },
+    // Remount the chart when the set of visible series changes so its internal
+    // legend/crosshair state stays in sync with the categories/colors. Data-only
+    // refreshes within the same set keep the same key (and animate in place).
+    chartKey() {
+      return this.visibleCategories.join("|");
+    },
+    // Colors aligned 1:1 with visibleCategories.
+    visibleColors() {
+      return this.visibleSeries.map((s) => s.color);
+    },
+    // One row per time bucket: { label: <x-axis tick>, <series label>: count }.
+    // Only visible series are included so the chart redraws on toggle.
+    chartData() {
+      return this.buckets.map((bucket, i) => {
+        const row = { label: bucket.label };
+        for (const s of this.visibleSeries) {
+          row[s.label] = s.values[i] ?? 0;
+        }
+        return row;
+      });
     },
-    failedStates() {
-      return [models.ExperimentState.FAILED];
+    rangeSummary() {
+      const from = moment(this.fromTime).format("MMM D, YYYY HH:mm");
+      const to = moment(this.toTime).format("MMM D, YYYY HH:mm");
+      const n = this.buckets.length;
+      const granularity = n > 0 ? this.buckets[0].granularity : "";
+      return `${from} – ${to} · ${n} ${granularity} bucket${
+        n === 1 ? "" : "s"
+      }`;
     },
     fields() {
       return [
-        {
-          key: "name",
-          label: "Name",
-        },
-        {
-          key: "user_name",
-          label: "Owner",
-        },
-        {
-          key: "execution_id",
-          label: "Application",
-        },
-        {
-          key: "resource_host_id",
-          label: "Resource",
-        },
-        {
-          key: "creation_time",
-          label: "Creation Time",
-        },
-        {
-          key: "experiment_status",
-          label: "Status",
-        },
-        {
-          key: "actions",
-          label: "Actions",
-        },
+        { key: "name", label: "Name" },
+        { key: "user_name", label: "Owner" },
+        { key: "execution_id", label: "Application" },
+        { key: "resource_host_id", label: "Resource" },
+        { key: "creation_time", label: "Creation Time" },
+        { key: "experiment_status", label: "Status" },
+        { key: "actions", label: "Actions" },
       ];
     },
-    items() {
-      if (this.selectedExperimentSummaries) {
-        return this.selectedExperimentSummaries;
-      } else {
-        return [];
-      }
+    summaryOptions() {
+      return SERIES_DEFS.map((def) => ({
+        value: def.summaryKey,
+        text: `${def.label} Experiments`,
+      }));
     },
-    fromTimeDisplay() {
-      return moment(this.fromTime).format("MMM Do YYYY");
+    selectedStatusLabel() {
+      const opt = this.summaryOptions.find(
+        (o) => o.value === this.selectedExperimentSummariesKey,
+      );
+      return opt ? opt.text : "Experiments";
     },
-    toTimeDisplay() {
-      return moment(this.toTime).format("MMM Do YYYY");
+    items() {
+      return this.selectedExperimentSummaries;
     },
     selectedExperimentSummaries() {
       if (
@@ -481,12 +637,10 @@ export default {
     },
     applicationNameOptions() {
       if (this.appInterfaces) {
-        const options = this.appInterfaces.map((appInterface) => {
-          return {
-            value: appInterface.application_interface_id,
-            text: appInterface.application_name,
-          };
-        });
+        const options = this.appInterfaces.map((appInterface) => ({
+          value: appInterface.application_interface_id,
+          text: appInterface.application_name,
+        }));
         return utils.StringUtils.sortIgnoreCase(options, (o) => o.text);
       } else {
         return [];
@@ -495,7 +649,6 @@ export default {
     hostnameOptions() {
       if (this.computeResourceNames && this.groupResourceProfiles) {
         // Only show compute resources that are configured in the Group Resource Profiles
-        // First create a Set of all compute resource ids in the GRPs
         const groupResourceProfileCompResources = new Set(
           this.groupResourceProfiles.flatMap((grp) =>
             grp.compute_preferences.map((cp) => cp.compute_resource_id),
@@ -503,50 +656,164 @@ export default {
         );
         const options = this.computeResourceNames
           .filter((name) => groupResourceProfileCompResources.has(name.host_id))
-          .map((name) => {
-            return {
-              value: name.host_id,
-              text: name.host,
-            };
-          });
+          .map((name) => ({
+            value: name.host_id,
+            text: name.host,
+          }));
         return utils.StringUtils.sortIgnoreCase(options, (o) => o.text);
       } else {
         return [];
       }
     },
-    selectedExperimentsTabTitle() {
-      if (this.selectedExperimentSummariesKey === "all_experiments") {
-        return "All Experiments";
-      } else if (
-        this.selectedExperimentSummariesKey === "created_experiments"
-      ) {
-        return "Created Experiments";
-      } else if (
-        this.selectedExperimentSummariesKey === "running_experiments"
-      ) {
-        return "Running Experiments";
-      } else if (
-        this.selectedExperimentSummariesKey === "completed_experiments"
-      ) {
-        return "Completed Experiments";
-      } else if (
-        this.selectedExperimentSummariesKey === "cancelled_experiments"
-      ) {
-        return "Cancelled Experiments";
-      } else if (this.selectedExperimentSummariesKey === "failed_experiments") {
-        return "Failed Experiments";
+  },
+  methods: {
+    // Compact count formatting for the Y axis (e.g. 1.2k, 3m).
+    formatCount(value) {
+      const n = Math.round(value);
+      if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "m";
+      if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
+      return String(n);
+    },
+    totalForKey(summaryKey) {
+      const def = SERIES_DEFS.find((d) => d.summaryKey === summaryKey);
+      const stats = this.experimentStatistics;
+      return (def && stats && stats[def.countKey]) || 0;
+    },
+    toggleSeries(key) {
+      this.seriesVisible = {
+        ...this.seriesVisible,
+        [key]: !this.seriesVisible[key],
+      };
+    },
+    // Split [fromTime, toTime] into N time windows. Granularity adapts to the
+    // range length to keep the bucket count at or below MAX_BUCKETS: hourly for
+    // short ranges, daily for medium, weekly for long.
+    computeBuckets() {
+      const from = this.fromTime.getTime();
+      const to = this.toTime.getTime();
+      const span = Math.max(to - from, MS_PER_HOUR);
+
+      let step;
+      let granularity;
+      if (span <= MAX_BUCKETS * MS_PER_HOUR) {
+        step = MS_PER_HOUR;
+        granularity = "hourly";
+      } else if (span <= MAX_BUCKETS * MS_PER_DAY) {
+        step = MS_PER_DAY;
+        granularity = "daily";
       } else {
-        return "Experiments";
+        step = 7 * MS_PER_DAY;
+        granularity = "weekly";
+      }
+      // If even the chosen granularity exceeds the cap (very long custom ranges),
+      // widen the step so the count stays within MAX_BUCKETS.
+      let count = Math.ceil(span / step);
+      if (count > MAX_BUCKETS) {
+        step = Math.ceil(span / MAX_BUCKETS);
+        count = MAX_BUCKETS;
+        granularity = "interval";
+      }
+
+      const buckets = [];
+      for (let i = 0; i < count; i++) {
+        const start = from + i * step;
+        const end = Math.min(start + step, to);
+        if (start >= to) break;
+        buckets.push({
+          start,
+          end,
+          granularity,
+          label:
+            granularity === "hourly"
+              ? moment(start).format("HH:mm")
+              : moment(start).format("MMM D"),
+        });
+      }
+      return buckets;
+    },
+    baseRequestParams() {
+      const params = {};
+      if (this.usernameFilterEnabled && this.usernameFilter) {
+        params.userName = this.usernameFilter;
+      }
+      if (this.applicationNameFilterEnabled && this.applicationNameFilter) {
+        params.applicationName = this.applicationNameFilter;
+      }
+      if (this.hostnameFilterEnabled && this.hostnameFilter) {
+        params.resourceHostName = this.hostnameFilter;
+      }
+      return params;
+    },
+    async loadStatistics() {
+      this.loading = true;
+      const buckets = this.computeBuckets();
+      const baseParams = this.baseRequestParams();
+      try {
+        // One aggregate request per bucket, issued concurrently, plus one
+        // full-range request that feeds the drill-down list + total counts.
+        const bucketPromises = buckets.map((bucket) =>
+          services.ExperimentStatisticsService.get({
+            ...baseParams,
+            fromTime: new Date(bucket.start).toJSON(),
+            toTime: new Date(bucket.end).toJSON(),
+          }),
+        );
+        const fullRangePromise = services.ExperimentStatisticsService.get({
+          ...baseParams,
+          fromTime: this.fromTime.toJSON(),
+          toTime: this.toTime.toJSON(),
+        });
+        const [fullRange, ...bucketResults] = await Promise.all([
+          fullRangePromise,
+          ...bucketPromises,
+        ]);
+        this.experimentStatisticsPaginator = fullRange;
+        this.bucketStats = bucketResults.map((paginator) =>
+          paginator ? paginator.results : null,
+        );
+        this.buckets = buckets;
+      } catch (error) {
+        utils.FetchUtils.reportError(error);
+      } finally {
+        this.loading = false;
       }
     },
-  },
-  methods: {
     dateRangeChanged(selectedDates) {
-      [this.fromTime, this.toTime] = selectedDates;
-      if (this.fromTime && this.toTime) {
+      // Ignore the onChange that flatpickr fires when a shortcut updates the
+      // picker programmatically; that path already set the range + reloaded.
+      if (this.suppressDateChange) {
+        return;
+      }
+      if (selectedDates.length === 2) {
+        this.fromTime = selectedDates[0];
+        // flatpickr range end is midnight of the end day; include the whole day.
+        this.toTime = new Date(selectedDates[1].getTime() + MS_PER_DAY);
+        this.activeShortcut = null;
         this.loadStatistics();
       }
     },
+    applyShortcut(key) {
+      const to = new Date();
+      let from;
+      if (key === "24h") {
+        from = new Date(to.getTime() - MS_PER_DAY);
+      } else if (key === "week") {
+        from = new Date(to.getTime() - 7 * MS_PER_DAY);
+      } else {
+        from = new Date(to.getTime() - 30 * MS_PER_DAY);
+      }
+      this.fromTime = from;
+      this.toTime = to;
+      this.activeShortcut = key;
+      // Update the picker without letting its onChange clobber the shortcut
+      // state; reset the flag after the change has been flushed.
+      this.suppressDateChange = true;
+      this.dateRange = [from, to];
+      this.$nextTick(() => {
+        this.suppressDateChange = false;
+      });
+      this.loadStatistics();
+    },
     loadApplicationInterfaces() {
       return services.ApplicationInterfaceService.list().then(
         (appInterfaces) => (this.appInterfaces = appInterfaces),
@@ -561,46 +828,6 @@ export default {
       this.groupResourceProfiles =
         await services.GroupResourceProfileService.list();
     },
-    loadStatistics() {
-      const requestData = {
-        fromTime: this.fromTime.toJSON(),
-        toTime: this.toTime.toJSON(),
-      };
-      if (this.usernameFilterEnabled && this.usernameFilter) {
-        requestData["userName"] = this.usernameFilter;
-      }
-      if (this.applicationNameFilterEnabled && this.applicationNameFilter) {
-        requestData["applicationName"] = this.applicationNameFilter;
-      }
-      if (this.hostnameFilterEnabled && this.hostnameFilter) {
-        requestData["resourceHostName"] = this.hostnameFilter;
-      }
-      return services.ExperimentStatisticsService.get(requestData).then(
-        (stats) => {
-          this.experimentStatisticsPaginator = stats;
-        },
-      );
-    },
-    getPast24Hours() {
-      this.fromTime = new Date().fp_incr(0);
-      //this.fromTime = new Date(this.fromTime.setHours(0,0,0));
-      this.toTime = new Date().fp_incr(1);
-      this.updateDateRange();
-    },
-    getPastWeek() {
-      this.fromTime = new Date().fp_incr(-7);
-      this.toTime = new Date().fp_incr(1);
-      this.updateDateRange();
-    },
-    updateDateRange() {
-      this.dateRange = [
-        moment(this.fromTime).format("YYYY-MM-DD"),
-        moment(this.toTime).format("YYYY-MM-DD"),
-      ];
-    },
-    daysAgo(days) {
-      return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
-    },
     removeUsernameFilter() {
       this.usernameFilter = null;
       this.usernameFilterEnabled = false;
@@ -705,15 +932,6 @@ export default {
     scrollTabsIntoView() {
       this.$refs.tabs.$el.scrollIntoView({ behavior: "smooth" });
     },
-    selectExperiments(experimentSummariesKey) {
-      if (
-        this.experimentStatisticsPaginator &&
-        this.experimentStatisticsPaginator.offset > 0
-      ) {
-        this.loadStatistics();
-      }
-      this.selectedExperimentSummariesKey = experimentSummariesKey;
-    },
   },
 };
 </script>
diff --git a/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/tests/unit/components/statistics/ExperimentStatisticsContainer.spec.js b/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/tests/unit/components/statistics/ExperimentStatisticsContainer.spec.js
index 1ed0ef276..f88500f49 100644
--- a/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/tests/unit/components/statistics/ExperimentStatisticsContainer.spec.js
+++ b/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin/tests/unit/components/statistics/ExperimentStatisticsContainer.spec.js
@@ -35,6 +35,19 @@ function renderWithProviders(component) {
   return render(Wrapper, renderOptions);
 }
 
+// The AreaChart is built on @unovis, which renders via SVG/measurement APIs that
+// jsdom does not implement. These tests exercise the lookup + filter logic, not
+// the chart rendering, so stub the chart-area module with a trivial component.
+vi.mock("django-airavata-common-ui/js/components/ui/chart-area", () => ({
+  __esModule: true,
+  AreaChart: {
+    name: "area-chart-stub",
+    props: ["data", "categories", "index", "colors", "curveType", "yFormatter"],
+    template: '<div data-testid="area-chart-stub" />',
+  },
+  CurveType: { MonotoneX: "monotoneX" },
+}));
+
 import { models, services, utils } from "django-airavata-api";
 import ExperimentStatus from "django-airavata-api/static/django_airavata_api/js/models/ExperimentStatus";
 vi.mock("django-airavata-api", async () => {
@@ -72,6 +85,36 @@ vi.mock("django-airavata-api", async () => {
   };
 });
 
+// The container issues one ExperimentStatistics request per time bucket plus a
+// full-range request, all concurrently. Return a fresh empty-stats paginator on
+// every call so a single mock satisfies them all.
+function emptyStatsPaginator() {
+  return new utils.PaginationIterator(
+    {
+      count: 0,
+      next: null,
+      previous: null,
+      results: {
+        all_experiment_count: 0,
+        completed_experiment_count: 0,
+        cancelled_experiment_count: 0,
+        failed_experiment_count: 0,
+        created_experiment_count: 0,
+        running_experiment_count: 0,
+        all_experiments: [],
+        completed_experiments: [],
+        failed_experiments: [],
+        cancelled_experiments: [],
+        created_experiments: [],
+        running_experiments: [],
+      },
+      limit: 50,
+      offset: 0,
+    },
+    models.ExperimentStatistics,
+  );
+}
+
 beforeEach(() => {
   vi.resetAllMocks();
 
@@ -86,31 +129,8 @@ beforeEach(() => {
 test("load experiment by job id when job id matches unique experiment", async () => {
   // Service call mocks
   services.ApplicationInterfaceService.list.mockResolvedValue([]);
-  services.ExperimentStatisticsService.get.mockResolvedValue(
-    new utils.PaginationIterator(
-      {
-        count: 0,
-        next: null,
-        previous: null,
-        results: {
-          all_experiment_count: 0,
-          completed_experiment_count: 0,
-          cancelled_experiment_count: 0,
-          failed_experiment_count: 0,
-          created_experiment_count: 0,
-          running_experiment_count: 0,
-          all_experiments: [],
-          completed_experiments: [],
-          failed_experiments: [],
-          cancelled_experiments: [],
-          created_experiments: [],
-          running_experiments: [],
-        },
-        limit: 50,
-        offset: 0,
-      },
-      models.ExperimentStatistics
-    )
+  services.ExperimentStatisticsService.get.mockImplementation(() =>
+    Promise.resolve(emptyStatsPaginator()),
   );
   services.ComputeResourceService.namesList.mockResolvedValue([]);
   services.ExperimentSearchService.list.mockResolvedValue(
@@ -121,8 +141,8 @@ test("load experiment by job id when job id matches unique experiment", async ()
         previous: null,
         results: [{ experiment_id: "test-experiment-id" }],
       },
-      models.ExperimentSummary
-    )
+      models.ExperimentSummary,
+    ),
   );
   // Mock just enough of Experiment and FullExperiment to get ExperimentDetailsView to render
   const experiment = new models.Experiment({
@@ -141,7 +161,7 @@ test("load experiment by job id when job id matches unique experiment", async ()
     new models.FullExperiment({
       experiment_id: "test-experiment-id",
       experiment,
-    })
+    }),
   );
   services.ExperimentArchiveService.get.mockResolvedValue({
     archived: false,
@@ -152,7 +172,7 @@ test("load experiment by job id when job id matches unique experiment", async ()
 
   // The render method returns a collection of utilities to query your component.
   const { findByText, findByPlaceholderText } = renderWithProviders(
-    ExperimentStatisticsContainer
+    ExperimentStatisticsContainer,
   );
 
   const byJobIDTab = await findByText("By Job ID");
@@ -169,7 +189,7 @@ test("load experiment by job id when job id matches unique experiment", async ()
   await fireEvent.update(jobIDInputField, "12345");
 
   const loadButton = await within(jobIDInputField.parentElement).findByText(
-    "Load"
+    "Load",
   );
 
   await fireEvent.click(loadButton);
@@ -186,7 +206,7 @@ test("load experiment by job id when job id matches unique experiment", async ()
     },
     {
       ignoreErrors: true,
-    }
+    },
   );
   // The experiment-details tab is activated asynchronously (setTimeout) and its
   // panel (ExperimentDetailsView) mounts lazily on activation, which is when it
@@ -194,38 +214,15 @@ test("load experiment by job id when job id matches unique experiment", async ()
   await waitFor(() =>
     expect(services.FullExperimentService.retrieve).toHaveBeenCalledWith({
       lookup: experiment.experiment_id,
-    })
+    }),
   );
 });
 
 test("Hostname filter only shows compute resources that are configured in a GRP", async () => {
   // Service call mocks
   services.ApplicationInterfaceService.list.mockResolvedValue([]);
-  services.ExperimentStatisticsService.get.mockResolvedValue(
-    new utils.PaginationIterator(
-      {
-        count: 0,
-        next: null,
-        previous: null,
-        results: {
-          all_experiment_count: 0,
-          completed_experiment_count: 0,
-          cancelled_experiment_count: 0,
-          failed_experiment_count: 0,
-          created_experiment_count: 0,
-          running_experiment_count: 0,
-          all_experiments: [],
-          completed_experiments: [],
-          failed_experiments: [],
-          cancelled_experiments: [],
-          created_experiments: [],
-          running_experiments: [],
-        },
-        limit: 50,
-        offset: 0,
-      },
-      models.ExperimentStatistics
-    )
+  services.ExperimentStatisticsService.get.mockImplementation(() =>
+    Promise.resolve(emptyStatsPaginator()),
   );
   services.ComputeResourceService.namesList.mockResolvedValue([
     { host_id: "compute4-abcd", host: "d-compute4" },
@@ -261,7 +258,7 @@ test("Hostname filter only shows compute resources that are configured in a GRP"
   // The render method returns a collection of utilities to query your component.
   const { findByText } = renderWithProviders(ExperimentStatisticsContainer);
 
-  const addFiltersMenu = await findByText("Add Filters");
+  const addFiltersMenu = await findByText("Add filter");
 
   await fireEvent.click(addFiltersMenu);
 
@@ -269,14 +266,12 @@ test("Hostname filter only shows compute resources that are configured in a GRP"
 
   await fireEvent.click(hostnameMenuItem);
 
-  const computeResourcesSelect = await findByText(
-    "Select compute resource to filter on"
-  );
+  const computeResourcesSelect = await findByText("Compute resource");
 
   const options = computeResourcesSelect.parentElement.options;
 
   expect(options.length).toBe(4);
-  // option 0 is the null one ("Select compute resource to filter on")
+  // option 0 is the null one ("Compute resource")
   // verify that options 1-3 are compute resources 1, 3, 4. That is, verify that
   // filtering worked and that they were sorted.
   expect(options[1].value).toBe("compute1-abcd");
diff --git a/airavata-django-portal/django_airavata/static/common/css/app.css b/airavata-django-portal/django_airavata/static/common/css/app.css
index b375f4023..88f0e3b51 100644
--- a/airavata-django-portal/django_airavata/static/common/css/app.css
+++ b/airavata-django-portal/django_airavata/static/common/css/app.css
@@ -138,3 +138,36 @@
     @apply bg-background text-foreground;
   }
 }
+
+/*
+ * Unovis chart theming for the shadcn-vue chart components (ui/chart,
+ * ui/chart-area). Wire Unovis' CSS variables to the design tokens and size the
+ * axis/tooltip fonts to the design-system scale (text-xs, 0.75rem) so charts
+ * read consistently with the rest of the UI. The tooltip itself is rendered by
+ * our ChartTooltip (a Card), so Unovis' default tooltip chrome is neutralized.
+ */
+:root {
+  --vis-font-family: inherit;
+  --vis-axis-tick-label-font-size: 0.75rem;
+  --vis-axis-tick-label-color: var(--muted-foreground);
+  --vis-axis-tick-color: var(--border);
+  --vis-text-color: var(--muted-foreground);
+  --vis-primary-color: var(--primary);
+  --vis-secondary-color: var(--muted-foreground);
+  --vis-crosshair-line-stroke-color: var(--muted-foreground);
+  --vis-crosshair-circle-stroke-color: var(--background);
+
+  --vis-tooltip-background-color: none;
+  --vis-tooltip-border-color: none;
+  --vis-tooltip-text-color: none;
+  --vis-tooltip-shadow-color: none;
+  --vis-tooltip-backdrop-filter: none;
+  --vis-tooltip-padding: none;
+}
+
+/* Let Unovis' XY container fill its parent. Without this it falls back to a
+   fixed ~300px width, collapsing the area chart into a narrow box. */
+.unovis-xy-container {
+  width: 100%;
+  height: 100%;
+}
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/chart-area/AreaChart.vue b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart-area/AreaChart.vue
new file mode 100644
index 000000000..e4beceb30
--- /dev/null
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart-area/AreaChart.vue
@@ -0,0 +1,173 @@
+<script setup>
+import { Area, Axis, CurveType, Line } from "@unovis/ts";
+import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";
+import { useMounted } from "@vueuse/core";
+import { useId } from "reka-ui";
+import { computed, ref } from "vue";
+import { cn } from "@/lib/utils";
+import { ChartCrosshair, ChartLegend, defaultColors } from "../chart";
+
+const props = defineProps({
+  // The source data, where each entry is a dictionary.
+  data: { type: Array, required: true },
+  // Categories selected from the data; populate the legend and tooltip.
+  categories: { type: Array, required: true },
+  // The key used to map the data to the x axis.
+  index: { type: String, required: true },
+  // Override the default colors.
+  colors: { type: Array, required: false },
+  // Margin of the container.
+  margin: {
+    type: Object,
+    required: false,
+    default: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
+  },
+  // Opacity of non-selected series.
+  filterOpacity: { type: Number, required: false, default: 0.2 },
+  // Function to format the X tick label.
+  xFormatter: { type: Function, required: false },
+  // Function to format the Y tick label.
+  yFormatter: { type: Function, required: false },
+  showXAxis: { type: Boolean, required: false, default: true },
+  showYAxis: { type: Boolean, required: false, default: true },
+  showTooltip: { type: Boolean, required: false, default: true },
+  showLegend: { type: Boolean, required: false, default: true },
+  showGridLine: { type: Boolean, required: false, default: true },
+  // Render a custom tooltip component.
+  customTooltip: { type: null, required: false },
+  // Curve interpolation type. Defaults to a smooth monotone curve.
+  curveType: { type: String, required: false, default: CurveType.MonotoneX },
+  // Controls the visibility of the translucent area gradient fill.
+  showGradient: { type: Boolean, required: false, default: true },
+});
+
+const emits = defineEmits(["legendItemClick"]);
+
+const chartRef = useId();
+
+const index = computed(() => props.index);
+const colors = computed(() =>
+  props.colors?.length ? props.colors : defaultColors(props.categories.length),
+);
+
+const legendItems = ref(
+  props.categories.map((category, i) => ({
+    name: category,
+    color: colors.value[i],
+    inactive: false,
+  })),
+);
+
+const isMounted = useMounted();
+
+function handleLegendItemClick(d, i) {
+  emits("legendItemClick", d, i);
+}
+</script>
+
+<template>
+  <div :class="cn('flex h-full w-full flex-col items-end', $attrs.class ?? '')">
+    <ChartLegend
+      v-if="showLegend"
+      v-model:items="legendItems"
+      @legend-item-click="handleLegendItemClick"
+    />
+
+    <VisXYContainer
+      :style="{ height: isMounted ? '100%' : 'auto' }"
+      :margin="{ left: 20, right: 20 }"
+      :data="data"
+    >
+      <svg width="0" height="0">
+        <defs>
+          <linearGradient
+            v-for="(color, i) in colors"
+            :id="`${chartRef}-color-${i}`"
+            :key="i"
+            x1="0"
+            y1="0"
+            x2="0"
+            y2="1"
+          >
+            <template v-if="showGradient">
+              <stop offset="5%" :stop-color="color" stop-opacity="0.4" />
+              <stop offset="95%" :stop-color="color" stop-opacity="0" />
+            </template>
+            <template v-else>
+              <stop offset="0%" :stop-color="color" />
+            </template>
+          </linearGradient>
+        </defs>
+      </svg>
+
+      <ChartCrosshair
+        v-if="showTooltip"
+        :colors="colors"
+        :items="legendItems"
+        :index="index"
+        :custom-tooltip="customTooltip"
+      />
+
+      <template v-for="(category, i) in categories" :key="category">
+        <VisArea
+          :x="(d, i) => i"
+          :y="(d) => d[category]"
+          color="auto"
+          :curve-type="curveType"
+          :attributes="{
+            [Area.selectors.area]: {
+              fill: `url(#${chartRef}-color-${i})`,
+            },
+          }"
+          :opacity="
+            legendItems.find((item) => item.name === category)?.inactive
+              ? filterOpacity
+              : 1
+          "
+        />
+      </template>
+
+      <template v-for="(category, i) in categories" :key="category">
+        <VisLine
+          :x="(d, i) => i"
+          :y="(d) => d[category]"
+          :color="colors[i]"
+          :curve-type="curveType"
+          :attributes="{
+            [Line.selectors.line]: {
+              opacity: legendItems.find((item) => item.name === category)
+                ?.inactive
+                ? filterOpacity
+                : 1,
+            },
+          }"
+        />
+      </template>
+
+      <VisAxis
+        v-if="showXAxis"
+        type="x"
+        :tick-format="xFormatter ?? ((v) => data[v]?.[index])"
+        :grid-line="false"
+        :tick-line="false"
+        tick-text-color="var(--muted-foreground)"
+      />
+      <VisAxis
+        v-if="showYAxis"
+        type="y"
+        :tick-line="false"
+        :tick-format="yFormatter"
+        :domain-line="false"
+        :grid-line="showGridLine"
+        :attributes="{
+          [Axis.selectors.grid]: {
+            class: 'text-muted',
+          },
+        }"
+        tick-text-color="var(--muted-foreground)"
+      />
+
+      <slot />
+    </VisXYContainer>
+  </div>
+</template>
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/chart-area/index.js b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart-area/index.js
new file mode 100644
index 000000000..38d8fa6d3
--- /dev/null
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart-area/index.js
@@ -0,0 +1,4 @@
+export { default as AreaChart } from "./AreaChart.vue";
+// Re-export the curve-type enum so consumers can pick a curve without taking a
+// direct dependency on @unovis (which is installed in this common package).
+export { CurveType } from "@unovis/ts";
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartCrosshair.vue b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartCrosshair.vue
new file mode 100644
index 000000000..ea7bc8014
--- /dev/null
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartCrosshair.vue
@@ -0,0 +1,45 @@
+<script setup>
+import { omit } from "@unovis/ts";
+import { VisCrosshair, VisTooltip } from "@unovis/vue";
+import { createApp } from "vue";
+import { ChartTooltip } from ".";
+
+const props = defineProps({
+  colors: { type: Array, required: false, default: () => [] },
+  index: { type: String, required: true },
+  items: { type: Array, required: true },
+  customTooltip: { type: null, required: false },
+});
+
+// Use a WeakMap to cache the rendered tooltip HTML per datapoint.
+const wm = new WeakMap();
+function template(d) {
+  if (wm.has(d)) {
+    return wm.get(d);
+  } else {
+    const componentDiv = document.createElement("div");
+    const omittedData = Object.entries(omit(d, [props.index])).map(
+      ([key, value]) => {
+        const legendReference = props.items.find((i) => i.name === key);
+        return { ...legendReference, value };
+      },
+    );
+    const TooltipComponent = props.customTooltip ?? ChartTooltip;
+    createApp(TooltipComponent, {
+      title: d[props.index].toString(),
+      data: omittedData,
+    }).mount(componentDiv);
+    wm.set(d, componentDiv.innerHTML);
+    return componentDiv.innerHTML;
+  }
+}
+
+function color(d, i) {
+  return props.colors[i] ?? "transparent";
+}
+</script>
+
+<template>
+  <VisTooltip :horizontal-shift="20" :vertical-shift="20" />
+  <VisCrosshair :template="template" :color="color" />
+</template>
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartLegend.vue b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartLegend.vue
new file mode 100644
index 000000000..85fd92887
--- /dev/null
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartLegend.vue
@@ -0,0 +1,65 @@
+<script setup>
+import { BulletLegend } from "@unovis/ts";
+import { VisBulletLegend } from "@unovis/vue";
+import { nextTick, onMounted, ref } from "vue";
+import { buttonVariants } from "../button";
+
+const props = defineProps({
+  items: { type: Array, required: false, default: () => [] },
+});
+
+const emits = defineEmits(["legendItemClick", "update:items"]);
+
+const elRef = ref();
+
+function keepStyling() {
+  const selector = `.${BulletLegend.selectors.item}`;
+  nextTick(() => {
+    const elements = elRef.value?.querySelectorAll(selector);
+    const classes = buttonVariants({ variant: "ghost", size: "sm" }).split(" ");
+    elements?.forEach((el) =>
+      el.classList.add(...classes, "!inline-flex", "!mr-2"),
+    );
+  });
+}
+
+onMounted(() => {
+  keepStyling();
+});
+
+function onLegendItemClick(d, i) {
+  emits("legendItemClick", d, i);
+  const isBulletActive = !props.items[i].inactive;
+  const isFilterApplied = props.items.some((i) => i.inactive);
+  if (isFilterApplied && isBulletActive) {
+    // reset filter
+    emits(
+      "update:items",
+      props.items.map((item) => ({ ...item, inactive: false })),
+    );
+  } else {
+    // apply selection, set other item as inactive
+    emits(
+      "update:items",
+      props.items.map((item) =>
+        item.name === d.name
+          ? { ...d, inactive: false }
+          : { ...item, inactive: true },
+      ),
+    );
+  }
+  keepStyling();
+}
+</script>
+
+<template>
+  <div
+    ref="elRef"
+    class="w-max"
+    :style="{
+      '--vis-legend-bullet-size': '16px',
+    }"
+  >
+    <VisBulletLegend :items="items" :on-legend-item-click="onLegendItemClick" />
+  </div>
+</template>
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartSingleTooltip.vue b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartSingleTooltip.vue
new file mode 100644
index 000000000..472eba7c9
--- /dev/null
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartSingleTooltip.vue
@@ -0,0 +1,73 @@
+<script setup>
+import { omit } from "@unovis/ts";
+import { VisTooltip } from "@unovis/vue";
+import { createApp } from "vue";
+import { ChartTooltip } from ".";
+
+const props = defineProps({
+  selector: { type: String, required: true },
+  index: { type: String, required: true },
+  items: { type: Array, required: false },
+  valueFormatter: { type: Function, required: false },
+  customTooltip: { type: null, required: false },
+});
+
+// Use a WeakMap to cache the rendered tooltip HTML per datapoint.
+const wm = new WeakMap();
+function template(d, i, elements) {
+  const valueFormatter = props.valueFormatter ?? ((tick) => `${tick}`);
+  if (props.index in d) {
+    if (wm.has(d)) {
+      return wm.get(d);
+    } else {
+      const componentDiv = document.createElement("div");
+      const omittedData = Object.entries(omit(d, [props.index])).map(
+        ([key, value]) => {
+          const legendReference = props.items?.find((i) => i.name === key);
+          return { ...legendReference, value: valueFormatter(value) };
+        },
+      );
+      const TooltipComponent = props.customTooltip ?? ChartTooltip;
+      createApp(TooltipComponent, {
+        title: d[props.index],
+        data: omittedData,
+      }).mount(componentDiv);
+      wm.set(d, componentDiv.innerHTML);
+      return componentDiv.innerHTML;
+    }
+  } else {
+    const data = d.data;
+
+    if (wm.has(data)) {
+      return wm.get(data);
+    } else {
+      const style = getComputedStyle(elements[i]);
+      const omittedData = [
+        {
+          name: data.name,
+          value: valueFormatter(data[props.index]),
+          color: style.fill,
+        },
+      ];
+      const componentDiv = document.createElement("div");
+      const TooltipComponent = props.customTooltip ?? ChartTooltip;
+      createApp(TooltipComponent, {
+        title: d[props.index],
+        data: omittedData,
+      }).mount(componentDiv);
+      wm.set(d, componentDiv.innerHTML);
+      return componentDiv.innerHTML;
+    }
+  }
+}
+</script>
+
+<template>
+  <VisTooltip
+    :horizontal-shift="20"
+    :vertical-shift="20"
+    :triggers="{
+      [selector]: template,
+    }"
+  />
+</template>
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartTooltip.vue b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartTooltip.vue
new file mode 100644
index 000000000..ac98364c9
--- /dev/null
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/ChartTooltip.vue
@@ -0,0 +1,36 @@
+<script setup>
+import { Card, CardContent, CardHeader, CardTitle } from "../card";
+
+defineProps({
+  title: { type: String, required: false },
+  data: { type: Array, required: true },
+});
+</script>
+
+<template>
+  <Card class="text-sm">
+    <CardHeader v-if="title" class="border-b p-3">
+      <CardTitle>
+        {{ title }}
+      </CardTitle>
+    </CardHeader>
+    <CardContent class="flex min-w-[180px] flex-col gap-1 p-3">
+      <div v-for="(item, key) in data" :key="key" class="flex justify-between">
+        <div class="flex items-center">
+          <span class="mr-2 h-2.5 w-2.5">
+            <svg width="100%" height="100%" viewBox="0 0 30 30">
+              <path
+                d=" M 15 15 m -14, 0 a 14,14 0 1,1 28,0 a 14,14 0 1,1 -28,0"
+                :stroke="item.color"
+                :fill="item.color"
+                stroke-width="1"
+              />
+            </svg>
+          </span>
+          <span>{{ item.name }}</span>
+        </div>
+        <span class="ml-4 font-semibold">{{ item.value }}</span>
+      </div>
+    </CardContent>
+  </Card>
+</template>
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/index.js b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/index.js
new file mode 100644
index 000000000..b626f6d53
--- /dev/null
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/chart/index.js
@@ -0,0 +1,24 @@
+// Vendored from shadcn-vue's chart component family (the @unovis-based
+// AreaChart/LineChart helpers), converted from TypeScript to the plain-JS
+// <script setup> convention this repo uses for its ui/ components.
+export { default as ChartCrosshair } from "./ChartCrosshair.vue";
+export { default as ChartLegend } from "./ChartLegend.vue";
+export { default as ChartSingleTooltip } from "./ChartSingleTooltip.vue";
+export { default as ChartTooltip } from "./ChartTooltip.vue";
+
+export function defaultColors(count = 3) {
+  const quotient = Math.floor(count / 2);
+  const remainder = count % 2;
+
+  const primaryCount = quotient + remainder;
+  const secondaryCount = quotient;
+  return [
+    ...Array.from(new Array(primaryCount).keys()).map(
+      (i) => `hsl(var(--vis-primary-color) / ${1 - (1 / primaryCount) * i})`,
+    ),
+    ...Array.from(new Array(secondaryCount).keys()).map(
+      (i) =>
+        `hsl(var(--vis-secondary-color) / ${1 - (1 / secondaryCount) * i})`,
+    ),
+  ];
+}
diff --git a/airavata-django-portal/django_airavata/static/common/js/components/ui/index.js b/airavata-django-portal/django_airavata/static/common/js/components/ui/index.js
index ebc654b99..91717a29f 100644
--- a/airavata-django-portal/django_airavata/static/common/js/components/ui/index.js
+++ b/airavata-django-portal/django_airavata/static/common/js/components/ui/index.js
@@ -3,6 +3,8 @@ export * from "./alert";
 export * from "./badge";
 export * from "./button";
 export * from "./card";
+export * from "./chart";
+export * from "./chart-area";
 export * from "./checkbox";
 export * from "./dialog";
 export * from "./dropdown-menu";
diff --git a/airavata-django-portal/django_airavata/static/common/package.json b/airavata-django-portal/django_airavata/static/common/package.json
index 40fd171b2..977724e84 100644
--- a/airavata-django-portal/django_airavata/static/common/package.json
+++ b/airavata-django-portal/django_airavata/static/common/package.json
@@ -17,6 +17,8 @@
   "dependencies": {
     "@lucide/vue": "^1.18.0",
     "@tanstack/vue-table": "^8.21.3",
+    "@unovis/ts": "^1.6.5",
+    "@unovis/vue": "^1.6.5",
     "@uppy/core": "^5.2.0",
     "@uppy/drag-drop": "^5.1.0",
     "@uppy/status-bar": "^5.1.0",
diff --git a/airavata-django-portal/django_airavata/static/common/yarn.lock b/airavata-django-portal/django_airavata/static/common/yarn.lock
index d848f05b1..abb603c48 100644
--- a/airavata-django-portal/django_airavata/static/common/yarn.lock
+++ b/airavata-django-portal/django_airavata/static/common/yarn.lock
@@ -2,6 +2,39 @@
 # yarn lockfile v1
 
 
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.29.7":
+  version "7.29.7"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7"
+  integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.29.7"
+    js-tokens "^4.0.0"
+    picocolors "^1.1.1"
+
+"@babel/generator@^7.29.7":
+  version "7.29.7"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3"
+  integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==
+  dependencies:
+    "@babel/parser" "^7.29.7"
+    "@babel/types" "^7.29.7"
+    "@jridgewell/gen-mapping" "^0.3.12"
+    "@jridgewell/trace-mapping" "^0.3.28"
+    jsesc "^3.0.2"
+
+"@babel/helper-globals@^7.29.7":
+  version "7.29.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b"
+  integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==
+
+"@babel/helper-module-imports@^7.16.7":
+  version "7.29.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396"
+  integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==
+  dependencies:
+    "@babel/traverse" "^7.29.7"
+    "@babel/types" "^7.29.7"
+
 "@babel/helper-string-parser@^7.29.7":
   version "7.29.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f"
@@ -19,6 +52,33 @@
   dependencies:
     "@babel/types" "^7.29.7"
 
+"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3":
+  version "7.29.7"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768"
+  integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==
+
+"@babel/template@^7.29.7":
+  version "7.29.7"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700"
+  integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==
+  dependencies:
+    "@babel/code-frame" "^7.29.7"
+    "@babel/parser" "^7.29.7"
+    "@babel/types" "^7.29.7"
+
+"@babel/traverse@^7.29.7":
+  version "7.29.7"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d"
+  integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==
+  dependencies:
+    "@babel/code-frame" "^7.29.7"
+    "@babel/generator" "^7.29.7"
+    "@babel/helper-globals" "^7.29.7"
+    "@babel/parser" "^7.29.7"
+    "@babel/template" "^7.29.7"
+    "@babel/types" "^7.29.7"
+    debug "^4.3.1"
+
 "@babel/types@^7.29.7":
   version "7.29.7"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92"
@@ -71,6 +131,86 @@
   dependencies:
     tslib "^2.4.0"
 
+"@emotion/babel-plugin@^11.13.5":
+  version "11.13.5"
+  resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0"
+  integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==
+  dependencies:
+    "@babel/helper-module-imports" "^7.16.7"
+    "@babel/runtime" "^7.18.3"
+    "@emotion/hash" "^0.9.2"
+    "@emotion/memoize" "^0.9.0"
+    "@emotion/serialize" "^1.3.3"
+    babel-plugin-macros "^3.1.0"
+    convert-source-map "^1.5.0"
+    escape-string-regexp "^4.0.0"
+    find-root "^1.1.0"
+    source-map "^0.5.7"
+    stylis "4.2.0"
+
+"@emotion/cache@^11.13.5":
+  version "11.14.0"
+  resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76"
+  integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==
+  dependencies:
+    "@emotion/memoize" "^0.9.0"
+    "@emotion/sheet" "^1.4.0"
+    "@emotion/utils" "^1.4.2"
+    "@emotion/weak-memoize" "^0.4.0"
+    stylis "4.2.0"
+
+"@emotion/css@^11.7.1":
+  version "11.13.5"
+  resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.5.tgz#db2d3be6780293640c082848e728a50544b9dfa4"
+  integrity sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==
+  dependencies:
+    "@emotion/babel-plugin" "^11.13.5"
+    "@emotion/cache" "^11.13.5"
+    "@emotion/serialize" "^1.3.3"
+    "@emotion/sheet" "^1.4.0"
+    "@emotion/utils" "^1.4.2"
+
+"@emotion/hash@^0.9.2":
+  version "0.9.2"
+  resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b"
+  integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==
+
+"@emotion/memoize@^0.9.0":
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102"
+  integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
+
+"@emotion/serialize@^1.3.3":
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.3.tgz#d291531005f17d704d0463a032fe679f376509e8"
+  integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==
+  dependencies:
+    "@emotion/hash" "^0.9.2"
+    "@emotion/memoize" "^0.9.0"
+    "@emotion/unitless" "^0.10.0"
+    "@emotion/utils" "^1.4.2"
+    csstype "^3.0.2"
+
+"@emotion/sheet@^1.4.0":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c"
+  integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
+
+"@emotion/unitless@^0.10.0":
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745"
+  integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==
+
+"@emotion/utils@^1.4.2":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52"
+  integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==
+
+"@emotion/weak-memoize@^0.4.0":
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6"
+  integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
+
 "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.8.0":
   version "4.9.1"
   resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
@@ -198,7 +338,7 @@
   dependencies:
     "@swc/helpers" "^0.5.0"
 
-"@jridgewell/gen-mapping@^0.3.5":
+"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
   version "0.3.13"
   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
   integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
@@ -224,7 +364,7 @@
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
   integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
 
-"@jridgewell/trace-mapping@^0.3.24":
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
   version "0.3.31"
   resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
   integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
@@ -232,11 +372,66 @@
     "@jridgewell/resolve-uri" "^3.1.0"
     "@jridgewell/sourcemap-codec" "^1.4.14"
 
+"@juggle/resize-observer@^3.3.1":
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
+  integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
+
 "@lucide/vue@^1.18.0":
   version "1.18.0"
   resolved "https://registry.yarnpkg.com/@lucide/vue/-/vue-1.18.0.tgz#57bcc6cfe4f13c203a827f3794baec3f11fbf732"
   integrity sha512-DmnUpDB85PlMZ+ofjZLcKq3JoJnaD1bk7SIj9xwUvqerfNqA6hCLa0/m3gIybH6rdrErABbqvTD8yYJdNqiZ3Q==
 
+"@mapbox/geojson-rewind@^0.5.2":
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz#591a5d71a9cd1da1a0bf3420b3bea31b0fc7946a"
+  integrity sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==
+  dependencies:
+    get-stream "^6.0.1"
+    minimist "^1.2.6"
+
+"@mapbox/jsonlint-lines-primitives@^2.0.2":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234"
+  integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==
+
+"@mapbox/mapbox-gl-supported@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz#c15367178d8bfe4765e6b47b542fe821ce259c7b"
+  integrity sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==
+
+"@mapbox/point-geometry@*":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz#3328fb54b3a1273bc619bf0a6baad8de37181749"
+  integrity sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==
+
+"@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2"
+  integrity sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==
+
+"@mapbox/tiny-sdf@^2.0.5":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz#95dae0dc371b8c55c53d70f3e25d56823427f4c8"
+  integrity sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==
+
+"@mapbox/unitbezier@^0.0.1":
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz#d32deb66c7177e9e9dfc3bbd697083e2e657ff01"
+  integrity sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==
+
+"@mapbox/vector-tile@^1.3.1":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz#d3a74c90402d06e89ec66de49ec817ff53409666"
+  integrity sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==
+  dependencies:
+    "@mapbox/point-geometry" "~0.1.0"
+
+"@mapbox/whoots-js@^3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz#497c67a1cef50d1a2459ba60f315e448d2ad87fe"
+  integrity sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==
+
 "@napi-rs/wasm-runtime@^1.1.4":
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz#cccd6ebc40b991dea6936f9126b1b8155b6c4c95"
@@ -483,6 +678,245 @@
   dependencies:
     tslib "^2.4.0"
 
+"@types/d3-array@*", "@types/d3-array@~3.2.2":
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c"
+  integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==
+
+"@types/d3-axis@*", "@types/d3-axis@~3.0.6":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795"
+  integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-brush@*", "@types/d3-brush@~3.0.6":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c"
+  integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-chord@*", "@types/d3-chord@~3.0.6":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d"
+  integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==
+
+"@types/d3-collection@^1.0.10":
+  version "1.0.13"
+  resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.13.tgz#4bd051f698515963a9a7c925c04aaf0acd3902f8"
+  integrity sha512-v0Rgw3IZebRyamcwVmtTDCZ8OmQcj4siaYjNc7wGMZT7PmdSHawGsCOQMxyLvZ7lWjfohYLK0oXtilMOMgfY8A==
+
+"@types/d3-color@*", "@types/d3-color@~3.1.3":
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+  integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-contour@*":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231"
+  integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==
+  dependencies:
+    "@types/d3-array" "*"
+    "@types/geojson" "*"
+
+"@types/d3-delaunay@*":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1"
+  integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==
+
+"@types/d3-dispatch@*":
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz#ef004d8a128046cfce434d17182f834e44ef95b2"
+  integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==
+
+"@types/d3-drag@*", "@types/d3-drag@~3.0.7":
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
+  integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-dsv@*":
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17"
+  integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==
+
+"@types/d3-ease@*", "@types/d3-ease@~3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+  integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-fetch@*":
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980"
+  integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==
+  dependencies:
+    "@types/d3-dsv" "*"
+
+"@types/d3-force@*", "@types/d3-force@~3.0.10":
+  version "3.0.10"
+  resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a"
+  integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==
+
+"@types/d3-format@*":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90"
+  integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==
+
+"@types/d3-geo@*", "@types/d3-geo@~3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440"
+  integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/d3-hierarchy@*", "@types/d3-hierarchy@~3.1.7":
+  version "3.1.7"
+  resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b"
+  integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==
+
+"@types/d3-interpolate@*", "@types/d3-interpolate@~3.0.4":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+  integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+  dependencies:
+    "@types/d3-color" "*"
+
+"@types/d3-path@*", "@types/d3-path@~3.1.1":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a"
+  integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==
+
+"@types/d3-path@^1":
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.11.tgz#45420fee2d93387083b34eae4fe6d996edf482bc"
+  integrity sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==
+
+"@types/d3-polygon@*":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c"
+  integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==
+
+"@types/d3-quadtree@*":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f"
+  integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==
+
+"@types/d3-random@*":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb"
+  integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==
+
+"@types/d3-sankey@^0.12.4":
+  version "0.12.5"
+  resolved "https://registry.yarnpkg.com/@types/d3-sankey/-/d3-sankey-0.12.5.tgz#9b79ff5768250cfd8c6340b167c409a0374f40bd"
+  integrity sha512-/3RZSew0cLAtzGQ+C89hq/Rp3H20QJuVRSqFy6RKLe7E0B8kd2iOS1oBsodrgds4PcNVpqWhdUEng/SHvBcJ6Q==
+  dependencies:
+    "@types/d3-shape" "^1"
+
+"@types/d3-scale-chromatic@*":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39"
+  integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==
+
+"@types/d3-scale@*", "@types/d3-scale@~4.0.9":
+  version "4.0.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb"
+  integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==
+  dependencies:
+    "@types/d3-time" "*"
+
+"@types/d3-selection@*", "@types/d3-selection@~3.0.0":
+  version "3.0.11"
+  resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3"
+  integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==
+
+"@types/d3-shape@*", "@types/d3-shape@~3.1.7":
+  version "3.1.8"
+  resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.8.tgz#d1516cc508753be06852cd06758e3bb54a22b0e3"
+  integrity sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==
+  dependencies:
+    "@types/d3-path" "*"
+
+"@types/d3-shape@^1":
+  version "1.3.12"
+  resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.12.tgz#8f2f9f7a12e631ce6700d6d55b84795ce2c8b259"
+  integrity sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==
+  dependencies:
+    "@types/d3-path" "^1"
+
+"@types/d3-time-format@*":
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2"
+  integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==
+
+"@types/d3-time@*":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f"
+  integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
+
+"@types/d3-timer@*", "@types/d3-timer@~3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+  integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
+"@types/d3-transition@*", "@types/d3-transition@~3.0.9":
+  version "3.0.9"
+  resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706"
+  integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-zoom@*", "@types/d3-zoom@~3.0.8":
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
+  integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
+  dependencies:
+    "@types/d3-interpolate" "*"
+    "@types/d3-selection" "*"
+
+"@types/d3@^7.4.0":
+  version "7.4.3"
+  resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2"
+  integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==
+  dependencies:
+    "@types/d3-array" "*"
+    "@types/d3-axis" "*"
+    "@types/d3-brush" "*"
+    "@types/d3-chord" "*"
+    "@types/d3-color" "*"
+    "@types/d3-contour" "*"
+    "@types/d3-delaunay" "*"
+    "@types/d3-dispatch" "*"
+    "@types/d3-drag" "*"
+    "@types/d3-dsv" "*"
+    "@types/d3-ease" "*"
+    "@types/d3-fetch" "*"
+    "@types/d3-force" "*"
+    "@types/d3-format" "*"
+    "@types/d3-geo" "*"
+    "@types/d3-hierarchy" "*"
+    "@types/d3-interpolate" "*"
+    "@types/d3-path" "*"
+    "@types/d3-polygon" "*"
+    "@types/d3-quadtree" "*"
+    "@types/d3-random" "*"
+    "@types/d3-scale" "*"
+    "@types/d3-scale-chromatic" "*"
+    "@types/d3-selection" "*"
+    "@types/d3-shape" "*"
+    "@types/d3-time" "*"
+    "@types/d3-time-format" "*"
+    "@types/d3-timer" "*"
+    "@types/d3-transition" "*"
+    "@types/d3-zoom" "*"
+
+"@types/dagre@^0.7.50":
+  version "0.7.54"
+  resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.54.tgz#4f3be2a3f89938b0483013739362a477ec978a84"
+  integrity sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==
+
 "@types/esrecurse@^4.3.1":
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec"
@@ -493,21 +927,217 @@
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24"
   integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==
 
+"@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.8":
+  version "7946.0.16"
+  resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a"
+  integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==
+
 "@types/json-schema@^7.0.15":
   version "7.0.15"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
 
+"@types/leaflet@1.7.6":
+  version "1.7.6"
+  resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.6.tgz#6580f4babb648972c5af3abc3d66866753fa9311"
+  integrity sha512-Emkz3V08QnlelSbpT46OEAx+TBZYTOX2r1yM7W+hWg5+djHtQ1GbEXBDRLaqQDOYcDI51Ss0ayoqoKD4CtLUDA==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/mapbox__point-geometry@*":
+  version "1.0.87"
+  resolved "https://registry.yarnpkg.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-1.0.87.tgz#164ba7a9f2cd899e7fa5086f6bb6abd158241fdd"
+  integrity sha512-32whuSWD/aDBHpRrZmEMlRjxR0L3sSYUaRnVmw0XwyVMvuU5LsRqrFwYud7gXjqmUE0MSoJB91+IJMqCyYyxag==
+  dependencies:
+    "@mapbox/point-geometry" "*"
+
+"@types/mapbox__point-geometry@^0.1.2":
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz#0ef017b75eedce02ff6243b4189210e2e6d5e56d"
+  integrity sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==
+
+"@types/mapbox__vector-tile@^1.3.0":
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz#ad757441ef1d34628d9e098afd9c91423c1f8734"
+  integrity sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==
+  dependencies:
+    "@types/geojson" "*"
+    "@types/mapbox__point-geometry" "*"
+    "@types/pbf" "*"
+
+"@types/parse-json@^4.0.0":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
+  integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==
+
+"@types/pbf@*", "@types/pbf@^3.0.2":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404"
+  integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==
+
 "@types/retry@0.12.2":
   version "0.12.2"
   resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a"
   integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==
 
+"@types/supercluster@^5.0.2":
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@types/supercluster/-/supercluster-5.0.3.tgz#aa03a77c6545265e63b50fa267ab12afe0c27658"
+  integrity sha512-XMSqQEr7YDuNtFwSgaHHOjsbi0ZGL62V9Js4CW45RBuRYlNWSW/KDqN+RFFE7HdHcGhJPtN0klKvw06r9Kg7rg==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/three@^0.135.0":
+  version "0.135.0"
+  resolved "https://registry.yarnpkg.com/@types/three/-/three-0.135.0.tgz#db7f9d9699b60aba844fc5b2893adf6dd6811665"
+  integrity sha512-l7WLhIHjhHMtlpyTSltPPAKLpiMwgMD1hXHj59AVUpYRoZP7Fd9NNOSRSvZBCPLpTHPYojgQvSJCoza9zoL7bg==
+
+"@types/throttle-debounce@^5.0.0":
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-5.0.2.tgz#3489d91673a4be830c2c9e2acf1f6cdab724102c"
+  integrity sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==
+
+"@types/topojson-client@*", "@types/topojson-client@^3.0.0":
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/@types/topojson-client/-/topojson-client-3.1.5.tgz#3fdbcd52161db8747a071b1d0f635ac700cf599d"
+  integrity sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==
+  dependencies:
+    "@types/geojson" "*"
+    "@types/topojson-specification" "*"
+
+"@types/topojson-server@*":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/topojson-server/-/topojson-server-3.0.4.tgz#7a18bfa740d95ad3ce57a68f4e662fa8dc3965ca"
+  integrity sha512-5+ieK8ePfP+K2VH6Vgs1VCt+fO1U8XZHj0UsF+NktaF0DavAo1q3IvCBXgokk/xmtvoPltSUs6vxuR/zMdOE1g==
+  dependencies:
+    "@types/geojson" "*"
+    "@types/topojson-specification" "*"
+
+"@types/topojson-simplify@*":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/topojson-simplify/-/topojson-simplify-3.0.3.tgz#03013bde7b3eb47cac98b74ecb2a739927087415"
+  integrity sha512-sBO5UZ0O2dB0bNwo0vut2yLHhj3neUGi9uL7/ROdm8Gs6dtt4jcB9OGDKr+M2isZwQM2RuzVmifnMZpxj4IGNw==
+  dependencies:
+    "@types/geojson" "*"
+    "@types/topojson-specification" "*"
+
+"@types/topojson-specification@*", "@types/topojson-specification@^1.0.2":
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@types/topojson-specification/-/topojson-specification-1.0.5.tgz#bf0009b2e0debb2d97237b124c00b9ea92570375"
+  integrity sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/topojson@^3.2.3":
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/@types/topojson/-/topojson-3.2.6.tgz#8ca07f8e71b0d6f978bc2b1f0a1c745adc0b52cd"
+  integrity sha512-ppfdlxjxofWJ66XdLgIlER/85RvpGyfOf8jrWf+3kVIjEatFxEZYD/Ea83jO672Xu1HRzd/ghwlbcZIUNHTskw==
+  dependencies:
+    "@types/geojson" "*"
+    "@types/topojson-client" "*"
+    "@types/topojson-server" "*"
+    "@types/topojson-simplify" "*"
+    "@types/topojson-specification" "*"
+
 "@types/web-bluetooth@^0.0.21":
   version "0.0.21"
   resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
   integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
 
+"@unovis/dagre-layout@0.8.8-2":
+  version "0.8.8-2"
+  resolved "https://registry.yarnpkg.com/@unovis/dagre-layout/-/dagre-layout-0.8.8-2.tgz#0a4905dc062398927d11facbe1224da5311e070d"
+  integrity sha512-ZfDvfcYtzzhZhgKZty8XDi+zQIotfRqfNVF5M3dFQ9d9C5MTaRdbeBnPUkNrmlLJGgQ42HMOE2ajZLfm2VlRhg==
+  dependencies:
+    "@unovis/graphlibrary" "^2.2.0-2"
+    lodash-es "^4.17.21"
+
+"@unovis/graphlibrary@2.2.0-2", "@unovis/graphlibrary@^2.2.0-2":
+  version "2.2.0-2"
+  resolved "https://registry.yarnpkg.com/@unovis/graphlibrary/-/graphlibrary-2.2.0-2.tgz#fa18dead78745a94ea21f23aa4eeee95785954d1"
+  integrity sha512-HeEzpd/vDyWiIJt0rnh+2ICXUIuF2N0+Z9OJJiKg0DB+eFUcD+bk+9QPhYHwkFwfxdjDA9fHi1DZ/O/bbV58Nw==
+  dependencies:
+    lodash-es "^4.17.21"
+
+"@unovis/ts@^1.6.5":
+  version "1.6.5"
+  resolved "https://registry.yarnpkg.com/@unovis/ts/-/ts-1.6.5.tgz#69fb1ebf12bfabb7d0fb1f5024bf1b0fb8957959"
+  integrity sha512-4qeo0nKESrCX7vZr+zEGFjg2QbyjgxXN8//mPnvZi0e2mf+ZrjIRmHReHwO3u7fqMD7kgMMMD1ABJJFBcOempA==
+  dependencies:
+    "@emotion/css" "^11.7.1"
+    "@juggle/resize-observer" "^3.3.1"
+    "@types/d3" "^7.4.0"
+    "@types/d3-array" "~3.2.2"
+    "@types/d3-axis" "~3.0.6"
+    "@types/d3-brush" "~3.0.6"
+    "@types/d3-chord" "~3.0.6"
+    "@types/d3-collection" "^1.0.10"
+    "@types/d3-color" "~3.1.3"
+    "@types/d3-drag" "~3.0.7"
+    "@types/d3-ease" "~3.0.2"
+    "@types/d3-force" "~3.0.10"
+    "@types/d3-geo" "~3.1.0"
+    "@types/d3-hierarchy" "~3.1.7"
+    "@types/d3-interpolate" "~3.0.4"
+    "@types/d3-path" "~3.1.1"
+    "@types/d3-sankey" "^0.12.4"
+    "@types/d3-scale" "~4.0.9"
+    "@types/d3-selection" "~3.0.0"
+    "@types/d3-shape" "~3.1.7"
+    "@types/d3-timer" "~3.0.2"
+    "@types/d3-transition" "~3.0.9"
+    "@types/d3-zoom" "~3.0.8"
+    "@types/dagre" "^0.7.50"
+    "@types/geojson" "^7946.0.8"
+    "@types/leaflet" "1.7.6"
+    "@types/supercluster" "^5.0.2"
+    "@types/three" "^0.135.0"
+    "@types/throttle-debounce" "^5.0.0"
+    "@types/topojson" "^3.2.3"
+    "@types/topojson-client" "^3.0.0"
+    "@types/topojson-specification" "^1.0.2"
+    "@unovis/dagre-layout" "0.8.8-2"
+    "@unovis/graphlibrary" "2.2.0-2"
+    d3 "^7.2.1"
+    d3-array "~3"
+    d3-axis "~3"
+    d3-brush "~3"
+    d3-chord "~3"
+    d3-collection "^1.0.7"
+    d3-color "~3"
+    d3-drag "~3"
+    d3-ease "~3"
+    d3-force "~3"
+    d3-geo "~3"
+    d3-geo-projection "^4.0.0"
+    d3-hierarchy "~3"
+    d3-interpolate "~3"
+    d3-interpolate-path "^2.2.3"
+    d3-path "~3"
+    d3-sankey "^0.12.3"
+    d3-scale "~4"
+    d3-selection "~3"
+    d3-shape "~3"
+    d3-timer "~3"
+    d3-transition "~3"
+    d3-zoom "~3"
+    elkjs "^0.10.0"
+    geojson "^0.5.0"
+    leaflet "1.7.1"
+    maplibre-gl "^2.1.9"
+    striptags "^3.2.0"
+    supercluster "^7.1.5"
+    three "^0.135.0"
+    throttle-debounce "^5.0.0"
+    topojson-client "^3.1.0"
+    tslib "^2.3.1"
+    typescript "~4.2.4"
+
+"@unovis/vue@^1.6.5":
+  version "1.6.5"
+  resolved "https://registry.yarnpkg.com/@unovis/vue/-/vue-1.6.5.tgz#61f41c1ff378d173b7b2ac5297bcb86729e3fe67"
+  integrity sha512-BSyEeuenhSCqo0kox4xe3bWMDUn0gfF4haggrVhhNWcp6pDmctniDebOP/8AFKlhPhfGJ6b39MdvoQNhXwc46A==
+
 "@uppy/companion-client@^5.1.1":
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-5.1.1.tgz#5bfeda3312ba2b70e94d4fd73a31d0f7757884c0"
@@ -712,6 +1342,15 @@ aria-hidden@^1.2.4:
   dependencies:
     tslib "^2.0.0"
 
+babel-plugin-macros@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
+  integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    cosmiconfig "^7.0.0"
+    resolve "^1.19.0"
+
 balanced-match@^4.0.2:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
@@ -734,6 +1373,11 @@ buffer-from@^1.1.2:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
   integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
 class-variance-authority@^0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787"
@@ -759,6 +1403,32 @@ combine-errors@^3.0.3:
     custom-error-instance "2.1.1"
     lodash.uniqby "4.5.0"
 
+commander@2:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@7:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+convert-source-map@^1.5.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
+  integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+
+cosmiconfig@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
+  integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.2.1"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.10.0"
+
 cross-spawn@^7.0.6:
   version "7.0.6"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -768,12 +1438,17 @@ cross-spawn@^7.0.6:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+csscolorparser@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b"
+  integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==
+
 cssesc@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
-csstype@^3.2.3:
+csstype@^3.0.2, csstype@^3.2.3:
   version "3.2.3"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
   integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
@@ -783,6 +1458,296 @@ custom-error-instance@2.1.1:
   resolved "https://registry.yarnpkg.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz#3cf6391487a6629a6247eb0ca0ce00081b7e361a"
   integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==
 
+"d3-array@1 - 2":
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
+  integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
+  dependencies:
+    internmap "^1.0.0"
+
+"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0, d3-array@~3:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5"
+  integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+  dependencies:
+    internmap "1 - 2"
+
+d3-axis@3, d3-axis@~3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
+  integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3, d3-brush@~3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
+  integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "3"
+    d3-transition "3"
+
+d3-chord@3, d3-chord@~3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
+  integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+  dependencies:
+    d3-path "1 - 3"
+
+d3-collection@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
+  integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
+
+"d3-color@1 - 3", d3-color@3, d3-color@~3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+  integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
+  integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+  dependencies:
+    d3-array "^3.2.0"
+
+d3-delaunay@6:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
+  integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
+  dependencies:
+    delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+  integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3, d3-drag@~3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+  integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
+  integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+  dependencies:
+    commander "7"
+    iconv-lite "0.6"
+    rw "1"
+
+"d3-ease@1 - 3", d3-ease@3, d3-ease@~3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+  integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
+  integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+  dependencies:
+    d3-dsv "1 - 3"
+
+d3-force@3, d3-force@~3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+  integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-quadtree "1 - 3"
+    d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.2.tgz#01fdb46b58beb1f55b10b42ad70b6e344d5eb2ae"
+  integrity sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==
+
+d3-geo-projection@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz#dc229e5ead78d31869a4e87cf1f45bd2716c48ca"
+  integrity sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==
+  dependencies:
+    commander "7"
+    d3-array "1 - 3"
+    d3-geo "1.12.0 - 3"
+
+"d3-geo@1.12.0 - 3", d3-geo@3, d3-geo@~3:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d"
+  integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==
+  dependencies:
+    d3-array "2.5.0 - 3"
+
+d3-hierarchy@3, d3-hierarchy@~3:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
+  integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+d3-interpolate-path@^2.2.3:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/d3-interpolate-path/-/d3-interpolate-path-2.3.0.tgz#ff919acb52968619f4b0dc5fa95653e581d5a0ca"
+  integrity sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ==
+
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3, d3-interpolate@~3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+  integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+  dependencies:
+    d3-color "1 - 3"
+
+d3-path@1:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
+  integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0, d3-path@~3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
+  integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-polygon@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
+  integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+  integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
+  integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-sankey@^0.12.3:
+  version "0.12.3"
+  resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d"
+  integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==
+  dependencies:
+    d3-array "1 - 2"
+    d3-shape "^1.2.0"
+
+d3-scale-chromatic@3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#34c39da298b23c20e02f1a4b239bd0f22e7f1314"
+  integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==
+  dependencies:
+    d3-color "1 - 3"
+    d3-interpolate "1 - 3"
+
+d3-scale@4, d3-scale@~4:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+  integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+  dependencies:
+    d3-array "2.10.0 - 3"
+    d3-format "1 - 3"
+    d3-interpolate "1.2.0 - 3"
+    d3-time "2.1.1 - 3"
+    d3-time-format "2 - 4"
+
+"d3-selection@2 - 3", d3-selection@3, d3-selection@~3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+  integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@3, d3-shape@~3:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
+  integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+  dependencies:
+    d3-path "^3.1.0"
+
+d3-shape@^1.2.0:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
+  integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+  dependencies:
+    d3-path "1"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+  integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+  dependencies:
+    d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
+  integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+  dependencies:
+    d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3, d3-timer@~3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+  integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3", d3-transition@3, d3-transition@~3:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+  integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+  dependencies:
+    d3-color "1 - 3"
+    d3-dispatch "1 - 3"
+    d3-ease "1 - 3"
+    d3-interpolate "1 - 3"
+    d3-timer "1 - 3"
+
+d3-zoom@3, d3-zoom@~3:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+  integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-drag "2 - 3"
+    d3-interpolate "1 - 3"
+    d3-selection "2 - 3"
+    d3-transition "2 - 3"
+
+d3@^7.2.1:
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d"
+  integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==
+  dependencies:
+    d3-array "3"
+    d3-axis "3"
+    d3-brush "3"
+    d3-chord "3"
+    d3-color "3"
+    d3-contour "4"
+    d3-delaunay "6"
+    d3-dispatch "3"
+    d3-drag "3"
+    d3-dsv "3"
+    d3-ease "3"
+    d3-fetch "3"
+    d3-force "3"
+    d3-format "3"
+    d3-geo "3"
+    d3-hierarchy "3"
+    d3-interpolate "3"
+    d3-path "3"
+    d3-polygon "3"
+    d3-quadtree "3"
+    d3-random "3"
+    d3-scale "4"
+    d3-scale-chromatic "3"
+    d3-selection "3"
+    d3-shape "3"
+    d3-time "3"
+    d3-time-format "4"
+    d3-timer "3"
+    d3-transition "3"
+    d3-zoom "3"
+
 debug@^4.3.1, debug@^4.3.2, debug@^4.4.0:
   version "4.4.3"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
@@ -800,6 +1765,13 @@ defu@^6.1.5:
   resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.7.tgz#72543567c8e9f97ff13ce402b6dbe09ac5ae4d23"
   integrity sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==
 
+delaunator@5:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.1.0.tgz#d13271fbf3aff6753f9ea6e235557f20901046ea"
+  integrity sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==
+  dependencies:
+    robust-predicates "^3.0.2"
+
 detect-libc@^2.0.3:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
@@ -809,6 +1781,16 @@ detect-libc@^2.0.3:
   version "0.0.0"
   uid ""
 
+earcut@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a"
+  integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==
+
+elkjs@^0.10.0:
+  version "0.10.2"
+  resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.10.2.tgz#7b95dbe5d41a5971b2eb13e54201855799389a43"
+  integrity sha512-Yx3ORtbAFrXelYkAy2g0eYyVY8QG0XEmGdQXmy0eithKKjbWRfl3Xe884lfkszfBF6UKyIy4LwfcZ3AZc8oxFw==
+
 enhanced-resolve@5.21.6:
   version "5.21.6"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz#aa207b43cf658e6ab3ba06896edc00c13c3127c6"
@@ -822,6 +1804,13 @@ entities@^7.0.1:
   resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
   integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
 
+error-ex@^1.3.1:
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414"
+  integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==
+  dependencies:
+    is-arrayish "^0.2.1"
+
 error-stack-parser@^2.0.6:
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286"
@@ -829,6 +1818,11 @@ error-stack-parser@^2.0.6:
   dependencies:
     stackframe "^1.3.4"
 
+es-errors@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+  integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
 escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -967,6 +1961,11 @@ file-entry-cache@^8.0.0:
   dependencies:
     flat-cache "^4.0.0"
 
+find-root@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
 find-up@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
@@ -993,6 +1992,31 @@ fsevents@~2.3.3:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
   integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
 
+function-bind@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+geojson-vt@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/geojson-vt/-/geojson-vt-3.2.1.tgz#f8adb614d2c1d3f6ee7c4265cad4bbf3ad60c8b7"
+  integrity sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==
+
+geojson@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0"
+  integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==
+
+get-stream@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+  integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
+gl-matrix@^3.4.3:
+  version "3.4.4"
+  resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.4.tgz#7789ee4982f62c7a7af447ee488f3bd6b0c77003"
+  integrity sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==
+
 glob-parent@^6.0.2:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
@@ -1000,6 +2024,15 @@ glob-parent@^6.0.2:
   dependencies:
     is-glob "^4.0.3"
 
+global-prefix@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
+  integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
+  dependencies:
+    ini "^1.3.5"
+    kind-of "^6.0.2"
+    which "^1.3.1"
+
 globals@^17.6.0:
   version "17.6.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-17.6.0.tgz#0f0be018d5cca8690e6375ead1f65c4bb96191fc"
@@ -1010,16 +2043,70 @@ graceful-fs@^4.2.4:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
   integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
 
+hasown@^2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003"
+  integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==
+  dependencies:
+    function-bind "^1.1.2"
+
+iconv-lite@0.6:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
+ieee754@^1.1.12:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
 ignore@^5.2.0:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
   integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
 
+import-fresh@^3.2.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
+  integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
 imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
 
+ini@^1.3.5:
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
+"internmap@1 - 2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+  integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
+internmap@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
+  integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-core-module@^2.16.1:
+  version "2.16.2"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082"
+  integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==
+  dependencies:
+    hasown "^2.0.3"
+
 is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -1057,11 +2144,26 @@ js-base64@^3.7.2:
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.8.tgz#af44496bc09fa178ed9c4adf67eb2b46f5c6d2a4"
   integrity sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==
 
+js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+jsesc@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
+  integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
+
 json-buffer@3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
   integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
 
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -1072,6 +2174,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
   resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
   integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
 
+kdbush@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0"
+  integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==
+
 keyv@^4.5.4:
   version "4.5.4"
   resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -1079,6 +2186,16 @@ keyv@^4.5.4:
   dependencies:
     json-buffer "3.0.1"
 
+kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+leaflet@1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
+  integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
+
 levn@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -1161,6 +2278,11 @@ lightningcss@1.32.0, lightningcss@^1.32.0:
     lightningcss-win32-arm64-msvc "1.32.0"
     lightningcss-win32-x64-msvc "1.32.0"
 
+lines-and-columns@^1.1.6:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
 linkifyjs@^4.3.3:
   version "4.3.3"
   resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.3.tgz#da08f0eeb4d89a24541d09591fbdcc211eb8fef0"
@@ -1173,6 +2295,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash-es@^4.17.21:
+  version "4.18.1"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
+  integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
+
 lodash._baseiteratee@~4.7.0:
   version "4.7.0"
   resolved "https://registry.yarnpkg.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz#34a9b5543572727c3db2e78edae3c0e9e66bd102"
@@ -1235,6 +2362,36 @@ magic-string@^0.30.21:
   dependencies:
     "@jridgewell/sourcemap-codec" "^1.5.5"
 
+maplibre-gl@^2.1.9:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-2.4.0.tgz#2b53dbf526626bf4ee92ad4f33f13ef09e5af182"
+  integrity sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==
+  dependencies:
+    "@mapbox/geojson-rewind" "^0.5.2"
+    "@mapbox/jsonlint-lines-primitives" "^2.0.2"
+    "@mapbox/mapbox-gl-supported" "^2.0.1"
+    "@mapbox/point-geometry" "^0.1.0"
+    "@mapbox/tiny-sdf" "^2.0.5"
+    "@mapbox/unitbezier" "^0.0.1"
+    "@mapbox/vector-tile" "^1.3.1"
+    "@mapbox/whoots-js" "^3.1.0"
+    "@types/geojson" "^7946.0.10"
+    "@types/mapbox__point-geometry" "^0.1.2"
+    "@types/mapbox__vector-tile" "^1.3.0"
+    "@types/pbf" "^3.0.2"
+    csscolorparser "~1.0.3"
+    earcut "^2.2.4"
+    geojson-vt "^3.2.1"
+    gl-matrix "^3.4.3"
+    global-prefix "^3.0.0"
+    murmurhash-js "^1.0.0"
+    pbf "^3.2.1"
+    potpack "^1.0.2"
+    quickselect "^2.0.0"
+    supercluster "^7.1.5"
+    tinyqueue "^2.0.3"
+    vt-pbf "^3.1.3"
+
 mime-match@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8"
@@ -1249,6 +2406,11 @@ minimatch@^10.2.4:
   dependencies:
     brace-expansion "^5.0.5"
 
+minimist@^1.2.6:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
 moment@^2.30.1:
   version "2.30.1"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
@@ -1259,6 +2421,11 @@ ms@^2.1.3:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
+murmurhash-js@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51"
+  integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
+
 namespace-emitter@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c"
@@ -1326,6 +2493,23 @@ p-retry@^6.1.0:
     is-network-error "^1.0.0"
     retry "^0.13.1"
 
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
 path-exists@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -1336,6 +2520,24 @@ path-key@^3.1.0:
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
   integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+pbf@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.3.0.tgz#1790f3d99118333cc7f498de816028a346ef367f"
+  integrity sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==
+  dependencies:
+    ieee754 "^1.1.12"
+    resolve-protobuf-schema "^2.1.0"
+
 picocolors@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -1363,6 +2565,11 @@ postcss@^8.5.15:
     picocolors "^1.1.1"
     source-map-js "^1.2.1"
 
+potpack@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14"
+  integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==
+
 preact@^10.26.10, preact@^10.5.13:
   version "10.29.2"
   resolved "https://registry.yarnpkg.com/preact/-/preact-10.29.2.tgz#3e6069c471718b8d124d1cd67565114532e88d70"
@@ -1387,6 +2594,11 @@ proper-lockfile@^4.1.2:
     retry "^0.12.0"
     signal-exit "^3.0.2"
 
+protocol-buffers-schema@^3.3.1:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz#fd9a58a5c4e96385b964808f3ddd58f9ef18c3c8"
+  integrity sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==
+
 punycode@^2.1.0:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@@ -1397,6 +2609,11 @@ querystringify@^2.1.1:
   resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
   integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
 
+quickselect@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018"
+  integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==
+
 reka-ui@^2.9.10:
   version "2.9.10"
   resolved "https://registry.yarnpkg.com/reka-ui/-/reka-ui-2.9.10.tgz#bc9766dfac7c81dd413098c3d712f0f8f68553ff"
@@ -1418,6 +2635,28 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
 
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-protobuf-schema@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758"
+  integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==
+  dependencies:
+    protocol-buffers-schema "^3.3.1"
+
+resolve@^1.19.0:
+  version "1.22.12"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f"
+  integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==
+  dependencies:
+    es-errors "^1.3.0"
+    is-core-module "^2.16.1"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
 retry@^0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
@@ -1428,6 +2667,11 @@ retry@^0.13.1:
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
   integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
 
+robust-predicates@^3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.3.tgz#1099061b3349e2c5abec6c2ab0acd440d24d4062"
+  integrity sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==
+
 rolldown@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac"
@@ -1452,6 +2696,16 @@ rolldown@1.0.3:
     "@rolldown/binding-win32-arm64-msvc" "1.0.3"
     "@rolldown/binding-win32-x64-msvc" "1.0.3"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+  integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
+"safer-buffer@>= 2.1.2 < 3.0.0":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
 semver@^7.6.3:
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.4.tgz#c73eceebae0616934be8dff28a7fd70757c8e696"
@@ -1484,6 +2738,11 @@ source-map@0.5.6:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
   integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==
 
+source-map@^0.5.7:
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+  integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
+
 stack-generator@^2.0.5:
   version "2.0.10"
   resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
@@ -1513,6 +2772,28 @@ stacktrace-js@^2.0.2:
     stack-generator "^2.0.5"
     stacktrace-gps "^3.0.4"
 
+striptags@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052"
+  integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==
+
+stylis@4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51"
+  integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
+
+supercluster@^7.1.5:
+  version "7.1.5"
+  resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3"
+  integrity sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==
+  dependencies:
+    kdbush "^3.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
 tailwind-merge@^3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.6.0.tgz#88d83242d1dd7bc847223f73dcf210dd1f2ee11c"
@@ -1528,6 +2809,16 @@ tapable@^2.3.3:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
   integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
 
+three@^0.135.0:
+  version "0.135.0"
+  resolved "https://registry.yarnpkg.com/three/-/three-0.135.0.tgz#cff4ec8edd3c51ce9bc7e88d13e8eafed4d0ddfc"
+  integrity sha512-kuEpuuxRzLv0MDsXai9huCxOSQPZ4vje6y0gn80SRmQvgz6/+rI0NAvCRAw56zYaWKMGMfqKWsxF9Qa2Z9xymQ==
+
+throttle-debounce@^5.0.0:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz#ec5549d84e053f043c9fd0f2a6dd892ff84456b1"
+  integrity sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==
+
 tinyglobby@^0.2.17:
   version "0.2.17"
   resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
@@ -1536,7 +2827,19 @@ tinyglobby@^0.2.17:
     fdir "^6.5.0"
     picomatch "^4.0.4"
 
-tslib@^2.0.0, tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1:
+tinyqueue@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
+  integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==
+
+topojson-client@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"
+  integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==
+  dependencies:
+    commander "2"
+
+tslib@^2.0.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1:
   version "2.8.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -1566,6 +2869,11 @@ type-check@^0.4.0, type-check@~0.4.0:
   dependencies:
     prelude-ls "^1.2.1"
 
+typescript@~4.2.4:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
+  integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
+
 uri-js@^4.2.2:
   version "4.4.1"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -1599,6 +2907,15 @@ vite@^8.0.16:
   optionalDependencies:
     fsevents "~2.3.3"
 
+vt-pbf@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac"
+  integrity sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==
+  dependencies:
+    "@mapbox/point-geometry" "0.1.0"
+    "@mapbox/vector-tile" "^1.3.1"
+    pbf "^3.2.1"
+
 vue-demi@>=0.13.0:
   version "0.14.10"
   resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
@@ -1627,6 +2944,13 @@ vue@^3.5.38:
     "@vue/server-renderer" "3.5.38"
     "@vue/shared" "3.5.38"
 
+which@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
 which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -1649,6 +2973,11 @@ xml-name-validator@^4.0.0:
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
   integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
 
+yaml@^1.10.0:
+  version "1.10.3"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3"
+  integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
+
 yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"