From 0abea114cd90f319ca30af1019156c4f307948b0 Mon Sep 17 00:00:00 2001 From: Taylor Buist Date: Wed, 12 Feb 2025 19:53:13 -0600 Subject: [PATCH 1/8] Add progress bars for categories and goals --- .../src/components/budget/ProgressBar.tsx | 198 ++++++++++++++++++ .../src/components/budget/SidebarCategory.tsx | 2 + .../src/components/settings/Themes.tsx | 4 + packages/loot-core/src/types/prefs.d.ts | 1 + upcoming-release-notes/4371.md | 6 + 5 files changed, 211 insertions(+) create mode 100644 packages/desktop-client/src/components/budget/ProgressBar.tsx create mode 100644 upcoming-release-notes/4371.md diff --git a/packages/desktop-client/src/components/budget/ProgressBar.tsx b/packages/desktop-client/src/components/budget/ProgressBar.tsx new file mode 100644 index 00000000000..2fc4da2027a --- /dev/null +++ b/packages/desktop-client/src/components/budget/ProgressBar.tsx @@ -0,0 +1,198 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { View } from '@actual-app/components/view'; + +import { envelopeBudget } from 'loot-core/client/queries'; +import { integerToCurrency } from 'loot-core/shared/util'; + +import { type CategoryEntity } from '../../../../loot-core/src/types/models'; +import { useSheetValue } from '../spreadsheet/useSheetValue'; + +enum ColorDefinitions { + UnderBudgetRemaining = '#006309', // Dark green + UnderBudgetSpent = '#beffa8', // Light green + OverBudgetSpent = '#979797', // Dark gray + OverBudgetOverSpent = '#c40000', // Red + GoalRemaining = '#90a7fd', // Light blue 90a7fd + GoalSaved = '#001a7b', // Blue + Empty = '', // No color for default +} + +class ColorBar { + color: string; + width: number; + category: string; + rawValue: string; + + constructor( + color: string = ColorDefinitions.Empty, + width: number = 50, + category: string = '', + rawValue: string = '', + ) { + this.color = color; + this.width = width; + this.category = category; + this.rawValue = rawValue; + } +} + +/** Generate what the color status bar should look like based on our numbers and the category type */ +function getColorBars( + budgeted: number, + spent: number, + balance: number, + goal: number, + isLongGoal: boolean, +) { + const leftBar = new ColorBar(); + const rightBar = new ColorBar(); + + if (isLongGoal) { + // We have a long-term goal set. + // Note that long term goals take visual precedence over a monthly template goal, even if both exist + leftBar.width = Math.min(Math.round((balance / goal) * 100), 100); + rightBar.width = 100 - leftBar.width; + + leftBar.color = ColorDefinitions.GoalSaved; + rightBar.color = ColorDefinitions.GoalRemaining; + + leftBar.rawValue = integerToCurrency(balance); + rightBar.rawValue = integerToCurrency(goal - balance); + + leftBar.category = 'Saved'; + rightBar.category = 'Remaining'; + } else if (spent * -1 >= budgeted) { + // We overspent (or are exactly at budget) + const overage = -1 * spent - budgeted; + const total = budgeted + overage; + leftBar.width = Math.round((budgeted / total) * 100); + rightBar.width = 100 - leftBar.width; + + leftBar.color = ColorDefinitions.OverBudgetSpent; + rightBar.color = ColorDefinitions.OverBudgetOverSpent; + + leftBar.rawValue = integerToCurrency(budgeted); + rightBar.rawValue = integerToCurrency(overage); + + leftBar.category = 'Budgeted'; + rightBar.category = 'Overspent'; + } else { + // We are under budget + const remaining = budgeted - -1 * spent; + leftBar.width = Math.round((remaining / budgeted) * 100); + rightBar.width = 100 - leftBar.width; + + leftBar.color = ColorDefinitions.UnderBudgetRemaining; + rightBar.color = ColorDefinitions.UnderBudgetSpent; + + leftBar.rawValue = integerToCurrency(remaining); + rightBar.rawValue = integerToCurrency(spent); + + leftBar.category = 'Remaining'; + rightBar.category = 'Spent'; + } + + return [leftBar, rightBar]; +} + +type ProgressBarProps = { + category: CategoryEntity; +}; + +export function ProgressBar({ category }: ProgressBarProps) { + const { t } = useTranslation(); + + const [leftBar, setLeftBar] = useState(new ColorBar()); + const [rightBar, setRightBar] = useState(new ColorBar()); + + // The budgeted amount for this month + const budgeted = Number( + useSheetValue<'envelope-budget', 'budget'>( + envelopeBudget.catBudgeted(category.id), + ), + ); + // The amount spent this month + const spent = Number( + useSheetValue<'envelope-budget', 'sum-amount'>( + envelopeBudget.catSumAmount(category.id), + ), + ); + /* Goal is either the template value or the goal value, so use it in conjunction with long-goal. */ + const goal = Number( + useSheetValue<'envelope-budget', 'goal'>( + envelopeBudget.catGoal(category.id), + ), + ); + // If a #goal for the category exists. + const longGoal = Number( + useSheetValue<'envelope-budget', 'long-goal'>( + envelopeBudget.catLongGoal(category.id), + ), + ); + const isLongGoal = Boolean(longGoal && longGoal > 0); + // The current category balance based on the budgeted, spent, and previous balance amounts + const balance = Number( + useSheetValue<'envelope-budget', 'leftover'>( + envelopeBudget.catBalance(category.id), + ), + ); + + useEffect(() => { + const setColorBars = async () => { + setTimeout(() => { + // Don't show visuals for income categories + if (category.is_income) { + return null; + } + const [freshLeftBar, freshRightBar] = getColorBars( + budgeted, + spent, + balance, + goal, + isLongGoal, + ); + setLeftBar(freshLeftBar); + setRightBar(freshRightBar); + }, 100); + }; + + setColorBars(); + }, [category, budgeted, spent, balance, goal, isLongGoal]); + + const barHeight = 5; + const borderRadius = 10; + + return ( + + {/* Left side of the bar */} + + {/* Right side of the bar */} + + + ); +} diff --git a/packages/desktop-client/src/components/budget/SidebarCategory.tsx b/packages/desktop-client/src/components/budget/SidebarCategory.tsx index 0b08646ce7f..4a62f6f9faa 100644 --- a/packages/desktop-client/src/components/budget/SidebarCategory.tsx +++ b/packages/desktop-client/src/components/budget/SidebarCategory.tsx @@ -19,6 +19,8 @@ import { theme } from '../../style'; import { NotesButton } from '../NotesButton'; import { InputCell } from '../table'; +import { ProgressBar } from './ProgressBar'; + import { CategoryAutomationButton } from './goals/CategoryAutomationButton'; type SidebarCategoryProps = { diff --git a/packages/desktop-client/src/components/settings/Themes.tsx b/packages/desktop-client/src/components/settings/Themes.tsx index 8da67b36411..6bd2fbff9a1 100644 --- a/packages/desktop-client/src/components/settings/Themes.tsx +++ b/packages/desktop-client/src/components/settings/Themes.tsx @@ -1,12 +1,16 @@ import React, { type ReactNode } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +<<<<<<< HEAD import { Text } from '@actual-app/components/text'; import { View } from '@actual-app/components/view'; +======= +>>>>>>> ab132db3 (Fix linting) import { css } from '@emotion/css'; import { type DarkTheme, type Theme } from 'loot-core/types/prefs'; +import { useGlobalPref } from '../../hooks/useGlobalPref'; import { themeOptions, useTheme, diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index fad8fc946d3..d401b9a1a8a 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -88,6 +88,7 @@ export type GlobalPrefs = Partial<{ preferredDarkTheme: DarkTheme; documentDir: string; // Electron only serverSelfSignedCert: string; // Electron only + useProgressBars: boolean; }>; // GlobalPrefsJson represents what's saved in the global-store.json file diff --git a/upcoming-release-notes/4371.md b/upcoming-release-notes/4371.md new file mode 100644 index 00000000000..ac5475b4969 --- /dev/null +++ b/upcoming-release-notes/4371.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [tbuist] +--- + +Add progress bars for categories and goals \ No newline at end of file From 409d30e69d2546f81c734b4f8138c2a9a19e2954 Mon Sep 17 00:00:00 2001 From: Taylor Date: Wed, 26 Feb 2025 21:15:36 -0600 Subject: [PATCH 2/8] Update bar visuals --- .../src/components/budget/BudgetTable.tsx | 8 ++ .../src/components/budget/BudgetTotals.tsx | 8 ++ .../src/components/budget/ProgressBar.tsx | 118 ++++++++++++------ .../src/components/budget/SidebarCategory.tsx | 2 - .../envelope/EnvelopeBudgetComponents.tsx | 11 ++ .../budget/envelope/EnvelopeBudgetContext.tsx | 17 ++- .../src/components/budget/index.tsx | 3 + .../src/components/settings/Themes.tsx | 4 - packages/loot-core/src/types/prefs.d.ts | 2 +- 9 files changed, 129 insertions(+), 44 deletions(-) diff --git a/packages/desktop-client/src/components/budget/BudgetTable.tsx b/packages/desktop-client/src/components/budget/BudgetTable.tsx index bcdf1719277..19b54720d2f 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.tsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.tsx @@ -85,6 +85,9 @@ export function BudgetTable(props: BudgetTableProps) { const [showHiddenCategories, setShowHiddenCategoriesPef] = useLocalPref( 'budget.showHiddenCategories', ); + const [showProgressBars, setShowProgressBars] = useLocalPref( + 'budget.showProgressBars', + ); const [editing, setEditing] = useState<{ id: string; cell: string } | null>( null, ); @@ -223,6 +226,10 @@ export function BudgetTable(props: BudgetTableProps) { onCollapse(categoryGroups.map(g => g.id)); }; + const toggleProgressBars = () => { + setShowProgressBars(!showProgressBars); + }; + return ( void; expandAllCategories: () => void; collapseAllCategories: () => void; + toggleProgressBars: () => void; }; export const BudgetTotals = memo(function BudgetTotals({ @@ -25,6 +26,7 @@ export const BudgetTotals = memo(function BudgetTotals({ toggleHiddenCategories, expandAllCategories, collapseAllCategories, + toggleProgressBars, }: BudgetTotalsProps) { const { t } = useTranslation(); const [menuOpen, setMenuOpen] = useState(false); @@ -85,6 +87,8 @@ export const BudgetTotals = memo(function BudgetTotals({ onMenuSelect={type => { if (type === 'toggle-visibility') { toggleHiddenCategories(); + } else if (type === 'toggle-progress-bars') { + toggleProgressBars(); } else if (type === 'expandAllCategories') { expandAllCategories(); } else if (type === 'collapseAllCategories') { @@ -97,6 +101,10 @@ export const BudgetTotals = memo(function BudgetTotals({ name: 'toggle-visibility', text: t('Toggle hidden categories'), }, + { + name: 'toggle-progress-bars', + text: t('Toggle progress bars'), + }, { name: 'expandAllCategories', text: t('Expand all'), diff --git a/packages/desktop-client/src/components/budget/ProgressBar.tsx b/packages/desktop-client/src/components/budget/ProgressBar.tsx index 2fc4da2027a..dba58494372 100644 --- a/packages/desktop-client/src/components/budget/ProgressBar.tsx +++ b/packages/desktop-client/src/components/budget/ProgressBar.tsx @@ -4,20 +4,22 @@ import { useTranslation } from 'react-i18next'; import { View } from '@actual-app/components/view'; import { envelopeBudget } from 'loot-core/client/queries'; +import * as monthUtils from 'loot-core/shared/months'; import { integerToCurrency } from 'loot-core/shared/util'; import { type CategoryEntity } from '../../../../loot-core/src/types/models'; +import { theme } from '../../style'; import { useSheetValue } from '../spreadsheet/useSheetValue'; -enum ColorDefinitions { - UnderBudgetRemaining = '#006309', // Dark green - UnderBudgetSpent = '#beffa8', // Light green - OverBudgetSpent = '#979797', // Dark gray - OverBudgetOverSpent = '#c40000', // Red - GoalRemaining = '#90a7fd', // Light blue 90a7fd - GoalSaved = '#001a7b', // Blue - Empty = '', // No color for default -} +import { useEnvelopeBudget } from './envelope/EnvelopeBudgetContext'; + +const ColorDefUnderBudgetRemaining = theme.reportsGreen; +const ColorDefUnderBudgetSpent = theme.reportsGray; +const ColorDefOverBudgetSpent = theme.reportsGray; +const ColorDefOverBudgetOverSpent = theme.reportsRed; +const ColorDefGoalRemaining = theme.reportsLabel; +const ColorDefGoalSaved = theme.reportsBlue; +const ColorDefEmpty = ''; // No color for default class ColorBar { color: string; @@ -26,7 +28,7 @@ class ColorBar { rawValue: string; constructor( - color: string = ColorDefinitions.Empty, + color: string = ColorDefEmpty, width: number = 50, category: string = '', rawValue: string = '', @@ -50,19 +52,35 @@ function getColorBars( const rightBar = new ColorBar(); if (isLongGoal) { - // We have a long-term goal set. - // Note that long term goals take visual precedence over a monthly template goal, even if both exist - leftBar.width = Math.min(Math.round((balance / goal) * 100), 100); - rightBar.width = 100 - leftBar.width; - - leftBar.color = ColorDefinitions.GoalSaved; - rightBar.color = ColorDefinitions.GoalRemaining; - - leftBar.rawValue = integerToCurrency(balance); - rightBar.rawValue = integerToCurrency(goal - balance); - - leftBar.category = 'Saved'; - rightBar.category = 'Remaining'; + // We have a long-term #goal set. These take visual precedence over a monthly template goal, even if both exist + if (balance < 0) { + // Standard goal with a non-negative balance + const toGoal = -1 * balance + goal; + leftBar.width = Math.min(Math.round((goal / toGoal) * 100), 100); + rightBar.width = 100 - leftBar.width; + + leftBar.color = ColorDefGoalRemaining; + rightBar.color = ColorDefOverBudgetOverSpent; + + leftBar.rawValue = integerToCurrency(toGoal); + rightBar.rawValue = integerToCurrency(balance); + + leftBar.category = 'Remaining'; + rightBar.category = 'Overspent'; + } else { + // Standard goal with a non-negative balance + leftBar.width = Math.min(Math.round((balance / goal) * 100), 100); + rightBar.width = 100 - leftBar.width; + + leftBar.color = ColorDefGoalSaved; + rightBar.color = ColorDefGoalRemaining; + + leftBar.rawValue = integerToCurrency(balance); + rightBar.rawValue = integerToCurrency(goal - balance); + + leftBar.category = 'Saved'; + rightBar.category = 'Remaining'; + } } else if (spent * -1 >= budgeted) { // We overspent (or are exactly at budget) const overage = -1 * spent - budgeted; @@ -70,8 +88,8 @@ function getColorBars( leftBar.width = Math.round((budgeted / total) * 100); rightBar.width = 100 - leftBar.width; - leftBar.color = ColorDefinitions.OverBudgetSpent; - rightBar.color = ColorDefinitions.OverBudgetOverSpent; + leftBar.color = ColorDefOverBudgetSpent; + rightBar.color = ColorDefOverBudgetOverSpent; leftBar.rawValue = integerToCurrency(budgeted); rightBar.rawValue = integerToCurrency(overage); @@ -84,8 +102,8 @@ function getColorBars( leftBar.width = Math.round((remaining / budgeted) * 100); rightBar.width = 100 - leftBar.width; - leftBar.color = ColorDefinitions.UnderBudgetRemaining; - rightBar.color = ColorDefinitions.UnderBudgetSpent; + leftBar.color = ColorDefUnderBudgetRemaining; + rightBar.color = ColorDefUnderBudgetSpent; leftBar.rawValue = integerToCurrency(remaining); rightBar.rawValue = integerToCurrency(spent); @@ -98,14 +116,16 @@ function getColorBars( } type ProgressBarProps = { + month: string; category: CategoryEntity; }; -export function ProgressBar({ category }: ProgressBarProps) { +export function ProgressBar({ month, category }: ProgressBarProps) { const { t } = useTranslation(); - const [leftBar, setLeftBar] = useState(new ColorBar()); const [rightBar, setRightBar] = useState(new ColorBar()); + const { hoveredMonth } = useEnvelopeBudget(); + const isCurrentMonth = monthUtils.isCurrentMonth(month); // The budgeted amount for this month const budgeted = Number( @@ -159,21 +179,45 @@ export function ProgressBar({ category }: ProgressBarProps) { }; setColorBars(); - }, [category, budgeted, spent, balance, goal, isLongGoal]); + }, [category, budgeted, spent, balance, goal, isLongGoal, hoveredMonth]); + + const barHeight = 3; + const borderRadius = 30; - const barHeight = 5; - const borderRadius = 10; + let barOpacity = '0'; + if (isLongGoal) { + barOpacity = '0.5'; // By default, all goals are partly visible + } + if (isCurrentMonth) { + barOpacity = '1'; // If it's the current month, goals are fully visible + } + if (isCurrentMonth && hoveredMonth && hoveredMonth !== month) { + barOpacity = '0.5'; // Lower visibility for the current month when other months are hovered + } else if (isLongGoal && hoveredMonth === month) { + barOpacity = '1'; // If we're hovering over a month, make the goals fully visible + } return ( - + {/* Left side of the bar */} , @@ -138,6 +141,7 @@ export const ExpenseGroupMonth = memo(function ExpenseGroupMonth({ group, }: ExpenseGroupMonthProps) { const { id } = group; + const { setHoveredMonth } = useEnvelopeBudget(); return ( setHoveredMonth(month)} + onMouseOut={() => setHoveredMonth('')} > setHoveredMonth(month)} + onMouseOut={() => setHoveredMonth('')} > + {useProgressBars && } void; onToggleSummaryCollapse: () => void; currentMonth: string; + setHoveredMonth: (month: string) => void; + hoveredMonth: string; }; const EnvelopeBudgetContext = createContext({ @@ -20,6 +27,10 @@ const EnvelopeBudgetContext = createContext({ ); }, currentMonth: 'unknown', + hoveredMonth: 'unknown', + setHoveredMonth: (month: string) => { + throw new Error('Unitialised context method called: setHoveredMonth'); + }, }); type EnvelopeBudgetProviderProps = Omit< @@ -33,6 +44,8 @@ export function EnvelopeBudgetProvider({ onBudgetAction, onToggleSummaryCollapse, children, + hoveredMonth, + setHoveredMonth, }: EnvelopeBudgetProviderProps) { const currentMonth = monthUtils.currentMonth(); @@ -43,6 +56,8 @@ export function EnvelopeBudgetProvider({ summaryCollapsed, onBudgetAction, onToggleSummaryCollapse, + hoveredMonth, + setHoveredMonth, }} > {children} diff --git a/packages/desktop-client/src/components/budget/index.tsx b/packages/desktop-client/src/components/budget/index.tsx index e62b0de395c..9ccf526d8de 100644 --- a/packages/desktop-client/src/components/budget/index.tsx +++ b/packages/desktop-client/src/components/budget/index.tsx @@ -83,6 +83,7 @@ function BudgetInner(props: BudgetInnerProps) { const maxMonths = maxMonthsPref || 1; const [initialized, setInitialized] = useState(false); const { grouped: categoryGroups } = useCategories(); + const [hoveredMonth, setHoveredMonth] = useState(); useEffect(() => { async function run() { @@ -349,6 +350,8 @@ function BudgetInner(props: BudgetInnerProps) { summaryCollapsed={summaryCollapsed} onBudgetAction={onBudgetAction} onToggleSummaryCollapse={onToggleCollapse} + setHoveredMonth={setHoveredMonth} + hoveredMonth={hoveredMonth} > >>>>>> ab132db3 (Fix linting) import { css } from '@emotion/css'; import { type DarkTheme, type Theme } from 'loot-core/types/prefs'; -import { useGlobalPref } from '../../hooks/useGlobalPref'; import { themeOptions, useTheme, diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index d401b9a1a8a..da094d3747a 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -73,6 +73,7 @@ export type LocalPrefs = Partial<{ reportsViewLabel: boolean; sidebarWidth: number; 'mobile.showSpentColumn': boolean; + 'budget.showProgressBars': boolean; }>; export type Theme = 'light' | 'dark' | 'auto' | 'midnight' | 'development'; @@ -88,7 +89,6 @@ export type GlobalPrefs = Partial<{ preferredDarkTheme: DarkTheme; documentDir: string; // Electron only serverSelfSignedCert: string; // Electron only - useProgressBars: boolean; }>; // GlobalPrefsJson represents what's saved in the global-store.json file From 3e954efcbae3055729525aa1b3290277b969c6f7 Mon Sep 17 00:00:00 2001 From: Taylor Date: Thu, 27 Feb 2025 22:05:26 -0600 Subject: [PATCH 3/8] linting --- .../components/budget/envelope/EnvelopeBudgetContext.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetContext.tsx b/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetContext.tsx index 404789586e6..cca3c3a6de5 100644 --- a/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetContext.tsx +++ b/packages/desktop-client/src/components/budget/envelope/EnvelopeBudgetContext.tsx @@ -1,9 +1,4 @@ -import React, { - type ReactNode, - createContext, - useContext, - useState, -} from 'react'; +import React, { type ReactNode, createContext, useContext } from 'react'; import * as monthUtils from 'loot-core/shared/months'; @@ -28,7 +23,7 @@ const EnvelopeBudgetContext = createContext({ }, currentMonth: 'unknown', hoveredMonth: 'unknown', - setHoveredMonth: (month: string) => { + setHoveredMonth: () => { throw new Error('Unitialised context method called: setHoveredMonth'); }, }); From 46fbdeb61250c0c85c8927f8e3f748391c9bb4ed Mon Sep 17 00:00:00 2001 From: Taylor Date: Sat, 1 Mar 2025 10:39:56 -0600 Subject: [PATCH 4/8] re-enable bars for non-goal categories in non-current months --- .../src/components/budget/ProgressBar.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/desktop-client/src/components/budget/ProgressBar.tsx b/packages/desktop-client/src/components/budget/ProgressBar.tsx index dba58494372..bfa3f23b303 100644 --- a/packages/desktop-client/src/components/budget/ProgressBar.tsx +++ b/packages/desktop-client/src/components/budget/ProgressBar.tsx @@ -184,17 +184,14 @@ export function ProgressBar({ month, category }: ProgressBarProps) { const barHeight = 3; const borderRadius = 30; - let barOpacity = '0'; - if (isLongGoal) { - barOpacity = '0.5'; // By default, all goals are partly visible - } + let barOpacity = '0.5'; // By default, all categories in all months with some activity are partly visible if (isCurrentMonth) { - barOpacity = '1'; // If it's the current month, goals are fully visible + barOpacity = '1'; // By default, categories in the current month are fully visible } if (isCurrentMonth && hoveredMonth && hoveredMonth !== month) { - barOpacity = '0.5'; // Lower visibility for the current month when other months are hovered - } else if (isLongGoal && hoveredMonth === month) { - barOpacity = '1'; // If we're hovering over a month, make the goals fully visible + barOpacity = '0.5'; // If a non-current month is hovered over, lower visibility for the current month + } else if (hoveredMonth === month) { + barOpacity = '1'; // If a non-current month is hovered over, raise that month to fully visible } return ( From dd009659addf5bdc193d462a47d398deb36a0685 Mon Sep 17 00:00:00 2001 From: Taylor Date: Sat, 1 Mar 2025 10:53:20 -0600 Subject: [PATCH 5/8] use shorter bars --- packages/desktop-client/src/components/budget/ProgressBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/desktop-client/src/components/budget/ProgressBar.tsx b/packages/desktop-client/src/components/budget/ProgressBar.tsx index bfa3f23b303..1490d69f7b4 100644 --- a/packages/desktop-client/src/components/budget/ProgressBar.tsx +++ b/packages/desktop-client/src/components/budget/ProgressBar.tsx @@ -199,9 +199,10 @@ export function ProgressBar({ month, category }: ProgressBarProps) { style={{ display: 'flex', position: 'absolute', + right: 0, bottom: 0, marginBottom: 1, - width: '100%', + width: '50%', opacity: barOpacity, transition: 'opacity 0.25s', }} From 8706c79ecd54ff7c5f8914eaecb90a71dc2e02fd Mon Sep 17 00:00:00 2001 From: Taylor Buist Date: Sat, 1 Mar 2025 11:13:53 -0600 Subject: [PATCH 6/8] Force rebuild From 85e9767e496fd03c4b5d28d94100ac7f09de0939 Mon Sep 17 00:00:00 2001 From: Taylor Date: Sat, 8 Mar 2025 09:10:14 -0600 Subject: [PATCH 7/8] Extend bars to cell width --- .../src/components/budget/ProgressBar.tsx | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/packages/desktop-client/src/components/budget/ProgressBar.tsx b/packages/desktop-client/src/components/budget/ProgressBar.tsx index 1490d69f7b4..cf42a3a6660 100644 --- a/packages/desktop-client/src/components/budget/ProgressBar.tsx +++ b/packages/desktop-client/src/components/budget/ProgressBar.tsx @@ -160,38 +160,34 @@ export function ProgressBar({ month, category }: ProgressBarProps) { ); useEffect(() => { - const setColorBars = async () => { - setTimeout(() => { - // Don't show visuals for income categories - if (category.is_income) { - return null; - } - const [freshLeftBar, freshRightBar] = getColorBars( - budgeted, - spent, - balance, - goal, - isLongGoal, - ); - setLeftBar(freshLeftBar); - setRightBar(freshRightBar); - }, 100); - }; - - setColorBars(); - }, [category, budgeted, spent, balance, goal, isLongGoal, hoveredMonth]); - - const barHeight = 3; - const borderRadius = 30; - - let barOpacity = '0.5'; // By default, all categories in all months with some activity are partly visible + // Don't show visuals for income categories + if (category.is_income) { + return; + } + const [freshLeftBar, freshRightBar] = getColorBars( + budgeted, + spent, + balance, + goal, + isLongGoal, + ); + setLeftBar(freshLeftBar); + setRightBar(freshRightBar); + }, [category, budgeted, spent, balance, goal, isLongGoal]); + + const BAR_HEIGHT = 3; + const BORDER_RADIUS = 30; + const PARTIAL_OPACITY = '0.5'; + const FULL_OPACITY = '1'; + + let barOpacity = PARTIAL_OPACITY; // By default, all categories in all months with some activity are partly visible if (isCurrentMonth) { - barOpacity = '1'; // By default, categories in the current month are fully visible + barOpacity = FULL_OPACITY; // By default, categories in the current month are fully visible } if (isCurrentMonth && hoveredMonth && hoveredMonth !== month) { - barOpacity = '0.5'; // If a non-current month is hovered over, lower visibility for the current month + barOpacity = PARTIAL_OPACITY; // If a non-current month is hovered over, lower visibility for the current month } else if (hoveredMonth === month) { - barOpacity = '1'; // If a non-current month is hovered over, raise that month to fully visible + barOpacity = FULL_OPACITY; // If a non-current month is hovered over, raise that month to fully visible } return ( @@ -202,7 +198,7 @@ export function ProgressBar({ month, category }: ProgressBarProps) { right: 0, bottom: 0, marginBottom: 1, - width: '50%', + width: '100%', opacity: barOpacity, transition: 'opacity 0.25s', }} @@ -210,14 +206,14 @@ export function ProgressBar({ month, category }: ProgressBarProps) { {/* Left side of the bar */} Date: Sat, 8 Mar 2025 12:43:22 -0600 Subject: [PATCH 8/8] Fix ratio calculations --- .../src/components/budget/ProgressBar.tsx | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/desktop-client/src/components/budget/ProgressBar.tsx b/packages/desktop-client/src/components/budget/ProgressBar.tsx index cf42a3a6660..d0aa32ab65b 100644 --- a/packages/desktop-client/src/components/budget/ProgressBar.tsx +++ b/packages/desktop-client/src/components/budget/ProgressBar.tsx @@ -51,41 +51,37 @@ function getColorBars( const leftBar = new ColorBar(); const rightBar = new ColorBar(); - if (isLongGoal) { + if (budgeted === 0) { + // If we have nothing budgeted, don't show a bar for this + } else if (isLongGoal) { // We have a long-term #goal set. These take visual precedence over a monthly template goal, even if both exist - if (balance < 0) { - // Standard goal with a non-negative balance - const toGoal = -1 * balance + goal; - leftBar.width = Math.min(Math.round((goal / toGoal) * 100), 100); - rightBar.width = 100 - leftBar.width; - - leftBar.color = ColorDefGoalRemaining; - rightBar.color = ColorDefOverBudgetOverSpent; - - leftBar.rawValue = integerToCurrency(toGoal); - rightBar.rawValue = integerToCurrency(balance); - - leftBar.category = 'Remaining'; - rightBar.category = 'Overspent'; + const toGoal = goal - balance; + + if (toGoal <= 0) { + // If over the goal, consider it complete + leftBar.width = 100; + } else if (balance < 0) { + // If balance is < 0, show no progress + leftBar.width = 0; } else { - // Standard goal with a non-negative balance - leftBar.width = Math.min(Math.round((balance / goal) * 100), 100); - rightBar.width = 100 - leftBar.width; + // Otherwise, standard ratio with a positive balance and a positive amount to reach the goal + leftBar.width = bound(Math.round((balance / goal) * 100), 0, 100); + } + rightBar.width = 100 - leftBar.width; - leftBar.color = ColorDefGoalSaved; - rightBar.color = ColorDefGoalRemaining; + leftBar.color = ColorDefGoalSaved; + rightBar.color = ColorDefGoalRemaining; - leftBar.rawValue = integerToCurrency(balance); - rightBar.rawValue = integerToCurrency(goal - balance); + leftBar.rawValue = integerToCurrency(balance); + rightBar.rawValue = integerToCurrency(toGoal); - leftBar.category = 'Saved'; - rightBar.category = 'Remaining'; - } + leftBar.category = 'Saved'; + rightBar.category = 'Remaining'; } else if (spent * -1 >= budgeted) { // We overspent (or are exactly at budget) - const overage = -1 * spent - budgeted; - const total = budgeted + overage; - leftBar.width = Math.round((budgeted / total) * 100); + const overage = budgeted + spent; + const total = budgeted + Math.abs(overage); + leftBar.width = bound(Math.round((budgeted / total) * 100), 0, 100); rightBar.width = 100 - leftBar.width; leftBar.color = ColorDefOverBudgetSpent; @@ -98,8 +94,8 @@ function getColorBars( rightBar.category = 'Overspent'; } else { // We are under budget - const remaining = budgeted - -1 * spent; - leftBar.width = Math.round((remaining / budgeted) * 100); + const remaining = budgeted + spent; + leftBar.width = bound(Math.round((remaining / budgeted) * 100), 0, 100); rightBar.width = 100 - leftBar.width; leftBar.color = ColorDefUnderBudgetRemaining; @@ -115,6 +111,10 @@ function getColorBars( return [leftBar, rightBar]; } +function bound(val: number, min: number, max: number): number { + return Math.max(min, Math.min(val, max)); +} + type ProgressBarProps = { month: string; category: CategoryEntity; @@ -177,7 +177,7 @@ export function ProgressBar({ month, category }: ProgressBarProps) { const BAR_HEIGHT = 3; const BORDER_RADIUS = 30; - const PARTIAL_OPACITY = '0.5'; + const PARTIAL_OPACITY = '0.4'; const FULL_OPACITY = '1'; let barOpacity = PARTIAL_OPACITY; // By default, all categories in all months with some activity are partly visible