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.