From 35bc31fb3d3bc7ee12272615c4fd343528a4281c Mon Sep 17 00:00:00 2001 From: Felix Schulze Date: Sat, 6 Dec 2025 22:58:10 +0100 Subject: [PATCH] tootip and graph style fixes --- src/app/components/FireCalculatorForm.tsx | 156 +++++++++++++--------- 1 file changed, 96 insertions(+), 60 deletions(-) diff --git a/src/app/components/FireCalculatorForm.tsx b/src/app/components/FireCalculatorForm.tsx index bbb54eb..dd42900 100644 --- a/src/app/components/FireCalculatorForm.tsx +++ b/src/app/components/FireCalculatorForm.tsx @@ -11,7 +11,12 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from '@/components/ui/chart'; import { Area, AreaChart, @@ -24,7 +29,7 @@ import { } from 'recharts'; import { Slider } from '@/components/ui/slider'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; +import type { NameType, Payload, ValueType } from 'recharts/types/component/DefaultTooltipContent'; import { Calculator, Info, Share2, Check } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import BlurThing from './blur-thing'; @@ -87,20 +92,57 @@ const formatNumber = (value: number | null) => { }).format(value); }; -// Helper function to render tooltip for chart -const tooltipRenderer = ({ active, payload }: TooltipProps) => { - if (active && payload?.[0]?.payload) { - const data = payload[0].payload as YearlyData; - return ( -
-

{`Year: ${data.year.toString()} (Age: ${data.age.toString()})`}

-

{`Median Balance: ${formatNumber(data.balanceP50 ?? data.balance)}`}

-

{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}

-

{`Phase: ${data.phase === 'accumulation' ? 'Accumulation' : 'Retirement'}`}

-
- ); +const formatNumberShort = (value: number) => { + if (value >= 1000000) { + return `${(value / 1000000).toPrecision(3)}M`; + } else if (value >= 1000) { + return `${(value / 1000).toPrecision(3)}K`; + } else if (value <= -1000000) { + return `${(value / 1000000).toPrecision(3)}M`; + } else if (value <= -1000) { + return `${(value / 1000).toPrecision(3)}K`; } - return null; + return value.toString(); +}; + +// Chart tooltip with the same styling as ChartTooltipContent, but with our custom label info +const tooltipRenderer = ({ active, payload, label }: TooltipProps) => { + const allowedKeys = new Set(['balance', 'monthlyAllowance']); + const filteredPayload: Payload[] = (payload ?? []) + .filter( + (item): item is Payload => + typeof item.dataKey === 'string' && allowedKeys.has(item.dataKey), + ) + .map((item) => ({ + ...item, + value: formatNumberShort(item.value as number), + })); + const safeLabel = typeof label === 'string' || typeof label === 'number' ? label : undefined; + + return ( + []) => { + const point = items.length > 0 ? (items[0]?.payload as YearlyData | undefined) : undefined; + if (!point) { + return null; + } + + const phaseLabel = point.phase === 'retirement' ? 'Retirement phase' : 'Accumulation phase'; + + return ( +
+ {`Year ${String(point.year)} (Age ${String(point.age)})`} + {phaseLabel} +
+ ); + }} + /> + ); }; export default function FireCalculatorForm({ @@ -402,6 +444,28 @@ export default function FireCalculatorForm({ mcRange: (row.balanceP90 ?? 0) - (row.balanceP10 ?? 0), })) ?? []; + const projectionChartConfig: ChartConfig = { + year: { + label: 'Year', + }, + balance: { + label: 'Balance', + color: 'var(--color-orange-500)', + }, + balanceP10: { + label: 'P10 balance', + color: 'var(--color-orange-500)', + }, + balanceP90: { + label: 'P90 balance', + color: 'var(--color-orange-500)', + }, + monthlyAllowance: { + label: 'Monthly allowance', + color: 'var(--color-secondary)', + }, + }; + return ( <> @@ -857,7 +921,7 @@ export default function FireCalculatorForm({ Shaded band shows 40th-60th percentile outcomes across 2000 simulations.

)} - + { - if (value >= 1000000) { - return `${(value / 1000000).toPrecision(3)}M`; - } else if (value >= 1000) { - return `${(value / 1000).toPrecision(3)}K`; - } else if (value <= -1000000) { - return `${(value / 1000000).toPrecision(3)}M`; - } else if (value <= -1000) { - return `${(value / 1000).toPrecision(3)}K`; - } - return value.toString(); - }} + tickFormatter={formatNumberShort} width={30} stroke="var(--color-orange-500)" tick={{}} @@ -892,37 +945,19 @@ export default function FireCalculatorForm({ { - if (value >= 1000000) { - return `${(value / 1000000).toPrecision(3)}M`; - } else if (value >= 1000) { - return `${(value / 1000).toPrecision(3)}K`; - } - return value.toString(); - }} + tickFormatter={formatNumberShort} width={30} - stroke="var(--color-red-600)" - /> - { - return value; - }} - labelKey="year" - indicator="line" - /> - } + stroke="var(--color-primary)" /> + - + - - + +