Query builder crashes when querying unmapped field
**Describe the bug**
If you try to hit count while one of the fields is unmapped. An error occurs. Moreover, the stats page is no longer viewable and needs to be refreshed.

**To Reproduce**
Steps to reproduce the behavior:
Note: This can be done in any collection.
1. Log in as `petrifiedroadkill` in` NHMD Test` Database.
2. Go to `NHDM Amber` -> Statistics. 
3. Go to any category and pick a field. 
4. Un map any value and press count.
5. See Error

**Expected behavior**
An alert dialog saying this is not allowed or returning to the main stats page.

**Screen Recording and Crash Report**

https://user-images.githubusercontent.com/53784701/230217194-957c79c7-eca8-44e6-ba19-4759fde1cf00.mov



[Specify 7 Crash Report - 2023-04-05T21_31_58.526Z (1).txt](https://github.com/specify/specify7/files/11163292/Specify.7.Crash.Report.-.2023-04-05T21_31_58.526Z.1.txt)




Desktop:

OS: Mac OSx Ventura
Browser: Chrome
Specify 7 Version: statspage branch
Database Name:
NHMD Test

Reported By
Carlos @carlosmbe
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b75499d90..3a393da95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,7 +27,15 @@ Coming in the next few months:
 
 ## [7.8.10](https://github.com/specify/specify7/compare/v7.8.9...HEAD) (Unreleased)
 
-- [Statistics page](https://discourse.specifysoftware.org/t/statistics-panel-for-specify-7/828)
+### Added
+
+- Add the statistics page allowing users to build custom queries and view counts. ([#501](https://github.com/specify/specify7/issues/501) - *Requested by SDSU, CSIRO, Cleveland Museum of Natural History*)
+- Add support for relative dates in full date queries (Queries using relative dates will not be visible in Specify 6 'Saved Queries'. By default, they will appear in 'Other Queries'. 
+- Add support for picking the Current User as name in queries that end with Specify User (Queries using Current User will not be Specify 6 'Saved Queries'. By default, they will appear in 'Other Queries'.
+
+### Fixed
+- Available resources for creating are now properly scoped
+- A minor visual glitch in Auto Complete Input Box has been fixed for slower machines
 
 ## [7.8.9](https://github.com/specify/specify7/compare/v7.8.8...v7.8.9) (4 April 2023)
 
diff --git a/specifyweb/context/collection_resources.py b/specifyweb/context/collection_resources.py
index 13d1a9e0d..31c8364a3 100644
--- a/specifyweb/context/collection_resources.py
+++ b/specifyweb/context/collection_resources.py
@@ -85,6 +85,8 @@ collection_resources = openapi(schema={
                 'spappresourcedir__usertype__isnull': True,
 }, _spappresourcefilterpost=lambda request: {
     'specifyuser': request.specify_user
+},_spappresourcedircreate=lambda request:{
+    'ispersonal': False
 }))
 
 
diff --git a/specifyweb/context/resources.py b/specifyweb/context/resources.py
index 82fe9eb69..1ea130d3a 100644
--- a/specifyweb/context/resources.py
+++ b/specifyweb/context/resources.py
@@ -13,6 +13,7 @@ class Resources(View):
     _spappresourcefilter = None
     _spappresourcedirfilter = None
     _spappresourcefilterpost = None
+    _spappresourcedircreate = None
     def get(self, request) -> http.HttpResponse:
         resources = Spappresource.objects.filter(
             spappresourcedir__collection=request.specify_collection,
@@ -30,12 +31,31 @@ class Resources(View):
 
     def post(self, request) -> http.HttpResponse:
         post_data = json.loads(request.body)
+        _spappresourcedir_base = {
+            'collection': request.specify_collection,
+            'discipline': request.specify_collection.discipline,
+        }
+        _spappresourcedir_filter = {
+            **_spappresourcedir_base,
+            **self._spappresourcedirfilter(request)
+        }
+
         with transaction.atomic():
-            directory, _ = Spappresourcedir.objects.get_or_create(
-                collection=request.specify_collection,
-                discipline=request.specify_collection.discipline,
-                **self._spappresourcedirfilter(request)
+            directories_matched = Spappresourcedir.objects.filter(
+                **_spappresourcedir_filter
             )
+
+            if len(directories_matched) == 0:
+                _spappresourcedir_create = {
+                    **_spappresourcedir_base,
+                    **self._spappresourcedircreate(request)
+                }
+                directory = Spappresourcedir.objects.create(
+                **_spappresourcedir_create
+                )
+            else:
+                directory = directories_matched[0]
+
             resource = Spappresource.objects.create(
                 spappresourcedir=directory,
                 level=0,
diff --git a/specifyweb/context/user_resources.py b/specifyweb/context/user_resources.py
index 13fea6d47..e071ec9a0 100644
--- a/specifyweb/context/user_resources.py
+++ b/specifyweb/context/user_resources.py
@@ -6,6 +6,11 @@ from specifyweb.context.resources import Resource, Resources
 Spappresource = getattr(models, 'Spappresource')
 Spappresourcedir = getattr(models, 'Spappresourcedir')
 
+user_resource_dir_filter_gen = lambda request: {
+                'specifyuser': request.specify_user,
+                'ispersonal': True,
+                'usertype': get_usertype(request.specify_user),
+}
 
 user_resources = openapi(schema={
     "get": {
@@ -75,16 +80,14 @@ user_resources = openapi(schema={
             }
         }
     }
-})(Resources.as_view(_spappresourcedirfilter= lambda request: {
-                'specifyuser': request.specify_user,
-                'ispersonal': True,
-                'usertype': get_usertype(request.specify_user),
-}, _spappresourcefilter= lambda request: {
+})(Resources.as_view(_spappresourcedirfilter= user_resource_dir_filter_gen,
+                     _spappresourcefilter= lambda request: {
                 'spappresourcedir__specifyuser': request.specify_user,
                 'spappresourcedir__ispersonal':True
 }, _spappresourcefilterpost=lambda request: {
                 'specifyuser': request.specify_user
-}))
+},_spappresourcedircreate=user_resource_dir_filter_gen
+                     ))
 
 
 user_resource = openapi(schema={
diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx
index 42e25a106..7c0000fe5 100644
--- a/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx
+++ b/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx
@@ -108,6 +108,9 @@ const UserPreferencesEditor: AppResourceTab = function ({
       userPreferences.setRaw(
         JSON.parse(data === null || data.length === 0 ? '{}' : data)
       );
+      userPreferences.events.on('update', () =>
+        handleChange(JSON.stringify(userPreferences.getRaw()))
+      );
       return userPreferences;
     }, [handleChange])
   );
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/CheckReadAccess.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/CheckReadAccess.tsx
index 358dec300..a37b0b19a 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/CheckReadAccess.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/CheckReadAccess.tsx
@@ -8,34 +8,16 @@ import type { SerializedResource } from '../DataModel/helperTypes';
 import type { SpQuery, Tables } from '../DataModel/types';
 import { Dialog } from '../Molecules/Dialog';
 import { TableIcon } from '../Molecules/TableIcon';
-import { hasTablePermission } from '../Permissions/helpers';
-import { QueryFieldSpec } from './fieldSpec';
+import { getNoAccessTables } from './helpers';
 
 export function CheckReadAccess({
   query,
 }: {
   readonly query: SerializedResource<SpQuery>;
 }): JSX.Element | null {
-  const [noAccessTables, setNoAccessTables] =
-    React.useState<RA<keyof Tables>>(getNoAccessTables);
-
-  function getNoAccessTables(): RA<keyof Tables> {
-    const tableNames = query.fields.flatMap((field) => {
-      const fieldSpec = QueryFieldSpec.fromStringId(
-        field.stringId,
-        field.isRelFld ?? false
-      );
-      return fieldSpec.joinPath.map((field) =>
-        field.isRelationship ? field.relatedModel.name : field.model.name
-      );
-    });
-
-    const withoutDuplicates = new Set(tableNames);
-
-    return Array.from(withoutDuplicates).filter(
-      (name) => !hasTablePermission(name, 'read')
-    );
-  }
+  const [noAccessTables, setNoAccessTables] = React.useState<RA<keyof Tables>>(
+    getNoAccessTables(query.fields)
+  );
 
   return noAccessTables.length > 0 ? (
     <Dialog
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/RelativeDate.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/RelativeDate.tsx
index 6d9993619..431d664d4 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/RelativeDate.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/RelativeDate.tsx
@@ -58,11 +58,16 @@ export function DateQueryInputField({
   );
 
   return (
-    <div className="flex items-center">
+    <div className="flex items-center gap-1">
       <Button.Icon
-        disabled={handleChange === undefined}
         icon="selector"
-        title="switch"
+        title={
+          isAbsolute
+            ? queryText.switchToRelative()
+            : queryText.switchToAbsolute()
+        }
+        aria-pressed={!isAbsolute}
+        disabled={handleChange === undefined}
         onClick={(): void => {
           toggleAbsolute();
           if (isAbsolute) {
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx
index eb40f7743..fd08afb32 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx
@@ -17,6 +17,7 @@ import {
 } from './helpers';
 import type { QueryResultRow } from './Results';
 import { QueryResults } from './Results';
+import { mappingPathIsComplete } from '../WbPlanView/helpers';
 
 // TODO: [FEATURE] allow customizing this and other constants as make sense
 const fetchSize = 40;
@@ -102,7 +103,7 @@ export function QueryResultsWrapper({
     const countOnly = queryResource.get('countOnly') === true;
     const allFields = augmentQueryFields(
       baseTableName,
-      fields,
+      fields.filter(({ mappingPath }) => mappingPathIsComplete(mappingPath)),
       queryResource.get('selectDistinct')
     );
 
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/helpers.ts b/specifyweb/frontend/js_src/lib/components/QueryBuilder/helpers.ts
index 5e876442c..f2ccc3565 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/helpers.ts
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/helpers.ts
@@ -25,6 +25,7 @@ import type { QueryFieldFilter } from './FieldFilter';
 import { queryFieldFilters } from './FieldFilter';
 import { QueryFieldSpec } from './fieldSpec';
 import { currentUserValue } from './SpecifyUserAutoComplete';
+import { hasTablePermission } from '../Permissions/helpers';
 
 export type SortTypes = 'ascending' | 'descending' | undefined;
 export const sortTypes: RA<SortTypes> = [undefined, 'ascending', 'descending'];
@@ -375,3 +376,26 @@ export function isModern(query: SpecifyResource<SpQuery>): boolean {
     containsRelativeDate(fieldSpecsMapped)
   );
 }
+
+export function getNoAccessTables(
+  queryFields: RA<SerializedResource<SpQueryField>>
+): RA<keyof Tables> {
+  const tableNames = queryFields.flatMap((field) => {
+    const fieldSpec = QueryFieldSpec.fromStringId(
+      field.stringId,
+      field.isRelFld ?? false
+    );
+    return filterArray(
+      fieldSpec.joinPath.flatMap((field) => [
+        field.model.name,
+        field.isRelationship ? field.relatedModel.name : undefined,
+      ])
+    );
+  });
+
+  const withoutDuplicates = new Set(tableNames);
+
+  return Array.from(withoutDuplicates).filter(
+    (name) => !hasTablePermission(name, 'read')
+  );
+}
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/AddStatDialog.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/AddStatDialog.tsx
index ec857a4c8..8b1db5ac5 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/AddStatDialog.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/AddStatDialog.tsx
@@ -136,8 +136,8 @@ export function AddStatDialog({
   ) : (
     <FrontEndStatsResultDialog
       label={queryText.newQueryName()}
-      matchClone={false}
       query={newQuery}
+      showClone={false}
       onClone={undefined}
       onClose={() => setNewQuery(undefined)}
       onEdit={(query) => {
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx
index 4278c111c..5a2f24b69 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx
@@ -8,10 +8,10 @@ import { Ul } from '../Atoms';
 import { Button } from '../Atoms/Button';
 import { className } from '../Atoms/className';
 import { Input } from '../Atoms/Form';
-import { hasTablePermission } from '../Permissions/helpers';
-import { generateStatUrl } from './hooks';
+import type { Tables } from '../DataModel/types';
+import { generateStatUrl, makeSerializedFieldsFromPaths } from './hooks';
 import { StatItem } from './StatItems';
-import { dynamicStatsSpec, statsSpec } from './StatsSpec';
+import { backEndStatsSpec, dynamicStatsSpec, statsSpec } from './StatsSpec';
 import type {
   CustomStat,
   DefaultStat,
@@ -19,11 +19,15 @@ import type {
   StatFormatterSpec,
   StatLayout,
 } from './types';
+import { filterArray } from '../../utils/types';
+import { getNoAccessTables } from '../QueryBuilder/helpers';
 
 /**
- * Used for overriding phantom items (dynamic categories).
+ * Used for overriding backend and dynamic items (dynamic categories).
  * If user doesn't have permission for dynamic category, then shows
- * no permission text otherwise show loading
+ * no permission text otherwise show loading. Also needs to handle
+ * cases where they don't have permission for any table that comes
+ * up when doing dynamic categories.
  *
  */
 function ItemOverride({
@@ -39,13 +43,29 @@ function ItemOverride({
           item.itemName
         )
       : undefined;
+
+  const backEndSpecResolve = backEndStatsSpec.find(
+    ({ responseKey }) => responseKey === urlToFetch
+  );
   const dynamicSpecResolve = dynamicStatsSpec.find(
     ({ responseKey }) => responseKey === urlToFetch
   );
+  const noAccessTables: RA<keyof Tables> = React.useMemo(
+    () =>
+      filterArray([
+        backEndSpecResolve?.querySpec,
+        dynamicSpecResolve?.dynamicQuerySpec,
+      ])
+        .map((querySpec) =>
+          makeSerializedFieldsFromPaths(querySpec.tableName, querySpec.fields)
+        )
+        .flatMap(getNoAccessTables),
+    [urlToFetch]
+  );
+
   return (
     <>
-      {dynamicSpecResolve !== undefined &&
-      !hasTablePermission(dynamicSpecResolve.tableName, 'read')
+      {noAccessTables.length > 0
         ? userText.noPermission()
         : commonText.loading()}
     </>
@@ -53,13 +73,12 @@ function ItemOverride({
 }
 
 function areItemsValid(items: RA<CustomStat | DefaultStat>) {
-  return !(
-    items.find(
-      (item) =>
-        item.type === 'DefaultStat' &&
-        item.itemName === 'phantomItem' &&
-        item.pathToValue === undefined
-    ) !== undefined
+  const itemNameToSearch = new Set(['phantomItem', 'dynamicPhantomItem']);
+  return !items.some(
+    (item) =>
+      item.type === 'DefaultStat' &&
+      itemNameToSearch.has(item.itemName) &&
+      item.pathToValue === undefined
   );
 }
 
@@ -151,7 +170,7 @@ export function Categories({
               )}
               <Ul
                 className={
-                  handleRename === undefined
+                  handleCategoryRename === undefined
                     ? `flex-1 overflow-auto ${
                         checkEmptyItems ? 'p-0' : 'p-3 pt-3'
                       }`
@@ -165,6 +184,7 @@ export function Categories({
                       <StatItem
                         categoryIndex={categoryIndex}
                         formatterSpec={formatterSpec}
+                        hasPermission={hasPermission}
                         item={item}
                         itemIndex={itemIndex}
                         key={itemIndex}
@@ -187,19 +207,7 @@ export function Categories({
                                 })
                             : undefined
                         }
-                        onClone={
-                          hasPermission
-                            ? (querySpec) =>
-                                handleClick(
-                                  {
-                                    type: 'CustomStat',
-                                    querySpec,
-                                    label: item.label,
-                                  },
-                                  categoryIndex
-                                )
-                            : undefined
-                        }
+                        onClone={() => handleClick(item, categoryIndex)}
                         onEdit={
                           // REFACTOR: Use if/else conditions
                           checkEmptyItems || handleEdit === undefined
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/ResultsDialog.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/ResultsDialog.tsx
index 284ce847e..d2b94b13e 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/ResultsDialog.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/ResultsDialog.tsx
@@ -34,14 +34,14 @@ export function FrontEndStatsResultDialog({
   query: originalQuery,
   onClose: handleClose,
   label,
+  showClone,
   onEdit: handleEdit,
   onClone: handleClone,
-  matchClone,
 }: {
   readonly query: SpecifyResource<SpQuery>;
   readonly onClose: () => void;
   readonly label: string;
-  readonly matchClone: boolean;
+  readonly showClone: boolean;
   readonly onEdit: ((querySpec: QuerySpec) => void) | undefined;
   readonly onClone: ((querySpec: QuerySpec) => void) | undefined;
 }): JSX.Element | null {
@@ -51,15 +51,14 @@ export function FrontEndStatsResultDialog({
       [originalQuery]
     )
   );
-  const isDisabled =
-    query.fields.length === 0 || (matchClone && handleClone === undefined);
+  const isDisabled = query.fields.length === 0 || handleEdit === undefined;
   return (
     <Dialog
       buttons={
         <div className="flex flex-1 gap-2">
-          {matchClone && (
+          {showClone && (
             <Button.Blue
-              disabled={isDisabled}
+              disabled={handleClone === undefined}
               onClick={(): void => {
                 handleClone?.(query);
                 handleClose();
@@ -70,17 +69,15 @@ export function FrontEndStatsResultDialog({
           )}
           <span className="-ml-2 flex-1" />
           <Button.DialogClose>{commonText.close()}</Button.DialogClose>
-          {typeof handleEdit === 'function' && (
-            <Button.Blue
-              disabled={isDisabled}
-              onClick={(): void => {
-                handleEdit(query);
-                handleClose();
-              }}
-            >
-              {commonText.save()}
-            </Button.Blue>
-          )}
+          <Button.Blue
+            disabled={isDisabled}
+            onClick={(): void => {
+              handleEdit?.(query);
+              handleClose();
+            }}
+          >
+            {commonText.save()}
+          </Button.Blue>
         </div>
       }
       className={{
@@ -90,6 +87,7 @@ export function FrontEndStatsResultDialog({
       onClose={handleClose}
     >
       <QueryBuilder
+        autoRun={showClone}
         forceCollection={undefined}
         isEmbedded
         query={originalQuery}
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/StatItems.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/StatItems.tsx
index 5d4a6dc75..7faa1850a 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/StatItems.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/StatItems.tsx
@@ -7,9 +7,9 @@ import { ajax } from '../../utils/ajax';
 import { Http } from '../../utils/ajax/definitions';
 import { throttledPromise } from '../../utils/ajax/throttledPromise';
 import { formatNumber } from '../Atoms/Internationalization';
-import type { Tables } from '../DataModel/types';
-import { hasTablePermission } from '../Permissions/helpers';
 import {
+  appendDynamicPathToValue,
+  makeSerializedFieldsFromPaths,
   queryCountPromiseGenerator,
   querySpecToResource,
   useResolvedStatSpec,
@@ -23,12 +23,15 @@ import type {
   QuerySpec,
   StatFormatterSpec,
 } from './types';
+import { getNoAccessTables } from '../QueryBuilder/helpers';
+import { serializeResource } from '../DataModel/helpers';
 
 export function StatItem({
   item,
   categoryIndex,
   itemIndex,
   formatterSpec,
+  hasPermission,
   onRemove: handleRemove,
   onClick: handleClick,
   onEdit: handleEdit,
@@ -40,6 +43,7 @@ export function StatItem({
   readonly formatterSpec: StatFormatterSpec;
   readonly categoryIndex: number;
   readonly itemIndex: number;
+  readonly hasPermission: boolean;
   readonly onRemove: (() => void) | undefined;
   readonly onClick: (() => void) | undefined;
   readonly onEdit:
@@ -53,7 +57,7 @@ export function StatItem({
       ) => void)
     | undefined;
   readonly onRename: ((newLabel: string) => void) | undefined;
-  readonly onClone: ((querySpec: QuerySpec) => void) | undefined;
+  readonly onClone: (() => void) | undefined;
 }): JSX.Element | null {
   const handleLoadItem = React.useCallback(
     (value: number | string) => handleLoad?.(categoryIndex, itemIndex, value),
@@ -61,9 +65,10 @@ export function StatItem({
   );
   const resolvedSpec = useResolvedStatSpec(item, formatterSpec);
 
-  return resolvedSpec?.type === 'QueryBuilderStat' &&
+  return resolvedSpec?.type === 'QueryStat' &&
     resolvedSpec.querySpec !== undefined ? (
     <QueryItem
+      hasPermission={hasPermission}
       label={item.label}
       querySpec={resolvedSpec.querySpec}
       value={item.itemValue}
@@ -86,45 +91,66 @@ export function StatItem({
     <BackEndItem
       fetchUrl={resolvedSpec.fetchUrl}
       formatter={resolvedSpec.formatter}
+      hasPermission={hasPermission}
       label={item.label}
-      pathToValue={resolvedSpec.pathToValue}
-      tableName={resolvedSpec.tableName}
+      pathToValue={resolvedSpec.pathToValue.toString()}
+      querySpec={resolvedSpec.querySpec}
       value={item.itemValue}
       onClick={handleClick}
       onLoad={handleLoadItem}
       onRemove={handleRemove}
       onRename={handleRename}
+      onClone={handleClone}
     />
   ) : null;
 }
 
 function BackEndItem({
   value,
-  tableName,
   fetchUrl,
   pathToValue,
   formatter,
   label,
+  querySpec,
+  hasPermission,
   onClick: handleClick,
   onRemove: handleRemove,
   onRename: handleRename,
   onLoad: handleLoad,
+  onClone: handleClone,
 }: {
   readonly value: number | string | undefined;
   readonly fetchUrl: string;
   readonly pathToValue: string;
-  readonly tableName: keyof Tables;
   readonly label: string;
+  readonly querySpec: QuerySpec | undefined;
+  readonly hasPermission: boolean;
   readonly formatter: (rawValue: any) => string | undefined;
   readonly onClick: (() => void) | undefined;
   readonly onRemove: (() => void) | undefined;
   readonly onRename: ((newLabel: string) => void) | undefined;
   readonly onLoad: ((value: number | string) => void) | undefined;
+  readonly onClone: undefined | (() => void);
 }): JSX.Element {
-  const [hasStatPermission, setStatPermission] = React.useState<boolean>(
-    hasTablePermission(tableName, 'read')
+  const statStateRef = React.useMemo(
+    () =>
+      querySpec === undefined ||
+      getNoAccessTables(
+        makeSerializedFieldsFromPaths(querySpec.tableName, querySpec.fields)
+      ).length === 0,
+    [querySpec]
   );
+
+  const [hasStatPermission, setStatPermission] =
+    React.useState<boolean>(statStateRef);
   const handleLoadResolve = hasStatPermission ? handleLoad : undefined;
+  const querySpecResolved =
+    querySpec === undefined
+      ? undefined
+      : {
+          ...querySpec,
+          fields: appendDynamicPathToValue(pathToValue, querySpec.fields),
+        };
   const promiseGenerator = React.useCallback(
     async () =>
       throttledPromise<BackendStatsResult | undefined>(
@@ -160,11 +186,16 @@ function BackEndItem({
   useStatValueLoad(value, promiseGenerator, handleLoadResolve);
   return (
     <StatsResult
+      hasPermission={hasPermission}
       label={label}
-      query={undefined}
+      query={
+        querySpecResolved === undefined
+          ? undefined
+          : querySpecToResource(label, querySpecResolved)
+      }
       value={hasStatPermission ? value : userText.noPermission()}
       onClick={handleClick}
-      onClone={undefined}
+      onClone={handleClone}
       onEdit={undefined}
       onRemove={handleRemove}
       onRename={handleRename}
@@ -176,6 +207,7 @@ function QueryItem({
   value,
   label,
   querySpec,
+  hasPermission,
   onClick: handleClick,
   onRemove: handleRemove,
   onEdit: handleEdit,
@@ -186,35 +218,44 @@ function QueryItem({
   readonly value: number | string | undefined;
   readonly querySpec: QuerySpec;
   readonly label: string;
+  readonly hasPermission: boolean;
   readonly onClick: (() => void) | undefined;
   readonly onRemove: (() => void) | undefined;
   readonly onEdit: ((querySpec: QuerySpec) => void) | undefined;
   readonly onRename: ((newLabel: string) => void) | undefined;
   readonly onLoad: ((value: number | string) => void) | undefined;
-  readonly onClone: ((querySpec: QuerySpec) => void) | undefined;
+  readonly onClone: (() => void) | undefined;
 }): JSX.Element | null {
+  const query = React.useMemo(
+    () => querySpecToResource(label, querySpec),
+    [label, querySpec]
+  );
+  const serializedQuery = serializeResource(query);
+  const statStateRef = React.useMemo(
+    () =>
+      getNoAccessTables(serializedQuery.fields).length === 0
+        ? 'valid'
+        : 'noPermission',
+    [querySpec]
+  );
+
   const [statState, setStatState] = React.useState<
     'error' | 'noPermission' | 'valid'
-  >(hasTablePermission(querySpec.tableName, 'read') ? 'valid' : 'noPermission');
+  >(statStateRef);
 
   React.useEffect(() => {
-    setStatState(
-      hasTablePermission(querySpec.tableName, 'read') ? 'valid' : 'noPermission'
-    );
+    setStatState(statStateRef);
   }, [querySpec]);
   const handleLoadResolve = statState === 'valid' ? handleLoad : undefined;
-  const query = React.useMemo(
-    () => querySpecToResource(label, querySpec),
-    [label, querySpec]
-  );
-
   const promiseGenerator = React.useCallback(
     async () =>
       throttledPromise<AjaxResponseObject<{ readonly count: number }>>(
         'queryStats',
-        queryCountPromiseGenerator(query),
+        queryCountPromiseGenerator(serializedQuery),
         JSON.stringify(querySpec)
-      ).then(({ data, status }) => {
+      ).then((response) => {
+        if (response === undefined) return undefined;
+        const { data, status } = response;
         if (status === Http.OK) {
           setStatState('valid');
           return formatNumber(data.count);
@@ -231,6 +272,7 @@ function QueryItem({
 
   return (
     <StatsResult
+      hasPermission={hasPermission}
       label={label}
       query={query}
       value={
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/StatsResult.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/StatsResult.tsx
index 514b4b1ad..fefd5bb68 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/StatsResult.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/StatsResult.tsx
@@ -13,6 +13,7 @@ export function StatsResult({
   value,
   query,
   label,
+  hasPermission,
   onClick: handleClick,
   onRemove: handleRemove,
   onEdit: handleEdit,
@@ -21,26 +22,40 @@ export function StatsResult({
 }: {
   readonly value: number | string | undefined;
   readonly query: SpecifyResource<SpQuery> | undefined;
+  readonly hasPermission: boolean;
   readonly label: string | undefined;
   readonly onClick: (() => void) | undefined;
   readonly onRemove: (() => void) | undefined;
   readonly onEdit: ((querySpec: QuerySpec) => void) | undefined;
   readonly onRename: ((newLabel: string) => void) | undefined;
-  readonly onClone: ((querySpec: QuerySpec) => void) | undefined;
+  readonly onClone: (() => void) | undefined;
 }): JSX.Element {
   const [isOpen, handleOpen, handleClose] = useBooleanState();
-  const isDisabled =
-    handleEdit === undefined &&
-    handleRename === undefined &&
-    handleClick === undefined;
-  const handleClickResolved = isDisabled
-    ? undefined
-    : handleClick ?? (query === undefined ? undefined : handleOpen);
+
+  /**
+   * Cases
+   *
+   * 1. In the add stats dialog, default to clicking.
+   *
+   * 2. In the normal page and normal mode
+   *    a. If it doesn't have query, disable
+   *    b. If it has query, allow viewing the query.
+   *
+   * 3. In the normal page and edit mode and have permission
+   *    a. It is a query stat or a back end stat -> use handleRename
+   *
+   * 4. In the normal page and edit mode but no permission
+   *    a. Disable everything.
+   *
+   */
+  const isDisabled = handleRename !== undefined && !hasPermission;
+  const handleClickResolved =
+    handleClick ?? (isDisabled || query === undefined ? undefined : handleOpen);
   return (
     <>
       {label === undefined ? (
         <li>{commonText.loading()}</li>
-      ) : typeof handleRename === 'function' ? (
+      ) : typeof handleRename === 'function' && hasPermission ? (
         <>
           <Button.Icon
             icon="trash"
@@ -68,11 +83,11 @@ export function StatsResult({
       {isOpen && query !== undefined && label !== undefined ? (
         <FrontEndStatsResultDialog
           label={label}
-          matchClone
+          showClone
           query={query}
-          onClone={handleClone}
+          onClone={hasPermission ? handleClone : undefined}
           onClose={handleClose}
-          onEdit={handleEdit}
+          onEdit={hasPermission ? handleEdit : undefined}
         />
       ) : undefined}
     </>
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/StatsSpec.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/StatsSpec.tsx
index a4893e507..09e9cf3ff 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/StatsSpec.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/StatsSpec.tsx
@@ -1,17 +1,20 @@
 import { statsText } from '../../localization/stats';
-import { getDateInputValue } from '../../utils/dayJs';
-import { f } from '../../utils/functools';
 import { today } from '../../utils/relativeDate';
 import type { RA } from '../../utils/types';
-import { ensure } from '../../utils/types';
+import { ensure, filterArray } from '../../utils/types';
 import { formatNumber } from '../Atoms/Internationalization';
-import type { Tables } from '../DataModel/types';
 import { userInformation } from '../InitialContext/userInformation';
 import { queryFieldFilters } from '../QueryBuilder/FieldFilter';
-import { formattedEntry } from '../WbPlanView/mappingHelpers';
+import { flippedSortTypes } from '../QueryBuilder/helpers';
+import {
+  anyTreeRank,
+  formattedEntry,
+  formatTreeRank,
+} from '../WbPlanView/mappingHelpers';
 import { generateStatUrl } from './hooks';
 import type {
   BackEndStat,
+  QuerySpec,
   StatFormatterGenerator,
   StatLayout,
   StatsSpec,
@@ -29,7 +32,7 @@ export const statsSpec: StatsSpec = {
           specimens: {
             label: statsText.collectionObjects(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -45,7 +48,7 @@ export const statsSpec: StatsSpec = {
           preparations: {
             label: statsText.preparations(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'Preparation',
                 fields: [
@@ -60,7 +63,7 @@ export const statsSpec: StatsSpec = {
           typeSpecimens: {
             label: statsText.typeSpecimens(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'Determination',
                 fields: [
@@ -72,6 +75,10 @@ export const statsSpec: StatsSpec = {
                     operStart: queryFieldFilters.equal.id,
                     isNot: true,
                   },
+                  {
+                    path: 'isCurrent',
+                    operStart: queryFieldFilters.true.id,
+                  },
                 ],
               },
             },
@@ -86,7 +93,6 @@ export const statsSpec: StatsSpec = {
             spec: {
               type: 'BackEndStat',
               pathToValue: undefined,
-              tableName: 'Preparation',
               formatterGenerator:
                 ({ showTotal }) =>
                 (
@@ -102,6 +108,24 @@ export const statsSpec: StatsSpec = {
                     : showTotal
                     ? `${formatNumber(prep.lots)} / ${formatNumber(prep.total)}`
                     : formatNumber(prep.lots),
+
+              querySpec: {
+                tableName: 'Preparation',
+                fields: [
+                  {
+                    path: formattedEntry,
+                    isDisplay: true,
+                    operStart: queryFieldFilters.any.id,
+                  },
+                  {
+                    path: 'collectionobject.catalogNumber',
+                    isDisplay: true,
+                    operStart: queryFieldFilters.any.id,
+                  },
+                  { path: 'preptype.name', isDisplay: true },
+                ],
+                isDistinct: false,
+              },
             },
           },
         },
@@ -112,7 +136,7 @@ export const statsSpec: StatsSpec = {
           itemsOnLoans: {
             label: statsText.itemsOnLoans(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'LoanPreparation',
                 fields: [
@@ -131,7 +155,7 @@ export const statsSpec: StatsSpec = {
           openLoansCount: {
             label: statsText.openLoans(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'Loan',
                 fields: [
@@ -150,7 +174,7 @@ export const statsSpec: StatsSpec = {
           overdueLoansCount: {
             label: statsText.overdueLoans(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'Loan',
                 fields: [
@@ -160,7 +184,7 @@ export const statsSpec: StatsSpec = {
                   {
                     path: 'currentDueDate',
                     operStart: queryFieldFilters.lessOrEqual.id,
-                    startValue: getDateInputValue(new Date()),
+                    startValue: `${today} + 0 day`,
                   },
                   {
                     path: 'isClosed',
@@ -173,111 +197,64 @@ export const statsSpec: StatsSpec = {
           },
         },
       },
-      taxonomicTree: {
-        label: statsText.taxonomicTree(),
+      taxonsRepresented: {
+        label: statsText.taxonRepresented(),
         items: {
-          classesCount: {
-            label: statsText.classes(),
+          dynamicPhantomItem: {
+            label: statsText.taxonRepresented(),
             spec: {
-              type: 'QueryBuilderStat',
-              querySpec: {
-                tableName: 'Taxon',
+              type: 'DynamicStat',
+              dynamicQuerySpec: {
+                tableName: 'CollectionObject',
                 fields: [
                   {
-                    path: formattedEntry,
-                  },
-                  {
-                    path: 'rankId',
-                    operStart: queryFieldFilters.equal.id,
-                    startValue: '60',
+                    path: 'determinations.isCurrent',
+                    operStart: queryFieldFilters.true.id,
                     isDisplay: false,
                   },
-                ],
-              },
-            },
-          },
-          ordersCount: {
-            label: statsText.orders(),
-            spec: {
-              type: 'QueryBuilderStat',
-              querySpec: {
-                tableName: 'Taxon',
-                fields: [
-                  {
-                    path: formattedEntry,
-                  },
                   {
-                    path: 'rankId',
-                    operStart: queryFieldFilters.equal.id,
-                    startValue: '100',
-                    isDisplay: false,
-                  },
-                ],
-              },
-            },
-          },
-          familiesCount: {
-            label: statsText.families(),
-            spec: {
-              type: 'QueryBuilderStat',
-              querySpec: {
-                tableName: 'Taxon',
-                fields: [
-                  {
-                    path: formattedEntry,
-                  },
-                  {
-                    path: 'rankId',
-                    operStart: queryFieldFilters.equal.id,
-                    startValue: '140',
+                    path: `determinations.preferredTaxon.${formatTreeRank(
+                      anyTreeRank
+                    )}.definitionItem.rankId`,
+                    operStart: queryFieldFilters.any.id,
+                    sortType: flippedSortTypes.ascending,
                     isDisplay: false,
                   },
-                ],
-              },
-            },
-          },
-          generaCount: {
-            label: statsText.genera(),
-            spec: {
-              type: 'QueryBuilderStat',
-              querySpec: {
-                tableName: 'Taxon',
-                fields: [
-                  {
-                    path: formattedEntry,
-                  },
                   {
-                    path: 'rankId',
-                    operStart: queryFieldFilters.equal.id,
-                    startValue: '180',
-                    isDisplay: false,
+                    isNot: true,
+                    path: `determinations.preferredTaxon.${formatTreeRank(
+                      anyTreeRank
+                    )}.definitionItem.name`,
+                    operStart: queryFieldFilters.empty.id,
                   },
                 ],
+                isDistinct: true,
               },
-            },
-          },
-          speciesCount: {
-            label: statsText.species(),
-            spec: {
-              type: 'QueryBuilderStat',
               querySpec: {
-                tableName: 'Taxon',
+                tableName: 'CollectionObject',
                 fields: [
                   {
-                    path: formattedEntry,
+                    path: `determinations.preferredTaxon.${formatTreeRank(
+                      anyTreeRank
+                    )}.fullName`,
+                    isDisplay: true,
+                    operStart: queryFieldFilters.any.id,
                   },
                   {
-                    path: 'rankId',
-                    operStart: queryFieldFilters.equal.id,
-                    startValue: '220',
-                    isDisplay: false,
+                    path: `determinations.preferredTaxon.${formatTreeRank(
+                      anyTreeRank
+                    )}.id`,
+                    isDisplay: true,
+                    operStart: queryFieldFilters.any.id,
                   },
                 ],
+                isDistinct: true,
               },
             },
           },
         },
       },
+
       // eslint-disable-next-line @typescript-eslint/naming-convention
       locality_geography: {
         // FEATURE: refactor all strings to use localized table names
@@ -286,7 +263,7 @@ export const statsSpec: StatsSpec = {
           localityCount: {
             label: statsText.localities(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'Locality',
                 fields: [
@@ -294,17 +271,26 @@ export const statsSpec: StatsSpec = {
                     path: formattedEntry,
                   },
                   {
-                    path: 'localityId',
+                    path: 'localityName',
+                    operStart: queryFieldFilters.any.id,
+                  },
+                  {
+                    path: 'latitude1',
+                    operStart: queryFieldFilters.any.id,
+                  },
+                  {
+                    path: 'longitude1',
                     operStart: queryFieldFilters.any.id,
                   },
                 ],
+                isDistinct: true,
               },
             },
           },
           geographyEntryCount: {
             label: statsText.geographyEntries(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'Geography',
                 fields: [
@@ -322,7 +308,7 @@ export const statsSpec: StatsSpec = {
           georeferencedLocalityCount: {
             label: statsText.georeferencedLocalities(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'Locality',
                 fields: [
@@ -339,20 +325,98 @@ export const statsSpec: StatsSpec = {
               },
             },
           },
+          percentGeoReferenced: {
+            label: statsText.percentGeoReferenced(),
+            spec: {
+              type: 'BackEndStat',
+              pathToValue: 'percentGeoReferenced',
+              formatterGenerator: () => (rawResult: string) => `${rawResult}%`,
+            },
+          },
         },
       },
+
+      geographiesRepresented: {
+        label: statsText.geographiesRepresented(),
+        items: {
+          dynamicPhantomItem: {
+            label: statsText.geographiesRepresented(),
+            spec: {
+              type: 'DynamicStat',
+              dynamicQuerySpec: {
+                tableName: 'CollectionObject',
+                fields: [
+                  {
+                    path: `collectingevent.locality.geography.${formatTreeRank(
+                      anyTreeRank
+                    )}.definitionItem.rankId`,
+                    operStart: queryFieldFilters.any.id,
+                    sortType: flippedSortTypes.ascending,
+                    isDisplay: false,
+                  },
+                  {
+                    isNot: true,
+                    path: `collectingevent.locality.geography.${formatTreeRank(
+                      anyTreeRank
+                    )}.definitionItem.name`,
+                    operStart: queryFieldFilters.empty.id,
+                  },
+                ],
+                isDistinct: true,
+              },
+              querySpec: {
+                tableName: 'CollectionObject',
+                fields: [
+                  {
+                    path: `collectingevent.locality.geography.${formatTreeRank(
+                      anyTreeRank
+                    )}.fullName`,
+                    isDisplay: true,
+                    operStart: queryFieldFilters.any.id,
+                  },
+                  {
+                    path: `collectingevent.locality.geography.${formatTreeRank(
+                      anyTreeRank
+                    )}.id`,
+                    isDisplay: true,
+                    operStart: queryFieldFilters.any.id,
+                  },
+                ],
+                isDistinct: true,
+              },
+            },
+          },
+        },
+      },
+
       // eslint-disable-next-line @typescript-eslint/naming-convention
       type_specimens: {
         label: statsText.typeSpecimens(),
         items: {
-          phantomItem: {
+          dynamicPhantomItem: {
             label: statsText.typeSpecimens(),
             spec: {
-              type: 'BackEndStat',
-              pathToValue: undefined,
-              tableName: 'Determination',
-              formatterGenerator: () => (rawNumber: number | undefined) =>
-                f.maybe(rawNumber, formatNumber),
+              type: 'DynamicStat',
+              dynamicQuerySpec: {
+                tableName: 'Determination',
+                fields: [
+                  {
+                    path: 'isCurrent',
+                    operStart: queryFieldFilters.true.id,
+                    isDisplay: false,
+                  },
+                  {
+                    isNot: true,
+                    path: 'typeStatusName',
+                    operStart: queryFieldFilters.empty.id,
+                  },
+                ],
+                isDistinct: true,
+              },
+              querySpec: {
+                tableName: 'Determination',
+                fields: [{ path: formattedEntry }],
+              },
             },
           },
         },
@@ -363,7 +427,7 @@ export const statsSpec: StatsSpec = {
           catalogedLastSevenDays: {
             label: statsText.digitizedLastSevenDays(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -382,7 +446,7 @@ export const statsSpec: StatsSpec = {
           catalogedLastMonth: {
             label: statsText.digitizedLastMonth(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -401,7 +465,7 @@ export const statsSpec: StatsSpec = {
           catalogedLastYear: {
             label: statsText.digitizedLastYear(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -425,7 +489,7 @@ export const statsSpec: StatsSpec = {
           collectionObjectsWithAttachments: {
             label: statsText.collectionObjectsWithAttachments(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -446,7 +510,7 @@ export const statsSpec: StatsSpec = {
           collectionObjectsWithImages: {
             label: statsText.collectionObjectsWithImages(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -464,6 +528,14 @@ export const statsSpec: StatsSpec = {
               },
             },
           },
+          percentCoImaged: {
+            label: statsText.percentImaged(),
+            spec: {
+              type: 'BackEndStat',
+              pathToValue: 'percentCoImaged',
+              formatterGenerator: () => (rawResult: string) => `${rawResult}%`,
+            },
+          },
         },
       },
     },
@@ -473,12 +545,12 @@ export const statsSpec: StatsSpec = {
     urlPrefix: 'collection/user',
     categories: {
       holdings: {
-        label: statsText.collection(),
+        label: statsText.curation(),
         items: {
           collectionObjectsCataloged: {
             label: statsText.collectionObjectsCataloged(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -494,7 +566,7 @@ export const statsSpec: StatsSpec = {
           collectionObjectsDetermined: {
             label: statsText.collectionObjectsDetermined(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -507,22 +579,6 @@ export const statsSpec: StatsSpec = {
               },
             },
           },
-          collectionObjectInventorized: {
-            label: statsText.collectionObjectsInventorized(),
-            spec: {
-              type: 'QueryBuilderStat',
-              querySpec: {
-                tableName: 'CollectionObject',
-                fields: [
-                  {
-                    path: 'inventorizedBy.specifyuser.name',
-                    startValue: userInformation.name,
-                    operStart: queryFieldFilters.equal.id,
-                  },
-                ],
-              },
-            },
-          },
         } as const,
       },
     },
@@ -543,13 +599,13 @@ const statSpecToItems = (
     categoryName,
     label,
     itemValue: undefined,
-    itemType: spec.type === 'BackEndStat' ? 'BackEndStat' : 'QueryStat',
+    itemType: spec.type,
     pathToValue: spec.type === 'BackEndStat' ? spec.pathToValue : undefined,
   }));
 
-function generateDynamicSpec(statsSpec: StatsSpec): RA<{
+function generateBackEndSpec(statsSpec: StatsSpec): RA<{
   readonly responseKey: string;
-  readonly tableName: keyof Tables;
+  readonly querySpec: QuerySpec | undefined;
   readonly formatterGenerator: StatFormatterGenerator;
 }> {
   return Object.entries(statsSpec).flatMap(([_, { categories, urlPrefix }]) =>
@@ -561,13 +617,33 @@ function generateDynamicSpec(statsSpec: StatsSpec): RA<{
         )
         .map(([itemKey, { spec }]) => ({
           responseKey: generateStatUrl(urlPrefix, categoryKey, itemKey),
-          tableName: (spec as BackEndStat).tableName,
+          querySpec: (spec as BackEndStat).querySpec,
           formatterGenerator: (spec as BackEndStat).formatterGenerator,
         }))
     )
   );
 }
 
+function generateDynamicSpec(statsSpec: StatsSpec): RA<{
+  readonly responseKey: string;
+  readonly dynamicQuerySpec: QuerySpec;
+}> {
+  return Object.entries(statsSpec).flatMap(([_, { categories, urlPrefix }]) =>
+    Object.entries(categories).flatMap(([categoryKey, { items }]) =>
+      filterArray(
+        Object.entries(items).map(([itemKey, { spec }]) =>
+          spec.type === 'DynamicStat'
+            ? {
+                responseKey: generateStatUrl(urlPrefix, categoryKey, itemKey),
+                dynamicQuerySpec: spec.dynamicQuerySpec,
+              }
+            : undefined
+        )
+      )
+    )
+  );
+}
+
 export function generateDefaultLayout(
   statsSpecBasis: StatsSpec
 ): RA<StatLayout> {
@@ -586,4 +662,5 @@ export function generateDefaultLayout(
 }
 
 export const dynamicStatsSpec = generateDynamicSpec(statsSpec);
+export const backEndStatsSpec = generateBackEndSpec(statsSpec);
 export const defaultLayoutGenerated = generateDefaultLayout(statsSpec);
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts b/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts
index e5893f4c5..9f925ac11 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts
@@ -6,7 +6,7 @@ import {
   applyStatBackendResponse,
   generateStatUrl,
   getDefaultLayoutFlagged,
-  getDynamicCategoriesToFetch,
+  getBackendUrlToFetch,
   getOffsetOne,
 } from '../hooks';
 import type { StatLayout } from '../types';
@@ -270,7 +270,7 @@ theories(getDefaultLayoutFlagged, [
   },
 ]);
 
-theories(getDynamicCategoriesToFetch, [
+theories(getBackendUrlToFetch, [
   {
     in: [defaultLayoutTest],
     out: [
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/layout.tests.ts b/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/layout.tests.ts
index fe842a3cd..eb9ed3205 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/layout.tests.ts
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/__tests__/layout.tests.ts
@@ -19,7 +19,7 @@ export const statsSpecTest: StatsSpec = {
           specimens: {
             label: statsText.collectionObjects(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
@@ -42,7 +42,6 @@ export const statsSpecTest: StatsSpec = {
             spec: {
               type: 'BackEndStat',
               pathToValue: undefined,
-              tableName: 'Preparation',
               formatterGenerator:
                 ({ showTotal }) =>
                 (
@@ -72,7 +71,6 @@ export const statsSpecTest: StatsSpec = {
             spec: {
               type: 'BackEndStat',
               pathToValue: 'countries',
-              tableName: 'Geography',
               formatterGenerator: () => (rawNumber: number | undefined) =>
                 f.maybe(rawNumber, formatNumber),
             },
@@ -88,7 +86,6 @@ export const statsSpecTest: StatsSpec = {
             spec: {
               type: 'BackEndStat',
               pathToValue: undefined,
-              tableName: 'Determination',
               formatterGenerator: () => (rawNumber: number | undefined) =>
                 f.maybe(rawNumber, formatNumber),
             },
@@ -107,7 +104,7 @@ export const statsSpecTest: StatsSpec = {
           collectionObjectsCataloged: {
             label: statsText.collectionObjectsCataloged(),
             spec: {
-              type: 'QueryBuilderStat',
+              type: 'QueryStat',
               querySpec: {
                 tableName: 'CollectionObject',
                 fields: [
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx
index 4a39cdc7c..7d5a8fbd2 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx
@@ -12,22 +12,26 @@ import { keysToLowerCase } from '../../utils/utils';
 import { MILLISECONDS } from '../Atoms/Internationalization';
 import { addMissingFields } from '../DataModel/addMissingFields';
 import { deserializeResource, serializeResource } from '../DataModel/helpers';
+import type { SerializedResource } from '../DataModel/helperTypes';
 import type { SpecifyResource } from '../DataModel/legacyTypes';
 import { schema } from '../DataModel/schema';
-import type { SpQuery } from '../DataModel/types';
+import type { SpQuery, SpQueryField, Tables } from '../DataModel/types';
+import { queryFieldFilters } from '../QueryBuilder/FieldFilter';
 import { makeQueryField } from '../QueryBuilder/fromTree';
-import { dynamicStatsSpec, statsSpec } from './StatsSpec';
+import { backEndStatsSpec, dynamicStatsSpec, statsSpec } from './StatsSpec';
 import type {
   BackEndStatResolve,
   BackendStatsResult,
   CustomStat,
   DefaultStat,
+  DynamicQuerySpec,
   QueryBuilderStat,
   QuerySpec,
   StatFormatterSpec,
   StatLayout,
   StatsSpec,
 } from './types';
+import type { PartialQueryFieldWithPath } from './types';
 
 /**
  * Returns state which gets updated everytime backend stat is fetched. Used for dynamic categories since they don't
@@ -48,6 +52,23 @@ export function useBackendApi(
   return backendStat;
 }
 
+export function useDynamicGroups(
+  dynamicEphemeralFieldSpecs: RA<DynamicQuerySpec>
+): IR<RA<string> | undefined> | undefined {
+  const dynamicEphereralPromises = React.useMemo(
+    () =>
+      dynamicEphemeralFieldSpecs.length === 0
+        ? undefined
+        : dynamicEphermeralPromiseGenerator(dynamicEphemeralFieldSpecs),
+    [dynamicEphemeralFieldSpecs]
+  );
+  const [dynamicEphemeralResults] = useMultipleAsyncState(
+    dynamicEphereralPromises,
+    false
+  );
+  return dynamicEphemeralResults;
+}
+
 function backEndStatPromiseGenerator(
   urlsToFetch: RA<string>
 ): IR<() => Promise<BackendStatsResult | undefined>> {
@@ -75,6 +96,44 @@ function backEndStatPromiseGenerator(
     ])
   );
 }
+// REFACTOR: use runQuery() function once merged with xml-editor
+function dynamicEphermeralPromiseGenerator(
+  dynamicEphemeralFieldSpecs: RA<DynamicQuerySpec>
+): IR<() => Promise<RA<string> | undefined>> {
+  return Object.fromEntries(
+    dynamicEphemeralFieldSpecs.map(({ key, spec }) => [
+      key,
+      async () =>
+        throttledPromise<RA<string> | undefined>(
+          'queryStats',
+          async () =>
+            ajax<{ readonly results: RA<RA<number | string | null>> }>(
+              '/stored_query/ephemeral/',
+              {
+                method: 'POST',
+                headers: {
+                  Accept: 'application/json',
+                },
+                body: keysToLowerCase({
+                  ...serializeResource(
+                    querySpecToResource(statsText.statistics(), spec)
+                  ),
+                  limit: 0,
+                }),
+              },
+              { expectedResponseCodes: Object.values(Http) }
+            ).then(({ data }) =>
+              filterArray(
+                data.results.map(([distinctGroup]) =>
+                  distinctGroup === null ? undefined : distinctGroup.toString()
+                )
+              )
+            ),
+          key
+        ),
+    ])
+  );
+}
 
 /**
  * Flags default statistics which aren't on the current page. We cannot use filter to return a list
@@ -132,7 +191,7 @@ export function useDefaultStatsToAdd(
 }
 
 export function queryCountPromiseGenerator(
-  query: SpecifyResource<SpQuery>
+  query: SerializedResource<SpQuery>
 ): () => Promise<AjaxResponseObject<{ readonly count: number }>> {
   return async () =>
     ajax<{
@@ -146,7 +205,7 @@ export function queryCountPromiseGenerator(
           Accept: 'application/json',
         },
         body: keysToLowerCase({
-          ...serializeResource(query),
+          ...query,
           countOnly: true,
         }),
       },
@@ -154,6 +213,19 @@ export function queryCountPromiseGenerator(
     );
 }
 
+export const makeSerializedFieldsFromPaths = (
+  tableName: keyof Tables,
+  fields: RA<PartialQueryFieldWithPath>
+): RA<SerializedResource<SpQueryField>> =>
+  fields.map(({ path, ...field }, index) =>
+    serializeResource(
+      makeQueryField(tableName, path, {
+        ...field,
+        position: index,
+      })
+    )
+  );
+
 export const querySpecToResource = (
   label: string,
   querySpec: QuerySpec
@@ -165,13 +237,9 @@ export const querySpecToResource = (
       contextTableId: schema.models[querySpec.tableName].tableId,
       countOnly: false,
       selectDistinct: querySpec.isDistinct ?? false,
-      fields: querySpec.fields.map(({ path, ...field }, index) =>
-        serializeResource(
-          makeQueryField(querySpec.tableName, path, {
-            ...field,
-            position: index,
-          })
-        )
+      fields: makeSerializedFieldsFromPaths(
+        querySpec.tableName,
+        querySpec.fields
       ),
     })
   );
@@ -182,33 +250,50 @@ export function resolveStatsSpec(
 ): BackEndStatResolve | QueryBuilderStat | undefined {
   if (item.type === 'CustomStat') {
     return {
-      type: 'QueryBuilderStat',
+      type: 'QueryStat',
       querySpec: item.querySpec,
     };
-  } else {
-    const statSpecItem =
-      statsSpec[item.pageName]?.categories?.[item.categoryName]?.items?.[
-        item.itemName
-      ];
-    return statSpecItem === undefined
-      ? undefined
-      : statSpecItem.spec.type === 'BackEndStat'
-      ? {
-          type: 'BackEndStat',
-          pathToValue: item.pathToValue ?? statSpecItem.spec.pathToValue,
-          fetchUrl: generateStatUrl(
-            statsSpec[item.pageName].urlPrefix,
-            item.categoryName,
-            item.itemName
-          ),
-          formatter: statSpecItem.spec.formatterGenerator(formatterSpec),
-          tableName: statSpecItem.spec.tableName,
-        }
-      : {
-          type: 'QueryBuilderStat',
-          querySpec: statSpecItem.spec.querySpec,
-        };
   }
+  const statSpecItem =
+    statsSpec[item.pageName]?.categories?.[item.categoryName]?.items?.[
+      item.itemName
+    ];
+  if (statSpecItem === undefined) return undefined;
+  const statUrl = generateStatUrl(
+    statsSpec[item.pageName].urlPrefix,
+    item.categoryName,
+    item.itemName
+  );
+  if (statSpecItem.spec.type === 'BackEndStat')
+    return {
+      type: 'BackEndStat',
+      pathToValue: item.pathToValue ?? statSpecItem.spec.pathToValue,
+      fetchUrl: statUrl,
+      formatter: statSpecItem.spec.formatterGenerator(formatterSpec),
+      querySpec: statSpecItem.spec.querySpec,
+    };
+  if (
+    statSpecItem.spec.type === 'DynamicStat' &&
+    item.pathToValue !== undefined
+  ) {
+    return {
+      type: 'QueryStat',
+      querySpec: {
+        tableName: statSpecItem.spec.dynamicQuerySpec.tableName,
+        fields: appendDynamicPathToValue(item.pathToValue, [
+          ...statSpecItem.spec.querySpec.fields,
+          ...statSpecItem.spec.dynamicQuerySpec.fields,
+        ]),
+        isDistinct: statSpecItem.spec.querySpec.isDistinct,
+      },
+    };
+  }
+  if (statSpecItem.spec.type === 'QueryStat')
+    return {
+      type: 'QueryStat',
+      querySpec: statSpecItem.spec.querySpec,
+    };
+  return undefined;
 }
 
 export function useResolvedStatSpec(
@@ -225,9 +310,7 @@ export function useResolvedStatSpec(
  *  stats page before categories are loaded.
  *
  */
-export function getDynamicCategoriesToFetch(
-  layout: RA<StatLayout>
-): RA<string> {
+export function getBackendUrlToFetch(layout: RA<StatLayout>): RA<string> {
   return Array.from(
     new Set(
       layout.flatMap(({ categories }) =>
@@ -252,6 +335,36 @@ export function getDynamicCategoriesToFetch(
   );
 }
 
+export function getDynamicQuerySpecsToFetch(
+  layout: RA<StatLayout>
+): RA<DynamicQuerySpec> {
+  return layout.flatMap(({ categories }) =>
+    categories.flatMap(({ items }) =>
+      filterArray(
+        items.map((item) => {
+          if (item.type === 'DefaultStat' && item.itemType === 'DynamicStat') {
+            const itemKey = generateStatUrl(
+              statsSpec[item.pageName].urlPrefix,
+              item.categoryName,
+              item.itemName
+            );
+            const dynamicSpec = dynamicStatsSpec.find(
+              ({ responseKey }) => responseKey === itemKey
+            );
+            if (dynamicSpec !== undefined) {
+              return {
+                key: itemKey,
+                spec: dynamicSpec.dynamicQuerySpec,
+              };
+            }
+          }
+          return undefined;
+        })
+      )
+    )
+  );
+}
+
 /**
  * Converts the entire layout into TSV format. Currently, it is restricted to current source
  * and current page, but it can convert the entire layout too. Skips undefined items and empty
@@ -330,6 +443,7 @@ export function applyStatBackendResponse(
     (item) =>
       item.type === 'DefaultStat' &&
       item.itemName === 'phantomItem' &&
+      item.itemType === 'BackEndStat' &&
       item.pathToValue === undefined
   );
 
@@ -366,12 +480,12 @@ export function applyStatBackendResponse(
 }
 
 /**
- * Iterates over the default layout and applies backend response for dynamic categories
+ * Iterates over the default layout and applies backend response for backend categories
  * to each source and page.
  *
  */
 
-export function useDefaultDynamicCategorySetter(
+export function useDefaultBackendCategorySetter(
   defaultBackEndResponse: BackendStatsResult | undefined,
   setDefaultLayout: (
     previousGenerator: (
@@ -381,7 +495,7 @@ export function useDefaultDynamicCategorySetter(
   statFormatterSpec: StatFormatterSpec
 ) {
   React.useEffect(() => {
-    dynamicStatsSpec.forEach(({ responseKey, formatterGenerator }) => {
+    backEndStatsSpec.forEach(({ responseKey, formatterGenerator }) => {
       if (
         defaultBackEndResponse !== undefined &&
         defaultBackEndResponse[responseKey] !== undefined
@@ -413,7 +527,7 @@ export function useDefaultDynamicCategorySetter(
  * to the current layout
  *
  */
-export function useDynamicCategorySetter(
+export function useBackEndCategorySetter(
   backEndResponse: BackendStatsResult | undefined,
   handleChange: (
     newCategories: (
@@ -424,7 +538,7 @@ export function useDynamicCategorySetter(
   formatterSpec: StatFormatterSpec
 ) {
   React.useEffect(() => {
-    dynamicStatsSpec.forEach(({ responseKey, formatterGenerator }) => {
+    backEndStatsSpec.forEach(({ responseKey, formatterGenerator }) => {
       if (
         backEndResponse !== undefined &&
         backEndResponse[responseKey] !== undefined &&
@@ -447,6 +561,113 @@ export function useDynamicCategorySetter(
   }, [backEndResponse, handleChange]);
 }
 
+export function useDynamicCategorySetter(
+  dynamicEphemeralResponse: IR<RA<string> | undefined> | undefined,
+  handleChange: (
+    newCategories: (
+      oldCategory: StatLayout['categories']
+    ) => StatLayout['categories']
+  ) => void
+) {
+  React.useEffect(() => {
+    dynamicStatsSpec.forEach(({ responseKey }) => {
+      if (
+        dynamicEphemeralResponse !== undefined &&
+        dynamicEphemeralResponse[responseKey] !== undefined
+      ) {
+        handleChange((oldCategory) =>
+          oldCategory.map((dynamicCategory) => ({
+            ...dynamicCategory,
+            items: applyDynamicCategoryResponse(
+              dynamicEphemeralResponse[responseKey],
+              dynamicCategory.items,
+              responseKey,
+              statsSpec
+            ),
+          }))
+        );
+      }
+    });
+  }, [handleChange, dynamicEphemeralResponse]);
+}
+
+export function useDefaultDynamicCategorySetter(
+  defaultDynamicEphemeralResponse: IR<RA<string> | undefined> | undefined,
+  setDefaultLayout: (
+    previousGenerator: (
+      oldLayout: RA<StatLayout> | undefined
+    ) => RA<StatLayout> | undefined
+  ) => void
+) {
+  React.useEffect(() => {
+    dynamicStatsSpec.forEach(({ responseKey }) => {
+      if (
+        defaultDynamicEphemeralResponse !== undefined &&
+        defaultDynamicEphemeralResponse[responseKey] !== undefined
+      ) {
+        setDefaultLayout((oldLayout) =>
+          oldLayout === undefined
+            ? undefined
+            : oldLayout.map((oldPage) => ({
+                ...oldPage,
+                categories: oldPage.categories.map((oldCategory) => ({
+                  ...oldCategory,
+                  items: applyDynamicCategoryResponse(
+                    defaultDynamicEphemeralResponse[responseKey],
+                    oldCategory.items,
+                    responseKey,
+                    statsSpec
+                  ),
+                })),
+              }))
+        );
+      }
+    });
+  }, [defaultDynamicEphemeralResponse, setDefaultLayout]);
+}
+
+function applyDynamicCategoryResponse(
+  dynamicEphemeralResponse: RA<string> | undefined,
+  items: RA<CustomStat | DefaultStat>,
+  responseKey: string,
+  statsSpec: StatsSpec
+): RA<CustomStat | DefaultStat> {
+  if (dynamicEphemeralResponse === undefined) return items;
+  const dynamicPhantomItem = items.find(
+    (item) =>
+      item.type === 'DefaultStat' &&
+      item.itemType === 'DynamicStat' &&
+      item.pathToValue === undefined
+  );
+  const dynamicPhantomUrlPrefix =
+    dynamicPhantomItem === undefined || dynamicPhantomItem.type === 'CustomStat'
+      ? undefined
+      : statsSpec[dynamicPhantomItem.pageName].urlPrefix;
+  const dynamicPhantomItemResponseKey =
+    dynamicPhantomUrlPrefix === undefined
+      ? undefined
+      : generateStatUrl(
+          dynamicPhantomUrlPrefix,
+          (dynamicPhantomItem as DefaultStat).categoryName,
+          (dynamicPhantomItem as DefaultStat).itemName
+        );
+  const isMyResponse = dynamicPhantomItemResponseKey === responseKey;
+  return dynamicPhantomItem !== undefined &&
+    isMyResponse &&
+    dynamicPhantomItem.type === 'DefaultStat'
+    ? dynamicEphemeralResponse.map((pathToValue) => ({
+        type: 'DefaultStat',
+        pageName: dynamicPhantomItem.pageName,
+        itemName: 'dynamicPhantomItem',
+        categoryName: dynamicPhantomItem.categoryName,
+        label: pathToValue,
+        itemValue: undefined,
+        itemType: 'QueryStat',
+        pathToValue,
+      }))
+    : items;
+}
+
 /**
  * Generates the API endpoint url using stats spec definition
  *
@@ -499,3 +720,19 @@ export function applyRefreshLayout(
     return pageLayout;
   });
 }
+
+export function appendDynamicPathToValue(
+  pathToValue: number | string,
+  fields: RA<PartialQueryFieldWithPath>
+): RA<PartialQueryFieldWithPath> {
+  const groupField = fields.at(-1);
+  if (groupField === undefined) return fields;
+  const startField = {
+    ...groupField,
+    operStart: queryFieldFilters.equal.id,
+    startValue: pathToValue.toString(),
+    isDisplay: false,
+    isNot: false,
+  };
+  return [...fields.slice(0, -1), startField];
+}
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/index.tsx b/specifyweb/frontend/js_src/lib/components/Statistics/index.tsx
index 63d922808..c3e07a81c 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/index.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/index.tsx
@@ -4,7 +4,7 @@ import type { State } from 'typesafe-reducer';
 import { useErrorContext } from '../../hooks/useErrorContext';
 import { commonText } from '../../localization/common';
 import { statsText } from '../../localization/stats';
-import { cleanMaybeFulfilled } from '../../utils/ajax/throttledPromise';
+import { cleanThrottledPromises } from '../../utils/ajax/throttledPromise';
 import { f } from '../../utils/functools';
 import type { RA } from '../../utils/types';
 import { getUniqueName } from '../../utils/uniquifyName';
@@ -29,18 +29,31 @@ import { StatsAsideButton } from './Buttons';
 import { Categories } from './Categories';
 import {
   applyRefreshLayout,
-  getDynamicCategoriesToFetch,
+  getBackendUrlToFetch,
+  getDynamicQuerySpecsToFetch,
   getOffsetOne,
   setLayoutUndefined,
   statsToTsv,
   useBackendApi,
+  useBackEndCategorySetter,
+  useDefaultBackendCategorySetter,
   useDefaultDynamicCategorySetter,
   useDefaultStatsToAdd,
   useDynamicCategorySetter,
+  useDynamicGroups,
 } from './hooks';
 import { StatsPageEditing } from './StatsPageEditing';
-import { defaultLayoutGenerated, dynamicStatsSpec } from './StatsSpec';
-import type { CustomStat, DefaultStat, StatLayout } from './types';
+import {
+  backEndStatsSpec,
+  defaultLayoutGenerated,
+  dynamicStatsSpec,
+} from './StatsSpec';
+import type {
+  CustomStat,
+  DefaultStat,
+  DynamicQuerySpec,
+  StatLayout,
+} from './types';
 
 export function StatsPage(): JSX.Element {
   return (
@@ -202,26 +215,37 @@ function ProtectedStatsPage(): JSX.Element | null {
   const sourceLayout = getSourceLayout(activePage.isShared);
 
   const allCategories = React.useMemo(
-    () => dynamicStatsSpec.map(({ responseKey }) => responseKey),
+    () => backEndStatsSpec.map(({ responseKey }) => responseKey),
+    []
+  );
+  const allDynamicQueries = React.useMemo(
+    () =>
+      dynamicStatsSpec.map(({ responseKey, dynamicQuerySpec }) => ({
+        key: responseKey,
+        spec: dynamicQuerySpec,
+      })),
     []
   );
   const [categoriesToFetch, setCategoriesToFetch] = React.useState<RA<string>>(
     []
   );
+  const [dynamicQueriesToRun, setDynamicQueriesToRun] = React.useState<
+    RA<DynamicQuerySpec>
+  >([]);
 
   const [defaultCategoriesToFetch, setDefaultCategoriesToFetch] =
     React.useState<RA<string>>([]);
 
+  const [defaultDynamicQueriesToRun, setDefaultDynamicQueriesToRun] =
+    React.useState<RA<DynamicQuerySpec>>([]);
   /**
    * Checks layout for absent dynamic categories and makes request for those categories.
    *
    */
   React.useEffect(() => {
-    const absentDynamicCategories =
-      sourceLayout === undefined
-        ? []
-        : getDynamicCategoriesToFetch(sourceLayout);
-    const notCurrentlyFetching = absentDynamicCategories.filter(
+    const absentBackEndCategories =
+      sourceLayout === undefined ? [] : getBackendUrlToFetch(sourceLayout);
+    const notCurrentlyFetching = absentBackEndCategories.filter(
       (category) => !categoriesToFetch.includes(category)
     );
     if (notCurrentlyFetching.length > 0) {
@@ -229,8 +253,27 @@ function ProtectedStatsPage(): JSX.Element | null {
     }
   }, [sourceLayout, categoriesToFetch, setCategoriesToFetch]);
 
+  React.useEffect(() => {
+    const absentDynamicCategories =
+      sourceLayout === undefined
+        ? []
+        : getDynamicQuerySpecsToFetch(sourceLayout);
+    const notCurrentlyRunning = absentDynamicCategories.filter(
+      (maybeRunningSpec) =>
+        !dynamicQueriesToRun.some(({ key }) => key === maybeRunningSpec.key)
+    );
+    if (notCurrentlyRunning.length > 0) {
+      setDynamicQueriesToRun([...dynamicQueriesToRun, ...notCurrentlyRunning]);
+    }
+  }, [sourceLayout, dynamicQueriesToRun, setDynamicQueriesToRun]);
+
   const backEndResponse = useBackendApi(categoriesToFetch);
   const defaultBackEndResponse = useBackendApi(defaultCategoriesToFetch);
+  const dynamicCategoriesResponse = useDynamicGroups(dynamicQueriesToRun);
+  const defaultDynamicCategoriesResponse = useDynamicGroups(
+    defaultDynamicQueriesToRun
+  );
+
   /*
    * Initial Load For Shared and Personal Pages
    * If collection and personal layout are undefined initially, then we need to
@@ -254,6 +297,7 @@ function ProtectedStatsPage(): JSX.Element | null {
   /* Set Default Layout every time page is started from scratch*/
   React.useEffect(() => {
     setDefaultLayout(defaultLayoutGenerated);
+    return cleanThrottledPromises;
   }, [setDefaultLayout]);
 
   const pageLastUpdated = activePage.isShared
@@ -289,17 +333,22 @@ function ProtectedStatsPage(): JSX.Element | null {
     [activePage.pageIndex, activePage.isShared]
   );
   // Used to set unknown categories once for layout initially, and every time for default layout
-  useDynamicCategorySetter(
+  useBackEndCategorySetter(
     backEndResponse,
     handleChange,
     categoriesToFetch,
     formatterSpec
   );
-  useDefaultDynamicCategorySetter(
+  useDefaultBackendCategorySetter(
     defaultBackEndResponse,
     setDefaultLayout,
     formatterSpec
   );
+  useDefaultDynamicCategorySetter(
+    defaultDynamicCategoriesResponse,
+    setDefaultLayout
+  );
+  useDynamicCategorySetter(dynamicCategoriesResponse, handleChange);
 
   const filters = React.useMemo(
     () => ({
@@ -436,7 +485,7 @@ function ProtectedStatsPage(): JSX.Element | null {
   );
 
   const refreshPage = () => {
-    cleanMaybeFulfilled();
+    cleanThrottledPromises();
     setCurrentLayout((layout) =>
       layout === undefined
         ? undefined
@@ -509,10 +558,11 @@ function ProtectedStatsPage(): JSX.Element | null {
             {process.env.NODE_ENV === 'development' && (
               <Button.Gray
                 onClick={(): void => {
-                  cleanMaybeFulfilled();
+                  cleanThrottledPromises();
                   handleSharedLayoutChange(undefined);
                   handlePersonalLayoutChange(undefined);
                   setCategoriesToFetch([]);
+                  setDynamicQueriesToRun([]);
                   setActivePage({
                     isShared: true,
                     pageIndex: 0,
@@ -809,7 +859,7 @@ function ProtectedStatsPage(): JSX.Element | null {
                 )
               }
               onRename={
-                isEditing && canEditIndex(activePage.isShared)
+                isEditing
                   ? (categoryIndex, itemIndex, newLabel): void =>
                       handleChange((oldCategory) =>
                         replaceItem(oldCategory, categoryIndex, {
@@ -860,7 +910,10 @@ function ProtectedStatsPage(): JSX.Element | null {
                   }))
             );
           }}
-          onInitialLoad={() => setDefaultCategoriesToFetch(allCategories)}
+          onInitialLoad={() => {
+            setDefaultCategoriesToFetch(allCategories);
+            setDefaultDynamicQueriesToRun(allDynamicQueries);
+          }}
           onLoad={handleDefaultLoad}
         />
       )}
diff --git a/specifyweb/frontend/js_src/lib/components/Statistics/types.ts b/specifyweb/frontend/js_src/lib/components/Statistics/types.ts
index 2d919ee91..95b7c6b45 100644
--- a/specifyweb/frontend/js_src/lib/components/Statistics/types.ts
+++ b/specifyweb/frontend/js_src/lib/components/Statistics/types.ts
@@ -13,17 +13,23 @@ export type CustomStat = State<
   }
 >;
 
+export type PartialQueryFieldWithPath = Partial<
+  SerializedResource<SpQueryField>
+> & {
+  readonly path: string;
+};
+
 export type DefaultStat = State<
   'DefaultStat',
   {
-    readonly itemType: 'BackEndStat' | 'QueryStat';
+    readonly itemType: 'BackEndStat' | 'DynamicStat' | 'QueryStat';
     readonly pageName: string;
     readonly categoryName: string;
     readonly itemName: string;
     readonly label: string;
     readonly itemValue: number | string | undefined;
     readonly isVisible?: boolean;
-    readonly pathToValue?: string;
+    readonly pathToValue?: number | string;
   }
 >;
 
@@ -38,9 +44,7 @@ export type StatLayout = {
 
 export type QuerySpec = {
   readonly tableName: keyof Tables;
-  readonly fields: RA<
-    Partial<SerializedResource<SpQueryField>> & { readonly path: string }
-  >;
+  readonly fields: RA<PartialQueryFieldWithPath>;
   readonly isDistinct?: boolean | null;
 };
 
@@ -59,7 +63,7 @@ export type StatsSpec = IR<{
 }>;
 
 export type QueryBuilderStat = State<
-  'QueryBuilderStat',
+  'QueryStat',
   {
     readonly querySpec: QuerySpec;
   }
@@ -73,14 +77,15 @@ export type StatFormatterSpec = {
 export type StatFormatterGenerator = (
   spec: StatFormatterSpec
 ) => (rawResult: any) => string | undefined;
+
 export type BackEndStat = BackEndBase & {
   readonly formatterGenerator: StatFormatterGenerator;
 };
 export type BackEndBase = State<
   'BackEndStat',
   {
-    readonly pathToValue: string | undefined;
-    readonly tableName: keyof Tables;
+    readonly pathToValue: number | string | undefined;
+    readonly querySpec?: QuerySpec;
   }
 >;
 export type BackEndStatResolve = BackEndBase & {
@@ -88,4 +93,17 @@ export type BackEndStatResolve = BackEndBase & {
   // Add type assertions for rawResult
   readonly formatter: (rawResult: any) => string | undefined;
 };
-export type StatItemSpec = BackEndStat | QueryBuilderStat;
+export type StatItemSpec = BackEndStat | DynamicStat | QueryBuilderStat;
+
+export type DynamicStat = State<
+  'DynamicStat',
+  {
+    readonly dynamicQuerySpec: QuerySpec;
+    readonly querySpec: QuerySpec;
+  }
+>;
+
+export type DynamicQuerySpec = {
+  readonly key: string;
+  readonly spec: QuerySpec;
+};
diff --git a/specifyweb/frontend/js_src/lib/hooks/useAsyncState.tsx b/specifyweb/frontend/js_src/lib/hooks/useAsyncState.tsx
index 42e4fbf94..9939caa9c 100644
--- a/specifyweb/frontend/js_src/lib/hooks/useAsyncState.tsx
+++ b/specifyweb/frontend/js_src/lib/hooks/useAsyncState.tsx
@@ -72,26 +72,25 @@ export function useMultipleAsyncState<RESPONSE extends Record<any, unknown>>(
       }
     | undefined,
   loadingScreen: boolean
-): GetOrSet<Partial<RESPONSE>> {
+): GetOrSet<Partial<RESPONSE> | undefined> {
   const loading = React.useContext(LoadingContext);
-  const [state, setState] = React.useState<Partial<RESPONSE>>({});
+  const [state, setState] = React.useState<Partial<RESPONSE> | undefined>(
+    undefined
+  );
   React.useLayoutEffect(() => {
     let destructorCalled = false;
-    setState((oldState) => (destructorCalled ? oldState : {}));
+    setState((oldState) => (destructorCalled ? oldState : undefined));
     if (callbacks === undefined) return;
     const callbackEntries = Object.entries(callbacks);
     const wrappedPromise = Promise.all(
       callbackEntries.map(async ([key, promiseGenerator]) =>
         promiseGenerator().then((data) => {
           if (destructorCalled) return undefined;
-          setState((oldState) =>
-            destructorCalled
-              ? oldState
-              : {
-                  ...oldState,
-                  [key]: data,
-                }
-          );
+          setState((oldState) => {
+            if (destructorCalled) return oldState;
+            const oldStateSafe = oldState ?? {};
+            return { ...oldStateSafe, [key]: data };
+          });
           return undefined;
         })
       )
diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts
index ea7a38f47..7ce7613fd 100644
--- a/specifyweb/frontend/js_src/lib/localization/query.ts
+++ b/specifyweb/frontend/js_src/lib/localization/query.ts
@@ -655,6 +655,12 @@ export const queryText = createDictionary({
   year: {
     'en-us': 'Year',
   },
+  switchToRelative: {
+    'en-us': 'Switch to relative',
+  },
+  switchToAbsolute: {
+    'en-us': 'Switch to absolute',
+  },
   importHiddenFields: {
     'en-us': 'The following fields are hidden in the query you imported:',
     'es-es': 'Los siguientes campos están ocultos en la consulta que importó:',
diff --git a/specifyweb/frontend/js_src/lib/localization/stats.tsx b/specifyweb/frontend/js_src/lib/localization/stats.tsx
index d031b75fa..0476ba74c 100644
--- a/specifyweb/frontend/js_src/lib/localization/stats.tsx
+++ b/specifyweb/frontend/js_src/lib/localization/stats.tsx
@@ -32,6 +32,9 @@ export const statsText = createDictionary({
   typeSpecimens: {
     'en-us': 'Type Specimens',
   },
+  curation: {
+    'en-us': 'Curation',
+  },
   familiesRepresented: {
     'en-us': 'Families Represented',
     'ru-ru': 'Семей',
@@ -61,8 +64,7 @@ export const statsText = createDictionary({
     'ru-ru': 'Просроченные Займы',
   },
   taxonomicTree: {
-    'en-us': 'Taxonomic Tree',
-    'ru-ru': 'Таксономическое дерево',
+    'en-us': 'Taxon Tree Nodes',
   },
   classes: {
     'en-us': 'Classes',
@@ -226,5 +228,17 @@ export const statsText = createDictionary({
   error: {
     'en-us': 'Error',
   },
+  taxonRepresented: {
+    'en-us': 'Taxa Represented',
+  },
+  geographiesRepresented: {
+    'en-us': 'Geographies Represented',
+  },
+  percentGeoReferenced: {
+    'en-us': 'Percent Georeferenced',
+  },
+  percentImaged: {
+    'en-us': 'Percent Imaged',
+  },
 });
 /* eslint-enable @typescript-eslint/naming-convention */
diff --git a/specifyweb/frontend/js_src/lib/tests/setup.ts b/specifyweb/frontend/js_src/lib/tests/setup.ts
index b5760699b..378af3526 100644
--- a/specifyweb/frontend/js_src/lib/tests/setup.ts
+++ b/specifyweb/frontend/js_src/lib/tests/setup.ts
@@ -10,7 +10,7 @@ import './__mocks__/matchMedia';
 import { configure } from '@testing-library/react';
 import failOnConsole from 'jest-fail-on-console';
 
-// Fail a test if it calls console.error or console.log
+// Fail a test if it calls console.error or console.warn
 failOnConsole();
 
 /*
diff --git a/specifyweb/frontend/js_src/lib/utils/ajax/throttledPromise.ts b/specifyweb/frontend/js_src/lib/utils/ajax/throttledPromise.ts
index d0415464e..e663c843f 100644
--- a/specifyweb/frontend/js_src/lib/utils/ajax/throttledPromise.ts
+++ b/specifyweb/frontend/js_src/lib/utils/ajax/throttledPromise.ts
@@ -1,25 +1,34 @@
 import type { RR, WritableArray } from '../types';
-
-export type PromiseWithSpec<T> = Promise<T> & {
+type PromiseWithSpec<T> = Promise<T> & {
   spec: number | string;
 };
+
+type KillablePromise<T> = PromiseWithSpec<T> & {
+  killPromise: () => void;
+};
 const currentRequestsGenerator = <T>(): WritableArray<PromiseWithSpec<T>> => [];
 
-let maybeFulfilled: WritableArray<PromiseWithSpec<any>> = [];
+let maybeFulfilled: WritableArray<KillablePromise<any>> = [];
 
-export const cleanMaybeFulfilled = (): void => {
+export const cleanThrottledPromises = (): void => {
+  maybeFulfilled.forEach((promise) => promise.killPromise());
   maybeFulfilled = [];
+  // Since the kill promise is already supplied, the promises will resolve within a work loop.
+  // So, it is sufficient to directly empty the currentRequests for future requests.
+  Object.entries(networkRequestsSpec).forEach(([key, _]) => {
+    networkRequestsSpec[key].currentRequests = [];
+  });
 };
 
 export const networkRequestsSpec: RR<
   'backendStats' | 'queryStats',
   {
-    readonly currentRequests: WritableArray<PromiseWithSpec<any>>;
+    currentRequests: WritableArray<Promise<any>>;
     readonly maxFetchCount: number;
   }
 > = {
   queryStats: {
-    maxFetchCount: 5,
+    maxFetchCount: 4,
     currentRequests: currentRequestsGenerator<number | string | undefined>(),
   },
   backendStats: {
@@ -28,25 +37,38 @@ export const networkRequestsSpec: RR<
   },
 };
 
-export async function throttledPromise<T>(
+export function throttledPromise<T>(
   key: keyof typeof networkRequestsSpec,
   promiseGenerator: () => Promise<T>,
   promiseSpec: number | string
-): Promise<T> {
+): Promise<T | undefined> {
   const { maxFetchCount, currentRequests } = networkRequestsSpec[key];
-  while (currentRequests.length > maxFetchCount) {
-    await Promise.any(currentRequests);
-  }
   const promiseInFulfilled = maybeFulfilled.find(
     (fulfilledPromise) => fulfilledPromise.spec === promiseSpec
   );
   if (promiseInFulfilled !== undefined) return promiseInFulfilled;
+  let promiseKilled = false;
+  const returnPromise: KillablePromise<T | undefined> = new Promise(
+    async (resolve) => {
+      while (currentRequests.length >= maxFetchCount && !promiseKilled) {
+        await Promise.any(currentRequests);
+      }
+      if (promiseKilled) {
+        resolve(undefined);
+        return;
+      }
+      const promiseToAdd = promiseGenerator().then((result) => {
+        currentRequests.splice(currentRequests.indexOf(promiseToAdd), 1);
+        resolve(result);
+      });
+      currentRequests.push(promiseToAdd);
+    }
+  ) as KillablePromise<T | undefined>;
+  returnPromise.spec = promiseSpec;
 
-  const newPromise = promiseGenerator().finally(() => {
-    currentRequests.splice(currentRequests.indexOf(newPromise), 1);
-  }) as PromiseWithSpec<T>;
-  newPromise.spec = promiseSpec;
-  currentRequests.push(newPromise);
-  maybeFulfilled.push(newPromise);
-  return newPromise;
+  returnPromise.killPromise = () => {
+    promiseKilled = true;
+  };
+  maybeFulfilled.push(returnPromise);
+  return returnPromise;
 }
diff --git a/specifyweb/frontend/js_src/lib/utils/cache/index.ts b/specifyweb/frontend/js_src/lib/utils/cache/index.ts
index 9b164953a..78b5e7d41 100644
--- a/specifyweb/frontend/js_src/lib/utils/cache/index.ts
+++ b/specifyweb/frontend/js_src/lib/utils/cache/index.ts
@@ -140,7 +140,13 @@ function genericSet<T>(
 
   const formattedKey = formatCacheKey(category, key);
   if (cache[formattedKey] === undefined) fetchBucket(formattedKey);
-  if (cache[formattedKey] === value) return value;
+  if (
+    cache[formattedKey] === value ||
+    (typeof value === 'object' &&
+      typeof cache[formattedKey] === 'object' &&
+      JSON.stringify(cache[formattedKey]) === JSON.stringify(value))
+  )
+    return value;
 
   cache[formattedKey] = value;
 
diff --git a/specifyweb/report_runner/views.py b/specifyweb/report_runner/views.py
index ea5ea5623..e2c4a91cf 100644
--- a/specifyweb/report_runner/views.py
+++ b/specifyweb/report_runner/views.py
@@ -129,10 +129,12 @@ def create_report(user_id, discipline_id, query_id, mimetype, name):
         "Can not create report: mimetype not 'jrxml/label' or 'jrxml/report'", 
         {"localizationKey" : "invalidReportMimetype"})
     query = Spquery.objects.get(id=query_id)
-    try:
-        spappdir = Spappresourcedir.objects.get(discipline_id=discipline_id, collection_id=None)
-    except Spappresourcedir.DoesNotExist:
+
+    spappdirs_matched = Spappresourcedir.objects.filter(discipline_id=discipline_id, collection_id=None)
+    if len(spappdirs_matched) == 0:
         spappdir = Spappresourcedir.objects.create(discipline_id=discipline_id)
+    else:
+        spappdir = spappdirs_matched[0]
 
     appresource = spappdir.sppersistedappresources.create(
         version=0,
diff --git a/specifyweb/stats/urls.py b/specifyweb/stats/urls.py
index 1dc7dc56a..8e9d7c54c 100644
--- a/specifyweb/stats/urls.py
+++ b/specifyweb/stats/urls.py
@@ -6,5 +6,6 @@ urlpatterns = [
     url(r'^collection/user/(?P<user_id>\d+)/$', views.collection_user),
     url(r'^collection/preparations/$', views.collection_preparations),
     url(r'^collection/type_specimens/$', views.collection_type_specimens),
-    url(r'^collection/locality_geography/countries/$', views.collection_locality_geography),
+    url(r'^collection/locality_geography/(?P<stat>\w+)/', views.collection_locality_geography),
+    url(r'^collection/attachments/(?P<stat>\w+)/', views.collection_attachments)
 ]
diff --git a/specifyweb/stats/views.py b/specifyweb/stats/views.py
index eb618d1e2..cb9c0470c 100644
--- a/specifyweb/stats/views.py
+++ b/specifyweb/stats/views.py
@@ -2,8 +2,9 @@ from django import http
 
 from specifyweb.permissions.permissions import check_table_permissions
 from specifyweb.specify.views import openapi
-from django.http import HttpResponse
-from ..specify.models import Preparation, Determination
+from django.http import HttpResponse, Http404
+
+from ..specify.models import Preparation, Determination, Discipline, Locality, Collectionobject, Collectionobjectattachment, Attachment
 from specifyweb.specify.views import login_maybe_required
 import logging
 
@@ -72,24 +73,12 @@ def collection_preparations(request) -> HttpResponse:
             }
         }
     }}, )
-def collection_locality_geography(request) -> HttpResponse:
-    cursor = connection.cursor()
-    ####LOC_GEO
-    # COUNTRIES
+def collection_locality_geography(request, stat) -> HttpResponse:
     geography_dict = {}
-    #don't need geographyid
-    cursor.execute(
-        """
-        SELECT count(Name)
-        FROM (SELECT DISTINCT Name
-              FROM (SELECT g.GeographyID, g.nodenumber
-                    FROM locality as l
-                             inner join geography as g on l.GeographyID = g.GeographyID) as GEO,
-                   geography as g
-              WHERE g.rankid = 200
-                and GEO.nodenumber between g.nodenumber and g.HighestChildNodeNumber) As GEO2
-        """)
-    geography_dict['countries'] = int((cursor.fetchone()[0]))
+    if stat == 'percentGeoReferenced':
+        geography_dict[stat] = get_percent_georeferenced(request)
+    else:
+        raise Http404
     return http.JsonResponse(geography_dict)
 
 @login_maybe_required
@@ -120,6 +109,7 @@ def collection_type_specimens(request) -> HttpResponse:
         FROM determination
         WHERE CollectionMemberID = %s
           AND TypeStatusName is not null
+          AND IsCurrent
         group by TypeStatusName
         """,
         [request.specify_collection.id]
@@ -132,3 +122,61 @@ def collection_type_specimens(request) -> HttpResponse:
 
 def collection_user():
     return http.Http404
+
+def get_percent_georeferenced(request):
+    check_table_permissions(request.specify_collection, request.specify_user,
+                            Discipline, "read")
+    check_table_permissions(request.specify_collection, request.specify_user,
+                            Locality, "read")
+    cursor = connection.cursor()
+    cursor.execute("""
+       SELECT CASE WHEN 
+       (SELECT count(*) FROM locality
+        JOIN discipline ON 
+        locality.DisciplineID = discipline.DisciplineID 
+        WHERE discipline.DisciplineID = %s) = 0 THEN 0 
+        ELSE ((count(localityid) * 1.0) / 
+        ((SELECT count(*) FROM locality) * 1.0)) * 100.0 
+        END AS PercentGeoReferencedLocalities FROM locality WHERE not latitude1 is null""",
+                   [request.specify_collection.discipline.id])
+    percent_georeferenced = round(cursor.fetchone()[0],2)
+    return percent_georeferenced
+
+@login_maybe_required
+@openapi(schema={
+    'get': {
+        'responses': {
+            '200': {
+                'description': 'Returns Attachment Stats for Specify',
+                'content': {
+                    'application/json': {
+                        'schema': {
+                            'type': 'object',
+                            'additionalProperties': True,
+                        }
+                    }
+                }
+            }
+        }
+    }}, )
+def collection_attachments(request, stat)->HttpResponse:
+    attachments_dict = {}
+    if stat == 'percentCoImaged':
+        attachments_dict[stat] = get_percent_imaged(request)
+    else:
+        raise Http404
+    return http.JsonResponse(attachments_dict)
+
+def get_percent_imaged(request):
+    check_table_permissions(request.specify_collection, request.specify_user, Collectionobject, "read")
+    check_table_permissions(request.specify_collection, request.specify_user, Collectionobjectattachment, "read")
+    check_table_permissions(request.specify_collection, request.specify_user, Attachment, "read")
+    cursor = connection.cursor()
+    cursor.execute("""select 
+	100.0*(select count(distinct(co.collectionobjectid)) 
+    as co_count from   collectionobject co join collectionobjectattachment using (collectionobjectid) 
+    join attachment using (attachmentid) where attachment.mimetype regexp 'image' and co.collectionid = %(coid)s         
+    and collectionobjectattachment.collectionmemberid = %(coid)s) / (select greatest(sub.co_count, 1) from 
+    (select count(*) as co_count from collectionobject co where co.CollectionID = %(coid)s) as sub) as percent_imaged""", {"coid": request.specify_collection.id})
+    percent_imaged = round(cursor.fetchone()[0], 2)
+    return percent_imaged
\ No newline at end of file
diff --git a/specifyweb/stored_queries/format.py b/specifyweb/stored_queries/format.py
index c6c9f7a54..99c04c2ba 100644
--- a/specifyweb/stored_queries/format.py
+++ b/specifyweb/stored_queries/format.py
@@ -14,11 +14,12 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute
 from sqlalchemy.sql.elements import Extract
 from sqlalchemy import types
 
-from typing import Tuple, Optional, Union
+from typing import Tuple, Optional, Union, Any
 
 from specifyweb.context.app_resource import get_app_resource
 from specifyweb.context.remote_prefs import get_remote_prefs
-from specifyweb.specify.models import datamodel, Spappresourcedata, Splocalecontainer, Splocalecontaineritem
+from specifyweb.specify.models import datamodel, Spappresourcedata, \
+    Splocalecontainer, Splocalecontaineritem
 from specifyweb.specify.datamodel import Field, Relationship, Table
 from specifyweb.stored_queries.queryfield import QueryField
 
@@ -26,6 +27,7 @@ from . import models
 from .group_concat import group_concat
 from .blank_nulls import blank_nulls
 from .query_construct import QueryConstruct
+from .queryfieldspec import QueryFieldSpec
 
 logger = logging.getLogger(__name__)
 
@@ -33,9 +35,11 @@ CollectionObject_model = datamodel.get_table('CollectionObject')
 Agent_model = datamodel.get_table('Agent')
 Spauditlog_model = datamodel.get_table('SpAuditLog')
 
+
 class ObjectFormatter(object):
     def __init__(self, collection, user, replace_nulls):
-        formattersXML, _ = get_app_resource(collection, user, 'DataObjFormatters')
+        formattersXML, _ = get_app_resource(collection, user,
+                                            'DataObjFormatters')
         self.formattersDom = ElementTree.fromstring(formattersXML)
         self.date_format = get_date_format()
         self.date_format_year = MYSQL_TO_YEAR.get(self.date_format)
@@ -43,9 +47,11 @@ class ObjectFormatter(object):
         self.collection = collection
         self.replace_nulls = replace_nulls
 
-    def getFormatterDef(self, specify_model: Table, formatter_name) -> Optional[Element]:
+    def getFormatterDef(self, specify_model: Table, formatter_name) -> Optional[
+        Element]:
         def lookup(attr: str, val: str) -> Optional[Element]:
-            return self.formattersDom.find('format[@%s=%s]' % (attr, quoteattr(val)))
+            return self.formattersDom.find(
+                'format[@%s=%s]' % (attr, quoteattr(val)))
 
         def getFormatterFromSchema() -> Element:
             try:
@@ -60,14 +66,17 @@ class ObjectFormatter(object):
             return formatter_name and lookup('name', formatter_name)
 
         return (formatter_name and lookup('name', formatter_name)) \
-            or getFormatterFromSchema() \
-            or lookup('class', specify_model.classname)
+               or getFormatterFromSchema() \
+               or lookup('class', specify_model.classname)
 
-    def getAggregatorDef(self, specify_model: Table, aggregator_name) -> Optional[Element]:
+    def getAggregatorDef(self, specify_model: Table, aggregator_name) -> \
+    Optional[Element]:
         def lookup(attr: str, val: str) -> Optional[Element]:
-            return self.formattersDom.find('aggregators/aggregator[@%s=%s]' % (attr, quoteattr(val)))
+            return self.formattersDom.find(
+                'aggregators/aggregator[@%s=%s]' % (attr, quoteattr(val)))
+
         return (aggregator_name and lookup('name', aggregator_name)) \
-            or lookup('class', specify_model.classname)
+               or lookup('class', specify_model.classname)
 
     def catalog_number_is_numeric(self):
         return self.collection.catalognumformatname == 'CatalogNumberNumeric'
@@ -92,76 +101,98 @@ class ObjectFormatter(object):
         else:
             return format
 
-    def objformat(self, query: QueryConstruct, orm_table: SQLTable, formatter_name) -> Tuple[QueryConstruct, blank_nulls]:
+    def objformat(self, query: QueryConstruct, orm_table: SQLTable,
+                  formatter_name) -> Tuple[QueryConstruct, blank_nulls]:
         logger.info('formatting %s using %s', orm_table, formatter_name)
-        specify_model = datamodel.get_table(inspect(orm_table).class_.__name__, strict=True)
+        specify_model = datamodel.get_table(inspect(orm_table).class_.__name__,
+                                            strict=True)
         formatterNode = self.getFormatterDef(specify_model, formatter_name)
         if formatterNode is None:
             logger.warn("no dataobjformatter for %s", specify_model)
             return query, literal(_("<Formatter not defined.>"))
-        logger.debug("using dataobjformatter: %s", ElementTree.tostring(formatterNode))
-
-        def case_value_convert(value: Optional[str]) -> Optional[str]: 
-            return value
+        logger.debug("using dataobjformatter: %s",
+                     ElementTree.tostring(formatterNode))
+
+        def make_expr(query: QueryConstruct, fieldNodeText, fieldNodeAttrib) -> Tuple[
+            QueryConstruct, blank_nulls, QueryFieldSpec]:
+            path = fieldNodeText.split('.')
+            path = [inspect(orm_table).class_.__name__, *path]
+            formatter_field_spec = QueryFieldSpec.from_path(path)
+            if formatter_field_spec.is_relationship():
+                if formatter_field_spec.get_field().type != 'one-to-many':
+                    query, table, model, specify_field = query.build_join(
+                        specify_model, orm_table,
+                        formatter_field_spec.join_path)
+                    formatter_name = fieldNodeAttrib.get('formatter', None)
+                    query, expr = self.objformat(query, table, formatter_name)
+                else:
+                    query, orm_model, table, field = query.build_join(
+                        specify_model, orm_table,
+                        formatter_field_spec.join_path[:-1])
+                    aggregator_name = fieldNodeAttrib.get('aggregator', None)
+                    expr = query.objectformatter.aggregate(query,
+                                                           formatter_field_spec.get_field(),
+                                                           orm_model,
+                                                           aggregator_name)
 
-        switchNode = formatterNode.find('switch')
-        single = switchNode.attrib.get('single', 'true') == 'true'
-        if not single:
-            sp_control_field = specify_model.get_field(switchNode.attrib['field'])
-            if sp_control_field.type == 'java.lang.Boolean':
-                def case_value_convert(value): return value == 'true'
-
-        def make_expr(query: QueryConstruct, fieldNode: Element) -> Tuple[QueryConstruct, blank_nulls]:
-            path = fieldNode.text.split('.')
-            query, table, model, specify_field = query.build_join(specify_model, orm_table, path)
-            if specify_field.is_relationship:
-                formatter_name = fieldNode.attrib.get('formatter', None)
-                query, expr = self.objformat(query, table, formatter_name)
             else:
-                expr = self._fieldformat(specify_field, getattr(table, specify_field.name))
+                query, table, model, specify_field = query.build_join(
+                    specify_model, orm_table, formatter_field_spec.join_path)
+                expr = self._fieldformat(formatter_field_spec.get_field(),
+                                         getattr(table, specify_field.name))
 
-            if 'format' in fieldNode.attrib:
-                expr = self.pseudo_sprintf(fieldNode.attrib['format'], expr)
+            if 'format' in fieldNodeAttrib:
+                expr = self.pseudo_sprintf(fieldNodeAttrib['format'], expr)
 
-            if 'sep' in fieldNode.attrib:
-                expr = concat(fieldNode.attrib['sep'], expr)
-            return query, blank_nulls(expr)
+            if 'sep' in fieldNodeAttrib:
+                expr = concat(fieldNodeAttrib['sep'], expr)
+            return query, blank_nulls(expr), formatter_field_spec
 
-        def make_case(query: QueryConstruct, caseNode: Element) -> Tuple[QueryConstruct, Optional[str], blank_nulls]:
+        def make_case(query: QueryConstruct, caseNode: Element) -> Tuple[
+            QueryConstruct, Optional[str], blank_nulls]:
             field_exprs = []
             for node in caseNode.findall('field'):
-                query, expr = make_expr(query, node)
+                query, expr, _ = make_expr(query, node.text, node.attrib)
                 field_exprs.append(expr)
 
             expr = concat(*field_exprs) if len(field_exprs) > 1 else field_exprs[0]
-            return query, case_value_convert(caseNode.attrib.get('value', None)), expr
+            return query, caseNode.attrib.get('value', None), expr
 
+        switchNode = formatterNode.find('switch')
+        single = switchNode.attrib.get('single', 'true') == 'true'
         cases = []
         for caseNode in switchNode.findall('fields'):
             query, value, expr = make_case(query, caseNode)
             cases.append((value, expr))
 
         if not cases:
-            logger.warn("dataobjformatter for %s contains switch clause no fields", specify_model)
+            logger.warn(
+                "dataobjformatter for %s contains switch clause no fields",
+                specify_model)
             return query, literal(_("<Formatter not defined.>"))
 
         if single:
             value, expr = cases[0]
         else:
-            control_field = getattr(orm_table, switchNode.attrib['field'])
-            expr = case(cases, control_field)
-
+            query, formatted, swith_field_spec = make_expr(query, switchNode.attrib['field'], {})
+            def case_value_convert(value): return value == 'true' if swith_field_spec.get_field().type == 'java.lang.Boolean' else value
+            cases = [(case_value_convert(value), expr) for (value, expr) in cases]
+            expr = case(cases, formatted)
         return query, blank_nulls(expr)
 
-    def aggregate(self, query: QueryConstruct, field: Union[Field, Relationship] , rel_table: SQLTable, aggregator_name) -> ScalarSelect:
-        
-        logger.info('aggregating field %s on %s using %s', field, rel_table, aggregator_name)
+    def aggregate(self, query: QueryConstruct,
+                  field: Union[Field, Relationship], rel_table: SQLTable,
+                  aggregator_name) -> ScalarSelect:
+
+        logger.info('aggregating field %s on %s using %s', field, rel_table,
+                    aggregator_name)
         specify_model = datamodel.get_table(field.relatedModelName, strict=True)
         aggregatorNode = self.getAggregatorDef(specify_model, aggregator_name)
         if aggregatorNode is None:
             logger.warn("aggregator is not defined")
             return literal(_("<Aggregator not defined.>"))
-        logger.debug("using aggregator: %s", ElementTree.tostring(aggregatorNode))
+        logger.debug("using aggregator: %s",
+                     ElementTree.tostring(aggregatorNode))
         formatter_name = aggregatorNode.attrib.get('format', None)
         separator = aggregatorNode.attrib.get('separator', ',')
         order_by = aggregatorNode.attrib.get('orderfieldname', '')
@@ -169,19 +200,22 @@ class ObjectFormatter(object):
         orm_table = getattr(models, field.relatedModelName)
         order_by = [getattr(orm_table, order_by)] if order_by != '' else []
 
-        join_column = list(inspect(getattr(orm_table, field.otherSideName)).property.local_columns)[0]
+        join_column = list(inspect(
+            getattr(orm_table, field.otherSideName)).property.local_columns)[0]
         subquery = QueryConstruct(
             collection=query.collection,
             objectformatter=self,
             query=orm.Query([]).select_from(orm_table) \
-                             .filter(join_column == getattr(rel_table, rel_table._id)) \
-                             .correlate(rel_table)
+                .filter(join_column == getattr(rel_table, rel_table._id)) \
+                .correlate(rel_table)
         )
-        subquery, formatted = self.objformat(subquery, orm_table, formatter_name)
+        subquery, formatted = self.objformat(subquery, orm_table,
+                                             formatter_name)
         aggregated = blank_nulls(group_concat(formatted, separator, *order_by))
         return subquery.query.add_column(aggregated).as_scalar()
 
-    def fieldformat(self, query_field: QueryField, field: blank_nulls) -> blank_nulls:
+    def fieldformat(self, query_field: QueryField,
+                    field: blank_nulls) -> blank_nulls:
         field_spec = query_field.fieldspec
         if field_spec.get_field() is not None:
             if field_spec.is_temporal() and field_spec.date_part == "Full Date":
@@ -205,13 +239,15 @@ class ObjectFormatter(object):
         prec_fld = getattr(field.class_, specify_field.name + 'Precision', None)
 
         format_expr = \
-            case({2: self.date_format_month, 3: self.date_format_year}, prec_fld, else_=self.date_format) \
-            if prec_fld is not None \
-            else self.date_format
+            case({2: self.date_format_month, 3: self.date_format_year},
+                 prec_fld, else_=self.date_format) \
+                if prec_fld is not None \
+                else self.date_format
 
         return func.date_format(field, format_expr)
 
-    def _fieldformat(self, specify_field: Field, field: Union[InstrumentedAttribute, Extract]):
+    def _fieldformat(self, specify_field: Field,
+                     field: Union[InstrumentedAttribute, Extract]):
         if specify_field.type == "java.lang.Boolean":
             return field != 0
 
@@ -219,11 +255,13 @@ class ObjectFormatter(object):
             return field
 
         if specify_field is CollectionObject_model.get_field('catalogNumber') \
-           and self.catalog_number_is_numeric():
-            return cast(field, types.Numeric(65)) # 65 is the mysql max precision
+                and self.catalog_number_is_numeric():
+            return cast(field,
+                        types.Numeric(65))  # 65 is the mysql max precision
 
         return field
 
+
 def get_date_format() -> str:
     match = re.search(r'ui\.formatting\.scrdateformat=(.+)', get_remote_prefs())
     date_format = match.group(1).strip() if match is not None else 'yyyy-MM-dd'
@@ -287,24 +325,24 @@ MYSQL_TO_MONTH = {
 }
 
 LDLM_TO_MYSQL = {
-    "MM dd yy":   "%m %d %y",
+    "MM dd yy": "%m %d %y",
     "MM dd yyyy": "%m %d %Y",
-    "MM-dd-yy":   "%m-%d-%y",
+    "MM-dd-yy": "%m-%d-%y",
     "MM-dd-yyyy": "%m-%d-%Y",
-    "MM.dd.yy":   "%m.%d.%y",
+    "MM.dd.yy": "%m.%d.%y",
     "MM.dd.yyyy": "%m.%d.%Y",
-    "MM/dd/yy":   "%m/%d/%y",
+    "MM/dd/yy": "%m/%d/%y",
     "MM/dd/yyyy": "%m/%d/%Y",
-    "dd MM yy":   "%d %m %y",
+    "dd MM yy": "%d %m %y",
     "dd MM yyyy": "%d %m %Y",
-    "dd MMM yyyy":"%d %b %Y",
-    "dd-MM-yy":   "%d-%m-%y",
+    "dd MMM yyyy": "%d %b %Y",
+    "dd-MM-yy": "%d-%m-%y",
     "dd-MM-yyyy": "%d-%m-%Y",
-    "dd-MMM-yyyy":"%d-%b-%Y",
-    "dd.MM.yy":   "%d.%m.%y",
+    "dd-MMM-yyyy": "%d-%b-%Y",
+    "dd.MM.yy": "%d.%m.%y",
     "dd.MM.yyyy": "%d.%m.%Y",
-    "dd.MMM.yyyy":"%d.%b.%Y",
-    "dd/MM/yy":   "%d/%m/%y",
+    "dd.MMM.yyyy": "%d.%b.%Y",
+    "dd/MM/yy": "%d/%m/%y",
     "dd/MM/yyyy": "%d/%m/%Y",
     "dd/MMM/yyy": "%d/%b/%Y",
     "yyyy MM dd": "%Y %m %d",
['specifyweb/frontend/js_src/lib/localization/query.ts', 'specifyweb/frontend/js_src/lib/components/Statistics/hooks.tsx', 'specifyweb/stats/views.py', 'specifyweb/frontend/js_src/lib/components/Statistics/StatsSpec.tsx', 'specifyweb/frontend/js_src/lib/components/Statistics/types.ts', 'specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx', 'specifyweb/context/collection_resources.py', 'specifyweb/context/resources.py', 'specifyweb/frontend/js_src/lib/localization/stats.tsx', 'specifyweb/frontend/js_src/lib/hooks/useAsyncState.tsx', 'specifyweb/frontend/js_src/lib/components/Statistics/StatItems.tsx', 'specifyweb/frontend/js_src/lib/utils/cache/index.ts', 'specifyweb/report_runner/views.py', 'specifyweb/frontend/js_src/lib/components/QueryBuilder/helpers.ts', 'specifyweb/frontend/js_src/lib/components/Statistics/index.tsx', 'specifyweb/frontend/js_src/lib/components/Statistics/Categories.tsx', 'specifyweb/frontend/js_src/lib/components/Statistics/AddStatDialog.tsx', 'specifyweb/stats/urls.py', 'CHANGELOG.md', 'specifyweb/frontend/js_src/lib/components/QueryBuilder/RelativeDate.tsx', 'specifyweb/frontend/js_src/lib/components/Statistics/__tests__/hooks.test.ts', 'specifyweb/stored_queries/format.py', 'specifyweb/frontend/js_src/lib/components/Statistics/ResultsDialog.tsx', 'specifyweb/frontend/js_src/lib/components/QueryBuilder/CheckReadAccess.tsx', 'specifyweb/context/user_resources.py', 'specifyweb/frontend/js_src/lib/utils/ajax/throttledPromise.ts', 'specifyweb/frontend/js_src/lib/components/Statistics/__tests__/layout.tests.ts', 'specifyweb/frontend/js_src/lib/tests/setup.ts', 'specifyweb/frontend/js_src/lib/components/Statistics/StatsResult.tsx', 'specifyweb/frontend/js_src/lib/components/QueryBuilder/ResultsWrapper.tsx']