Fix net worth chart config UX and color scheme issues (#250)

## Summary

- **Clickable toggle rows**: The full row in the chart settings popover
now toggles the checkbox, not just the checkbox itself.
- **Chart stacking order**: When toggling "Include loans" or "Include
real estate", the chart now re-mounts so accounts appear in the correct
sorted position (biggest at bottom) instead of being appended on top.
- **Scheme-aware liability dot**: The red liability indicator dot in the
chart tooltip now respects the user's chart color scheme (neutral, blue,
pink) instead of always using the destructive/red color.
This commit is contained in:
Víctor Falcón 2026-03-31 11:19:35 +01:00 committed by GitHub
parent 0a535fbf47
commit 755452d6ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 35 additions and 15 deletions

View File

@ -109,16 +109,17 @@ export function ChartSettingsPopover({
{allToggles.map((toggle) => (
<div
key={toggle.id}
className="flex items-start justify-between gap-4"
className="flex cursor-pointer items-start justify-between gap-4"
onClick={() => toggle.onChange(!toggle.checked)}
>
<div className="space-y-1">
<Label
htmlFor={toggle.id}
className="text-sm leading-5 font-medium"
className="pointer-events-none text-sm leading-5 font-medium"
>
{toggle.label}
</Label>
<p className="text-xs text-muted-foreground">
<p className="pointer-events-none text-xs text-muted-foreground">
{toggle.description}
</p>
</div>

View File

@ -18,6 +18,7 @@ import {
import { ChartConfig } from '@/components/ui/chart';
import { StackedAreaChart } from '@/components/ui/stacked-area-chart';
import { StackedBarChart } from '@/components/ui/stacked-bar-chart';
import { useChartColors } from '@/hooks/use-chart-color-scheme';
import { useChartViews } from '@/hooks/use-chart-views';
import {
NetWorthEvolutionData,
@ -135,6 +136,7 @@ export function NetWorthChart({
const { props } = usePage<SharedData>();
const locale = useLocale();
const isMobile = useIsMobile();
const { liabilityDotColor } = useChartColors();
const [granularity, setGranularity] = useState<ChartGranularity>('monthly');
const [dailyData, setDailyData] = useState<NetWorthEvolutionData | null>(
null,
@ -598,6 +600,7 @@ export function NetWorthChart({
{chartViews.currentView === 'stacked' &&
(granularity === 'daily' ? (
<StackedAreaChart
key={dataKeys.join(',')}
data={scaledChartData.slice(1)}
dataKeys={dataKeys}
config={chartConfig}
@ -610,12 +613,16 @@ export function NetWorthChart({
showLegend={showLegend}
netWorthMode={
hasLiabilities
? { liabilityTypeLabel: __('Loan') }
? {
liabilityTypeLabel: __('Loan'),
liabilityDotColor,
}
: undefined
}
/>
) : (
<StackedBarChart
key={dataKeys.join(',')}
data={scaledChartData.slice(1)}
dataKeys={dataKeys}
config={chartConfig}
@ -628,7 +635,10 @@ export function NetWorthChart({
showLegend={showLegend}
netWorthMode={
hasLiabilities
? { liabilityTypeLabel: __('Loan') }
? {
liabilityTypeLabel: __('Loan'),
liabilityDotColor,
}
: undefined
}
/>

View File

@ -137,6 +137,7 @@ interface ChartTooltipContentProps {
/** When set, tooltip shows liability rows and net-worth total instead of simple sum. */
netWorthMode?: {
liabilityTypeLabel: string;
liabilityDotColor?: string;
};
}
@ -377,7 +378,10 @@ const ChartTooltipContent = React.forwardRef<
{hasLiabilities && displayCurrency && liabilities.map((liability, index) => (
<div key={index} className="flex justify-between gap-2">
<div className="flex items-center gap-2">
<div className="size-2.5 rounded-xs bg-destructive" />
<div
className="size-2.5 shrink-0 rounded-xs"
style={{ backgroundColor: netWorthMode?.liabilityDotColor ?? 'var(--color-destructive)' }}
/>
<span className="text-muted-foreground font-medium">
{netWorthMode?.liabilityTypeLabel}: {liability.name}
</span>

View File

@ -36,7 +36,7 @@ export interface StackedAreaChartProps<T extends Record<string, unknown>> {
className?: string;
showLegend?: boolean;
minBarWidth?: number;
netWorthMode?: { liabilityTypeLabel: string };
netWorthMode?: { liabilityTypeLabel: string; liabilityDotColor?: string };
}
export function StackedAreaChart<T extends Record<string, unknown>>({

View File

@ -128,7 +128,7 @@ export interface StackedBarChartProps<T extends Record<string, unknown>> {
className?: string;
showLegend?: boolean;
minBarWidth?: number;
netWorthMode?: { liabilityTypeLabel: string };
netWorthMode?: { liabilityTypeLabel: string; liabilityDotColor?: string };
}
export function StackedBarChart<T extends Record<string, unknown>>({

View File

@ -96,6 +96,10 @@ export function useChartColors() {
? 'var(--cashflow-expense)'
: 'var(--color-chart-5)';
const liabilityDotColor = isColorful
? 'var(--color-destructive)'
: 'var(--color-chart-5)';
const CHART_COLORS = [
'var(--chart-1)',
'var(--chart-2)',
@ -119,6 +123,7 @@ export function useChartColors() {
equityLineColor,
cashflowIncomeColor,
cashflowExpenseColor,
liabilityDotColor,
categoryBarColor,
};
}

View File

@ -117,8 +117,8 @@ test('budget show returns previous period when it exists', function () {
// Create a previous period (last month)
$budget->periods()->create([
'start_date' => now()->subMonth()->startOfMonth(),
'end_date' => now()->subMonth()->endOfMonth(),
'start_date' => now()->subMonthNoOverflow()->startOfMonth(),
'end_date' => now()->subMonthNoOverflow()->endOfMonth(),
'allocated_amount' => 30000,
'carried_over_amount' => 0,
]);
@ -138,7 +138,7 @@ test('budget show returns previous period when it exists', function () {
->component('budgets/show')
->has('currentPeriod')
->has('previousPeriod')
->where('previousPeriod.start_date', now()->subMonth()->startOfMonth()->toJSON())
->where('previousPeriod.start_date', now()->subMonthNoOverflow()->startOfMonth()->toJSON())
);
});
@ -252,8 +252,8 @@ test('budget show can navigate to a specific period via query param', function (
// Create a previous period
$previousPeriod = $budget->periods()->create([
'start_date' => now()->subMonth()->startOfMonth(),
'end_date' => now()->subMonth()->endOfMonth(),
'start_date' => now()->subMonthNoOverflow()->startOfMonth(),
'end_date' => now()->subMonthNoOverflow()->endOfMonth(),
'allocated_amount' => 20000,
'carried_over_amount' => 0,
]);

View File

@ -313,7 +313,7 @@ test('net worth evolution converts foreign currency accounts using cached exchan
'currency_code' => 'EUR',
]);
$lastMonth = now()->subMonth();
$lastMonth = now()->subMonthNoOverflow();
$endOfMonth = $lastMonth->copy()->endOfMonth();
AccountBalance::factory()->create([
@ -369,7 +369,7 @@ test('net worth evolution uses last balance of each month per account', function
'currency_code' => 'USD',
]);
$lastMonth = now()->subMonth();
$lastMonth = now()->subMonthNoOverflow();
AccountBalance::factory()->create([
'account_id' => $account->id,