diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.js b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
similarity index 62%
rename from packages/desktop-client/src/components/reports/ChooseGraph.js
rename to packages/desktop-client/src/components/reports/ChooseGraph.tsx
index bf2edbb2e445772fd5ca2aed6074adb30b33918e..4dca723153a139d059304567f4e18fd0e4fa7647 100644
--- a/packages/desktop-client/src/components/reports/ChooseGraph.js
+++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useRef } from 'react';
 
 import View from '../common/View';
 
@@ -15,8 +15,6 @@ import ReportTableList from './ReportTableList';
 import ReportTableTotals from './ReportTableTotals';
 
 export function ChooseGraph({
-  start,
-  end,
   data,
   mode,
   graphType,
@@ -27,18 +25,23 @@ export function ChooseGraph({
   setScrollWidth,
   months,
 }) {
-  function saveScrollWidth(parent, child) {
-    const width = parent > 0 && child > 0 && parent - child;
+  const saveScrollWidth = value => {
+    setScrollWidth(!value ? 0 : value);
+  };
 
-    setScrollWidth(!width ? 0 : width);
-  }
+  const headerScrollRef = useRef<HTMLDivElement>(null);
+  const listScrollRef = useRef<HTMLDivElement>(null);
+  const totalScrollRef = useRef<HTMLDivElement>(null);
+
+  const handleScrollTotals = scroll => {
+    headerScrollRef.current.scrollLeft = scroll.target.scrollLeft;
+    listScrollRef.current.scrollLeft = scroll.target.scrollLeft;
+  };
 
   if (graphType === 'AreaGraph') {
     return (
       <AreaGraph
         style={{ flexGrow: 1 }}
-        start={start}
-        end={end}
         data={data}
         balanceTypeOp={ReportOptions.balanceTypeMap.get(balanceType)}
       />
@@ -48,8 +51,6 @@ export function ChooseGraph({
     return (
       <BarGraph
         style={{ flexGrow: 1 }}
-        start={start}
-        end={end}
         data={data}
         groupBy={groupBy}
         empty={empty}
@@ -58,21 +59,12 @@ export function ChooseGraph({
     );
   }
   if (graphType === 'BarLineGraph') {
-    return (
-      <BarLineGraph
-        style={{ flexGrow: 1 }}
-        start={start}
-        end={end}
-        graphData={data.graphData}
-      />
-    );
+    return <BarLineGraph style={{ flexGrow: 1 }} graphData={data.graphData} />;
   }
   if (graphType === 'DonutGraph') {
     return (
       <DonutGraph
         style={{ flexGrow: 1 }}
-        start={start}
-        end={end}
         data={data}
         groupBy={groupBy}
         empty={empty}
@@ -81,40 +73,25 @@ export function ChooseGraph({
     );
   }
   if (graphType === 'LineGraph') {
-    return (
-      <LineGraph
-        style={{ flexGrow: 1 }}
-        start={start}
-        end={end}
-        graphData={data.graphData}
-      />
-    );
+    return <LineGraph style={{ flexGrow: 1 }} graphData={data.graphData} />;
   }
   if (graphType === 'StackedBarGraph') {
-    return (
-      <StackedBarGraph
-        style={{ flexGrow: 1 }}
-        start={start}
-        end={end}
-        data={data}
-        balanceTypeOp={ReportOptions.balanceTypeMap.get(balanceType)}
-      />
-    );
+    return <StackedBarGraph style={{ flexGrow: 1 }} data={data} />;
   }
   if (graphType === 'TableGraph') {
     return (
-      <View
-        style={{
-          overflow: 'auto',
-        }}
-      >
+      <View>
         <ReportTableHeader
+          headerScrollRef={headerScrollRef}
           interval={mode === 'time' && months}
           scrollWidth={scrollWidth}
           groupBy={groupBy}
           balanceType={balanceType}
         />
-        <ReportTable saveScrollWidth={saveScrollWidth}>
+        <ReportTable
+          saveScrollWidth={saveScrollWidth}
+          listScrollRef={listScrollRef}
+        >
           <ReportTableList
             data={data}
             empty={empty}
@@ -123,15 +100,16 @@ export function ChooseGraph({
             mode={mode}
             groupBy={groupBy}
           />
-          <ReportTableTotals
-            scrollWidth={scrollWidth}
-            data={data}
-            mode={mode}
-            balanceTypeOp={ReportOptions.balanceTypeMap.get(balanceType)}
-            monthsCount={months.length}
-            balanceType={balanceType}
-          />
         </ReportTable>
+        <ReportTableTotals
+          totalScrollRef={totalScrollRef}
+          handleScrollTotals={handleScrollTotals}
+          scrollWidth={scrollWidth}
+          data={data}
+          mode={mode}
+          balanceTypeOp={ReportOptions.balanceTypeMap.get(balanceType)}
+          monthsCount={months.length}
+        />
       </View>
     );
   }
diff --git a/packages/desktop-client/src/components/reports/ReportOptions.tsx b/packages/desktop-client/src/components/reports/ReportOptions.tsx
index 73e2fff5b516f4a6ed5018709263ef9f2289f09b..69f0ef1626c419bd7252a5bb667d007086ab9f19 100644
--- a/packages/desktop-client/src/components/reports/ReportOptions.tsx
+++ b/packages/desktop-client/src/components/reports/ReportOptions.tsx
@@ -1,3 +1,10 @@
+import {
+  type AccountEntity,
+  type CategoryEntity,
+  type CategoryGroupEntity,
+  type PayeeEntity,
+} from 'loot-core/src/types/models';
+
 const balanceTypeOptions = [
   { description: 'Expense', format: 'totalDebts' },
   { description: 'Income', format: 'totalAssets' },
@@ -44,3 +51,128 @@ const intervalOptions = [
 { value: 5, description: 'Yearly', name: 5,
 ];
 */
+export type QueryDataEntity = {
+  date: string;
+  category: string;
+  categoryGroup: string;
+  account: string;
+  accountOffBudget: boolean;
+  payee: string;
+  transferAccount: string;
+  amount: number;
+};
+
+export type UncategorizedEntity = CategoryEntity & {
+  /*
+    When looking at uncategorized and hidden transactions we
+    need a way to group them. To do this we give them a unique
+    uncategorized_id. We also need a way to filter the
+    transctions from our query. For this we use the 3 variables
+    below.
+  */
+  uncategorized_id: string;
+  is_off_budget: boolean;
+  is_transfer: boolean;
+  has_category: boolean;
+};
+
+const uncategorizedCategory: UncategorizedEntity = {
+  name: 'Uncategorized',
+  id: null,
+  uncategorized_id: '1',
+  hidden: false,
+  is_off_budget: false,
+  is_transfer: false,
+  has_category: false,
+};
+const transferCategory: UncategorizedEntity = {
+  name: 'Transfers',
+  id: null,
+  uncategorized_id: '2',
+  hidden: false,
+  is_off_budget: false,
+  is_transfer: true,
+  has_category: false,
+};
+const offBudgetCategory: UncategorizedEntity = {
+  name: 'Off Budget',
+  id: null,
+  uncategorized_id: '3',
+  hidden: false,
+  is_off_budget: true,
+  is_transfer: false,
+  has_category: true,
+};
+
+type UncategorizedGroupEntity = CategoryGroupEntity & {
+  categories?: UncategorizedEntity[];
+};
+
+const uncategouncatGrouprizedGroup: UncategorizedGroupEntity = {
+  name: 'Uncategorized & Off Budget',
+  id: null,
+  hidden: false,
+  categories: [uncategorizedCategory, transferCategory, offBudgetCategory],
+};
+
+export const categoryLists = (
+  showOffBudgetHidden: boolean,
+  showUncategorized: boolean,
+  categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] },
+) => {
+  const categoryList = showUncategorized
+    ? [
+        ...categories.list,
+        uncategorizedCategory,
+        transferCategory,
+        offBudgetCategory,
+      ]
+    : categories.list;
+  const categoryGroup = showUncategorized
+    ? [
+        ...categories.grouped.filter(f => showOffBudgetHidden || !f.hidden),
+        uncategouncatGrouprizedGroup,
+      ]
+    : categories.grouped;
+  return [categoryList, categoryGroup] as const;
+};
+
+export const groupBySelections = (
+  groupBy: string,
+  categoryList: CategoryEntity[],
+  categoryGroup: CategoryGroupEntity[],
+  payees: PayeeEntity[],
+  accounts: AccountEntity[],
+) => {
+  let groupByList;
+  let groupByLabel;
+  switch (groupBy) {
+    case 'Category':
+      groupByList = categoryList;
+      groupByLabel = 'category';
+      break;
+    case 'Group':
+      groupByList = categoryGroup;
+      groupByLabel = 'categoryGroup';
+      break;
+    case 'Payee':
+      groupByList = payees;
+      groupByLabel = 'payee';
+      break;
+    case 'Account':
+      groupByList = accounts;
+      groupByLabel = 'account';
+      break;
+    case 'Month':
+      groupByList = categoryList;
+      groupByLabel = 'category';
+      break;
+    case 'Year':
+      groupByList = categoryList;
+      groupByLabel = 'category';
+      break;
+    default:
+      throw new Error('Error loading data into the spreadsheet.');
+  }
+  return [groupByList, groupByLabel];
+};
diff --git a/packages/desktop-client/src/components/reports/ReportTable.tsx b/packages/desktop-client/src/components/reports/ReportTable.tsx
index 11673cc45d179a21a8ac915f72f2f29e5cde4c26..a16f47c4cd938b4c6f24e86f346c040bed26fbab 100644
--- a/packages/desktop-client/src/components/reports/ReportTable.tsx
+++ b/packages/desktop-client/src/components/reports/ReportTable.tsx
@@ -1,24 +1,37 @@
-import React, { useLayoutEffect, useRef } from 'react';
+import React, { useLayoutEffect, useRef, type ReactNode } from 'react';
+import { type RefProp } from 'react-spring';
 
+import { type CSSProperties } from '../../style';
 import View from '../common/View';
 
-export default function ReportTable({ saveScrollWidth, style, children }) {
-  const contentRef = useRef<HTMLDivElement>();
+type ReportTableProps = {
+  saveScrollWidth?: (value: number) => void;
+  listScrollRef?: RefProp<HTMLDivElement>;
+  style?: CSSProperties;
+  children?: ReactNode;
+};
+
+export default function ReportTable({
+  saveScrollWidth,
+  listScrollRef,
+  style,
+  children,
+}: ReportTableProps) {
+  const contentRef = useRef<HTMLDivElement>(null);
 
   useLayoutEffect(() => {
     if (contentRef.current && saveScrollWidth) {
-      saveScrollWidth(
-        contentRef.current.offsetParent
-          ? contentRef.current.parentElement.offsetWidth
-          : 0,
-        contentRef.current ? contentRef.current.offsetWidth : 0,
-      );
+      saveScrollWidth(contentRef.current ? contentRef.current.offsetWidth : 0);
     }
   });
 
   return (
     <View
+      innerRef={listScrollRef}
       style={{
+        overflowY: 'auto',
+        scrollbarWidth: 'none',
+        '::-webkit-scrollbar': { display: 'none' },
         flex: 1,
         outline: 'none',
         '& .animated .animated-row': { transition: '.25s transform' },
diff --git a/packages/desktop-client/src/components/reports/ReportTableHeader.tsx b/packages/desktop-client/src/components/reports/ReportTableHeader.tsx
index 48ce3585072975a5a0c871cb227a7a4d5401dc77..a2aaee9f506aa90ec3eee287bf33d960e20fa1b0 100644
--- a/packages/desktop-client/src/components/reports/ReportTableHeader.tsx
+++ b/packages/desktop-client/src/components/reports/ReportTableHeader.tsx
@@ -1,85 +1,107 @@
-import React from 'react';
+import React, { type Ref } from 'react';
 
 import * as d from 'date-fns';
 
 import { styles, theme } from '../../style';
+import View from '../common/View';
 import { Row, Cell } from '../table';
 
+type ReportTableHeaderProps = {
+  scrollWidth?: number;
+  groupBy: string;
+  interval?: Array<string>;
+  balanceType: string;
+  headerScrollRef?: Ref<HTMLDivElement>;
+};
+
 export default function ReportTableHeader({
   scrollWidth,
   groupBy,
   interval,
   balanceType,
-}) {
+  headerScrollRef,
+}: ReportTableHeaderProps) {
   return (
-    <Row
-      collapsed={true}
+    <View
+      innerRef={headerScrollRef}
       style={{
-        color: theme.tableHeaderText,
-        backgroundColor: theme.tableHeaderBackground,
-        fontWeight: 600,
+        overflowX: 'auto',
+        scrollbarWidth: 'none',
+        '::-webkit-scrollbar': { display: 'none' },
+        justifyContent: 'center',
+        borderTopWidth: 1,
+        borderColor: theme.tableBorder,
       }}
     >
-      <Cell
-        style={{
-          minWidth: 125,
-          ...styles.tnum,
-        }}
-        value={groupBy}
-        width="flex"
-      />
-      {interval
-        ? interval.map(header => {
-            return (
-              <Cell
-                style={{
-                  minWidth: 85,
-                  ...styles.tnum,
-                }}
-                key={header}
-                // eslint-disable-next-line rulesdir/typography
-                value={d.format(d.parseISO(`${header}-01`), "MMM ''yy")}
-                width="flex"
-              />
-            );
-          })
-        : balanceType === 'Net' && (
-            <>
-              <Cell
-                style={{
-                  minWidth: 85,
-                  ...styles.tnum,
-                }}
-                value={'Assets'}
-                width="flex"
-              />
-              <Cell
-                style={{
-                  minWidth: 85,
-                  ...styles.tnum,
-                }}
-                value={'Debts'}
-                width="flex"
-              />
-            </>
-          )}
-      <Cell
-        style={{
-          minWidth: 85,
-          ...styles.tnum,
-        }}
-        value={'Totals'}
-        width="flex"
-      />
-      <Cell
+      <Row
+        collapsed={true}
         style={{
-          minWidth: 85,
-          ...styles.tnum,
+          color: theme.tableHeaderText,
+          backgroundColor: theme.tableHeaderBackground,
+          fontWeight: 600,
         }}
-        value={'Average'}
-        width="flex"
-      />
-      {scrollWidth > 0 && <Cell width={scrollWidth} />}
-    </Row>
+      >
+        <Cell
+          style={{
+            minWidth: 125,
+            ...styles.tnum,
+          }}
+          value={groupBy}
+          width="flex"
+        />
+        {interval
+          ? interval.map(header => {
+              return (
+                <Cell
+                  style={{
+                    minWidth: 85,
+                    ...styles.tnum,
+                  }}
+                  key={header}
+                  // eslint-disable-next-line rulesdir/typography
+                  value={d.format(d.parseISO(`${header}-01`), "MMM ''yy")}
+                  width="flex"
+                />
+              );
+            })
+          : balanceType === 'Net' && (
+              <>
+                <Cell
+                  style={{
+                    minWidth: 85,
+                    ...styles.tnum,
+                  }}
+                  value={'Assets'}
+                  width="flex"
+                />
+                <Cell
+                  style={{
+                    minWidth: 85,
+                    ...styles.tnum,
+                  }}
+                  value={'Debts'}
+                  width="flex"
+                />
+              </>
+            )}
+        <Cell
+          style={{
+            minWidth: 85,
+            ...styles.tnum,
+          }}
+          value={'Totals'}
+          width="flex"
+        />
+        <Cell
+          style={{
+            minWidth: 85,
+            ...styles.tnum,
+          }}
+          value={'Average'}
+          width="flex"
+        />
+        {scrollWidth > 0 && <Cell width={scrollWidth} />}
+      </Row>
+    </View>
   );
 }
diff --git a/packages/desktop-client/src/components/reports/ReportTableList.tsx b/packages/desktop-client/src/components/reports/ReportTableList.tsx
index d789834dbae9996c177cf08c3ea0d64f9957fa77..ddfdeca2c409f0f96f18c39c4dbb55ea4f398cae 100644
--- a/packages/desktop-client/src/components/reports/ReportTableList.tsx
+++ b/packages/desktop-client/src/components/reports/ReportTableList.tsx
@@ -6,7 +6,7 @@ import {
   integerToCurrency,
 } from 'loot-core/src/shared/util';
 
-import { styles, theme } from '../../style';
+import { type CSSProperties, styles, theme } from '../../style';
 import View from '../common/View';
 import { Row, Cell } from '../table';
 
@@ -18,11 +18,11 @@ type TableRowProps = {
     totalAssets: number;
     totalDebts: number;
   };
-  balanceTypeOp?: string | null;
+  balanceTypeOp?: string;
   groupByItem: string;
   mode: string;
   monthsCount: number;
-  style?: object | null;
+  style?: CSSProperties;
 };
 
 const TableRow = memo(
@@ -197,7 +197,7 @@ export default function ReportTableList({
   const groupByItem = ['Month', 'Year'].includes(groupBy) ? 'date' : 'name';
   const groupByData =
     groupBy === 'Category'
-      ? 'groupData'
+      ? 'groupedData'
       : ['Month', 'Year'].includes(groupBy)
       ? 'monthData'
       : 'data';
diff --git a/packages/desktop-client/src/components/reports/ReportTableTotals.tsx b/packages/desktop-client/src/components/reports/ReportTableTotals.tsx
index 1286a7f87ddde337c0e5e3af9a1fbd3999b5df78..f60a91b5ab5ba4f2859c7ca297a9eac422382c43 100644
--- a/packages/desktop-client/src/components/reports/ReportTableTotals.tsx
+++ b/packages/desktop-client/src/components/reports/ReportTableTotals.tsx
@@ -7,6 +7,7 @@ import {
 } from 'loot-core/src/shared/util';
 
 import { styles, theme } from '../../style';
+import View from '../common/View';
 import { Row, Cell } from '../table';
 
 export default function ReportTableTotals({
@@ -15,100 +16,113 @@ export default function ReportTableTotals({
   balanceTypeOp,
   mode,
   monthsCount,
+  totalScrollRef,
+  handleScrollTotals,
 }) {
   const average = amountToInteger(data[balanceTypeOp]) / monthsCount;
   return (
-    <Row
-      collapsed={true}
+    <View
+      innerRef={totalScrollRef}
+      onScroll={handleScrollTotals}
       style={{
-        color: theme.tableHeaderText,
-        backgroundColor: theme.tableHeaderBackground,
-        fontWeight: 600,
+        overflowX: 'auto',
+        borderTopWidth: 1,
+        borderColor: theme.tableBorder,
+        justifyContent: 'center',
       }}
     >
-      <Cell
+      <Row
+        collapsed={true}
         style={{
-          minWidth: 125,
-          ...styles.tnum,
+          color: theme.tableHeaderText,
+          backgroundColor: theme.tableHeaderBackground,
+          fontWeight: 600,
         }}
-        value={'Totals'}
-        width="flex"
-      />
-      {mode === 'time'
-        ? data.monthData.map(item => {
-            return (
-              <Cell
-                style={{
-                  minWidth: 85,
-                  ...styles.tnum,
-                }}
-                key={amountToCurrency(item[balanceTypeOp])}
-                value={amountToCurrency(item[balanceTypeOp])}
-                title={
-                  Math.abs(item[balanceTypeOp]) > 100000 &&
-                  amountToCurrency(item[balanceTypeOp])
-                }
-                width="flex"
-                privacyFilter
-              />
-            );
-          })
-        : balanceTypeOp === 'totalTotals' && (
-            <>
-              <Cell
-                style={{
-                  minWidth: 85,
-                  ...styles.tnum,
-                }}
-                value={amountToCurrency(data.totalAssets)}
-                title={
-                  Math.abs(data.totalAssets) > 100000 &&
-                  amountToCurrency(data.totalAssets)
-                }
-                width="flex"
-              />
-              <Cell
-                style={{
-                  minWidth: 85,
-                  ...styles.tnum,
-                }}
-                value={amountToCurrency(data.totalDebts)}
-                title={
-                  Math.abs(data.totalDebts) > 100000 &&
-                  amountToCurrency(data.totalDebts)
-                }
-                width="flex"
-              />
-            </>
-          )}
-      <Cell
-        style={{
-          minWidth: 85,
-          ...styles.tnum,
-        }}
-        value={amountToCurrency(data[balanceTypeOp])}
-        title={
-          Math.abs(data[balanceTypeOp]) > 100000 &&
-          amountToCurrency(data[balanceTypeOp])
-        }
-        width="flex"
-        privacyFilter
-      />
-      <Cell
-        style={{
-          minWidth: 85,
-          ...styles.tnum,
-        }}
-        value={integerToCurrency(Math.round(average))}
-        title={
-          Math.abs(Math.round(average / 100)) > 100000 &&
-          integerToCurrency(Math.round(average))
-        }
-        width="flex"
-        privacyFilter
-      />
+      >
+        <Cell
+          style={{
+            minWidth: 125,
+            ...styles.tnum,
+          }}
+          value={'Totals'}
+          width="flex"
+        />
+        {mode === 'time'
+          ? data.monthData.map(item => {
+              return (
+                <Cell
+                  style={{
+                    minWidth: 85,
+                    ...styles.tnum,
+                  }}
+                  key={amountToCurrency(item[balanceTypeOp])}
+                  value={amountToCurrency(item[balanceTypeOp])}
+                  title={
+                    Math.abs(item[balanceTypeOp]) > 100000 &&
+                    amountToCurrency(item[balanceTypeOp])
+                  }
+                  width="flex"
+                  privacyFilter
+                />
+              );
+            })
+          : balanceTypeOp === 'totalTotals' && (
+              <>
+                <Cell
+                  style={{
+                    minWidth: 85,
+                    ...styles.tnum,
+                  }}
+                  value={amountToCurrency(data.totalAssets)}
+                  title={
+                    Math.abs(data.totalAssets) > 100000 &&
+                    amountToCurrency(data.totalAssets)
+                  }
+                  width="flex"
+                />
+                <Cell
+                  style={{
+                    minWidth: 85,
+                    ...styles.tnum,
+                  }}
+                  value={amountToCurrency(data.totalDebts)}
+                  title={
+                    Math.abs(data.totalDebts) > 100000 &&
+                    amountToCurrency(data.totalDebts)
+                  }
+                  width="flex"
+                />
+              </>
+            )}
+        <Cell
+          style={{
+            minWidth: 85,
+            ...styles.tnum,
+          }}
+          value={amountToCurrency(data[balanceTypeOp])}
+          title={
+            Math.abs(data[balanceTypeOp]) > 100000 &&
+            amountToCurrency(data[balanceTypeOp])
+          }
+          width="flex"
+          privacyFilter
+        />
+        <Cell
+          style={{
+            minWidth: 85,
+            ...styles.tnum,
+          }}
+          value={integerToCurrency(Math.round(average))}
+          title={
+            Math.abs(Math.round(average / 100)) > 100000 &&
+            integerToCurrency(Math.round(average))
+          }
+          width="flex"
+          privacyFilter
+        />
 
-      {scrollWidth > 0 && <Cell width={scrollWidth} />}
-    </Row>
+        {scrollWidth > 0 && <Cell width={scrollWidth} />}
+      </Row>
+    </View>
   );
 }
diff --git a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
index b50331b52f34f660695795a54a6426810dcdf68c..50022af9df197a6396f9baf617d54423cf36e9d0 100644
--- a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx
@@ -93,8 +93,8 @@ const CustomTooltip = ({
 type AreaGraphProps = {
   style?: CSSProperties;
   data;
-  balanceTypeOp;
-  compact: boolean;
+  balanceTypeOp: string;
+  compact?: boolean;
 };
 
 function AreaGraph({ style, data, balanceTypeOp, compact }: AreaGraphProps) {
@@ -151,7 +151,7 @@ function AreaGraph({ style, data, balanceTypeOp, compact }: AreaGraphProps) {
                 )}
                 {compact ? null : (
                   <YAxis
-                    dataKey={...balanceTypeOp}
+                    dataKey={balanceTypeOp}
                     domain={['auto', 'auto']}
                     tickFormatter={tickFormatter}
                     tick={{ fill: theme.pageText }}
@@ -183,7 +183,7 @@ function AreaGraph({ style, data, balanceTypeOp, compact }: AreaGraphProps) {
                   dot={false}
                   activeDot={false}
                   animationDuration={0}
-                  dataKey={...balanceTypeOp}
+                  dataKey={balanceTypeOp}
                   stroke={theme.reportsBlue}
                   fill="url(#splitColor)"
                   fillOpacity={1}
diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
index 2d9ec64b203aed08b90a3dbec9ee4d4572d4ef3f..dd8b5558e83e4be36e736e0a2288e5ba93988c16 100644
--- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx
@@ -130,13 +130,10 @@ const CustomLegend = ({ active, payload, label }: CustomLegendProps) => {
 type BarGraphProps = {
   style?: CSSProperties;
   data;
-  groupBy;
+  groupBy: string;
   balanceTypeOp;
-  empty;
-  compact: boolean;
-  domain?: {
-    y?: [number, number];
-  };
+  empty: boolean;
+  compact?: boolean;
 };
 
 function BarGraph({
@@ -146,7 +143,6 @@ function BarGraph({
   empty,
   balanceTypeOp,
   compact,
-  domain,
 }: BarGraphProps) {
   const privacyMode = usePrivacyMode();
 
diff --git a/packages/desktop-client/src/components/reports/graphs/BarLineGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarLineGraph.tsx
index 390fa5e5a108f9c9e370305c6d61e001229f70e7..816ffc8d5320c5fde921d64999dd3eea9c1e29af 100644
--- a/packages/desktop-client/src/components/reports/graphs/BarLineGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/BarLineGraph.tsx
@@ -72,18 +72,10 @@ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
 type BarLineGraphProps = {
   style?: CSSProperties;
   graphData;
-  compact: boolean;
-  domain?: {
-    y?: [number, number];
-  };
+  compact?: boolean;
 };
 
-function BarLineGraph({
-  style,
-  graphData,
-  compact,
-  domain,
-}: BarLineGraphProps) {
+function BarLineGraph({ style, graphData, compact }: BarLineGraphProps) {
   const tickFormatter = tick => {
     return `${Math.round(tick).toLocaleString()}`; // Formats the tick values as strings with commas
   };
diff --git a/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx b/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx
index 41798b395a4b19eefcf8fd42751b2b4b956fe79a..a8ad506a00ef2a40246923e3dd7461b9c3f24237 100644
--- a/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx
@@ -95,13 +95,10 @@ const CustomLegend = ({ active, payload, label }: CustomLegendProps) => {
 type DonutGraphProps = {
   style?: CSSProperties;
   data;
-  groupBy;
-  balanceTypeOp;
-  empty;
-  compact: boolean;
-  domain?: {
-    y?: [number, number];
-  };
+  groupBy: string;
+  balanceTypeOp: string;
+  empty: boolean;
+  compact?: boolean;
 };
 
 function DonutGraph({
@@ -111,7 +108,6 @@ function DonutGraph({
   empty,
   balanceTypeOp,
   compact,
-  domain,
 }: DonutGraphProps) {
   const colorScale = getColorScale('qualitative');
   const yAxis = ['Month', 'Year'].includes(groupBy) ? 'date' : 'name';
diff --git a/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx b/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx
index 1c373de3936f9cfbef61ae2b45784cec1692e36b..069082c263009218f7a7c816ec75282182115641 100644
--- a/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/LineGraph.tsx
@@ -71,13 +71,10 @@ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
 type LineGraphProps = {
   style?: CSSProperties;
   graphData;
-  compact: boolean;
-  domain?: {
-    y?: [number, number];
-  };
+  compact?: boolean;
 };
 
-function LineGraph({ style, graphData, compact, domain }: LineGraphProps) {
+function LineGraph({ style, graphData, compact }: LineGraphProps) {
   const tickFormatter = tick => {
     return `${Math.round(tick).toLocaleString()}`; // Formats the tick values as strings with commas
   };
diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
index 8b0ba322245ff1cec1d67444d1c5ca1bd1ca5a90..37e2b3ea41c859607b09243b5b03c55be42a5327 100644
--- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx
@@ -116,8 +116,7 @@ const CustomLegend = ({ active, payload, label }: CustomLegendProps) => {
 type StackedBarGraphProps = {
   style?: CSSProperties;
   data;
-  balanceTypeOp;
-  compact: boolean;
+  compact?: boolean;
 };
 
 function StackedBarGraph({ style, data, compact }: StackedBarGraphProps) {
@@ -132,14 +131,14 @@ function StackedBarGraph({ style, data, compact }: StackedBarGraphProps) {
       }}
     >
       {(width, height, portalHost) =>
-        data.stackedData && (
+        data.monthData && (
           <ResponsiveContainer>
             <div>
               {!compact && <div style={{ marginTop: '15px' }} />}
               <BarChart
                 width={width}
                 height={height}
-                data={data.stackedData}
+                data={data.monthData}
                 margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
               >
                 {
@@ -163,7 +162,7 @@ function StackedBarGraph({ style, data, compact }: StackedBarGraphProps) {
                     tickLine={{ stroke: theme.pageText }}
                   />
                 )}
-                {data.groupBy.reverse().map((c, index) => (
+                {data.data.reverse().map((c, index) => (
                   <Bar
                     key={c.date}
                     dataKey={c.name}
diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.js b/packages/desktop-client/src/components/reports/reports/CustomReport.js
index 5ec38146d7e943d371491f7839287c74bba0f25d..a8d1f3a42cfa419635f463aa28013a8ac9b0904f 100644
--- a/packages/desktop-client/src/components/reports/reports/CustomReport.js
+++ b/packages/desktop-client/src/components/reports/reports/CustomReport.js
@@ -25,6 +25,7 @@ import { ReportSidebar } from '../ReportSidebar';
 import { ReportLegend, ReportSummary } from '../ReportSummary';
 import { ReportTopbar } from '../ReportTopbar';
 import defaultSpreadsheet from '../spreadsheets/default-spreadsheet';
+import groupedSpreadsheet from '../spreadsheets/grouped-spreadsheet';
 import useReport from '../useReport';
 import { fromDateRepr } from '../util';
 
@@ -100,27 +101,49 @@ export default function CustomReport() {
     }
     run();
   }, []);
-
+  const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType);
   const payees = useCachedPayees();
   const accounts = useCachedAccounts();
 
+  const getGroupData = useMemo(() => {
+    return groupedSpreadsheet({
+      start,
+      end,
+      categories,
+      selectedCategories,
+      filters,
+      conditionsOp,
+      hidden,
+      uncat,
+    });
+  }, [
+    start,
+    end,
+    categories,
+    selectedCategories,
+    filters,
+    conditionsOp,
+    hidden,
+    uncat,
+  ]);
+
   const getGraphData = useMemo(() => {
     setDataCheck(false);
-    return defaultSpreadsheet(
+    return defaultSpreadsheet({
       start,
       end,
-      groupBy,
-      ReportOptions.balanceTypeMap.get(balanceType),
       categories,
       selectedCategories,
-      payees,
-      accounts,
       filters,
       conditionsOp,
       hidden,
       uncat,
+      groupBy,
+      balanceTypeOp,
+      payees,
+      accounts,
       setDataCheck,
-    );
+    });
   }, [
     start,
     end,
@@ -135,7 +158,10 @@ export default function CustomReport() {
     hidden,
     uncat,
   ]);
-  const data = useReport('default', getGraphData);
+  const graphData = useReport('default', getGraphData);
+  const groupedData = useReport('grouped', getGroupData);
+
+  const data = { ...graphData, groupedData };
 
   const [scrollWidth, setScrollWidth] = useState(0);
 
@@ -267,15 +293,7 @@ export default function CustomReport() {
                         right={
                           <Text>
                             <PrivacyFilter blurIntensity={5}>
-                              {amountToCurrency(
-                                Math.abs(
-                                  data[
-                                    ReportOptions.balanceTypeMap.get(
-                                      balanceType,
-                                    )
-                                  ],
-                                ),
-                              )}
+                              {amountToCurrency(Math.abs(data[balanceTypeOp]))}
                             </PrivacyFilter>
                           </Text>
                         }
@@ -317,9 +335,7 @@ export default function CustomReport() {
                     <ReportSummary
                       start={start}
                       end={end}
-                      balanceTypeOp={ReportOptions.balanceTypeMap.get(
-                        balanceType,
-                      )}
+                      balanceTypeOp={balanceTypeOp}
                       data={data}
                       monthsCount={months.length}
                     />
diff --git a/packages/desktop-client/src/components/reports/reports/CustomReportCard.js b/packages/desktop-client/src/components/reports/reports/CustomReportCard.js
index 6ad6a7c4a28ad2c59f989b3144926fcddef4150c..b4b3d53114f669f837bb78cda560576f3033c09d 100644
--- a/packages/desktop-client/src/components/reports/reports/CustomReportCard.js
+++ b/packages/desktop-client/src/components/reports/reports/CustomReportCard.js
@@ -21,7 +21,13 @@ function CustomReportCard() {
   const groupBy = 'Category';
 
   const getGraphData = useMemo(() => {
-    return defaultSpreadsheet(start, end, groupBy, 'totalDebts', categories);
+    return defaultSpreadsheet({
+      start,
+      end,
+      groupBy,
+      balanceTypeOp: 'totalDebts',
+      categories,
+    });
   }, [start, end, categories]);
   const data = useReport('default', getGraphData);
 
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/default-spreadsheet.tsx b/packages/desktop-client/src/components/reports/spreadsheets/default-spreadsheet.tsx
index e3a9d037fd2a99057badaad34273f5df255b6910..47c1cf8cdb1ce8de8ba157be0e224723217b64b3 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/default-spreadsheet.tsx
+++ b/packages/desktop-client/src/components/reports/spreadsheets/default-spreadsheet.tsx
@@ -1,61 +1,55 @@
 import * as d from 'date-fns';
 
-import q, { runQuery } from 'loot-core/src/client/query-helpers';
+import { runQuery } from 'loot-core/src/client/query-helpers';
 import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
-import { integerToAmount, amountToInteger } from 'loot-core/src/shared/util';
-
-import { index, indexStack } from '../util';
-
-export default function createSpreadsheet(
+import { integerToAmount } from 'loot-core/src/shared/util';
+import {
+  type AccountEntity,
+  type PayeeEntity,
+  type CategoryEntity,
+  type RuleConditionEntity,
+  type CategoryGroupEntity,
+} from 'loot-core/src/types/models';
+
+import { categoryLists, groupBySelections } from '../ReportOptions';
+
+import filterHiddenItems from './filterHiddenItems';
+import makeQuery from './makeQuery';
+import recalculate from './recalculate';
+
+export type createSpreadsheetProps = {
+  start: string;
+  end: string;
+  categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] };
+  selectedCategories: CategoryEntity[];
+  conditions: RuleConditionEntity[];
+  conditionsOp: string;
+  hidden: boolean;
+  uncat: boolean;
+  groupBy?: string;
+  balanceTypeOp?: string;
+  payees?: PayeeEntity[];
+  accounts?: AccountEntity[];
+  setDataCheck?: (value: boolean) => void;
+};
+
+export default function createSpreadsheet({
   start,
   end,
-  groupBy,
-  balanceTypeOp,
   categories,
   selectedCategories,
-  payees,
-  accounts,
   conditions = [],
   conditionsOp,
   hidden,
   uncat,
+  groupBy,
+  balanceTypeOp,
+  payees,
+  accounts,
   setDataCheck,
-) {
-  const uncatCat = {
-    name: 'Uncategorized',
-    id: null,
-    uncat_id: '1',
-    hidden: 0,
-    offBudget: false,
-  };
-  const uncatTransfer = {
-    name: 'Transfers',
-    id: null,
-    uncat_id: '2',
-    hidden: 0,
-    transfer: false,
-  };
-  const uncatOff = {
-    name: 'OffBudget',
-    id: null,
-    uncat_id: '3',
-    hidden: 0,
-    offBudget: true,
-  };
-
-  const uncatGroup = {
-    name: 'Uncategorized',
-    id: null,
-    hidden: 0,
-    categories: [uncatCat, uncatTransfer, uncatOff],
-  };
-  const catList = uncat
-    ? [...categories.list, uncatCat, uncatTransfer, uncatOff]
-    : categories.list;
-  const catGroup = uncat
-    ? [...categories.grouped, uncatGroup]
-    : categories.grouped;
+}) {
+  const [catList, catGroup] = categoryLists(hidden, uncat, categories);
 
   const categoryFilter = (catList || []).filter(
     category =>
@@ -66,35 +60,13 @@ export default function createSpreadsheet(
       ),
   );
 
-  let groupByList;
-  let groupByLabel;
-  switch (groupBy) {
-    case 'Category':
-      groupByList = catList;
-      groupByLabel = 'category';
-      break;
-    case 'Group':
-      groupByList = catList;
-      groupByLabel = 'category';
-      break;
-    case 'Payee':
-      groupByList = payees;
-      groupByLabel = 'payee';
-      break;
-    case 'Account':
-      groupByList = accounts;
-      groupByLabel = 'account';
-      break;
-    case 'Month':
-      groupByList = catList;
-      groupByLabel = 'category';
-      break;
-    case 'Year':
-      groupByList = catList;
-      groupByLabel = 'category';
-      break;
-    default:
-  }
+  const [groupByList, groupByLabel] = groupBySelections(
+    groupBy,
+    catList,
+    catGroup,
+    payees,
+    accounts,
+  );
 
   return async (spreadsheet, setData) => {
     if (groupByList.length === 0) {
@@ -106,388 +78,99 @@ export default function createSpreadsheet(
     });
     const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
 
-    function makeQuery(splt, name) {
-      const query = q('transactions')
-        .filter(
-          //Show Offbudget and hidden categories
-          !hidden && {
-            $and: [
-              {
-                'account.offbudget': false,
-                $or: [
-                  {
-                    'category.hidden': false,
-                    category: null,
-                  },
-                ],
-              },
-            ],
-            $or: [
-              {
-                'payee.transfer_acct.offbudget': true,
-                'payee.transfer_acct': null,
-              },
-            ],
-          },
-        )
-        //Apply Category_Selector
-        .filter(
-          selectedCategories && {
-            $or: [
-              {
-                category: null,
-                $or: categoryFilter.map(category => ({
-                  category: category.id,
-                })),
-              },
-            ],
-          },
-        )
-        //Calculate uncategorized transactions when box checked
-        .filter(
-          splt.uncat_id === '2'
-            ? {
-                'payee.transfer_acct.closed': false,
-              }
-            : {
-                'payee.transfer_acct': null,
-                'account.offbudget': splt.offBudget ? splt.offBudget : false,
-              },
-        )
-        //Apply filters and split by "Group By"
-        .filter({
-          [conditionsOpKey]: [...filters],
-          [groupByLabel]: splt.id,
-        })
-        //Apply month range filters
-        .filter({
-          $and: [
-            { date: { $transform: '$month', $gte: start } },
-            { date: { $transform: '$month', $lte: end } },
-          ],
-        })
-        //Show assets or debts
-        .filter(
-          name === 'assets' ? { amount: { $gt: 0 } } : { amount: { $lt: 0 } },
-        );
-
-      return query
-        .groupBy({ $month: '$date' })
-        .select([
-          { date: { $month: '$date' } },
-          { [name]: { $sum: '$amount' } },
-        ]);
-    }
-
-    const graphData = await Promise.all(
-      groupByList.map(async splt => {
-        const [starting, assets, debts] = await Promise.all([
-          runQuery(
-            q('transactions')
-              .filter(
-                !hidden && {
-                  $and: [
-                    {
-                      'account.offbudget': false,
-                      $or: [
-                        {
-                          'category.hidden': false,
-                          category: null,
-                        },
-                      ],
-                    },
-                  ],
-                  $or: [
-                    {
-                      'payee.transfer_acct.offbudget': true,
-                      'payee.transfer_acct': null,
-                    },
-                  ],
-                },
-              )
-              .filter(
-                splt.uncat_id === '2'
-                  ? {
-                      'payee.transfer_acct.closed': false,
-                    }
-                  : {
-                      'payee.transfer_acct': null,
-                      'account.offbudget': splt.offBudget
-                        ? splt.offBudget
-                        : false,
-                    },
-              )
-              .filter(
-                selectedCategories && {
-                  $or: categoryFilter.map(category => ({
-                    category: category.id,
-                  })),
-                },
-              )
-              .filter({
-                [conditionsOpKey]: [...filters],
-                [groupByLabel]: splt.id,
-              })
-              .filter({
-                $and: [{ date: { $lt: start + '-01' } }],
-              })
-              .calculate({ $sum: '$amount' }),
-          ).then(({ data }) => data),
-
-          runQuery(makeQuery(splt, 'assets')).then(({ data }) => data),
-          runQuery(makeQuery(splt, 'debts')).then(({ data }) => data),
-        ]);
-
-        return {
-          id: splt.id,
-          uncat_id: splt.uncat_id,
-          name: splt.name,
-          starting,
-          hidden: splt.hidden,
-          assets: index(assets, 'date'),
-          debts: index(debts, 'date'),
-        };
-      }),
-    );
+    const [assets, debts] = await Promise.all([
+      runQuery(
+        makeQuery(
+          'assets',
+          start,
+          end,
+          hidden,
+          selectedCategories,
+          categoryFilter,
+          conditionsOpKey,
+          filters,
+        ),
+      ).then(({ data }) => data),
+      runQuery(
+        makeQuery(
+          'debts',
+          start,
+          end,
+          hidden,
+          selectedCategories,
+          categoryFilter,
+          conditionsOpKey,
+          filters,
+        ),
+      ).then(({ data }) => data),
+    ]);
 
     const months = monthUtils.rangeInclusive(start, end);
-    const calcData = graphData.map(graph => {
-      let graphStarting = 0;
-      const mon = months.map(month => {
-        let graphAssets = 0;
-        let graphDebts = 0;
-        if (graph.assets[month] || graph.debts[month]) {
-          if (graph.assets[month]) {
-            graphAssets += graph.assets[month].assets;
-          }
-          if (graph.debts[month]) {
-            graphDebts += graph.debts[month].debts;
-          }
-        }
-
-        graphStarting += graph.starting;
-        return {
-          date: month,
-          assets: graphAssets,
-          debts: graphDebts,
-        };
-      });
-
-      return {
-        id: graph.id,
-        uncat_id: graph.uncat_id,
-        name: graph.name,
-        starting: graphStarting,
-        hidden: graph.hidden,
-        balances: index(mon, 'date'),
-      };
-    });
-
-    const categoryGroupCalcData = catGroup
-      .filter(f => hidden || f.hidden === 0)
-      .map(group => {
-        let groupedStarting = 0;
-        const mon = months.map(month => {
-          let groupedAssets = 0;
-          let groupedDebts = 0;
-          graphData.map(graph => {
-            if (graph.assets[month] || graph.debts[month]) {
-              if (group.categories.map(v => v.id).includes(graph.id)) {
-                if (graph.assets[month]) {
-                  groupedAssets += graph.assets[month].assets;
-                }
-                if (graph.debts[month]) {
-                  groupedDebts += graph.debts[month].debts;
-                }
-              }
-            }
-
-            groupedStarting += graph.starting;
-            return null;
-          });
-          return {
-            date: month,
-            assets: groupedAssets,
-            debts: groupedDebts,
-          };
-        });
-
-        return {
-          id: group.id,
-          name: group.name,
-          starting: groupedStarting,
-          hidden: group.hidden,
-          balances: index(mon, 'date'),
-        };
-      });
-
-    const groupByData = groupBy === 'Group' ? categoryGroupCalcData : calcData;
-
-    const data = groupByData.map(graph => {
-      const calc = recalculate(graph, start, end);
-      return { ...calc };
-    });
-
-    const categoryGroupData = catGroup
-      .filter(f => hidden || f.hidden === 0)
-      .map(group => {
-        const catData = group.categories.map(graph => {
-          let catMatch = null;
-          calcData.map(cat => {
-            if (
-              cat.id === null
-                ? cat.uncat_id === graph.uncat_id
-                : cat.id === graph.id
-            ) {
-              catMatch = cat;
-            }
-            return null;
-          });
-          const calcCat = catMatch && recalculate(catMatch, start, end);
-          return { ...calcCat };
-        });
-        let groupMatch = null;
-        categoryGroupCalcData.map(split => {
-          if (split.id === group.id) {
-            groupMatch = split;
-          }
-          return null;
-        });
-        const calcGroup = groupMatch && recalculate(groupMatch, start, end);
-        return {
-          ...calcGroup,
-          categories: catData,
-        };
-      });
 
     let totalAssets = 0;
     let totalDebts = 0;
-    let totalTotals = 0;
 
-    const monthData = months.map(month => {
+    const monthData = months.reduce((arr, month) => {
       let perMonthAssets = 0;
       let perMonthDebts = 0;
-      let perMonthTotals = 0;
-      graphData.map(graph => {
-        if (graph.assets[month] || graph.debts[month]) {
-          if (graph.assets[month]) {
-            perMonthAssets += graph.assets[month].assets;
-          }
-          if (graph.debts[month]) {
-            perMonthDebts += graph.debts[month].debts;
-          }
-          perMonthTotals = perMonthAssets + perMonthDebts;
+      const stacked = {};
+
+      groupByList.map(item => {
+        let stackAmounts = 0;
+
+        const monthAssets = filterHiddenItems(item, assets)
+          .filter(
+            asset => asset.date === month && asset[groupByLabel] === item.id,
+          )
+          .reduce((a, v) => (a = a + v.amount), 0);
+        perMonthAssets += monthAssets;
+
+        const monthDebts = filterHiddenItems(item, debts)
+          .filter(debt => debt.date === month && debt[groupByLabel] === item.id)
+          .reduce((a, v) => (a = a + v.amount), 0);
+        perMonthDebts += monthDebts;
+
+        if (balanceTypeOp === 'totalAssets') {
+          stackAmounts += monthAssets;
         }
+        if (balanceTypeOp === 'totalDebts') {
+          stackAmounts += monthDebts;
+        }
+        if (stackAmounts !== 0) {
+          stacked[item.name] = integerToAmount(Math.abs(stackAmounts));
+        }
+
         return null;
       });
       totalAssets += perMonthAssets;
       totalDebts += perMonthDebts;
-      totalTotals += perMonthTotals;
 
-      return {
+      arr.push({
         // eslint-disable-next-line rulesdir/typography
         date: d.format(d.parseISO(`${month}-01`), "MMM ''yy"),
+        ...stacked,
         totalDebts: integerToAmount(perMonthDebts),
         totalAssets: integerToAmount(perMonthAssets),
-        totalTotals: integerToAmount(perMonthTotals),
-      };
-    });
-
-    const stackedData = months.map(month => {
-      const stacked = data.map(graph => {
-        let stackAmounts = 0;
-        if (graph.indexedMonthData[month]) {
-          stackAmounts += graph.indexedMonthData[month][balanceTypeOp];
-        }
-        return {
-          name: graph.name,
-          id: graph.id,
-          amount: Math.abs(stackAmounts),
-        };
+        totalTotals: integerToAmount(perMonthDebts + perMonthAssets),
       });
 
-      const indexedStack = indexStack(
-        stacked.filter(i => i[balanceTypeOp] !== 0),
-        'name',
-        'amount',
-      );
-      return {
-        // eslint-disable-next-line rulesdir/typography
-        date: d.format(d.parseISO(`${month}-01`), "MMM ''yy"),
-        ...indexedStack,
-      };
+      return arr;
+    }, []);
+
+    const calcData = groupByList.map(item => {
+      const calc = recalculate({ item, months, assets, debts, groupByLabel });
+      return { ...calc };
     });
 
     setData({
-      stackedData,
-      groupBy: groupBy === 'Group' ? catGroup : groupByList,
-      data,
-      groupData: categoryGroupData,
+      data: calcData,
       monthData,
       start,
       end,
       totalDebts: integerToAmount(totalDebts),
       totalAssets: integerToAmount(totalAssets),
-      totalTotals: integerToAmount(totalTotals),
+      totalTotals: integerToAmount(totalAssets + totalDebts),
     });
     setDataCheck?.(true);
   };
 }
-
-function recalculate(item, start, end) {
-  const months = monthUtils.rangeInclusive(start, end);
-
-  let totalDebts = 0;
-  let totalAssets = 0;
-  let totalTotals = 0;
-  let exists = false;
-
-  const monthData = months.reduce((arr, month) => {
-    let debts = 0;
-    let assets = 0;
-    let total = 0;
-    const last = arr.length === 0 ? null : arr[arr.length - 1];
-
-    if (item.balances[month]) {
-      exists = true;
-      if (item.balances[month].debts) {
-        debts += item.balances[month].debts;
-        totalDebts += item.balances[month].debts;
-      }
-      if (item.balances[month].assets) {
-        assets += item.balances[month].assets;
-        totalAssets += item.balances[month].assets;
-      }
-      total = assets + debts;
-      totalTotals = totalAssets + totalDebts;
-    }
-
-    const dateParse = d.parseISO(`${month}-01`);
-    const change = last ? total - amountToInteger(last.totalTotals) : 0;
-
-    arr.push({
-      dateParse,
-      totalTotals: integerToAmount(total),
-      totalAssets: integerToAmount(assets),
-      totalDebts: integerToAmount(debts),
-      totalChange: integerToAmount(change),
-      // eslint-disable-next-line rulesdir/typography
-      date: d.format(dateParse, "MMM ''yy"),
-      dateLookup: month,
-    });
-
-    return arr;
-  }, []);
-
-  const indexedMonthData = exists ? index(monthData, 'dateLookup') : monthData;
-
-  return {
-    indexedMonthData,
-    monthData,
-    totalAssets: integerToAmount(totalAssets),
-    totalDebts: integerToAmount(totalDebts),
-    totalTotals: integerToAmount(totalTotals),
-    id: item.id,
-    name: item.name,
-  };
-}
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/filterHiddenItems.ts b/packages/desktop-client/src/components/reports/spreadsheets/filterHiddenItems.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0498ad09357f80a5bcbf5c6040fad66887e7ac84
--- /dev/null
+++ b/packages/desktop-client/src/components/reports/spreadsheets/filterHiddenItems.ts
@@ -0,0 +1,24 @@
+import {
+  type QueryDataEntity,
+  type UncategorizedEntity,
+} from '../ReportOptions';
+
+function filterHiddenItems(item: UncategorizedEntity, data: QueryDataEntity[]) {
+  return data.filter(asset => {
+    if (!item.uncategorized_id) {
+      return true;
+    }
+
+    const isTransfer = item.is_transfer
+      ? asset.transferAccount
+      : !asset.transferAccount;
+    const isHidden = item.has_category ? true : !asset.category;
+    const isOffBudget = item.is_off_budget
+      ? asset.accountOffBudget
+      : !asset.accountOffBudget;
+
+    return isTransfer && isHidden && isOffBudget;
+  });
+}
+
+export default filterHiddenItems;
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
new file mode 100644
index 0000000000000000000000000000000000000000..90d679c046ef6e84e5e56551f3fdb1f9891ca0b5
--- /dev/null
+++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts
@@ -0,0 +1,141 @@
+import { runQuery } from 'loot-core/src/client/query-helpers';
+import { send } from 'loot-core/src/platform/client/fetch';
+import * as monthUtils from 'loot-core/src/shared/months';
+import { integerToAmount } from 'loot-core/src/shared/util';
+
+import { categoryLists } from '../ReportOptions';
+
+import { type createSpreadsheetProps } from './default-spreadsheet';
+import filterHiddenItems from './filterHiddenItems';
+import makeQuery from './makeQuery';
+import recalculate from './recalculate';
+
+function createGroupedSpreadsheet({
+  start,
+  end,
+  categories,
+  selectedCategories,
+  conditions = [],
+  conditionsOp,
+  hidden,
+  uncat,
+}: createSpreadsheetProps) {
+  const [catList, catGroup] = categoryLists(hidden, uncat, categories);
+
+  const categoryFilter = (catList || []).filter(
+    category =>
+      !category.hidden &&
+      selectedCategories &&
+      selectedCategories.some(
+        selectedCategory => selectedCategory.id === category.id,
+      ),
+  );
+
+  return async (spreadsheet, setData) => {
+    if (catList.length === 0) {
+      return null;
+    }
+
+    const { filters } = await send('make-filters-from-conditions', {
+      conditions: conditions.filter(cond => !cond.customName),
+    });
+    const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
+
+    const [assets, debts] = await Promise.all([
+      runQuery(
+        makeQuery(
+          'assets',
+          start,
+          end,
+          hidden,
+          selectedCategories,
+          categoryFilter,
+          conditionsOpKey,
+          filters,
+        ),
+      ).then(({ data }) => data),
+      runQuery(
+        makeQuery(
+          'debts',
+          start,
+          end,
+          hidden,
+          selectedCategories,
+          categoryFilter,
+          conditionsOpKey,
+          filters,
+        ),
+      ).then(({ data }) => data),
+    ]);
+
+    const months = monthUtils.rangeInclusive(start, end);
+
+    const groupedData = catGroup.map(
+      group => {
+        let totalAssets = 0;
+        let totalDebts = 0;
+
+        const monthData = months.reduce((arr, month) => {
+          let groupedAssets = 0;
+          let groupedDebts = 0;
+
+          group.categories.map(item => {
+            const monthAssets = filterHiddenItems(item, assets)
+              .filter(
+                asset => asset.date === month && asset.category === item.id,
+              )
+              .reduce((a, v) => (a = a + v.amount), 0);
+            groupedAssets += monthAssets;
+
+            const monthDebts = filterHiddenItems(item, debts)
+              .filter(
+                debts => debts.date === month && debts.category === item.id,
+              )
+              .reduce((a, v) => (a = a + v.amount), 0);
+            groupedDebts += monthDebts;
+
+            return null;
+          });
+
+          totalAssets += groupedAssets;
+          totalDebts += groupedDebts;
+
+          arr.push({
+            date: month,
+            totalAssets: integerToAmount(groupedAssets),
+            totalDebts: integerToAmount(groupedDebts),
+            totalTotals: integerToAmount(groupedDebts + groupedAssets),
+          });
+
+          return arr;
+        }, []);
+
+        const stackedCategories = group.categories.map(item => {
+          const calc = recalculate({
+            item,
+            months,
+            assets,
+            debts,
+            groupByLabel: 'category',
+          });
+          return { ...calc };
+        });
+
+        return {
+          id: group.id,
+          name: group.name,
+          totalAssets: integerToAmount(totalAssets),
+          totalDebts: integerToAmount(totalDebts),
+          totalTotals: integerToAmount(totalAssets + totalDebts),
+          monthData,
+          categories: stackedCategories,
+        };
+      },
+      [start, end],
+    );
+
+    setData(groupedData);
+  };
+}
+
+export default createGroupedSpreadsheet;
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/makeQuery.ts b/packages/desktop-client/src/components/reports/spreadsheets/makeQuery.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19d56b811d339bcf673c7788354673d574599d08
--- /dev/null
+++ b/packages/desktop-client/src/components/reports/spreadsheets/makeQuery.ts
@@ -0,0 +1,86 @@
+import q from 'loot-core/src/client/query-helpers';
+import { type CategoryEntity } from 'loot-core/src/types/models';
+
+function makeQuery(
+  name: string,
+  start: string,
+  end: string,
+  hidden: boolean,
+  selectedCategories: CategoryEntity[],
+  categoryFilter: CategoryEntity[],
+  conditionsOpKey: string,
+  filters: unknown[],
+) {
+  const query = q('transactions')
+    .filter(
+      //Show Offbudget and hidden categories
+      !hidden && {
+        $and: [
+          {
+            'account.offbudget': false,
+            $or: [
+              {
+                'category.hidden': false,
+                category: null,
+              },
+            ],
+          },
+        ],
+        $or: [
+          {
+            'payee.transfer_acct.offbudget': true,
+            'payee.transfer_acct': null,
+          },
+        ],
+      },
+    )
+    //Apply Category_Selector
+    .filter(
+      selectedCategories && {
+        $or: [
+          {
+            category: null,
+            $or: categoryFilter.map(category => ({
+              category: category.id,
+            })),
+          },
+        ],
+      },
+    )
+    //Apply filters and split by "Group By"
+    .filter({
+      [conditionsOpKey]: [...filters],
+    })
+    //Apply month range filters
+    .filter({
+      $and: [
+        { date: { $transform: '$month', $gte: start } },
+        { date: { $transform: '$month', $lte: end } },
+      ],
+    })
+    //Show assets or debts
+    .filter(
+      name === 'assets' ? { amount: { $gt: 0 } } : { amount: { $lt: 0 } },
+    );
+
+  return query
+    .groupBy([
+      { $month: '$date' },
+      { $id: '$account' },
+      { $id: '$payee' },
+      { $id: '$category' },
+      { $id: '$payee.transfer_acct.id' },
+    ])
+    .select([
+      { date: { $month: '$date' } },
+      { category: { $id: '$category.id' } },
+      { categoryGroup: { $id: '$category.group.id' } },
+      { account: { $id: '$account.id' } },
+      { accountOffBudget: { $id: '$account.offbudget' } },
+      { payee: { $id: '$payee.id' } },
+      { transferAccount: { $id: '$payee.transfer_acct.id' } },
+      { amount: { $sum: '$amount' } },
+    ]);
+}
+
+export default makeQuery;
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts b/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e6b516dfaef6a9ba417f94ac988e36a4ec6a9589
--- /dev/null
+++ b/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
@@ -0,0 +1,69 @@
+import * as d from 'date-fns';
+
+import { amountToInteger, integerToAmount } from 'loot-core/src/shared/util';
+
+import { type QueryDataEntity } from '../ReportOptions';
+
+import filterHiddenItems from './filterHiddenItems';
+
+type recalculateProps = {
+  item;
+  months: Array<string>;
+  assets: QueryDataEntity[];
+  debts: QueryDataEntity[];
+  groupByLabel: string;
+};
+
+function recalculate({
+  item,
+  months,
+  assets,
+  debts,
+  groupByLabel,
+}: recalculateProps) {
+  let totalAssets = 0;
+  let totalDebts = 0;
+  const monthData = months.reduce((arr, month) => {
+    const last = arr.length === 0 ? null : arr[arr.length - 1];
+
+    const monthAssets = filterHiddenItems(item, assets)
+      .filter(asset => asset.date === month && asset[groupByLabel] === item.id)
+      .reduce((a, v) => (a = a + v.amount), 0);
+    totalAssets += monthAssets;
+
+    const monthDebts = filterHiddenItems(item, debts)
+      .filter(debt => debt.date === month && debt[groupByLabel] === item.id)
+      .reduce((a, v) => (a = a + v.amount), 0);
+    totalDebts += monthDebts;
+
+    const dateParse = d.parseISO(`${month}-01`);
+
+    const change = last
+      ? monthAssets + monthDebts - amountToInteger(last.totalTotals)
+      : 0;
+
+    arr.push({
+      dateParse,
+      totalAssets: integerToAmount(monthAssets),
+      totalDebts: integerToAmount(monthDebts),
+      totalTotals: integerToAmount(monthAssets + monthDebts),
+      change,
+      // eslint-disable-next-line rulesdir/typography
+      date: d.format(dateParse, "MMM ''yy"),
+      dateLookup: month,
+    });
+
+    return arr;
+  }, []);
+
+  return {
+    id: item.id,
+    name: item.name,
+    totalAssets: integerToAmount(totalAssets),
+    totalDebts: integerToAmount(totalDebts),
+    totalTotals: integerToAmount(totalAssets + totalDebts),
+    monthData,
+  };
+}
+
+export default recalculate;
diff --git a/packages/desktop-client/src/components/reports/util.ts b/packages/desktop-client/src/components/reports/util.ts
index 4d28f2ee4d40c534d63d0a42a7a1d3ac54b74eb6..35ff2166e5b1bc5ffe21bc6db889197c03aab147 100644
--- a/packages/desktop-client/src/components/reports/util.ts
+++ b/packages/desktop-client/src/components/reports/util.ts
@@ -29,17 +29,6 @@ export function index<
   return result;
 }
 
-export function indexStack<
-  T extends Record<string, string | number>,
-  K extends keyof T,
->(data: T[], fieldName: K, field: K) {
-  const result: Record<string | number, T[K]> = {};
-  data.forEach(item => {
-    result[item[fieldName]] = item[field];
-  });
-  return result;
-}
-
 export function indexCashFlow<
   T extends { date: string; isTransfer: boolean; amount: number },
 >(data: T[], date: string, isTransfer: string) {
diff --git a/packages/loot-core/src/server/aql/compiler.ts b/packages/loot-core/src/server/aql/compiler.ts
index 2610939a5159d83009c35232a1ebce68e314d9c9..70bdbfe899350c7f43ea39f36e0addaddc8cfc10 100644
--- a/packages/loot-core/src/server/aql/compiler.ts
+++ b/packages/loot-core/src/server/aql/compiler.ts
@@ -587,6 +587,12 @@ const compileFunction = saveStack('function', (state, func) => {
       );
     }
 
+    // id functions
+    case '$id': {
+      validateArgLength(args, 1);
+      return typed(val(state, args[0]), args[0].type);
+    }
+
     // date functions
     case '$month': {
       validateArgLength(args, 1);
diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts
index 12549e05495f7ab6f289a7932f588f04f86f9e99..6b9eb147f55fb23913799a1ad524cb0b64d1931e 100644
--- a/packages/loot-core/src/types/models/rule.d.ts
+++ b/packages/loot-core/src/types/models/rule.d.ts
@@ -9,7 +9,7 @@ export interface RuleEntity {
   tombstone?: boolean;
 }
 
-interface RuleConditionEntity {
+export interface RuleConditionEntity {
   field: unknown;
   op:
     | 'is'
@@ -28,6 +28,7 @@ interface RuleConditionEntity {
   options?: unknown;
   conditionsOp?: unknown;
   type?: string;
+  customName?: string;
 }
 
 export type RuleActionEntity =
diff --git a/upcoming-release-notes/1988.md b/upcoming-release-notes/1988.md
new file mode 100644
index 0000000000000000000000000000000000000000..8943ac2161968c6407ea38df64cfbf782bde465f
--- /dev/null
+++ b/upcoming-release-notes/1988.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [carkom]
+---
+
+Data loading performance improvements for custom reports
\ No newline at end of file