Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Area,
AreaChart,
CartesianGrid,
Line,
Tooltip,
XAxis,
YAxis,
Expand Down Expand Up @@ -39,7 +40,8 @@ type NetWorthDataPoint = {
change: string;
networth: string;
date: string;
} & Record<string, string | number>;
isProjection?: boolean;
} & Record<string, string | number | boolean>;

type TrendTooltipProps = TooltipContentProps<number, string> & {
style?: CSSProperties;
Expand All @@ -49,6 +51,7 @@ function TrendTooltip({ active, payload, style }: TrendTooltipProps) {
const { t } = useTranslation();

if (active && payload && payload.length) {
const isProjection = Boolean(payload[0].payload.isProjection);
return (
<div
className={css([
Expand All @@ -66,7 +69,10 @@ function TrendTooltip({ active, payload, style }: TrendTooltipProps) {
>
<div>
<div style={{ marginBottom: 10 }}>
<strong>{payload[0].payload.date}</strong>
<strong>
{payload[0].payload.date}
{isProjection ? ` ${t('(projected)')}` : ''}
</strong>
</div>
<div style={{ lineHeight: 1.5 }}>
<AlignedText
Expand Down Expand Up @@ -333,6 +339,28 @@ export function NetWorthGraph({
.map(point => point.x);
}, [interval, graphData.data]);

const trendData = useMemo(() => {
const projectionStartIndex = graphData.data.findIndex(point => {
return point.isProjection;
});
const projectionLineStartIndex = Math.max(0, projectionStartIndex - 1);

return graphData.data.map((point, index) => {
const hasProjection = projectionStartIndex !== -1;

return {
...point,
// Solid line ends at the last non-projected month.
yHistorical:
hasProjection && index >= projectionStartIndex ? null : point.y,
// Dotted series starts one point before the first projected month
// so the connection to the first projected point is visible.
yProjection:
hasProjection && index >= projectionLineStartIndex ? point.y : null,
};
});
}, [graphData.data]);

return (
<Container
style={{
Expand All @@ -357,7 +385,7 @@ export function NetWorthGraph({
responsive
width={width}
height={height}
data={graphData.data}
data={mode === 'trend' ? trendData : graphData.data}
margin={{
top: 0,
right: 0,
Expand Down Expand Up @@ -432,18 +460,31 @@ export function NetWorthGraph({
</defs>

{mode === 'trend' ? (
<Area
type={interpolationType}
dot={false}
activeDot={false}
{...animationProps}
dataKey="y"
stroke={theme.reportsChartFill}
strokeWidth={2}
fill={`url(#${gradientId})`}
fillOpacity={1}
connectNulls
/>
<>
<Line
type={interpolationType}
dot={false}
activeDot={false}
{...animationProps}
dataKey="yProjection"
stroke={theme.reportsChartFill}
strokeWidth={2}
strokeDasharray="5 5"
connectNulls
/>
<Area
type={interpolationType}
dot={false}
activeDot={false}
{...animationProps}
dataKey="yHistorical"
stroke={theme.reportsChartFill}
strokeWidth={2}
fill={`url(#${gradientId})`}
fillOpacity={1}
connectNulls
/>
</>
) : (
sortedAccounts.map(account => (
<Area
Expand Down
72 changes: 55 additions & 17 deletions packages/desktop-client/src/components/reports/reports/NetWorth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { NetWorthWidget, TimeFrame } from 'loot-core/types/models';

import { EditablePageHeaderTitle } from '@desktop-client/components/EditablePageHeaderTitle';
import { FinancialText } from '@desktop-client/components/FinancialText';
import { Checkbox } from '@desktop-client/components/forms';
import { MobileBackButton } from '@desktop-client/components/mobile/MobileBackButton';
import {
MobilePageHeader,
Expand All @@ -39,6 +40,7 @@ import { LoadingIndicator } from '@desktop-client/components/reports/LoadingIndi
import { ReportOptions } from '@desktop-client/components/reports/ReportOptions';
import { calculateTimeRange } from '@desktop-client/components/reports/reportRanges';
import { createSpreadsheet as netWorthSpreadsheet } from '@desktop-client/components/reports/spreadsheets/net-worth-spreadsheet';
import { useNetWorthProjectionRefresh } from '@desktop-client/components/reports/useNetWorthProjectionRefresh';
import { useReport } from '@desktop-client/components/reports/useReport';
import { fromDateRepr } from '@desktop-client/components/reports/util';
import { useAccounts } from '@desktop-client/hooks/useAccounts';
Expand Down Expand Up @@ -112,6 +114,9 @@ function NetWorthInner({ widget }: NetWorthInnerProps) {
const [graphMode, setGraphMode] = useState<'trend' | 'stacked'>(
widget?.meta?.mode || 'trend',
);
const [showProjection, setShowProjection] = useState(
widget?.meta?.showProjection ?? false,
);
// Combined setter: set mode and update interval (unless interval was set in widget meta)
const setModeAndInterval = useCallback(
(newMode: TimeFrame['mode']) => {
Expand All @@ -127,21 +132,18 @@ function NetWorthInner({ widget }: NetWorthInnerProps) {

const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx');
const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';

const reportParams = useMemo(
() =>
netWorthSpreadsheet(
start,
end,
accounts,
conditions,
conditionsOp,
locale,
interval,
firstDayOfWeekIdx,
format,
),
[
const [budgetType = 'envelope'] = useSyncedPref('budgetType');

const isProjectionEnabled =
showProjection && graphMode === 'trend' && interval === 'Monthly';
const projectionRevision = useNetWorthProjectionRefresh({
budgetType,
enabled: isProjectionEnabled,
});

const reportParams = useMemo(() => {
void projectionRevision;
return netWorthSpreadsheet(
start,
end,
accounts,
Expand All @@ -151,8 +153,23 @@ function NetWorthInner({ widget }: NetWorthInnerProps) {
interval,
firstDayOfWeekIdx,
format,
],
);
isProjectionEnabled,
budgetType,
);
}, [
start,
end,
accounts,
conditions,
conditionsOp,
locale,
interval,
firstDayOfWeekIdx,
format,
isProjectionEnabled,
budgetType,
projectionRevision,
]);
const data = useReport('net_worth', reportParams);
useEffect(() => {
async function run() {
Expand Down Expand Up @@ -238,6 +255,7 @@ function NetWorthInner({ widget }: NetWorthInnerProps) {
conditionsOp,
interval,
mode: graphMode,
showProjection,
timeFrame: {
start,
end,
Expand Down Expand Up @@ -326,6 +344,26 @@ function NetWorthInner({ widget }: NetWorthInnerProps) {
<>
<IntervalSelector interval={interval} onChange={setInterval} />
<ModeSelector mode={graphMode} onChange={setGraphMode} />
{graphMode === 'trend' && interval === 'Monthly' && (
<label
htmlFor="net-worth-projection"
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
userSelect: 'none',
marginLeft: 8,
}}
>
<Checkbox
id="net-worth-projection"
checked={showProjection}
onChange={event => setShowProjection(event.target.checked)}
style={{ marginRight: 6 }}
/>
<Trans>Show projection</Trans>
</label>
)}
</>
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ReportCard } from '@desktop-client/components/reports/ReportCard';
import { ReportCardName } from '@desktop-client/components/reports/ReportCardName';
import { calculateTimeRange } from '@desktop-client/components/reports/reportRanges';
import { createSpreadsheet as netWorthSpreadsheet } from '@desktop-client/components/reports/spreadsheets/net-worth-spreadsheet';
import { useNetWorthProjectionRefresh } from '@desktop-client/components/reports/useNetWorthProjectionRefresh';
import { useReport } from '@desktop-client/components/reports/useReport';
import { useWidgetCopyMenu } from '@desktop-client/components/reports/useWidgetCopyMenu';
import { useFormat } from '@desktop-client/hooks/useFormat';
Expand Down Expand Up @@ -51,6 +52,16 @@ export function NetWorthCard({
const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx');
const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';
const format = useFormat();
const [budgetTypePref] = useSyncedPref('budgetType');
const budgetType = budgetTypePref || 'envelope';
const isProjectionEnabled =
(meta?.showProjection ?? false) &&
(meta?.mode ?? 'trend') === 'trend' &&
(meta?.interval ?? 'Monthly') === 'Monthly';
const projectionRevision = useNetWorthProjectionRefresh({
budgetType,
enabled: isProjectionEnabled,
});

const [latestTransaction, setLatestTransaction] = useState<string>('');
const [nameMenuOpen, setNameMenuOpen] = useState(false);
Expand All @@ -77,31 +88,35 @@ export function NetWorthCard({
const onCardHover = useCallback(() => setIsCardHovered(true), []);
const onCardHoverEnd = useCallback(() => setIsCardHovered(false), []);

const params = useMemo(
() =>
netWorthSpreadsheet(
start,
end,
accounts,
meta?.conditions,
meta?.conditionsOp,
locale,
meta?.interval || 'Monthly',
firstDayOfWeekIdx,
format,
),
[
const params = useMemo(() => {
void projectionRevision;
return netWorthSpreadsheet(
start,
end,
accounts,
meta?.conditions,
meta?.conditionsOp,
locale,
meta?.interval,
meta?.interval || 'Monthly',
firstDayOfWeekIdx,
format,
],
);
isProjectionEnabled,
budgetType,
);
}, [
start,
end,
accounts,
meta?.conditions,
meta?.conditionsOp,
locale,
meta?.interval,
firstDayOfWeekIdx,
format,
isProjectionEnabled,
budgetType,
projectionRevision,
]);
const data = useReport('net_worth', params);

return (
Expand Down
Loading
Loading