-
Notifications
You must be signed in to change notification settings - Fork 0
/
script.js
374 lines (328 loc) · 17.9 KB
/
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
document.addEventListener('DOMContentLoaded', function() {
// Add transition behavior to tooltip elements
addTooltipBehavior(); // defined in uicomponents/tooltip.js
// Generate a plot with the default values
plotResultsBasedOnCurrentInputValues();
// Add slider inputs callback (except for the purchaseAge slider, which has a different callback)
const sliders = document.querySelectorAll('.slider');
sliders.forEach(slider => {
if (slider.id !== "purchaseAge") { // Fix: use !== for "not equal to"
slider.addEventListener('input', function() {
const valueSpan = document.getElementById(slider.id + 'Value');
if (valueSpan) {
valueSpan.textContent = slider.value;
}
plotResultsBasedOnCurrentInputValues();
});
}
});
// Add radio button groups callback
const radioButtonsCustom = document.querySelectorAll('.radio-button-custom');
radioButtonsCustom.forEach(radioButton => {
radioButton.addEventListener('value-has-been-modified', function() {
plotResultsBasedOnCurrentInputValues();
});
});
});
let timeseriesChart = null; // This variable will hold the timeseries chart instance
const radioButtonInstances = {}; // Global object to hold RadioButtonCustom instances
function plotResultsBasedOnCurrentInputValues() {
// Function to get or create a RadioButtonCustom instance
function getOrCreateRadioButtonCustom(key, options, defaultIndex) {
if (!radioButtonInstances[key]) {
radioButtonInstances[key] = new RadioButtonCustom(key, options, defaultIndex);
}
return radioButtonInstances[key];
}
// Retrieve or create RadioButtonCustom objects for mortgage parameters
const mortgageDurationYearsRadio = getOrCreateRadioButtonCustom('mortgageDurationYearsRadio', [20, 25, 30], 2);
// Retrieve or create RadioButtonCustom objects for interest rate, down payment, and other parameters
const mortgageInterestRateYearlyRadio = getOrCreateRadioButtonCustom('mortgageInterestRateYearlyRadio', [3.0, 4.5, 6.0], 3);
const downPaymentPercentRadio = getOrCreateRadioButtonCustom('downPaymentPercentRadio', [5, 10, 15], 1);
// Retrieve or create RadioButtonCustom objects for rates of change
const yearlyReturnOnSavingsRadio = getOrCreateRadioButtonCustom('yearlyReturnOnSavingsRadio', [2, 4, 7], 1);
const yearlyNetSalaryGrowthRadio = getOrCreateRadioButtonCustom('yearlyNetSalaryGrowthRadio', [0, 3, 6], 2);
const yearlyHousePriceGrowthRadio = getOrCreateRadioButtonCustom('yearlyHousePriceGrowthRadio', [0, 3, 6], 2);
const yearlyRentIncreaseRadio = getOrCreateRadioButtonCustom('yearlyRentIncreaseRadio', [0, 3, 6], 2);
const yearlyExpensesIncreaseRadio = getOrCreateRadioButtonCustom('yearlyExpensesIncreaseRadio', [0, 3, 6], 2);
const yearlyInterestOnDebtRadio = getOrCreateRadioButtonCustom('yearlyInterestOnDebtRadio', [5, 10, 15], 3);
const yearlyInflationRadio = getOrCreateRadioButtonCustom('yearlyInflationRadio', [0, 2, 4], 2);
// Get slider input values
const housePrice = parseFloat(document.getElementById('housePrice').value);
const savings = parseFloat(document.getElementById('savings').value);
const monthlyRent = parseFloat(document.getElementById('monthlyRent').value);
const monthlyNetSalary = parseFloat(document.getElementById('monthlyNetSalary').value);
const monthlyExpensesExceptRent = parseFloat(document.getElementById('monthlyExpensesExceptRent').value);
const serviceChargeYearly = parseFloat(document.getElementById('serviceChargeYearly').value);
const parameters = {
MortgageDurationYears: parseInt(mortgageDurationYearsRadio.getValue()),
MortgageInterestRateYearly: parseFloat(mortgageInterestRateYearlyRadio.getValue()) / 100,
DownPaymentPercent: parseInt(downPaymentPercentRadio.getValue()) / 100,
YearlyReturnOnSavings: parseFloat(yearlyReturnOnSavingsRadio.getValue()) / 100,
YearlyNetSalaryGrowth: parseFloat(yearlyNetSalaryGrowthRadio.getValue()) / 100,
YearlyHousePriceGrowth: parseFloat(yearlyHousePriceGrowthRadio.getValue()) / 100,
YearlyRentIncrease: parseFloat(yearlyRentIncreaseRadio.getValue()) / 100,
YearlyExpensesIncrease: parseFloat(yearlyExpensesIncreaseRadio.getValue()) / 100,
YearlyInterestOnDebt: parseFloat(yearlyInterestOnDebtRadio.getValue()) / 100,
YearlyInflation: parseFloat(yearlyInflationRadio.getValue()) / 100
};
const initialConditions = {
HousePrice: housePrice,
Savings: savings,
MonthlyRent: monthlyRent,
MonthlyNetSalary: monthlyNetSalary,
MonthlyExpensesExceptRent: monthlyExpensesExceptRent,
ServiceChargeYearly: serviceChargeYearly
};
const ageYears = parseInt(document.getElementById('ageYears').value);
const retirementAgeYears = parseInt(document.getElementById('retirementAgeYears').value);
plotResults(parameters, initialConditions, ageYears, retirementAgeYears);
}
/*
function scrollIntoTimeseriesChartView() {
// Scroll into timeseriesChart to focus user attention into it
const chartElement = document.getElementById('timeseriesChart');
chartElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
*/
function plotResults(parameters, initialConditions, ageYears, retirementAgeYears) {
// Timeseries slider and chart
const purchaseAgeSlider = document.getElementById('purchaseAge');
const purchaseAgeValue = document.getElementById('purchaseAgeValue');
purchaseAgeSlider.min = ageYears; // Update the slider's min value to ageYears
// Find optimal age to buy
const optimalAgeToBuy = findOptimalAgeHouseIsBought(parameters, initialConditions, ageYears, retirementAgeYears);
const maxNetWorthPossible = computeMaxNetWorth(parameters, initialConditions, ageYears, retirementAgeYears, optimalAgeToBuy);
// Update resultsSummaryHeadline
const optimalYearsWaitBuyHouse = document.getElementById('optimalYearsWaitBuyHouse');
const ageYearsValue = parseInt(document.getElementById('ageYears').value);
const yearsWaitValue = optimalAgeToBuy - ageYearsValue;
optimalYearsWaitBuyHouse.textContent = yearsWaitValue;
// Set initial timeseries slider value equal to optimal age to buy
purchaseAgeSlider.value = optimalAgeToBuy;
purchaseAgeValue.textContent = optimalAgeToBuy;
// Trigger input event to update the textContent and other listeners
purchaseAgeSlider.dispatchEvent(new Event('input'));
// Initial plot of the timeseries chart
updateTimeseriesChart(parameters, initialConditions, ageYears, retirementAgeYears, purchaseAgeSlider.value);
purchaseAgeSlider.oninput = function() {
purchaseAgeValue.textContent = this.value;
updateTimeseriesChart(parameters, initialConditions, ageYears, retirementAgeYears, this.value);
};
function updateTimeseriesChart(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought) {
const ctxTimeseries = document.getElementById('timeseriesChart').getContext('2d');
const savingsData = computeSavingsTimeseries(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought);
const totalNetWorthData = computeTotalNetWorthTimeseries(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought);
const labels = Array.from({length: savingsData.length}, (_, i) => i + ageYears);
if (timeseriesChart) {
timeseriesChart.destroy();
}
timeseriesChart = new Chart(ctxTimeseries, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Savings',
data: savingsData,
borderColor: 'rgb(0, 100, 0, 0.5)',
backgroundColor: 'rgba(0, 100, 0, 0.5)',
fill: true,
tension: 0.1
},
{
label: 'Savings + House Equity',
data: totalNetWorthData,
borderColor: 'rgb(152, 251, 152, 0.5)',
backgroundColor: 'rgba(152, 251, 152, 0.5)',
fill: true,
tension: 0.1
}
]
},
options: {
aspectRatio: 1.7, // width/height
animation: {
duration: 0 // Remove animation
},
scales: {
y: {
beginAtZero: true,
min: 0, // Set Y-axis min to 0
max: maxNetWorthPossible // Set Y-axis max to maxNetWorthPossible
}
},
plugins: {
filler: {
propagate: false
}
}
}
});
}
}
function computeMaxNetWorth(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought) {
Results = runSimulation(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought)
return Results.MaxNetWorth;
}
function findOptimalAgeHouseIsBought(parameters, initialConditions, ageYears, retirementAgeYears) {
// Set final age (time horizon)
const finalAge = 100;
// Generate an array of ages from current age to final age
const ages = Array.from({ length: finalAge - ageYears + 1 }, (_, index) => ageYears + index);
// Compute max net worth for each possible age of house purchase
const maxNetWorthForThisCase = ages.map(thisAge => computeMaxNetWorth(parameters, initialConditions, ageYears, retirementAgeYears, thisAge));
// Find the maximum net worth value
const maxNetWorthValue = Math.max(...maxNetWorthForThisCase);
// Find the index of the maximum net worth value, which corresponds to the optimal age offset from the current age
const optimalAgeIndex = maxNetWorthForThisCase.findIndex(netWorth => netWorth === maxNetWorthValue);
// Find the optimal age for buying the house using the index
const optimalAgeHouseIsBought = ages[optimalAgeIndex];
return optimalAgeHouseIsBought;
}
function computeSavingsTimeseries(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought) {
Results = runSimulation(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought)
return Results.SavingsTimeseries;
}
function computeTotalNetWorthTimeseries(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought) {
Results = runSimulation(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought)
return Results.TotalNetWorthTimeseries;
}
function computeHousingMonthlyCostTimeseries(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought) {
Results = runSimulation(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought)
return Results.HousingMonthlyCostTimeseries;
}
function runSimulation(parameters, initialConditions, ageYears, retirementAgeYears, ageHouseIsBought) {
// Extract parameters
const yearlyInterestRate = parameters.MortgageInterestRateYearly;
const returnOnSavingsYearly = parameters.YearlyReturnOnSavings;
const salaryGrowthYearly = parameters.YearlyNetSalaryGrowth;
const housePriceGrowthYearly = parameters.YearlyHousePriceGrowth;
const yearlyRentIncreasePercent = parameters.YearlyRentIncrease;
const yearlyExpensesIncreasePercent = parameters.YearlyExpensesIncrease;
const downPaymentPercent = parameters.DownPaymentPercent;
const mortgageDurationYears = parameters.MortgageDurationYears;
const yearlyInterestOnNonMortgageDebt = parameters.YearlyInterestOnDebt;
const yearlyInflation = parameters.YearlyInflation;
// Extract initial conditions
let housePrice = initialConditions.HousePrice;
let savings = initialConditions.Savings;
let yearlyRent = initialConditions.MonthlyRent * 12;
let yearlyNetSalary = initialConditions.MonthlyNetSalary * 12;
let yearlyExpensesExceptRent = initialConditions.MonthlyExpensesExceptRent * 12;
let serviceChargeYearly = initialConditions.ServiceChargeYearly;
// Set final age (time horizon)
let finalAge = 100;
// State variable size initialization
let savingsTimeseries = [];
let totalNetWorthTimeseries = [];
let housingMonthlyCostTimeseries = [];
// Variables at t=0
let thisYear = ageYears; // time iterator
let houseHasBeenBought = false;
let totalSavings = savings;
let mortgageDebtOutstanding = 0;
// Renting period begins
while (thisYear < ageHouseIsBought) {
thisYear++; // increase time
// Apply growth in this year
if (totalSavings > 0) {
totalSavings *= (1 + returnOnSavingsYearly); // Increase savings by yearly ROI
} else {
totalSavings *= (1 + yearlyInterestOnNonMortgageDebt); // Decrease savings by yearly debt interest
}
housePrice *= (1 + housePriceGrowthYearly);
yearlyNetSalary *= (1 + salaryGrowthYearly);
yearlyExpensesExceptRent *= (1 + yearlyExpensesIncreasePercent);
yearlyRent *= (1 + yearlyRentIncreasePercent);
// Check if already retired
if (thisYear > retirementAgeYears) {
yearlyNetSalary = 0;
}
// Savings balance
totalSavings += (yearlyNetSalary - yearlyRent - yearlyExpensesExceptRent);
// Pass value to state variable for this year
savingsTimeseries.push(totalSavings);
totalNetWorthTimeseries.push(totalSavings);
housingMonthlyCostTimeseries.push(yearlyRent / 12);
}
// House purchase event
if (ageHouseIsBought < finalAge) {
houseHasBeenBought = true;
// mortgage parameters
const downPayment = downPaymentPercent * housePrice;
const mortgageAmount = housePrice - downPayment;
const yearlyMortgagePayment = (mortgageAmount * yearlyInterestRate * Math.pow((1 + yearlyInterestRate), mortgageDurationYears)) / (Math.pow((1 + yearlyInterestRate), mortgageDurationYears) - 1);
// update balance after house purchase
totalSavings -= downPayment;
mortgageDebtOutstanding = housePrice - downPayment;
// Mortgage period begins
const yearEndOfMortgage = thisYear + mortgageDurationYears;
while (thisYear < yearEndOfMortgage && thisYear < finalAge) {
thisYear++; // increase time
// Apply growth in this year
if (totalSavings > 0) {
totalSavings *= (1 + returnOnSavingsYearly); // Increase savings by yearly ROI
} else {
totalSavings *= (1 + yearlyInterestOnNonMortgageDebt); // Decrease savings by yearly debt interest
}
housePrice *= (1 + housePriceGrowthYearly);
yearlyNetSalary *= (1 + salaryGrowthYearly);
yearlyExpensesExceptRent *= (1 + yearlyExpensesIncreasePercent);
serviceChargeYearly *= (1 + yearlyInflation); // Increase service charge with inflation
// Check if already retired
if (thisYear > retirementAgeYears) {
yearlyNetSalary = 0;
}
// Mortgage balance
const interestPaidOnMortgageThisYear = mortgageDebtOutstanding * yearlyInterestRate;
const incrementInEquityThisYear = yearlyMortgagePayment - interestPaidOnMortgageThisYear;
mortgageDebtOutstanding -= incrementInEquityThisYear;
// Savings balance
totalSavings += (yearlyNetSalary - yearlyMortgagePayment - yearlyExpensesExceptRent - serviceChargeYearly);
// Pass value to state variable for this year
savingsTimeseries.push(totalSavings);
totalNetWorthTimeseries.push(totalSavings + housePrice - mortgageDebtOutstanding);
housingMonthlyCostTimeseries.push(yearlyMortgagePayment / 12);
}
// Post-mortgage period begins
while (thisYear < finalAge) {
thisYear++; // increase time
// Apply growth in this year
if (totalSavings > 0) {
totalSavings *= (1 + returnOnSavingsYearly); // Increase savings by yearly ROI
} else {
totalSavings *= (1 + yearlyInterestOnNonMortgageDebt); // Decrease savings by yearly debt interest
}
housePrice *= (1 + housePriceGrowthYearly);
yearlyNetSalary *= (1 + salaryGrowthYearly);
yearlyExpensesExceptRent *= (1 + yearlyExpensesIncreasePercent);
serviceChargeYearly *= (1 + yearlyInflation); // Increase service charge with inflation
// Check if already retired
if (thisYear > retirementAgeYears) {
yearlyNetSalary = 0;
}
// Savings balance
totalSavings += (yearlyNetSalary - yearlyExpensesExceptRent - serviceChargeYearly);
// Pass value to state variable for this year
savingsTimeseries.push(totalSavings);
totalNetWorthTimeseries.push(totalSavings + housePrice);
housingMonthlyCostTimeseries.push(0);
}
}
// Adjust timeseries for inflation
for (let i = 0; i < savingsTimeseries.length; i++) {
let adjustmentFactor = Math.pow(1 + yearlyInflation, i);
savingsTimeseries[i] /= adjustmentFactor;
totalNetWorthTimeseries[i] /= adjustmentFactor;
housingMonthlyCostTimeseries[i] /= adjustmentFactor;
}
// Compute max inflation-adjusted net worth
const maxNetWorth = Math.max(...totalNetWorthTimeseries); // Using the spread operator to find the max value
return {
MaxNetWorth: maxNetWorth,
TotalNetWorthTimeseries: totalNetWorthTimeseries,
SavingsTimeseries: savingsTimeseries,
HousingMonthlyCostTimeseries: housingMonthlyCostTimeseries
};
}