diff --git a/packages/desktop-client/src/components/reports/Overview.jsx b/packages/desktop-client/src/components/reports/Overview.jsx
index 975735eec85..8100b300972 100644
--- a/packages/desktop-client/src/components/reports/Overview.jsx
+++ b/packages/desktop-client/src/components/reports/Overview.jsx
@@ -13,11 +13,9 @@ import { View } from '../common/View';
import { CashFlowCard } from './reports/CashFlowCard';
import { CustomReportListCards } from './reports/CustomReportListCards';
import { NetWorthCard } from './reports/NetWorthCard';
-import { SankeyCard } from './reports/SankeyCard';
export function Overview() {
const customReports = useReports();
- const sankeyFeatureFlag = useFeatureFlag('sankeyReport');
const customReportsFeatureFlag = useFeatureFlag('customReports');
@@ -54,14 +52,6 @@ export function Overview() {
-
- {sankeyFeatureFlag && }
-
{customReportsFeatureFlag && (
)}
diff --git a/packages/desktop-client/src/components/reports/ReportRouter.jsx b/packages/desktop-client/src/components/reports/ReportRouter.jsx
index 4318b314102..463e9484d4a 100644
--- a/packages/desktop-client/src/components/reports/ReportRouter.jsx
+++ b/packages/desktop-client/src/components/reports/ReportRouter.jsx
@@ -5,7 +5,6 @@ import { Overview } from './Overview';
import { CashFlow } from './reports/CashFlow';
import { CustomReport } from './reports/CustomReport';
import { NetWorth } from './reports/NetWorth';
-import { Sankey } from './reports/Sankey';
export function ReportRouter() {
return (
@@ -14,7 +13,6 @@ export function ReportRouter() {
} />
} />
} />
- } />
);
}
diff --git a/packages/desktop-client/src/components/reports/graphs/SankeyGraph.tsx b/packages/desktop-client/src/components/reports/graphs/SankeyGraph.tsx
deleted file mode 100644
index 5f97bfcba80..00000000000
--- a/packages/desktop-client/src/components/reports/graphs/SankeyGraph.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-// @ts-strict-ignore
-import React from 'react';
-
-import {
- Sankey,
- Tooltip,
- Rectangle,
- Layer,
- ResponsiveContainer,
-} from 'recharts';
-
-import { Container } from '../Container';
-import { numberFormatterTooltip } from '../numberFormatter';
-
-type SankeyProps = {
- style;
- data;
- compact: boolean;
-};
-
-function SankeyNode({ x, y, width, height, index, payload, containerWidth }) {
- const isOut = x + width + 6 > containerWidth;
- let payloadValue = Math.round(payload.value / 1000).toString();
- if (payload.value < 1000) {
- payloadValue = '<1k';
- } else {
- payloadValue = payloadValue + 'k';
- }
- return (
-
-
-
- {payload.name}
-
-
- {payloadValue}
-
-
- );
-}
-
-function convertToCondensed(data) {
- const budgetNodeIndex = data.nodes.findIndex(node => node.name === 'Budget');
-
- // Calculate total income (links going into the "Budget" node)
- const totalIncome = data.links.reduce((acc, link) => {
- return link.target === budgetNodeIndex ? acc + link.value : acc;
- }, 0);
-
- // Calculate total expenses (links going out of the "Budget" node)
- const totalExpenses = data.links.reduce((acc, link) => {
- return link.source === budgetNodeIndex ? acc + link.value : acc;
- }, 0);
-
- return {
- nodes: [{ name: 'Income' }, { name: 'Budget' }, { name: 'Expenses' }],
- links: [
- { source: 0, target: 1, value: totalIncome },
- { source: 1, target: 2, value: totalExpenses },
- ],
- };
-}
-
-export function SankeyGraph({ style, data, compact }: SankeyProps) {
- const sankeyData = compact ? convertToCondensed(data) : data;
-
- if (!data.links || data.links.length === 0) return null;
- const margin = {
- left: 0,
- right: 0,
- top: compact ? 0 : 10,
- bottom: compact ? 0 : 25,
- };
-
- return compact ? (
-
- }
- sort={true}
- iterations={1000}
- nodePadding={23}
- margin={margin}
- >
-
-
-
- ) : (
-
- {width => (
-
- }
- sort={true}
- iterations={1000}
- nodePadding={23}
- margin={margin}
- >
-
-
-
- )}
-
- );
-}
diff --git a/packages/desktop-client/src/components/reports/reports/Sankey.jsx b/packages/desktop-client/src/components/reports/reports/Sankey.jsx
deleted file mode 100644
index fad99a01c68..00000000000
--- a/packages/desktop-client/src/components/reports/reports/Sankey.jsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import React, { useState, useEffect, useMemo } from 'react';
-
-import * as d from 'date-fns';
-
-import { send } from 'loot-core/src/platform/client/fetch';
-import * as monthUtils from 'loot-core/src/shared/months';
-
-import { useCategories } from '../../../hooks/useCategories';
-import { useFilters } from '../../../hooks/useFilters';
-import { theme, styles } from '../../../style';
-import { Paragraph } from '../../common/Paragraph';
-import { View } from '../../common/View';
-import { SankeyGraph } from '../graphs/SankeyGraph';
-import { Header } from '../Header';
-import { createSpreadsheet as sankeySpreadsheet } from '../spreadsheets/sankey-spreadsheet';
-import { useReport } from '../useReport';
-import { fromDateRepr } from '../util';
-
-export function Sankey() {
- const { grouped: categoryGroups } = useCategories();
- const {
- filters,
- saved,
- conditionsOp,
- onApply: onApplyFilter,
- onDelete: onDeleteFilter,
- onUpdate: onUpdateFilter,
- onCondOpChange,
- } = useFilters();
-
- const [allMonths, setAllMonths] = useState(null);
- const [start, setStart] = useState(
- monthUtils.subMonths(monthUtils.currentMonth(), 5),
- );
- const [end, setEnd] = useState(monthUtils.currentMonth());
-
- const params = useMemo(
- () => sankeySpreadsheet(start, end, categoryGroups, filters, conditionsOp),
- [start, end, categoryGroups, filters, conditionsOp],
- );
- const data = useReport('sankey', params);
- useEffect(() => {
- async function run() {
- const trans = await send('get-earliest-transaction');
- const currentMonth = monthUtils.currentMonth();
- let earliestMonth = trans
- ? monthUtils.monthFromDate(d.parseISO(fromDateRepr(trans.date)))
- : currentMonth;
-
- // Make sure the month selects are at least populates with a
- // year's worth of months. We can undo this when we have fancier
- // date selects.
- const yearAgo = monthUtils.subMonths(monthUtils.currentMonth(), 12);
- if (earliestMonth > yearAgo) {
- earliestMonth = yearAgo;
- }
-
- const allMonths = monthUtils
- .rangeInclusive(earliestMonth, monthUtils.currentMonth())
- .map(month => ({
- name: month,
- pretty: monthUtils.format(month, 'MMMM, yyyy'),
- }))
- .reverse();
-
- setAllMonths(allMonths);
- }
- run();
- }, []);
-
- function onChangeDates(start, end) {
- setStart(start);
- setEnd(end);
- }
-
- if (!allMonths || !data) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- What is a Sankey plot?
-
-
- A Sankey plot visualizes the flow of quantities between multiple
- categories, emphasizing the distribution and proportional
- relationships of data streams. If you hover over the graph, you can
- see detailed flow values between categories.
-
-
-
-
- );
-}
diff --git a/packages/desktop-client/src/components/reports/reports/SankeyCard.jsx b/packages/desktop-client/src/components/reports/reports/SankeyCard.jsx
deleted file mode 100644
index 492962c23c2..00000000000
--- a/packages/desktop-client/src/components/reports/reports/SankeyCard.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { useMemo } from 'react';
-
-import * as monthUtils from 'loot-core/src/shared/months';
-
-import { useCategories } from '../../../hooks/useCategories';
-import { styles } from '../../../style';
-import { Block } from '../../common/Block';
-import { View } from '../../common/View';
-import { DateRange } from '../DateRange';
-import { SankeyGraph } from '../graphs/SankeyGraph';
-import { LoadingIndicator } from '../LoadingIndicator';
-import { ReportCard } from '../ReportCard';
-import { createSpreadsheet as sankeySpreadsheet } from '../spreadsheets/sankey-spreadsheet';
-import { useReport } from '../useReport';
-
-export function SankeyCard() {
- const { grouped: categoryGroups } = useCategories();
- const end = monthUtils.currentMonth();
- const start = monthUtils.subMonths(end, 5);
-
- const params = useMemo(
- () => sankeySpreadsheet(start, end, categoryGroups),
- [start, end, categoryGroups],
- );
- const data = useReport('sankey', params);
-
- return (
-
-
-
-
- Sankey
-
-
-
-
-
- {data ? (
-
- ) : (
-
- )}
-
-
- );
-}
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/sankey-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/sankey-spreadsheet.ts
deleted file mode 100644
index c51f835ab84..00000000000
--- a/packages/desktop-client/src/components/reports/spreadsheets/sankey-spreadsheet.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-// @ts-strict-ignore
-import { runQuery } from 'loot-core/src/client/query-helpers';
-import { send } from 'loot-core/src/platform/client/fetch';
-import { q } from 'loot-core/src/shared/query';
-import { integerToAmount } from 'loot-core/src/shared/util';
-
-export function createSpreadsheet(
- start,
- end,
- categories,
- conditions = [],
- conditionsOp,
-) {
- return async (spreadsheet, setData) => {
- // gather filters user has set
- const { filters } = await send('make-filters-from-conditions', {
- conditions: conditions.filter(cond => !cond.customName),
- });
- const conditionsOpKey = conditionsOp === 'or' ? '$or' : '$and';
-
- // create list of Income subcategories
- const allIncomeSubcategories = [].concat(
- ...categories
- .filter(category => category.is_income === 1)
- .map(category => category.categories),
- );
-
- // retrieve sum of subcategory expenses
- async function fetchCategoryData(categories) {
- try {
- return await Promise.all(
- categories.map(async mainCategory => {
- const subcategoryBalances = await Promise.all(
- mainCategory.categories
- .filter(subcategory => subcategory.is_income !== 1)
- .map(async subcategory => {
- const results = await runQuery(
- q('transactions')
- .filter({
- [conditionsOpKey]: filters,
- })
- .filter({
- $and: [
- { date: { $gte: start + '-01' } },
- { date: { $lte: end + '-31' } },
- ],
- })
- .filter({ category: subcategory.id })
- .calculate({ $sum: '$amount' }),
- );
- return {
- subcategory: subcategory.name,
- value: results.data * -1,
- };
- }),
- );
-
- // Here you could combine, reduce or transform the subcategoryBalances if needed
- return {
- name: mainCategory.name,
- balances: subcategoryBalances,
- };
- }),
- );
- } catch (error) {
- console.error('Error fetching category data:', error);
- throw error; // Re-throw if you want the error to propagate
- }
- }
-
- // retrieve all income subcategory payees
- async function fetchIncomeData() {
- // Map over allIncomeSubcategories and return an array of promises
- const promises = allIncomeSubcategories.map(subcategory => {
- return runQuery(
- q('transactions')
- .filter({
- [conditionsOpKey]: filters,
- })
- .filter({
- $and: [
- { date: { $gte: start + '-01' } },
- { date: { $lte: end + '-31' } },
- ],
- })
- .filter({ category: subcategory.id })
- .groupBy(['payee'])
- .select(['payee', { amount: { $sum: '$amount' } }]),
- );
- });
-
- // Use Promise.all() to wait for all queries to complete
- const resultsArrays = await Promise.all(promises);
-
- // unravel the results
- const payeesDict = {};
- resultsArrays.forEach(item => {
- item.data.forEach(innerItem => {
- payeesDict[innerItem.payee] = innerItem.amount;
- });
- });
-
- // First, collect all unique IDs from payeesDict
- const payeeIds = Object.keys(payeesDict);
-
- const results = await runQuery(
- q('payees')
- .filter({ id: { $oneof: payeeIds } })
- .select(['id', 'name']),
- );
-
- // Convert the resulting array to a payee-name-map
- const payeeNames = {};
- results.data.forEach(item => {
- if (item.name && payeesDict[item.id]) {
- payeeNames[item.name] = payeesDict[item.id];
- }
- });
- return payeeNames;
- }
- const categoryData = await fetchCategoryData(categories);
- const incomeData = await fetchIncomeData();
-
- // convert retrieved data into the proper sankey format
- setData(transformToSankeyData(categoryData, incomeData));
- };
-}
-
-function transformToSankeyData(categoryData, incomeData) {
- const data = { nodes: [], links: [] };
- const nodeNames = new Set();
-
- // Add the Budget node first.
- data.nodes.push({ name: 'Budget' });
- nodeNames.add('Budget');
-
- // Handle the income sources and link them to the Budget node.
- Object.entries(incomeData).forEach(([sourceName, value]) => {
- if (!nodeNames.has(sourceName) && integerToAmount(value) > 0) {
- data.nodes.push({ name: sourceName });
- nodeNames.add(sourceName);
- data.links.push({
- source: sourceName,
- target: 'Budget',
- value: integerToAmount(value),
- });
- }
- });
-
- // add all category expenses that have valid subcategories and a balance
- for (const mainCategory of categoryData) {
- if (!nodeNames.has(mainCategory.name) && mainCategory.balances.length > 0) {
- let mainCategorySum = 0;
- for (const subCategory of mainCategory.balances) {
- if (!nodeNames.has(subCategory.subcategory) && subCategory.value > 0) {
- mainCategorySum += subCategory.value;
- }
- }
- if (mainCategorySum === 0) {
- continue;
- }
- data.nodes.push({ name: mainCategory.name });
- nodeNames.add(mainCategory.name);
- data.links.push({
- source: 'Budget',
- target: mainCategory.name,
- value: integerToAmount(mainCategorySum),
- });
-
- // add the subcategories of the main category
- for (const subCategory of mainCategory.balances) {
- if (!nodeNames.has(subCategory.subcategory) && subCategory.value > 0) {
- data.nodes.push({ name: subCategory.subcategory });
- nodeNames.add(subCategory.subcategory);
-
- data.links.push({
- source: mainCategory.name,
- target: subCategory.subcategory,
- value: integerToAmount(subCategory.value),
- });
- }
- }
- }
- }
-
- // Map source and target in links to the index of the node
- data.links.forEach(link => {
- link.source = data.nodes.findIndex(node => node.name === link.source);
- link.target = data.nodes.findIndex(node => node.name === link.target);
- });
-
- return data;
-}
diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx
index 15f20989fec..c7bcff182c7 100644
--- a/packages/desktop-client/src/components/settings/Experimental.tsx
+++ b/packages/desktop-client/src/components/settings/Experimental.tsx
@@ -80,7 +80,6 @@ export function ExperimentalFeatures() {
expanded ? (
Custom reports
- Sankey report
diff --git a/packages/desktop-client/src/hooks/useFeatureFlag.ts b/packages/desktop-client/src/hooks/useFeatureFlag.ts
index c3d3fa1376f..467abc9ee2c 100644
--- a/packages/desktop-client/src/hooks/useFeatureFlag.ts
+++ b/packages/desktop-client/src/hooks/useFeatureFlag.ts
@@ -4,7 +4,6 @@ import { type State } from 'loot-core/src/client/state-types';
import type { FeatureFlag } from 'loot-core/src/types/prefs';
const DEFAULT_FEATURE_FLAG_STATE: Record = {
- sankeyReport: false,
reportBudget: false,
goalTemplatesEnabled: false,
customReports: false,
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index f4a13c81d9d..0db98547329 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -1,7 +1,6 @@
import { type numberFormats } from '../shared/util';
export type FeatureFlag =
- | 'sankeyReport'
| 'reportBudget'
| 'goalTemplatesEnabled'
| 'customReports'
diff --git a/upcoming-release-notes/2417.md b/upcoming-release-notes/2417.md
new file mode 100644
index 00000000000..c67a8abe74f
--- /dev/null
+++ b/upcoming-release-notes/2417.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Delete experimental sankey feature - development abandoned.