@retiregolden/planner-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +181 -0
- package/package.json +77 -0
- package/src/App.tsx +246 -0
- package/src/RouteErrorBoundary.tsx +45 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/data/fedInvestClient.ts +113 -0
- package/src/data/localStore.ts +42 -0
- package/src/data/planOrigin.ts +24 -0
- package/src/data/planStore.ts +165 -0
- package/src/data/v2Backup.ts +101 -0
- package/src/import/ImportPage.tsx +347 -0
- package/src/import/ReviewChecklistView.tsx +38 -0
- package/src/import/brokerCsv.ts +395 -0
- package/src/import/csv.ts +133 -0
- package/src/import/genericCsv.ts +224 -0
- package/src/import/projectionLab.ts +350 -0
- package/src/import/reviewChecklist.ts +33 -0
- package/src/import/tenForty.ts +275 -0
- package/src/index.css +630 -0
- package/src/index.ts +16 -0
- package/src/learn/ArticleBody.tsx +78 -0
- package/src/learn/ArticlePage.tsx +57 -0
- package/src/learn/GlossaryPage.tsx +33 -0
- package/src/learn/LearnAboutScreen.tsx +41 -0
- package/src/learn/LearnCards.tsx +41 -0
- package/src/learn/LearnLink.tsx +91 -0
- package/src/learn/LearningCenterPage.tsx +114 -0
- package/src/learn/SourcesPage.tsx +98 -0
- package/src/learn/components/ArticleFigure.tsx +34 -0
- package/src/learn/components/ArticleShell.tsx +86 -0
- package/src/learn/components/ComparisonTable.tsx +42 -0
- package/src/learn/components/FormulaBlock.tsx +34 -0
- package/src/learn/components/PurchasingPowerChart.tsx +41 -0
- package/src/learn/components/RelatedArticles.tsx +27 -0
- package/src/learn/components/ScenarioCard.tsx +24 -0
- package/src/learn/components/SourceList.tsx +23 -0
- package/src/learn/components/charts.tsx +21 -0
- package/src/learn/content/about-retiregolden.ts +100 -0
- package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
- package/src/learn/content/account-types-overview.ts +106 -0
- package/src/learn/content/after-tax-estate.ts +111 -0
- package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
- package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
- package/src/learn/content/assumption-general-inflation.ts +82 -0
- package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
- package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
- package/src/learn/content/assumption-investment-returns.ts +90 -0
- package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
- package/src/learn/content/assumption-recent-magi.ts +83 -0
- package/src/learn/content/assumption-social-security-cola.ts +89 -0
- package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
- package/src/learn/content/assumption-state-tax-override.ts +79 -0
- package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
- package/src/learn/content/break-even-useful-lens.ts +94 -0
- package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
- package/src/learn/content/cola-and-inflation-protection.ts +102 -0
- package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
- package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
- package/src/learn/content/earnings-test-before-fra.ts +100 -0
- package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
- package/src/learn/content/examplePlanArticles.ts +525 -0
- package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
- package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
- package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
- package/src/learn/content/funded-ratio.ts +70 -0
- package/src/learn/content/healthcare-after-65.ts +103 -0
- package/src/learn/content/healthcare-before-65.ts +104 -0
- package/src/learn/content/historical-vs-random-return-models.ts +101 -0
- package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
- package/src/learn/content/how-much-can-i-spend.ts +105 -0
- package/src/learn/content/how-social-security-is-taxed.ts +95 -0
- package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
- package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
- package/src/learn/content/how-to-model-accumulation.ts +67 -0
- package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
- package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
- package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
- package/src/learn/content/inflation-risk.ts +98 -0
- package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
- package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
- package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
- package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
- package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
- package/src/learn/content/longevity-risk.ts +99 -0
- package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
- package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
- package/src/learn/content/mortality-weighted-social-security.ts +113 -0
- package/src/learn/content/moving-to-retiregolden.ts +86 -0
- package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
- package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
- package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
- package/src/learn/content/pensions-and-annuities.ts +101 -0
- package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
- package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
- package/src/learn/content/planner-overview.ts +106 -0
- package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
- package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
- package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
- package/src/learn/content/reading-the-results-page.ts +96 -0
- package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
- package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
- package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
- package/src/learn/content/risk-based-guardrails.ts +100 -0
- package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
- package/src/learn/content/roth-conversion-basics.ts +104 -0
- package/src/learn/content/rsus-and-espp.ts +101 -0
- package/src/learn/content/rule-of-55-and-72t.ts +107 -0
- package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
- package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
- package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
- package/src/learn/content/sequence-of-returns-risk.ts +98 -0
- package/src/learn/content/social-security-bridge.ts +67 -0
- package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
- package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
- package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
- package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
- package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
- package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
- package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
- package/src/learn/content/step-up-in-basis.ts +102 -0
- package/src/learn/content/survivor-planning-for-couples.ts +110 -0
- package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
- package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
- package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
- package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
- package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
- package/src/learn/content/tips-ladders.ts +92 -0
- package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
- package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
- package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
- package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
- package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
- package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
- package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
- package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
- package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
- package/src/learn/content/what-is-fire.ts +65 -0
- package/src/learn/content/what-monte-carlo-proves.ts +98 -0
- package/src/learn/content/what-retiregolden-models.ts +103 -0
- package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
- package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
- package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
- package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
- package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
- package/src/learn/content/withdrawal-order-basics.ts +105 -0
- package/src/learn/glossary.ts +191 -0
- package/src/learn/inlineMarkdown.tsx +54 -0
- package/src/learn/learn.css +537 -0
- package/src/learn/learningRegistry.ts +502 -0
- package/src/longevity/LongevityResults.tsx +85 -0
- package/src/longevity/LongevityWizard.tsx +305 -0
- package/src/longevity/constants.ts +15 -0
- package/src/longevity/factors.ts +125 -0
- package/src/longevity/model.ts +31 -0
- package/src/longevity/persistedGuard.ts +129 -0
- package/src/longevity/storage.ts +40 -0
- package/src/mc/messages.ts +118 -0
- package/src/mc/monteCarlo.worker.ts +44 -0
- package/src/mc/pool.ts +267 -0
- package/src/mc/runRequest.ts +125 -0
- package/src/optimize/messages.ts +84 -0
- package/src/optimize/optimize.worker.ts +29 -0
- package/src/optimize/runOptimize.ts +92 -0
- package/src/optimize/runSpendingSolve.ts +47 -0
- package/src/optimize/runner.ts +21 -0
- package/src/optimize/spendingMessages.ts +44 -0
- package/src/optimize/spendingRunner.ts +21 -0
- package/src/optimize/spendingSolve.worker.ts +18 -0
- package/src/planner/AssumptionsCardPage.tsx +136 -0
- package/src/planner/BucketLensCard.tsx +114 -0
- package/src/planner/ComparePlansPage.tsx +219 -0
- package/src/planner/DisclaimerPage.tsx +88 -0
- package/src/planner/HowTestedPage.tsx +159 -0
- package/src/planner/LiveStatus.tsx +15 -0
- package/src/planner/LongevityModal.tsx +55 -0
- package/src/planner/Modal.tsx +97 -0
- package/src/planner/MonteCarloPage.tsx +907 -0
- package/src/planner/OptimizePage.tsx +611 -0
- package/src/planner/PlanContext.tsx +198 -0
- package/src/planner/PlanPickerPage.tsx +124 -0
- package/src/planner/PlanWorkspace.tsx +290 -0
- package/src/planner/ProvenancePanel.tsx +45 -0
- package/src/planner/RelocationComparePage.tsx +485 -0
- package/src/planner/ReportPage.tsx +375 -0
- package/src/planner/ResultsPage.tsx +817 -0
- package/src/planner/ScenariosPage.tsx +285 -0
- package/src/planner/SocialSecuritySection.tsx +556 -0
- package/src/planner/SpendingSolverPage.tsx +512 -0
- package/src/planner/SsAnalysisPage.tsx +1134 -0
- package/src/planner/SurvivalPercentileModal.tsx +161 -0
- package/src/planner/SurvivorTransitionPage.tsx +286 -0
- package/src/planner/assumptionsExport.ts +371 -0
- package/src/planner/bucketLens.ts +89 -0
- package/src/planner/chartFrame.ts +8 -0
- package/src/planner/chartStyle.ts +11 -0
- package/src/planner/dialogViews.tsx +184 -0
- package/src/planner/dialogs.tsx +133 -0
- package/src/planner/examples/ExampleLibrary.tsx +189 -0
- package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
- package/src/planner/examples/ExamplesPage.tsx +25 -0
- package/src/planner/examples/OpenExampleButton.tsx +61 -0
- package/src/planner/examples/buildAggressiveSaver.ts +102 -0
- package/src/planner/examples/buildAnnuityEstate.ts +137 -0
- package/src/planner/examples/buildBaristaFire.ts +115 -0
- package/src/planner/examples/buildBracketFillRoth.ts +65 -0
- package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
- package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
- package/src/planner/examples/buildCoastFire.ts +88 -0
- package/src/planner/examples/buildContext.ts +20 -0
- package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
- package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
- package/src/planner/examples/buildExampleCouple.ts +103 -0
- package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
- package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
- package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
- package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
- package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
- package/src/planner/examples/buildLeanFatFire.ts +109 -0
- package/src/planner/examples/buildLtcShock.ts +62 -0
- package/src/planner/examples/buildMovingStateTax.ts +53 -0
- package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
- package/src/planner/examples/buildRmdIrmaa.ts +55 -0
- package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
- package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
- package/src/planner/examples/buildSurvivorYears.ts +62 -0
- package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
- package/src/planner/examples/exampleCopy.ts +23 -0
- package/src/planner/examples/loadExample.ts +90 -0
- package/src/planner/examples/registry.ts +313 -0
- package/src/planner/explainPanels.tsx +233 -0
- package/src/planner/fields.tsx +381 -0
- package/src/planner/format.ts +33 -0
- package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
- package/src/planner/home/GettingStartedPaths.tsx +46 -0
- package/src/planner/home/GettingStartedReopener.tsx +32 -0
- package/src/planner/home/StartHereLinks.tsx +22 -0
- package/src/planner/home/WelcomeHero.tsx +39 -0
- package/src/planner/home/YourPlans.tsx +72 -0
- package/src/planner/home/importErrorMessage.ts +22 -0
- package/src/planner/home/startHereSlugs.ts +7 -0
- package/src/planner/home/useHomeData.ts +190 -0
- package/src/planner/home/useHomeMode.ts +47 -0
- package/src/planner/householdActions.ts +22 -0
- package/src/planner/insights/InsightCardView.tsx +340 -0
- package/src/planner/insights/InsightsPage.tsx +204 -0
- package/src/planner/insights/categoryLabels.ts +11 -0
- package/src/planner/learnLinks.ts +85 -0
- package/src/planner/marketModelPicker.ts +172 -0
- package/src/planner/optimizePageChart.ts +40 -0
- package/src/planner/optimizePageClaim.ts +64 -0
- package/src/planner/planCompleteness.ts +27 -0
- package/src/planner/planContextCore.ts +26 -0
- package/src/planner/planner.css +2304 -0
- package/src/planner/provenanceLinks.ts +25 -0
- package/src/planner/sections/AccountFields.tsx +872 -0
- package/src/planner/sections/AccountsSection.tsx +89 -0
- package/src/planner/sections/AllocationPanel.tsx +261 -0
- package/src/planner/sections/AssumptionsSection.tsx +256 -0
- package/src/planner/sections/HouseholdSection.tsx +243 -0
- package/src/planner/sections/IncomeFloorSection.tsx +418 -0
- package/src/planner/sections/IncomeSection.tsx +170 -0
- package/src/planner/sections/InsuranceSection.tsx +362 -0
- package/src/planner/sections/SpendingSection.tsx +904 -0
- package/src/planner/sections/StrategySection.tsx +349 -0
- package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
- package/src/planner/sections/sectionHelpers.ts +48 -0
- package/src/planner/sections/shared.tsx +15 -0
- package/src/planner/sections.tsx +15 -0
- package/src/planner/ssAnalysis.ts +325 -0
- package/src/planner/successBand.ts +20 -0
- package/src/planner/survivorAnalysis.ts +277 -0
- package/src/planner/usStates.ts +19 -0
- package/src/planner/useMcSuccessRate.ts +77 -0
- package/src/planner/useProjection.ts +63 -0
- package/src/relocation/messages.ts +21 -0
- package/src/relocation/relocation.worker.ts +18 -0
- package/src/relocation/runRelocation.ts +17 -0
- package/src/relocation/runner.ts +22 -0
- package/src/report/brandingContext.ts +15 -0
- package/src/report/downloadReport.ts +34 -0
- package/src/report/reportHtml.ts +547 -0
- package/src/routes/LearnRoutes.tsx +46 -0
- package/src/routes/PlanRoutes.tsx +55 -0
- package/src/routes/RouteFallback.tsx +9 -0
- package/src/socialSecurity/breakEven.ts +107 -0
- package/src/socialSecurity/expectedPv.ts +164 -0
- package/src/socialSecurity/explain.ts +92 -0
- package/src/socialSecurity/ficaReturn.ts +81 -0
- package/src/socialSecurity/persistedSsGuard.ts +138 -0
- package/src/socialSecurity/ssFormUtils.ts +48 -0
- package/src/socialSecurity/ssaStatementXml.ts +156 -0
- package/src/socialSecurity/storage.ts +69 -0
- package/src/socialSecurity/survivorSwitching.ts +153 -0
- package/src/testSupport/samplePlan.ts +2 -0
- package/src/workers/run.ts +45 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic results: net worth by account category, income vs spending,
|
|
3
|
+
* tax detail, modeling warnings, and the full year-by-year drill-down table
|
|
4
|
+
* with a nominal / today's-dollars toggle and CSV export.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMemo, useState } from 'react'
|
|
8
|
+
import { Link } from 'react-router-dom'
|
|
9
|
+
import {
|
|
10
|
+
Area,
|
|
11
|
+
AreaChart,
|
|
12
|
+
Bar,
|
|
13
|
+
BarChart,
|
|
14
|
+
CartesianGrid,
|
|
15
|
+
Legend,
|
|
16
|
+
Line,
|
|
17
|
+
LineChart,
|
|
18
|
+
ReferenceLine,
|
|
19
|
+
ResponsiveContainer,
|
|
20
|
+
Tooltip,
|
|
21
|
+
XAxis,
|
|
22
|
+
YAxis,
|
|
23
|
+
} from 'recharts'
|
|
24
|
+
|
|
25
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
26
|
+
import { startingInvestableOf } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
|
|
27
|
+
import { DEFAULT_PATH_COUNT } from '../mc/pool'
|
|
28
|
+
import type { YearResult } from '@retiregolden/engine/projection/types'
|
|
29
|
+
import { usePlan } from './planContextCore'
|
|
30
|
+
import { isPlanIncomplete } from './planCompleteness'
|
|
31
|
+
import { LearnAboutScreen } from '../learn/LearnAboutScreen'
|
|
32
|
+
import { downloadStandaloneReport } from '../report/downloadReport'
|
|
33
|
+
import { useReportBranding } from '../report/brandingContext'
|
|
34
|
+
import { fmtMoney, fmtMoneyCompact } from './format'
|
|
35
|
+
import { useProjection } from './useProjection'
|
|
36
|
+
import { BucketLensCard } from './BucketLensCard'
|
|
37
|
+
import { FundedRatioCard } from './sections/IncomeFloorSection'
|
|
38
|
+
import { chartTooltipStyle } from './chartStyle'
|
|
39
|
+
import { frameH } from './chartFrame'
|
|
40
|
+
import { useMcSuccessRate } from './useMcSuccessRate'
|
|
41
|
+
|
|
42
|
+
type Dollars = 'nominal' | 'today'
|
|
43
|
+
|
|
44
|
+
const CATEGORIES = ['cash', 'taxable', 'equityComp', 'traditional', 'roth', 'hsa'] as const
|
|
45
|
+
const CAT_LABEL: Record<(typeof CATEGORIES)[number], string> = {
|
|
46
|
+
cash: 'Cash',
|
|
47
|
+
taxable: 'Taxable',
|
|
48
|
+
equityComp: 'Equity comp',
|
|
49
|
+
traditional: 'Traditional',
|
|
50
|
+
roth: 'Roth',
|
|
51
|
+
hsa: 'HSA',
|
|
52
|
+
}
|
|
53
|
+
const CAT_COLOR: Record<(typeof CATEGORIES)[number], string> = {
|
|
54
|
+
cash: 'var(--chart-5)',
|
|
55
|
+
taxable: 'var(--chart-2)',
|
|
56
|
+
equityComp: 'var(--chart-6)',
|
|
57
|
+
traditional: 'var(--chart-3)',
|
|
58
|
+
roth: 'var(--chart-1)',
|
|
59
|
+
hsa: 'var(--chart-4)',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Income streams, stacked bottom-to-top in the income breakdown. */
|
|
63
|
+
const INCOME_SOURCES = [
|
|
64
|
+
{ key: 'wages', label: 'Wages', color: 'var(--chart-2)' },
|
|
65
|
+
{ key: 'socialSecurity', label: 'Social Security', color: 'var(--chart-1)' },
|
|
66
|
+
{ key: 'pension', label: 'Pension', color: 'var(--chart-3)' },
|
|
67
|
+
{ key: 'annuity', label: 'Annuity', color: 'var(--chart-4)' },
|
|
68
|
+
{ key: 'tipsLadder', label: 'TIPS ladder', color: 'var(--chart-8)' },
|
|
69
|
+
{ key: 'recurring', label: 'Other recurring', color: 'var(--chart-5)' },
|
|
70
|
+
{ key: 'oneTime', label: 'One-time', color: 'var(--chart-6)' },
|
|
71
|
+
{ key: 'taxableYield', label: 'Brokerage yield', color: 'var(--chart-7)' },
|
|
72
|
+
] as const
|
|
73
|
+
|
|
74
|
+
/** Spending categories, stacked bottom-to-top; sums to expenses + tax + penalties. */
|
|
75
|
+
const EXPENSE_CATEGORIES = [
|
|
76
|
+
{ key: 'base', label: 'Baseline living', color: 'var(--chart-1)' },
|
|
77
|
+
{ key: 'healthcare', label: 'Healthcare', color: 'var(--chart-2)' },
|
|
78
|
+
{ key: 'property', label: 'Property tax + insurance', color: 'var(--chart-3)' },
|
|
79
|
+
{ key: 'debt', label: 'Debt payments', color: 'var(--chart-4)' },
|
|
80
|
+
{ key: 'insurance', label: 'Insurance premiums', color: 'var(--chart-5)' },
|
|
81
|
+
{ key: 'care', label: 'Long-term care (net)', color: 'var(--chart-6)' },
|
|
82
|
+
{ key: 'goals', label: 'One-time goals', color: 'var(--chart-8)' },
|
|
83
|
+
{ key: 'taxes', label: 'Tax + penalties', color: 'var(--muted)' },
|
|
84
|
+
] as const
|
|
85
|
+
|
|
86
|
+
function categoryBalances(plan: Plan, y: YearResult): Record<(typeof CATEGORIES)[number], number> {
|
|
87
|
+
const out = { cash: 0, taxable: 0, equityComp: 0, traditional: 0, roth: 0, hsa: 0 }
|
|
88
|
+
for (const a of plan.accounts) {
|
|
89
|
+
if ((CATEGORIES as readonly string[]).includes(a.type)) {
|
|
90
|
+
out[a.type as (typeof CATEGORIES)[number]] += y.balances[a.id] ?? 0
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function moneyTick(v: number): string {
|
|
97
|
+
return fmtMoneyCompact(v)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function DollarsToggle({ value, onChange }: { value: Dollars; onChange: (v: Dollars) => void }) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="seg" role="group" aria-label="Dollar display">
|
|
103
|
+
<button type="button" aria-pressed={value === 'today'} onClick={() => onChange('today')}>
|
|
104
|
+
Today's $
|
|
105
|
+
</button>
|
|
106
|
+
<button type="button" aria-pressed={value === 'nominal'} onClick={() => onChange('nominal')}>
|
|
107
|
+
Nominal $
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tooltipProps = {
|
|
114
|
+
formatter: (v: unknown) => fmtMoney(Number(v)),
|
|
115
|
+
contentStyle: chartTooltipStyle,
|
|
116
|
+
wrapperStyle: { zIndex: 2 },
|
|
117
|
+
} as const
|
|
118
|
+
|
|
119
|
+
/** "1,000" — keeps verdict copy in sync if the default path count changes. */
|
|
120
|
+
const PATH_COUNT_LABEL = DEFAULT_PATH_COUNT.toLocaleString()
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The FIRE metrics + FI-target chart. Rendered as the leading card only for
|
|
124
|
+
* households still accumulating with retirement 5+ years out; for everyone
|
|
125
|
+
* else it lives behind a disclosure below the balances chart. The FI-date tile
|
|
126
|
+
* is omitted (not "—") when FI is never reached, so a successful near-retiree
|
|
127
|
+
* plan never wears a failure-flavored verdict on a question it didn't ask.
|
|
128
|
+
*/
|
|
129
|
+
function FireLens({
|
|
130
|
+
view,
|
|
131
|
+
plan,
|
|
132
|
+
rows,
|
|
133
|
+
dollarLabel,
|
|
134
|
+
}: {
|
|
135
|
+
view: ReturnType<typeof useProjection>
|
|
136
|
+
plan: Plan
|
|
137
|
+
rows: ReadonlyArray<{ year: number; investable: number; fiTarget: number }>
|
|
138
|
+
dollarLabel: string
|
|
139
|
+
}) {
|
|
140
|
+
return (
|
|
141
|
+
<>
|
|
142
|
+
<p className="card-hint">
|
|
143
|
+
{view.summary.fiYear !== null
|
|
144
|
+
? `Based on your safe withdrawal rate assumption (${plan.assumptions.safeWithdrawalRatePct ?? 4}%), you reach FI in ${view.summary.fiYear} (age ${view.summary.fiAge}).`
|
|
145
|
+
: 'Your investable balance stays below the FI target through the plan horizon.'}{' '}
|
|
146
|
+
Chart shown in {dollarLabel}.
|
|
147
|
+
</p>
|
|
148
|
+
<div className="metric-panel stat-grid">
|
|
149
|
+
<div>
|
|
150
|
+
<div className="stat-value stat-value--sm">{fmtMoney(view.summary.fiNumber)}</div>
|
|
151
|
+
<div className="muted">FI target portfolio</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div>
|
|
154
|
+
<div className="stat-value stat-value--sm">{fmtMoney(view.summary.coastFireNumber)}</div>
|
|
155
|
+
<div className="muted">Coast-FIRE target (now)</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<div className="stat-value stat-value--sm">{view.summary.averagePreRetirementSavingsRatePct.toFixed(1)}%</div>
|
|
159
|
+
<div className="muted">average savings rate</div>
|
|
160
|
+
</div>
|
|
161
|
+
{view.summary.fiYear !== null ? (
|
|
162
|
+
<div>
|
|
163
|
+
<div className="stat-value stat-value--sm">
|
|
164
|
+
{view.summary.fiYear} (Age {view.summary.fiAge})
|
|
165
|
+
</div>
|
|
166
|
+
<div className="muted">FI date / age</div>
|
|
167
|
+
</div>
|
|
168
|
+
) : null}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="chart-frame" style={frameH(320)}>
|
|
172
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
173
|
+
<LineChart
|
|
174
|
+
data={[...rows]}
|
|
175
|
+
margin={{ left: 12, right: 8, top: 8 }}
|
|
176
|
+
aria-label="Path to financial independence: investable portfolio vs. FI target, year by year"
|
|
177
|
+
>
|
|
178
|
+
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
|
179
|
+
<XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
|
|
180
|
+
<YAxis tickFormatter={moneyTick} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
|
|
181
|
+
<Legend />
|
|
182
|
+
<Line dataKey="investable" name="Investable Portfolio" stroke="var(--chart-1)" dot={false} strokeWidth={3} />
|
|
183
|
+
<Line dataKey="fiTarget" name="FI Target Line" stroke="var(--bad)" strokeDasharray="4 4" dot={false} strokeWidth={2} />
|
|
184
|
+
<Tooltip {...tooltipProps} />
|
|
185
|
+
</LineChart>
|
|
186
|
+
</ResponsiveContainer>
|
|
187
|
+
</div>
|
|
188
|
+
</>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function ResultsPage() {
|
|
193
|
+
const { plan } = usePlan()
|
|
194
|
+
const reportBranding = useReportBranding()
|
|
195
|
+
const view = useProjection(plan)
|
|
196
|
+
const [dollars, setDollars] = useState<Dollars>('today')
|
|
197
|
+
const dollarLabel = dollars === 'today' ? 'today\'s $' : 'nominal $'
|
|
198
|
+
const adj = useMemo(
|
|
199
|
+
() => (year: number, v: number) => (dollars === 'today' ? view.deflate(year, v) : v),
|
|
200
|
+
[dollars, view],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const hasCarryforward = plan.household.capitalLossCarryforward > 0
|
|
204
|
+
const hasAmt = view.result.years.some((y) => y.amt > 0.5)
|
|
205
|
+
const hasFlexibleGoalControls = plan.expenses.oneTimeGoals.some(
|
|
206
|
+
(g) =>
|
|
207
|
+
g.classification !== undefined ||
|
|
208
|
+
g.flexibility !== undefined ||
|
|
209
|
+
g.earliestYear !== undefined ||
|
|
210
|
+
g.latestYear !== undefined ||
|
|
211
|
+
g.priority !== undefined ||
|
|
212
|
+
g.allowPartialFunding === true ||
|
|
213
|
+
g.minFundingPct !== undefined,
|
|
214
|
+
)
|
|
215
|
+
const hasLayeredSpending =
|
|
216
|
+
plan.expenses.requiredAnnual !== undefined ||
|
|
217
|
+
(plan.expenses.idealAnnual ?? 0) > 0 ||
|
|
218
|
+
(plan.expenses.excessAnnual ?? 0) > 0 ||
|
|
219
|
+
(plan.expenses.spendingPolicy !== undefined && plan.expenses.spendingPolicy.mode !== 'fixedTarget') ||
|
|
220
|
+
hasFlexibleGoalControls
|
|
221
|
+
// Highlight the first year the carryforward shelters realized gains (the early
|
|
222
|
+
// brokerage-drawdown years it's meant for), else the first projection year.
|
|
223
|
+
const carryforwardHighlight = hasCarryforward
|
|
224
|
+
? (view.result.years.find((y) => y.capitalLossUsedAgainstGains > 0.5) ?? view.result.years[0])
|
|
225
|
+
: undefined
|
|
226
|
+
|
|
227
|
+
const rows = useMemo(
|
|
228
|
+
() =>
|
|
229
|
+
view.result.years.map((y) => {
|
|
230
|
+
const cats = categoryBalances(plan, y)
|
|
231
|
+
const nominalFiTarget = view.summary.fiNumber * Math.pow(1 + plan.assumptions.inflationPct / 100, y.year - view.startYear)
|
|
232
|
+
return {
|
|
233
|
+
year: y.year,
|
|
234
|
+
// Zero balances render as null so empty categories stay out of the
|
|
235
|
+
// tooltip (an "HSA: $0" row is noise in a six-series stack).
|
|
236
|
+
...Object.fromEntries(
|
|
237
|
+
CATEGORIES.map((c) => {
|
|
238
|
+
const v = adj(y.year, cats[c])
|
|
239
|
+
return [c, v > 0.5 ? v : null]
|
|
240
|
+
}),
|
|
241
|
+
),
|
|
242
|
+
income: adj(y.year, y.incomes.total),
|
|
243
|
+
spending: adj(y.year, y.expenses.total + y.tax + y.penalties),
|
|
244
|
+
tax: adj(y.year, y.tax),
|
|
245
|
+
magi: adj(y.year, y.magi),
|
|
246
|
+
shortfall: adj(y.year, y.shortfall),
|
|
247
|
+
investable: adj(y.year, y.investableTotal),
|
|
248
|
+
fiTarget: adj(y.year, nominalFiTarget),
|
|
249
|
+
}
|
|
250
|
+
}),
|
|
251
|
+
[view, plan, adj],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// Zero series values render as null so the tooltip (filterNull) skips them —
|
|
255
|
+
// a stack of 6–8 series otherwise lists every "$0" row as noise.
|
|
256
|
+
const orNull = (v: number) => (v > 0.5 ? v : null)
|
|
257
|
+
|
|
258
|
+
const incomeRows = useMemo(
|
|
259
|
+
() =>
|
|
260
|
+
view.result.years.map((y) => ({
|
|
261
|
+
year: y.year,
|
|
262
|
+
wages: orNull(adj(y.year, y.incomes.wages)),
|
|
263
|
+
socialSecurity: orNull(adj(y.year, y.incomes.socialSecurity)),
|
|
264
|
+
pension: orNull(adj(y.year, y.incomes.pension)),
|
|
265
|
+
annuity: orNull(adj(y.year, y.incomes.annuity)),
|
|
266
|
+
tipsLadder: orNull(adj(y.year, y.incomes.tipsLadder)),
|
|
267
|
+
recurring: orNull(adj(y.year, y.incomes.recurring)),
|
|
268
|
+
oneTime: orNull(adj(y.year, y.incomes.oneTime)),
|
|
269
|
+
taxableYield: orNull(adj(y.year, y.incomes.taxableYield)),
|
|
270
|
+
})),
|
|
271
|
+
[view, adj],
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
const expenseRows = useMemo(
|
|
275
|
+
() =>
|
|
276
|
+
view.result.years.map((y) => ({
|
|
277
|
+
year: y.year,
|
|
278
|
+
base: orNull(adj(y.year, y.expenses.baseSpending)),
|
|
279
|
+
healthcare: orNull(adj(y.year, y.expenses.healthcare)),
|
|
280
|
+
property: orNull(adj(y.year, y.expenses.propertyCosts)),
|
|
281
|
+
debt: orNull(adj(y.year, y.expenses.debtService)),
|
|
282
|
+
insurance: orNull(adj(y.year, y.expenses.insurancePremiums)),
|
|
283
|
+
care: orNull(adj(y.year, Math.max(0, y.expenses.careCost - y.expenses.ltcBenefit))),
|
|
284
|
+
goals: orNull(adj(y.year, y.expenses.oneTimeGoals)),
|
|
285
|
+
taxes: orNull(adj(y.year, y.tax + y.penalties)),
|
|
286
|
+
})),
|
|
287
|
+
[view, adj],
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const handleCsv = () => {
|
|
291
|
+
const cols = [
|
|
292
|
+
'year', 'filingStatus', 'wages', 'socialSecurity', 'pension', 'annuity', 'tipsLadder', 'recurring', 'oneTimeIncome', 'taxableInterest', 'ordinaryDividends', 'qualifiedDividends', 'taxableYield', 'totalIncome',
|
|
293
|
+
'baseSpending', 'goals', 'debtService', 'propertyCosts', 'healthcare', 'insurancePremiums', 'careCost', 'ltcBenefit', 'requiredSpending', 'targetSpending', 'idealSpending', 'excessSpending', 'intendedSpending', 'totalExpenses', 'contributions', 'employerMatch', 'rmd', 'qcd',
|
|
294
|
+
'rothConversion', 'tax', 'amt', 'penalties', 'magi', 'withdrawals', 'realizedGains', 'lossCarryforwardUsed', 'lossCarryforwardRemaining', 'shortfall', 'investable',
|
|
295
|
+
'requiredShortfall', 'targetShortfall', 'idealShortfall', 'excessShortfall', 'guardrailAction', 'guardrailFactor', 'flexibleGoalsFunded', 'flexibleGoalsPartiallyFunded', 'flexibleGoalsDeferred', 'flexibleGoalsSkipped', 'flexibleGoalFundedAmount', 'flexibleGoalUnfundedAmount', 'insuranceCashValue', 'ladderValue', 'deathBenefit', 'netWorth',
|
|
296
|
+
]
|
|
297
|
+
const lines = [cols.join(',')]
|
|
298
|
+
for (const y of view.result.years) {
|
|
299
|
+
lines.push(
|
|
300
|
+
[
|
|
301
|
+
y.year, y.filingStatus, y.incomes.wages, y.incomes.socialSecurity, y.incomes.pension, y.incomes.annuity, y.incomes.tipsLadder, y.incomes.recurring,
|
|
302
|
+
y.incomes.oneTime, y.incomes.taxableInterest, y.incomes.ordinaryDividends, y.incomes.qualifiedDividends, y.incomes.taxableYield, y.incomes.total, y.expenses.baseSpending, y.expenses.oneTimeGoals, y.expenses.debtService,
|
|
303
|
+
y.expenses.propertyCosts, y.expenses.healthcare, y.expenses.insurancePremiums, y.expenses.careCost, y.expenses.ltcBenefit, y.expenses.requiredSpending, y.expenses.targetSpending, y.expenses.idealSpending, y.expenses.excessSpending, y.expenses.intendedSpending, y.expenses.total, y.contributions, y.employerMatch, y.rmd, y.qcd, y.rothConversion, y.tax, y.amt, y.penalties,
|
|
304
|
+
y.magi, y.withdrawals.total, y.realizedGains, y.capitalLossUsedAgainstGains + y.capitalLossUsedAgainstOrdinary, y.capitalLossCarryforwardRemaining, y.shortfall, y.investableTotal,
|
|
305
|
+
y.requiredShortfall, y.targetShortfall, y.idealShortfall, y.excessShortfall, y.guardrailAction, y.expenses.guardrailFactor.toFixed(2), y.flexibleGoals.funded, y.flexibleGoals.partiallyFunded, y.flexibleGoals.deferred, y.flexibleGoals.skipped, y.flexibleGoals.fundedAmount, y.flexibleGoals.unfundedAmount, y.insuranceCashValue, y.ladderValue, y.deathBenefit, y.netWorth,
|
|
306
|
+
]
|
|
307
|
+
.map((v) => (typeof v === 'number' ? Math.round(v) : v))
|
|
308
|
+
.join(','),
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
const blob = new Blob([lines.join('\n')], { type: 'text/csv' })
|
|
312
|
+
const a = document.createElement('a')
|
|
313
|
+
a.href = URL.createObjectURL(blob)
|
|
314
|
+
a.download = `${plan.name.replace(/\W+/g, '-').toLowerCase()}-ledger.csv`
|
|
315
|
+
a.click()
|
|
316
|
+
URL.revokeObjectURL(a.href)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const handleHtmlReport = () => {
|
|
320
|
+
downloadStandaloneReport({
|
|
321
|
+
plan,
|
|
322
|
+
result: view.result,
|
|
323
|
+
summary: view.summary,
|
|
324
|
+
startYear: view.startYear,
|
|
325
|
+
branding: reportBranding,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const depletionYear = view.summary.depletionYear
|
|
330
|
+
const endYear = view.result.endYear
|
|
331
|
+
const endingToday = view.deflate(endYear, view.result.endingNetWorth)
|
|
332
|
+
// Same debounced, plan-keyed run the KPI bar uses (shared in-flight, so this
|
|
333
|
+
// never adds a second simulation) — the verdict must speak with both of the
|
|
334
|
+
// engine's voices, not just the steady-markets ledger.
|
|
335
|
+
const mcRate = useMcSuccessRate(plan, !isPlanIncomplete(plan))
|
|
336
|
+
// The first full year after depletion shows what the ledger already knows:
|
|
337
|
+
// guaranteed income keeps flowing, and the uncovered gap is the engine's own
|
|
338
|
+
// shortfall figure — no recomputation here. When depletion lands in the
|
|
339
|
+
// final plan year there is no later year, so that year carries the floor.
|
|
340
|
+
const floorYear =
|
|
341
|
+
depletionYear !== null
|
|
342
|
+
? (view.result.years.find((y) => y.year > depletionYear) ??
|
|
343
|
+
view.result.years.find((y) => y.year === depletionYear))
|
|
344
|
+
: undefined
|
|
345
|
+
// The FIRE lens leads only for households genuinely accumulating: wages in
|
|
346
|
+
// the projection AND retirement 5+ years out. For everyone else (retirees,
|
|
347
|
+
// near-retirees whose plan may succeed while "FI date: —" reads as failure)
|
|
348
|
+
// it stays available behind a disclosure below the balances chart, so it
|
|
349
|
+
// never takes the top spot or contradicts the verdict.
|
|
350
|
+
const isAccumulating = view.result.years.some((y) => y.incomes.wages > 0)
|
|
351
|
+
const yearsToLastRetirement = Math.max(
|
|
352
|
+
0,
|
|
353
|
+
...plan.household.people.map((p) =>
|
|
354
|
+
// No set retirement age = working indefinitely = still accumulating.
|
|
355
|
+
p.retirementAge === null ? Number.POSITIVE_INFINITY : Number(p.dob.slice(0, 4)) + p.retirementAge - view.startYear,
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
const fireLeads = isAccumulating && yearsToLastRetirement >= 5
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<section>
|
|
362
|
+
{!isPlanIncomplete(plan) ? (
|
|
363
|
+
<div className="results-verdict">
|
|
364
|
+
<h2>
|
|
365
|
+
{depletionYear !== null
|
|
366
|
+
? `This plan runs out of money in ${depletionYear}.`
|
|
367
|
+
: `Your money lasts the full plan — through ${endYear}.`}
|
|
368
|
+
</h2>
|
|
369
|
+
<p className="muted">
|
|
370
|
+
{depletionYear !== null ? (
|
|
371
|
+
<>
|
|
372
|
+
The portfolio depletes {endYear - depletionYear} year{endYear - depletionYear === 1 ? '' : 's'} before
|
|
373
|
+
the end of the plan.
|
|
374
|
+
{floorYear !== undefined && floorYear.incomes.total > 0.5 ? (
|
|
375
|
+
<>
|
|
376
|
+
{' '}
|
|
377
|
+
Income doesn't stop: about {fmtMoneyCompact(view.deflate(floorYear.year, floorYear.incomes.total))}
|
|
378
|
+
/yr (today's dollars) of Social Security, pensions, and other income keeps arriving
|
|
379
|
+
{floorYear.shortfall > 0.5 ? (
|
|
380
|
+
<>
|
|
381
|
+
, leaving an uncovered spending gap of about{' '}
|
|
382
|
+
{fmtMoneyCompact(view.deflate(floorYear.year, floorYear.shortfall))}/yr
|
|
383
|
+
</>
|
|
384
|
+
) : null}
|
|
385
|
+
.
|
|
386
|
+
</>
|
|
387
|
+
) : null}
|
|
388
|
+
{mcRate !== null ? (
|
|
389
|
+
<>
|
|
390
|
+
{' '}
|
|
391
|
+
Across {PATH_COUNT_LABEL} varied markets, this plan succeeds {Math.round(mcRate * 100)}% of the
|
|
392
|
+
time — <Link to={`/plan/${plan.id}/monte-carlo`}>see Monte Carlo</Link>.
|
|
393
|
+
</>
|
|
394
|
+
) : null}{' '}
|
|
395
|
+
<Link to={`/plan/${plan.id}/insights`}>See what would change this →</Link>
|
|
396
|
+
</>
|
|
397
|
+
) : (
|
|
398
|
+
<>
|
|
399
|
+
In steady markets, ending net worth is {fmtMoneyCompact(view.result.endingNetWorth)} (
|
|
400
|
+
{fmtMoneyCompact(endingToday)} in today's dollars).
|
|
401
|
+
{mcRate !== null ? (
|
|
402
|
+
<>
|
|
403
|
+
{' '}
|
|
404
|
+
Across {PATH_COUNT_LABEL} varied markets, this plan succeeds {Math.round(mcRate * 100)}% of the
|
|
405
|
+
time — <Link to={`/plan/${plan.id}/monte-carlo`}>see Monte Carlo</Link>.
|
|
406
|
+
</>
|
|
407
|
+
) : null}{' '}
|
|
408
|
+
The charts below are the evidence behind this verdict.
|
|
409
|
+
</>
|
|
410
|
+
)}
|
|
411
|
+
</p>
|
|
412
|
+
</div>
|
|
413
|
+
) : null}
|
|
414
|
+
|
|
415
|
+
<div className="results-toolbar">
|
|
416
|
+
<DollarsToggle value={dollars} onChange={setDollars} />
|
|
417
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={handleCsv}>
|
|
418
|
+
Download CSV
|
|
419
|
+
</button>
|
|
420
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={handleHtmlReport}>
|
|
421
|
+
Download HTML report
|
|
422
|
+
</button>
|
|
423
|
+
<Link to={`/plan/${plan.id}/report`} className="btn btn-secondary btn-small">
|
|
424
|
+
View printable report
|
|
425
|
+
</Link>
|
|
426
|
+
<Link to={`/plan/${plan.id}/assumptions-card`} className="btn btn-secondary btn-small">
|
|
427
|
+
View assumptions card
|
|
428
|
+
</Link>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<p className="results-jump">
|
|
432
|
+
<a href="#year-table">Jump to the year-by-year table ↓</a>
|
|
433
|
+
</p>
|
|
434
|
+
|
|
435
|
+
{view.result.warnings.length > 0 ? (
|
|
436
|
+
<div className="callout callout--warn">
|
|
437
|
+
<strong>Modeling notes</strong>
|
|
438
|
+
<ul>
|
|
439
|
+
{view.result.warnings.map((w) => (
|
|
440
|
+
<li key={w}>{w}</li>
|
|
441
|
+
))}
|
|
442
|
+
</ul>
|
|
443
|
+
</div>
|
|
444
|
+
) : null}
|
|
445
|
+
|
|
446
|
+
{plan.expenses.spendingPolicy?.mode === 'riskBasedGuardrails' ? (
|
|
447
|
+
<div className="callout callout--info">
|
|
448
|
+
<strong>Risk-based spending guardrails</strong>
|
|
449
|
+
{plan.expenses.spendingPolicy.lowerBalanceThresholdPct !== undefined ||
|
|
450
|
+
plan.expenses.spendingPolicy.upperBalanceThresholdPct !== undefined ? (
|
|
451
|
+
<p>
|
|
452
|
+
Solved for the {plan.expenses.spendingPolicy.targetSuccessLowerPct ?? 70}–
|
|
453
|
+
{plan.expenses.spendingPolicy.targetSuccessUpperPct ?? 95}% success band (today's dollars):{' '}
|
|
454
|
+
{plan.expenses.spendingPolicy.lowerBalanceThresholdPct !== undefined ? (
|
|
455
|
+
<>
|
|
456
|
+
if the portfolio falls below{' '}
|
|
457
|
+
<strong>
|
|
458
|
+
{fmtMoney((plan.expenses.spendingPolicy.lowerBalanceThresholdPct / 100) * startingInvestableOf(plan))}
|
|
459
|
+
</strong>
|
|
460
|
+
, flexible spending is trimmed in {plan.expenses.spendingPolicy.adjustmentPct ?? 10}% steps
|
|
461
|
+
</>
|
|
462
|
+
) : (
|
|
463
|
+
<>no cut threshold was solved for this band (see Spending for why)</>
|
|
464
|
+
)}
|
|
465
|
+
{'; '}
|
|
466
|
+
{plan.expenses.spendingPolicy.upperBalanceThresholdPct !== undefined ? (
|
|
467
|
+
<>
|
|
468
|
+
above{' '}
|
|
469
|
+
<strong>
|
|
470
|
+
{fmtMoney((plan.expenses.spendingPolicy.upperBalanceThresholdPct / 100) * startingInvestableOf(plan))}
|
|
471
|
+
</strong>
|
|
472
|
+
, spending can be restored or raised
|
|
473
|
+
</>
|
|
474
|
+
) : (
|
|
475
|
+
<>no raise threshold was solved for this band</>
|
|
476
|
+
)}
|
|
477
|
+
. The required floor is never cut. Watch the “Guardrails” column below for the years the rule acted.
|
|
478
|
+
</p>
|
|
479
|
+
) : (
|
|
480
|
+
<p>
|
|
481
|
+
The dollar thresholds for your success band have not been solved yet, so this policy currently holds
|
|
482
|
+
spending steady. Solve them on the Spending screen to activate adjustments.
|
|
483
|
+
</p>
|
|
484
|
+
)}
|
|
485
|
+
</div>
|
|
486
|
+
) : null}
|
|
487
|
+
|
|
488
|
+
{carryforwardHighlight ? (
|
|
489
|
+
<div className="callout callout--info">
|
|
490
|
+
<strong>Capital loss carryforward</strong>
|
|
491
|
+
<p>
|
|
492
|
+
Starting balance {fmtMoney(plan.household.capitalLossCarryforward)}. In {carryforwardHighlight.year} it offset{' '}
|
|
493
|
+
{fmtMoney(adj(carryforwardHighlight.year, carryforwardHighlight.capitalLossUsedAgainstGains))} of realized
|
|
494
|
+
gains and {fmtMoney(adj(carryforwardHighlight.year, carryforwardHighlight.capitalLossUsedAgainstOrdinary))} of
|
|
495
|
+
ordinary income;{' '}
|
|
496
|
+
{fmtMoney(adj(carryforwardHighlight.year, carryforwardHighlight.capitalLossCarryforwardRemaining))} carries
|
|
497
|
+
forward. It nets against realized gains first, then up to $3,000/yr against ordinary income — watch it deplete
|
|
498
|
+
in the “Loss carryf'd” column below.
|
|
499
|
+
</p>
|
|
500
|
+
</div>
|
|
501
|
+
) : null}
|
|
502
|
+
|
|
503
|
+
{fireLeads ? (
|
|
504
|
+
<div className="chart-card">
|
|
505
|
+
<h2>Path to Financial Independence (FIRE)</h2>
|
|
506
|
+
<FireLens view={view} plan={plan} rows={rows} dollarLabel={dollarLabel} />
|
|
507
|
+
</div>
|
|
508
|
+
) : null}
|
|
509
|
+
|
|
510
|
+
<div className="chart-card">
|
|
511
|
+
<h2>Investable balances by account type</h2>
|
|
512
|
+
<p className="card-hint">
|
|
513
|
+
End-of-year balances, shown in {dollarLabel}.
|
|
514
|
+
{view.summary.depletionYear !== null ? (
|
|
515
|
+
<>
|
|
516
|
+
{' '}
|
|
517
|
+
Portfolio depletes in {view.summary.depletionYear}. <Link to={`/plan/${plan.id}/insights`}>See what would change this →</Link>
|
|
518
|
+
</>
|
|
519
|
+
) : null}
|
|
520
|
+
</p>
|
|
521
|
+
<div className="chart-frame" style={frameH(320)}>
|
|
522
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
523
|
+
<AreaChart
|
|
524
|
+
data={rows}
|
|
525
|
+
margin={{ left: 12, right: 8, top: 8 }}
|
|
526
|
+
aria-label="Investable balances by account type, year by year"
|
|
527
|
+
>
|
|
528
|
+
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
|
529
|
+
<XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
|
|
530
|
+
<YAxis tickFormatter={moneyTick} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
|
|
531
|
+
<Legend />
|
|
532
|
+
{CATEGORIES.map((c) => (
|
|
533
|
+
<Area key={c} dataKey={c} stackId="bal" name={CAT_LABEL[c]} stroke={CAT_COLOR[c]} fill={CAT_COLOR[c]} fillOpacity={0.55} />
|
|
534
|
+
))}
|
|
535
|
+
{view.summary.depletionYear !== null ? (
|
|
536
|
+
<ReferenceLine x={view.summary.depletionYear} stroke="var(--bad)" strokeDasharray="4 4" label={{ value: 'depleted', fill: 'var(--bad)', fontSize: 12 }} />
|
|
537
|
+
) : null}
|
|
538
|
+
<Tooltip {...tooltipProps} />
|
|
539
|
+
</AreaChart>
|
|
540
|
+
</ResponsiveContainer>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<BucketLensCard result={view.result} adj={adj} />
|
|
545
|
+
|
|
546
|
+
{!fireLeads ? (
|
|
547
|
+
<details className="ss-explainer">
|
|
548
|
+
<summary>Path to Financial Independence (FIRE) — an optional lens</summary>
|
|
549
|
+
<p className="field-hint">
|
|
550
|
+
FI metrics matter most while accumulating; for plans at or near retirement they are shown here for
|
|
551
|
+
reference, not as a verdict.
|
|
552
|
+
</p>
|
|
553
|
+
<FireLens view={view} plan={plan} rows={rows} dollarLabel={dollarLabel} />
|
|
554
|
+
</details>
|
|
555
|
+
) : null}
|
|
556
|
+
|
|
557
|
+
<div className="chart-card">
|
|
558
|
+
<h2>Income vs. spending</h2>
|
|
559
|
+
<p className="card-hint">
|
|
560
|
+
Spending includes taxes and penalties; the gap is funded by withdrawals. Shown in {dollarLabel}.
|
|
561
|
+
</p>
|
|
562
|
+
<div className="chart-frame" style={frameH(280)}>
|
|
563
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
564
|
+
<BarChart data={rows} margin={{ left: 12, right: 8, top: 8 }} aria-label="Income vs. spending, year by year">
|
|
565
|
+
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
|
566
|
+
<XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
|
|
567
|
+
<YAxis tickFormatter={moneyTick} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
|
|
568
|
+
<Legend />
|
|
569
|
+
{/* Income is green (chart-3), spending gold: money in reads as green. */}
|
|
570
|
+
<Bar dataKey="income" name="Income" fill="var(--chart-3)" />
|
|
571
|
+
<Bar dataKey="spending" name="Spending + tax" fill="var(--chart-1)" />
|
|
572
|
+
<Tooltip {...tooltipProps} />
|
|
573
|
+
</BarChart>
|
|
574
|
+
</ResponsiveContainer>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div className="chart-card">
|
|
579
|
+
<h2>Income by source</h2>
|
|
580
|
+
<p className="card-hint">
|
|
581
|
+
Gross income streams each year, shown in {dollarLabel}. Any shortfall below spending is funded by portfolio
|
|
582
|
+
withdrawals.
|
|
583
|
+
</p>
|
|
584
|
+
<div className="chart-frame" style={frameH(280)}>
|
|
585
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
586
|
+
<BarChart data={incomeRows} margin={{ left: 12, right: 8, top: 8 }} aria-label="Income by source, year by year">
|
|
587
|
+
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
|
588
|
+
<XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
|
|
589
|
+
<YAxis tickFormatter={moneyTick} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
|
|
590
|
+
<Legend />
|
|
591
|
+
{INCOME_SOURCES.map((s) => (
|
|
592
|
+
<Bar key={s.key} dataKey={s.key} stackId="inc" name={s.label} fill={s.color} />
|
|
593
|
+
))}
|
|
594
|
+
<Tooltip {...tooltipProps} />
|
|
595
|
+
</BarChart>
|
|
596
|
+
</ResponsiveContainer>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<FundedRatioCard />
|
|
601
|
+
|
|
602
|
+
<div className="chart-card">
|
|
603
|
+
<h2>Spending by category</h2>
|
|
604
|
+
<p className="card-hint">
|
|
605
|
+
The big line items behind the Expenses column (taxes and penalties included), shown in {dollarLabel}. Mortgage
|
|
606
|
+
principal & interest is "Debt payments"; property tax & insurance are their own band; everything else
|
|
607
|
+
lives in "Baseline living."
|
|
608
|
+
</p>
|
|
609
|
+
<div className="chart-frame" style={frameH(280)}>
|
|
610
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
611
|
+
<BarChart
|
|
612
|
+
data={expenseRows}
|
|
613
|
+
margin={{ left: 12, right: 8, top: 8 }}
|
|
614
|
+
aria-label="Spending by category, year by year"
|
|
615
|
+
>
|
|
616
|
+
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
|
617
|
+
<XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
|
|
618
|
+
<YAxis tickFormatter={moneyTick} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
|
|
619
|
+
<Legend />
|
|
620
|
+
{EXPENSE_CATEGORIES.map((c) => (
|
|
621
|
+
<Bar key={c.key} dataKey={c.key} stackId="exp" name={c.label} fill={c.color} />
|
|
622
|
+
))}
|
|
623
|
+
<Tooltip {...tooltipProps} />
|
|
624
|
+
</BarChart>
|
|
625
|
+
</ResponsiveContainer>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
<div className="chart-card">
|
|
630
|
+
<h2>Tax and MAGI ({dollarLabel})</h2>
|
|
631
|
+
<p className="card-hint">
|
|
632
|
+
Modified adjusted gross income (MAGI) sets Medicare's income surcharge (IRMAA, which looks back two years)
|
|
633
|
+
and the marketplace health-insurance credit (ACA) before 65. Threshold checks always use each year's nominal
|
|
634
|
+
dollars.
|
|
635
|
+
</p>
|
|
636
|
+
<div className="chart-frame" style={frameH(280)}>
|
|
637
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
638
|
+
<LineChart data={rows} margin={{ left: 12, right: 8, top: 8 }} aria-label="Tax and MAGI, year by year">
|
|
639
|
+
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
|
640
|
+
<XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
|
|
641
|
+
<YAxis tickFormatter={moneyTick} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
|
|
642
|
+
<Legend />
|
|
643
|
+
<Line dataKey="tax" name="Tax" stroke="var(--chart-4)" dot={false} strokeWidth={2} />
|
|
644
|
+
<Line dataKey="magi" name="MAGI" stroke="var(--chart-2)" dot={false} strokeWidth={2} />
|
|
645
|
+
<Tooltip {...tooltipProps} />
|
|
646
|
+
</LineChart>
|
|
647
|
+
</ResponsiveContainer>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
<details open id="year-table">
|
|
652
|
+
{/* The h2 inside the summary keeps this reachable by heading navigation
|
|
653
|
+
(screen readers, heading-jump extensions) — a bare summary is not. */}
|
|
654
|
+
<summary className="year-table-summary">
|
|
655
|
+
<h2>Year-by-year detail</h2>
|
|
656
|
+
</summary>
|
|
657
|
+
<div className="year-table-wrap">
|
|
658
|
+
<table className="year-table">
|
|
659
|
+
<thead>
|
|
660
|
+
<tr>
|
|
661
|
+
<th>Year</th>
|
|
662
|
+
<th>Age</th>
|
|
663
|
+
<th>Income</th>
|
|
664
|
+
<th>Expenses</th>
|
|
665
|
+
{hasLayeredSpending ? <th title="Must-fund floor spending, including required lifestyle and system costs.">Required</th> : null}
|
|
666
|
+
{hasLayeredSpending ? <th title="Required plus target lifestyle spending before ideal/excess upside.">Target</th> : null}
|
|
667
|
+
{hasLayeredSpending ? <th title="Ideal and excess spending intended above target.">Upside</th> : null}
|
|
668
|
+
<th>Contrib.</th>
|
|
669
|
+
<th>Match</th>
|
|
670
|
+
<th>RMD</th>
|
|
671
|
+
<th>Conversion</th>
|
|
672
|
+
<th>Withdrawals</th>
|
|
673
|
+
<th>Tax</th>
|
|
674
|
+
{hasAmt ? <th title="Federal alternative minimum tax included in Tax.">AMT</th> : null}
|
|
675
|
+
<th title="Displayed in the active dollar mode. IRMAA and ACA threshold checks use the nominal dollars for each rule.">
|
|
676
|
+
MAGI ({dollarLabel})
|
|
677
|
+
</th>
|
|
678
|
+
<th title="Additional long-term gains you could realize this year at $0 federal tax: your remaining loss carryforward absorbs gains dollar-for-dollar, then the 0% long-term bracket covers more on top.">Tax-free gains room</th>
|
|
679
|
+
{hasCarryforward ? <th title="Capital-loss carryforward remaining at year end.">Loss carryf'd</th> : null}
|
|
680
|
+
<th>Shortfall</th>
|
|
681
|
+
{hasLayeredSpending ? <th title="Required-floor shortfall / target-lifestyle shortfall / upside miss.">Layer miss</th> : null}
|
|
682
|
+
{hasLayeredSpending ? <th title="Guardrail action and flexible goal outcomes.">Guardrails</th> : null}
|
|
683
|
+
<th>Investable</th>
|
|
684
|
+
<th>Net worth</th>
|
|
685
|
+
</tr>
|
|
686
|
+
</thead>
|
|
687
|
+
<tbody>
|
|
688
|
+
{view.result.years.map((y) => (
|
|
689
|
+
<tr key={y.year} className={y.shortfall > 0.005 ? 'row-depleted' : undefined}>
|
|
690
|
+
<td>{y.year}</td>
|
|
691
|
+
<td>{y.people.map((p) => (p.alive ? p.ageAttained : '—')).join(' / ')}</td>
|
|
692
|
+
<td>{fmtMoney(adj(y.year, y.incomes.total))}</td>
|
|
693
|
+
<td>{fmtMoney(adj(y.year, y.expenses.total))}</td>
|
|
694
|
+
{hasLayeredSpending ? <td>{fmtMoney(adj(y.year, y.expenses.requiredSpending))}</td> : null}
|
|
695
|
+
{hasLayeredSpending ? <td>{fmtMoney(adj(y.year, y.expenses.targetSpending))}</td> : null}
|
|
696
|
+
{hasLayeredSpending ? (
|
|
697
|
+
<td>
|
|
698
|
+
{y.expenses.idealSpending + y.expenses.excessSpending > 0.5
|
|
699
|
+
? fmtMoney(adj(y.year, y.expenses.idealSpending + y.expenses.excessSpending))
|
|
700
|
+
: ''}
|
|
701
|
+
</td>
|
|
702
|
+
) : null}
|
|
703
|
+
<td>{y.contributions > 0.005 ? fmtMoney(adj(y.year, y.contributions)) : ''}</td>
|
|
704
|
+
<td>{y.employerMatch > 0.005 ? fmtMoney(adj(y.year, y.employerMatch)) : ''}</td>
|
|
705
|
+
<td>{fmtMoney(adj(y.year, y.rmd))}</td>
|
|
706
|
+
<td>{fmtMoney(adj(y.year, y.rothConversion))}</td>
|
|
707
|
+
<td>{fmtMoney(adj(y.year, y.withdrawals.total))}</td>
|
|
708
|
+
<td>{fmtMoney(adj(y.year, y.tax + y.penalties))}</td>
|
|
709
|
+
{hasAmt ? <td>{y.amt > 0.5 ? fmtMoney(adj(y.year, y.amt)) : ''}</td> : null}
|
|
710
|
+
<td>{fmtMoney(adj(y.year, y.magi))}</td>
|
|
711
|
+
<td>{y.ltcgZeroHeadroom + y.capitalLossCarryforwardRemaining > 0.5 ? fmtMoney(adj(y.year, y.ltcgZeroHeadroom + y.capitalLossCarryforwardRemaining)) : ''}</td>
|
|
712
|
+
{hasCarryforward ? <td>{y.capitalLossCarryforwardRemaining > 0.5 ? fmtMoney(adj(y.year, y.capitalLossCarryforwardRemaining)) : '—'}</td> : null}
|
|
713
|
+
<td>{y.shortfall > 0.005 ? fmtMoney(adj(y.year, y.shortfall)) : ''}</td>
|
|
714
|
+
{hasLayeredSpending ? (
|
|
715
|
+
<td>
|
|
716
|
+
{y.requiredShortfall + y.targetShortfall + y.idealShortfall + y.excessShortfall > 0.5 ? (
|
|
717
|
+
<>
|
|
718
|
+
{y.requiredShortfall > 0.5 ? `Req ${fmtMoney(adj(y.year, y.requiredShortfall))} ` : ''}
|
|
719
|
+
{y.targetShortfall > 0.5 ? `Target ${fmtMoney(adj(y.year, y.targetShortfall))} ` : ''}
|
|
720
|
+
{y.idealShortfall + y.excessShortfall > 0.5
|
|
721
|
+
? `Upside ${fmtMoney(adj(y.year, y.idealShortfall + y.excessShortfall))}`
|
|
722
|
+
: ''}
|
|
723
|
+
</>
|
|
724
|
+
) : (
|
|
725
|
+
''
|
|
726
|
+
)}
|
|
727
|
+
</td>
|
|
728
|
+
) : null}
|
|
729
|
+
{hasLayeredSpending ? (
|
|
730
|
+
<td>
|
|
731
|
+
{y.guardrailAction !== 'hold' ? y.guardrailAction : ''}
|
|
732
|
+
{y.flexibleGoals.funded + y.flexibleGoals.partiallyFunded + y.flexibleGoals.deferred + y.flexibleGoals.skipped > 0 ? (
|
|
733
|
+
<span
|
|
734
|
+
title={`Goals: ${y.flexibleGoals.funded} funded, ${y.flexibleGoals.partiallyFunded} partially funded, ${y.flexibleGoals.deferred} deferred, ${y.flexibleGoals.skipped} skipped`}
|
|
735
|
+
aria-label={`Goals: ${y.flexibleGoals.funded} funded, ${y.flexibleGoals.partiallyFunded} partially funded, ${y.flexibleGoals.deferred} deferred, ${y.flexibleGoals.skipped} skipped`}
|
|
736
|
+
>
|
|
737
|
+
{y.guardrailAction !== 'hold' ? ' · ' : ''}
|
|
738
|
+
{y.flexibleGoals.funded}F/{y.flexibleGoals.partiallyFunded}P/{y.flexibleGoals.deferred}D/{y.flexibleGoals.skipped}S
|
|
739
|
+
</span>
|
|
740
|
+
) : null}
|
|
741
|
+
</td>
|
|
742
|
+
) : null}
|
|
743
|
+
<td>{fmtMoney(adj(y.year, y.investableTotal))}</td>
|
|
744
|
+
<td>{fmtMoney(adj(y.year, y.netWorth))}</td>
|
|
745
|
+
</tr>
|
|
746
|
+
))}
|
|
747
|
+
</tbody>
|
|
748
|
+
</table>
|
|
749
|
+
</div>
|
|
750
|
+
{/* Column semantics used to live only in title= tooltips — invisible on
|
|
751
|
+
touch and unreliable for screen readers. This legend is the
|
|
752
|
+
keyboard/touch-reachable copy of the same explanations. */}
|
|
753
|
+
<details className="ss-explainer">
|
|
754
|
+
<summary>What the columns mean</summary>
|
|
755
|
+
<ul>
|
|
756
|
+
<li>
|
|
757
|
+
<strong>Age</strong> — one entry per person (e.g. "67 / 64"). A person shows "—" after their modeled
|
|
758
|
+
death; income and spending reflect the survivor from that year on.
|
|
759
|
+
</li>
|
|
760
|
+
{hasLayeredSpending ? (
|
|
761
|
+
<>
|
|
762
|
+
<li>
|
|
763
|
+
<strong>Required</strong> — must-fund floor spending, including required lifestyle and system costs.
|
|
764
|
+
</li>
|
|
765
|
+
<li>
|
|
766
|
+
<strong>Target</strong> — required plus target lifestyle spending before ideal/excess upside.
|
|
767
|
+
</li>
|
|
768
|
+
<li>
|
|
769
|
+
<strong>Upside</strong> — ideal and excess spending intended above target.
|
|
770
|
+
</li>
|
|
771
|
+
</>
|
|
772
|
+
) : null}
|
|
773
|
+
{hasAmt ? (
|
|
774
|
+
<li>
|
|
775
|
+
<strong>AMT</strong> — federal alternative minimum tax, included in Tax.
|
|
776
|
+
</li>
|
|
777
|
+
) : null}
|
|
778
|
+
<li>
|
|
779
|
+
<strong>MAGI</strong> — modified adjusted gross income, displayed in the active dollar mode; IRMAA and
|
|
780
|
+
ACA threshold checks always use each year's nominal dollars.
|
|
781
|
+
</li>
|
|
782
|
+
<li>
|
|
783
|
+
<strong>Tax-free gains room</strong> — additional long-term gains you could realize this year at $0
|
|
784
|
+
federal tax: remaining loss carryforward absorbs gains first, then the 0% long-term bracket covers more.
|
|
785
|
+
</li>
|
|
786
|
+
{hasCarryforward ? (
|
|
787
|
+
<li>
|
|
788
|
+
<strong>Loss carryf'd</strong> — capital-loss carryforward remaining at year end.
|
|
789
|
+
</li>
|
|
790
|
+
) : null}
|
|
791
|
+
{hasLayeredSpending ? (
|
|
792
|
+
<>
|
|
793
|
+
<li>
|
|
794
|
+
<strong>Layer miss</strong> — required-floor shortfall / target-lifestyle shortfall / upside miss.
|
|
795
|
+
</li>
|
|
796
|
+
<li>
|
|
797
|
+
<strong>Guardrails</strong> — the guardrail action taken that year (cut / raise), and flexible goal
|
|
798
|
+
outcomes as counts: <strong>F</strong>unded / <strong>P</strong>artial / <strong>D</strong>eferred /{' '}
|
|
799
|
+
<strong>S</strong>kipped (e.g. “1F/0P/2D/1S”).
|
|
800
|
+
</li>
|
|
801
|
+
</>
|
|
802
|
+
) : null}
|
|
803
|
+
</ul>
|
|
804
|
+
</details>
|
|
805
|
+
</details>
|
|
806
|
+
|
|
807
|
+
<p className="field-hint">
|
|
808
|
+
Every figure above comes from the single year-by-year ledger in this table — the same ledger Monte Carlo and
|
|
809
|
+
the optimizer price against. <Link to={`/plan/${plan.id}/assumptions-card`}>See the assumptions behind it</Link>{' '}
|
|
810
|
+
· <Link to="/how-tested">How RetireGolden is tested</Link>
|
|
811
|
+
</p>
|
|
812
|
+
|
|
813
|
+
<LearnAboutScreen route="/plan/:planId/results" />
|
|
814
|
+
</section>
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
|