From b7fad81b6836310f62f9ed676b4e9af961ea5270 Mon Sep 17 00:00:00 2001 From: Gerry Chen Date: Mon, 8 Apr 2024 00:33:56 -0400 Subject: [PATCH 1/3] house buying draft progress - javascript getting closer --- .../2023-12-29_house-buying-2/gpt4/index.html | 90 ++++ .../2023-12-29_house-buying-2/gpt4/script.js | 400 ++++++++++++++++++ .../2023-12-29_house-buying-2/gpt4/style.css | 158 +++++++ .../index.md} | 113 ++++- 4 files changed, 758 insertions(+), 3 deletions(-) create mode 100644 _blog/2023-12-29_house-buying-2/gpt4/index.html create mode 100644 _blog/2023-12-29_house-buying-2/gpt4/script.js create mode 100644 _blog/2023-12-29_house-buying-2/gpt4/style.css rename _blog/{_drafts/2023-12-29_house-buying-2.md => 2023-12-29_house-buying-2/index.md} (60%) diff --git a/_blog/2023-12-29_house-buying-2/gpt4/index.html b/_blog/2023-12-29_house-buying-2/gpt4/index.html new file mode 100644 index 0000000..16a7819 --- /dev/null +++ b/_blog/2023-12-29_house-buying-2/gpt4/index.html @@ -0,0 +1,90 @@ + + + + Mortgage vs Rent Calculator + + + + + +

Mortgage vs Rent Calculator

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
$
%
%
% of House Value
% of House Value
% of House Value
%
%
+ + +
+ + + +
+
+ + + +
+ +

Total Cost (Cumulative)

+ + + + + diff --git a/_blog/2023-12-29_house-buying-2/gpt4/script.js b/_blog/2023-12-29_house-buying-2/gpt4/script.js new file mode 100644 index 0000000..c9971e7 --- /dev/null +++ b/_blog/2023-12-29_house-buying-2/gpt4/script.js @@ -0,0 +1,400 @@ +let myChart = null; + +function calculate() { + // Getting input values + const V = parseFloat(document.getElementById("houseValue").value); + const D = parseFloat(document.getElementById("downPayment").value) / 100; + const Ri = parseFloat(document.getElementById("mortgageRate").value) / 100; + const A = parseFloat(document.getElementById("closingPct").value) / 100; + const PC = parseFloat(document.getElementById("maintenancePct").value) / 100; + const Rl = parseFloat(document.getElementById("annualRentPct").value) / 100; + const Ra = parseFloat(document.getElementById("realEstateAppreciation").value) / 100; + const Rm = parseFloat(document.getElementById("investmentReturn").value) / 100; + const T = 30; + + // Calculations + + function m(ri) { + return (ri * Math.pow(1 + ri, 360)) / (Math.pow(1 + ri, 360) - 1); + } + function p(M, ri) { + return ( + Math.pow(1 + ri, M) - + ((Math.pow(1 + ri, M) - 1) / (Math.pow(1 + ri, 360) - 1)) * Math.pow(1 + ri, 360) + ); + } + function OwnCosts(M) { + const T = M / 12; + + const downPayment = V * D; + const closingCosts = V * A; + const principle = V * (1 - D); + + const mortgageMonthlyProp = m(Ri / 12); + const mortgageMonthly = mortgageMonthlyProp * principle; + const maintenanceAnnual = V * PC; + + const upfrontCosts = downPayment + closingCosts; + const ongoingCosts = mortgageMonthly * 12 + maintenanceAnnual; + + const totalEquity = V * Math.pow(1 + Ra, T) * (1 - A) - p(M, Ri / 12) * principle; + const totalSpent = upfrontCosts + ongoingCosts * T; + + const net = totalEquity - totalSpent; + + return { + upfrontCosts, + ongoingCosts, + totalEquity, + totalSpent, + net, + }; + } + + function RentCosts(M) { + const T = M / 12; + + const rentMonthly = (V * Rl) / 12; + + const upfrontCosts = 0; + const ongoingCosts = rentMonthly * 12; + + const totalEquity = 0; + const totalSpent = upfrontCosts + ongoingCosts * T; + + const net = totalEquity - totalSpent; + + return { + upfrontCosts, + ongoingCosts, + totalEquity, + totalSpent, + net, + }; + } + + function addInvestmentReturn(M) { + let own = 0, + ownSpend = 0; + let rent = 0, + rentSpend = 0; + const costAtI = (obj, i) => (i == 0 ? obj.upfrontCosts : obj.ongoingCosts / 12); + for (let i = 0; i <= M; i++) { + const own1 = costAtI(OwnCosts(i), i); + const rent1 = costAtI(RentCosts(i), i); + ownSpend += Math.max(own1, rent1) - own1; + rentSpend += Math.max(own1, rent1) - rent1; + own += (Math.max(own1, rent1) - own1) * Math.pow(1 + Rm, (M - i) / 12); + rent += (Math.max(own1, rent1) - rent1) * Math.pow(1 + Rm, (M - i) / 12); + } + return { own, rent, ownSpend, rentSpend }; + } + + // Update table with results + updateResultsTable(T, OwnCosts, RentCosts, addInvestmentReturn); + + // Update graph + updateGraph(T, OwnCosts, RentCosts, addInvestmentReturn); +} + +function toggleCollapseResultTableRows(s, e) { + const rows = document.getElementById("resultsTable").firstChild.rows; + const hiddenRows = Array.from(rows) + .slice(s, e + 1) + .filter((row) => row.classList.contains("hidden-row")); + if (hiddenRows.length > 0) { + for (let row of hiddenRows) { + row.classList.remove("hidden-row"); + } + rows[s - 1].firstChild.textContent = rows[s - 1].firstChild.textContent.replace("⯈", "⯆"); + hideIntermediateLines(false); + } else { + for (let i = s; i <= e; i++) { + rows[i].classList.add("hidden-row"); + } + rows[s - 1].firstChild.textContent = rows[s - 1].firstChild.textContent.replace("⯆", "⯈"); + hideIntermediateLines(true); + } +} + +function updateResultsTable(timeHorizon, owning, renting, investment, cumulative = false) { + const T = timeHorizon; + + const table = document.createElement("table"); + document.getElementById("resultsTable").appendChild(table); + + const rows = []; + for (let i = 0; i < 10 + 1; ++i) { + rows.push(table.insertRow(i)); + } + + function div(text, style = "") { + return `
${text}
`; + } + + rows[0].insertCell(0).innerHTML = div("Year"); + rows[1].insertCell(0).innerHTML = div("Owning Total (Cumulative) ⯈"); + rows[2].insertCell(0).innerHTML = div("Spend", "text-align: left; padding-left: 10px;"); + rows[3].insertCell(0).innerHTML = div("Equity", "text-align: left; padding-left: 10px;"); + rows[4].insertCell(0).innerHTML = div( + "Investment Spend", + "text-align: left; padding-left: 10px;" + ); + rows[5].insertCell(0).innerHTML = div( + "Investment Equity", + "text-align: left; padding-left: 10px;" + ); + rows[6].insertCell(0).innerHTML = div("Renting Total (Cumulative) ⯈"); + rows[7].insertCell(0).innerHTML = div("Spend", "text-align: left; padding-left: 10px;"); + rows[8].insertCell(0).innerHTML = div("Equity", "text-align: left; padding-left: 10px;"); + rows[9].insertCell(0).innerHTML = div( + "Investment Spend", + "text-align: left; padding-left: 10px;" + ); + rows[10].insertCell(0).innerHTML = div( + "Investment Equity", + "text-align: left; padding-left: 10px;" + ); + + rows[1].cells[0].addEventListener("click", () => toggleCollapseResultTableRows(2, 5)); + rows[6].cells[0].addEventListener("click", () => toggleCollapseResultTableRows(7, 10)); + rows[1].cells[0].style.cursor = "pointer"; + rows[6].cells[0].style.cursor = "pointer"; + rows[1].style.backgroundColor = "#f0f0f0"; + rows[6].style.backgroundColor = "#f0f0f0"; + toggleCollapseResultTableRows(2, 5); + toggleCollapseResultTableRows(7, 10); + + for (let row of rows) { + row.cells[0].tagName = "TH"; + } + + let prevOwn = { + upfrontCosts: 0, + ongoingCosts: 0, + totalEquity: 0, + totalSpent: 0, + net: 0, + }; + let prevRent = { + upfrontCosts: 0, + ongoingCosts: 0, + totalEquity: 0, + totalSpent: 0, + net: 0, + }; + let prevInvest = { + own: 0, + rent: 0, + ownSpend: 0, + rentSpend: 0, + }; + function diff(obj1, obj2) { + return Object.keys(obj1).reduce((diff, key) => { + diff[key] = obj1[key] - obj2[key]; + return diff; + }, {}); + } + function format(num) { + // return num.toFixed(2); + return num.toLocaleString(undefined, { style: "currency", currency: "USD" }); + } + let own, rent, invest; + for (let year = 0; year <= T; ++year) { + const thisOwn = owning(year * 12); + const thisRent = renting(year * 12); + const thisInvest = investment(year * 12); + if (cumulative) { + own = thisOwn; + rent = thisRent; + invest = thisInvest; + } else { + own = diff(thisOwn, prevOwn); + rent = diff(thisRent, prevRent); + invest = diff(thisInvest, prevInvest); + prevOwn = thisOwn; + prevRent = thisRent; + prevInvest = thisInvest; + } + rows[0].insertCell(-1).innerHTML = div(year); + rows[1].insertCell(-1).innerHTML = div( + format(thisOwn.net + thisInvest.own - thisInvest.ownSpend) + ); + rows[2].insertCell(-1).innerHTML = div(format(-own.totalSpent)); + rows[3].insertCell(-1).innerHTML = div(format(own.totalEquity)); + rows[4].insertCell(-1).innerHTML = div(format(-invest.ownSpend)); + rows[5].insertCell(-1).innerHTML = div(format(invest.own)); + rows[6].insertCell(-1).innerHTML = div( + format(thisRent.net + thisInvest.rent - thisInvest.rentSpend) + ); + rows[7].insertCell(-1).innerHTML = div(format(-rent.totalSpent)); + rows[8].insertCell(-1).innerHTML = div(format(rent.totalEquity)); + rows[9].insertCell(-1).innerHTML = div(format(-invest.rentSpend)); + rows[10].insertCell(-1).innerHTML = div(format(invest.rent)); + } + + const colsToKeep = [-1, 0, 1, 2, 3, 4, 5, 10, 15, 20, 25, 30]; + for (let row of rows) { + for (let i = row.cells.length - 1; i >= 0; --i) { + if (!colsToKeep.includes(i - 1)) { + row.deleteCell(i); + } + } + } +} + +function updateGraph(timeHorizon, owning, renting, addInvestmenReturn) { + const T = timeHorizon; + const ctx = document.getElementById("costGraph").getContext("2d"); + + // 0, 1, ..., T + // linspace continuous t = [0, T] with 3600 points + const t = Array.from({ length: T * 12 + 1 }, (_, i) => i / 12); + const m = t.map((t_) => 12 * t_); + + const own = { + costs: m.map((i) => owning(i).totalSpent), + equity: m.map((i) => owning(i).totalEquity), + investment: m.map((i) => addInvestmenReturn(i).own - addInvestmenReturn(i).ownSpend), + total: m.map((i) => owning(i).net + addInvestmenReturn(i).own - addInvestmenReturn(i).ownSpend), + }; + const rent = { + costs: m.map((i) => renting(i).totalSpent), + equity: m.map((i) => renting(i).totalEquity), + investment: m.map((i) => addInvestmenReturn(i).rent - addInvestmenReturn(i).rentSpend), + total: m.map( + (i) => renting(i).net + addInvestmenReturn(i).rent - addInvestmenReturn(i).rentSpend + ), + }; + + function negative(arr) { + return arr.map((x) => -x); + } + + myChart = new Chart(ctx, { + type: "line", + data: { + labels: t, + datasets: [ + { + label: "Owning", + data: negative(own.total), + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + pointRadius: 0, // No dots on the line + }, + { + label: "Renting", + data: negative(rent.total), + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + pointRadius: 0, // No dots on the line + }, + { + label: "Owning Costs", + data: own.costs, + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + borderDash: [5, 5], + pointRadius: 0, // No dots on the line + }, + { + label: "Renting Costs", + data: rent.costs, + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + borderDash: [5, 5], + pointRadius: 0, // No dots on the line + }, + { + label: "Owning Equity", + data: negative(own.equity), + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + borderDash: [1, 1], + pointRadius: 0, // No dots on the line + }, + { + label: "Renting Equity", + data: negative(rent.equity), + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + borderDash: [1, 1], + pointRadius: 0, // No dots on the line + }, + { + label: "Owning Investment", + data: negative(own.investment), + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + borderDash: [2, 2], + pointRadius: 0, // No dots on the line + }, + { + label: "Renting Investment", + data: negative(rent.investment), + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + borderDash: [2, 2], + pointRadius: 0, // No dots on the line + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + axis: "x", + intersect: false, // Ensure that the tooltip appears when hovering anywhere on the x-axis + }, + tooltips: { + mode: "index", // Show tooltip for all datasets + intersect: false, // Ensure that the tooltip appears when hovering anywhere on the x-axis + }, + scales: { + x: { + ticks: { + maxTicksLimit: 31, + }, + }, + y: { + beginAtZero: true, + }, + }, + plugins: { + tooltip: { + mode: "index", + intersect: false, + }, + }, + }, + }); +} + +function hideIntermediateLines(hide=true) { + if (!myChart) { + setTimeout(() => hideIntermediateLines(hide), 100); + return; + } + const data = myChart.data.datasets; + for (let i = 2; i < data.length; i++) { + data[i].hidden = hide; + } + myChart.update(); +} + +// Event listener for time horizon slider +// document.getElementById("timeHorizon").addEventListener("input", function () { +// document.getElementById("timeHorizonValue").textContent = this.value; +// }); + +// Initial calculation +calculate(); diff --git a/_blog/2023-12-29_house-buying-2/gpt4/style.css b/_blog/2023-12-29_house-buying-2/gpt4/style.css new file mode 100644 index 0000000..1c0bbfc --- /dev/null +++ b/_blog/2023-12-29_house-buying-2/gpt4/style.css @@ -0,0 +1,158 @@ +body { + font-family: Arial, sans-serif; + margin: 20px; +} + +#costGraph { + width: 100% !important; + max-height: 500px !important; +} + +#inputs tr { + margin-bottom: 5px; +} + +#inputs td { + vertical-align: middle; +} + +#inputs input { + width: 60px; + text-align: right; +} + +/* Disable arrows */ +#inputs input::-webkit-outer-spin-button, +#inputs input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +#inputs label, +#inputs input { + /* margin-bottom: 10px; */ +} + +#inputs input { + margin-left: 10px; +} + +#resultsTable table { + margin-top: 20px; + border-collapse: collapse; +} + +#resultsTable tr, +#resultsTable tr .td-div, +#resultsTable tr td { + transition: all 1s; + overflow: hidden; +} + +#resultsTable tr .td-div { + max-height: 70px; +} + +#resultsTable .hidden-row .td-div { + max-height: 0; +} + +#resultsTable .hidden-row td { + padding: 0; +} + +#resultsTable th, +#resultsTable td { + border: 1px solid black; + padding: 5px; + text-align: right; +} + +#resultsTable td:first-child { + font-weight: bold; + text-align: center; +} + +#resultsTable tr:first-child td { + font-weight: bold; + text-align: center; +} + + +/****************************** Slider Switch ******************************/ +/* The switch - the box around the slider */ +.switch { + position: relative; + display: inline-block; + width: calc(2rem + 8px); + height: 1.5rem; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 1rem; + width: 1rem; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked+.slider { + background-color: #2196F3; +} + +input:focus+.slider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked+.slider:before { + -webkit-transform: translateX(1rem); + -ms-transform: translateX(1rem); + transform: translateX(1rem); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +.switch-div { + display: flex; + align-items: center; + margin: 10px 0; +} + +.switch-div>* { + margin: 0 5px; +} + +.switch-div>*:first-child { + margin-left: 0; +} \ No newline at end of file diff --git a/_blog/_drafts/2023-12-29_house-buying-2.md b/_blog/2023-12-29_house-buying-2/index.md similarity index 60% rename from _blog/_drafts/2023-12-29_house-buying-2.md rename to _blog/2023-12-29_house-buying-2/index.md index e880167..461c281 100644 --- a/_blog/_drafts/2023-12-29_house-buying-2.md +++ b/_blog/2023-12-29_house-buying-2/index.md @@ -33,6 +33,7 @@ Kind of related to my previous post on [renting vs buying](/blog/2023-06-07_hous Temporarily, we will ignore the risk differential between the stock market and the housing market, but this is a very important point that we must consider later. +{% collapsible --expanded %} ## Parameters to Vary * General Parameters: * $$R_m$$: Stock Market Return percent (proportion), typically 7% @@ -40,6 +41,7 @@ Temporarily, we will ignore the risk differential between the stock market and t * Buy Parameters: * $$D$$: Down payment percent (proportion), typically 20% * $$R_i$$: Mortgage Interest rate, typically 3-7% + * $$A$$: Closing cost percent (proportion), typically 2-5% * $$R_a$$: Property Appreciation rate, typically 0-5% * Maintenance costs * $$P$$: Mortgage-dependent (i.e. PMI aka mortgage insurance for <20% down payment) @@ -55,8 +57,8 @@ To make things more tractable, let's make the following "nominal" assumptions, a * $$R_m = 7\%$$, * $$D = 20\%$$, * $$R_i = 5.5\%$$, -* $$P = 0%$$ (no PMI for 20% down payment), -* $$C = (0.016)V$$ +* $$P = 0\%$$ (no PMI for 20% down payment), +* $$C = (0.016)V$$. In case you are curious, here's some details of how I came up with nominals for the more contentious parameters: {% collapsible %} @@ -102,5 +104,110 @@ Also based on the super vague [Google mortgage calculator](https://www.google.co {% collapsible %} ### Rent Price -A landlord should aim for approximately market ($R_l$) returns adjusted for risk and liquidity. I've heard from random people that they do typically aim for around 7% annual rent payment +A landlord should aim for approximately market ($$R_l$$) returns adjusted for risk and liquidity. I've heard from random people that they do typically aim for around 7% annual rent payment {% endcollapsible %} + +{% endcollapsible %} + +{% collapsible --expanded %} +## Cost Equations + +### Owning + + + + + +| Cost | Description | Expression | +|:--------:|:-------:|:--------| +| Upfront Costs | (Downpayment) + (closing costs) | $$I ~:= (D + A)V$$ | +| Ongoing Costs | (Mortgage) + (maintenance) | $$M := m(R_i)(1-D)V + PV + CV$$ | +|--|--|--| +| Total equity at time T | (House Value) - (closing costs) - (mortgage principle) | $$E(T) ~:= V(1 + R_a)^T(1 - A) - p(T, R_i)(1-D)V$$ | +| Total spent at time T | (Upfront Costs) + (Ongoing Costs) $$\cdot$$ T | $$I + 12MT$$ | +|--|--|--| +| Net at time T | (Total Equity) - (Total Spent) | $$ E(T) - I - 12MT $$ | + +Where $$m(R_i)$$ is the monthly mortgage payment for a 30-year mortgage at interest rate $$R_i$$ (as a proportion of the total borrowed amount $$(1-D)V$$ ), and $$p(T, R_i)$$ is the total principal owed on a mortgage after $$T$$ years at interest rate $$R_i$$ (also as a percentage of the total borrowed amount $$(1-D)V$$). + +$$m(R_i)$$ is a standard equation. Denoting $$r_i:=R_i/12$$, then + +$$m(12r_i) = \frac{r_i(1+r_i)^{360}}{(1+r_i)^{360} - 1}.$$ + +$$p(T, R_i)$$ is also a reasonably standard calculation: + +$$\begin{align*} + p(T, 12r_i) &= (1+r_i)^{12T} - \frac{(1+r_i)^{12T} - 1}{r_i}m(R_i) \\ + &= (1+r_i)^{12T} - \frac{(1+r_i)^{12T} - 1}{(1+r_i)^{360} - 1}(1+r_i)^{360} +\end{align*}$$ + + +### Renting + +| Cost | Description | Expression | +|:--------:|:-------:|:--------| +| Upfront Costs | 0 | $$I ~:= 0$$ | +| Ongoing Costs | Rent | $$M := R_lV/12$$ | +|--|--|--| +| Total spent at time T | (Upfront Costs) + (Ongoing Costs) $$\cdot$$ T | $$I + 12MT$$ | +| Total equity at time T | 0 | $$E ~:= 0$$ | +|--|--|--| +| Net at time T | 0 | $$ E(T) - I - 12MT $$ | + +### Investment of excess cash + +| Source | Description | Expression | +|:--------:|:-------:|:--------| +| Upfront Costs | upfront money saved invested over T years | $$(\max(I_o, I_r) - I)(1+R_m)^T$$ | +| Ongoing Costs | monthly money saved invested over T years | $$\int_{0}^{T} (\max(M_o, M_r) - M)(1+R_m)^{T-t} dt$$ | + +Where subscripts $$_o$$ and $$_r$$ denote the owning and renting scenarios respectively. + +{% endcollapsible %} + + + + + + + + +## Cost Equations 2 + +At time 0: +* For a house, we have: + * Down payment -> upfront cost, goes into real-estate equity at appreciation rate $$R_a$$ + * **Cost:** $$(D+A)V$$ + * **Equity:** $$DV$$ + * **Initial Net Worth:** $$-AV$$ +* For renting, we have no upfront costs, so let's assume: + * "Down payment" instead goes into investment equity at apprecation rate $$R_m$$ + * **Equity:** $$(D+A)V$$ + * **Initial Net Worth:** $$(D+A)V$$ + +In steady state: +* For a house, we have a mortgage which goes partly towards interest (lost) and partly towards principle (equity): + * Mortgage + From 205ec882081988842911a644065c2d996a0041ea Mon Sep 17 00:00:00 2001 From: Gerry Chen Date: Mon, 8 Apr 2024 01:22:55 -0400 Subject: [PATCH 2/3] refactored functions into different files and split creation into create/update --- .../gpt4/calculate.js | 101 +++++ _blog/2023-12-29_house-buying-2/gpt4/graph.js | 178 ++++++++ .../2023-12-29_house-buying-2/gpt4/index.html | 55 +-- .../2023-12-29_house-buying-2/gpt4/script.js | 403 +----------------- _blog/2023-12-29_house-buying-2/gpt4/table.js | 121 ++++++ 5 files changed, 441 insertions(+), 417 deletions(-) create mode 100644 _blog/2023-12-29_house-buying-2/gpt4/calculate.js create mode 100644 _blog/2023-12-29_house-buying-2/gpt4/graph.js create mode 100644 _blog/2023-12-29_house-buying-2/gpt4/table.js diff --git a/_blog/2023-12-29_house-buying-2/gpt4/calculate.js b/_blog/2023-12-29_house-buying-2/gpt4/calculate.js new file mode 100644 index 0000000..a3abb6b --- /dev/null +++ b/_blog/2023-12-29_house-buying-2/gpt4/calculate.js @@ -0,0 +1,101 @@ +import { updateResultsTable, rows } from "./table.js"; +import { updateGraph } from "./graph.js"; + +export function calculate() { + // Getting input values + const V = parseFloat(document.getElementById("houseValue").value); + const D = parseFloat(document.getElementById("downPayment").value) / 100; + const Ri = parseFloat(document.getElementById("mortgageRate").value) / 100; + const A = parseFloat(document.getElementById("closingPct").value) / 100; + const PC = parseFloat(document.getElementById("maintenancePct").value) / 100; + const Rl = parseFloat(document.getElementById("annualRentPct").value) / 100; + const Ra = parseFloat(document.getElementById("realEstateAppreciation").value) / 100; + const Rm = parseFloat(document.getElementById("investmentReturn").value) / 100; + const T = 30; + + // Calculations + + function m(ri) { + return (ri * Math.pow(1 + ri, 360)) / (Math.pow(1 + ri, 360) - 1); + } + function p(M, ri) { + return ( + Math.pow(1 + ri, M) - + ((Math.pow(1 + ri, M) - 1) / (Math.pow(1 + ri, 360) - 1)) * Math.pow(1 + ri, 360) + ); + } + function OwnCosts(M) { + const T = M / 12; + + const downPayment = V * D; + const closingCosts = V * A; + const principle = V * (1 - D); + + const mortgageMonthlyProp = m(Ri / 12); + const mortgageMonthly = mortgageMonthlyProp * principle; + const maintenanceAnnual = V * PC; + + const upfrontCosts = downPayment + closingCosts; + const ongoingCosts = mortgageMonthly * 12 + maintenanceAnnual; + + const totalEquity = V * Math.pow(1 + Ra, T) * (1 - A) - p(M, Ri / 12) * principle; + const totalSpent = upfrontCosts + ongoingCosts * T; + + const net = totalEquity - totalSpent; + + return { + upfrontCosts, + ongoingCosts, + totalEquity, + totalSpent, + net, + }; + } + + function RentCosts(M) { + const T = M / 12; + + const rentMonthly = (V * Rl) / 12; + + const upfrontCosts = 0; + const ongoingCosts = rentMonthly * 12; + + const totalEquity = 0; + const totalSpent = upfrontCosts + ongoingCosts * T; + + const net = totalEquity - totalSpent; + + return { + upfrontCosts, + ongoingCosts, + totalEquity, + totalSpent, + net, + }; + } + + function addInvestmentReturn(M) { + let own = 0, + ownSpend = 0; + let rent = 0, + rentSpend = 0; + const costAtI = (obj, i) => (i == 0 ? obj.upfrontCosts : obj.ongoingCosts / 12); + for (let i = 0; i <= M; i++) { + const own1 = costAtI(OwnCosts(i), i); + const rent1 = costAtI(RentCosts(i), i); + ownSpend += Math.max(own1, rent1) - own1; + rentSpend += Math.max(own1, rent1) - rent1; + own += (Math.max(own1, rent1) - own1) * Math.pow(1 + Rm, (M - i) / 12); + rent += (Math.max(own1, rent1) - rent1) * Math.pow(1 + Rm, (M - i) / 12); + } + return { own, rent, ownSpend, rentSpend }; + } + + const cumulative = !document.getElementById("cumulative").checked; + + // Update table with results + updateResultsTable(rows, OwnCosts, RentCosts, addInvestmentReturn, cumulative); + + // Update graph + updateGraph(OwnCosts, RentCosts, addInvestmentReturn); +} diff --git a/_blog/2023-12-29_house-buying-2/gpt4/graph.js b/_blog/2023-12-29_house-buying-2/gpt4/graph.js new file mode 100644 index 0000000..a6ea8a2 --- /dev/null +++ b/_blog/2023-12-29_house-buying-2/gpt4/graph.js @@ -0,0 +1,178 @@ +function createGraph(timeHorizon) { + const T = timeHorizon; + const ctx = document.getElementById("costGraph").getContext("2d"); + + // 0, 1, ..., T + // linspace continuous t = [0, T] with 3600 points + const t = Array.from({ length: T * 12 + 1 }, (_, i) => i / 12); + + const myChart = new Chart(ctx, { + type: "line", + data: { + labels: t, + datasets: [ + { + label: "Owning", + data: t, + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + pointRadius: 0, // No dots on the line + }, + { + label: "Renting", + data: t, + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + pointRadius: 0, // No dots on the line + }, + { + label: "Owning Costs", + data: t, + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + borderDash: [5, 5], + pointRadius: 0, // No dots on the line + }, + { + label: "Renting Costs", + data: t, + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + borderDash: [5, 5], + pointRadius: 0, // No dots on the line + }, + { + label: "Owning Equity", + data: t, + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + borderDash: [1, 1], + pointRadius: 0, // No dots on the line + }, + { + label: "Renting Equity", + data: t, + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + borderDash: [1, 1], + pointRadius: 0, // No dots on the line + }, + { + label: "Owning Investment", + data: t, + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 1, + borderDash: [2, 2], + pointRadius: 0, // No dots on the line + }, + { + label: "Renting Investment", + data: t, + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1, + borderDash: [2, 2], + pointRadius: 0, // No dots on the line + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + axis: "x", + intersect: false, // Ensure that the tooltip appears when hovering anywhere on the x-axis + }, + tooltips: { + mode: "index", // Show tooltip for all datasets + intersect: false, // Ensure that the tooltip appears when hovering anywhere on the x-axis + }, + scales: { + x: { + ticks: { + maxTicksLimit: 31, + }, + }, + y: { + beginAtZero: true, + }, + }, + plugins: { + tooltip: { + mode: "index", + intersect: false, + }, + }, + }, + }); + + return myChart; +} + +export let myChart = createGraph(30); + +window.myChart = myChart; + +function negative(arr) { + return arr.map((x) => -x); +} + +export function updateGraph(owning, renting, addInvestmentReturn) { + console.log("updateGraph"); + + const t = myChart.data.labels; + const m = t.map((t_) => 12 * t_); + + const own = { + costs: m.map((i) => owning(i).totalSpent), + equity: m.map((i) => owning(i).totalEquity), + investment: m.map((i) => addInvestmentReturn(i).own - addInvestmentReturn(i).ownSpend), + total: m.map( + (i) => owning(i).net + addInvestmentReturn(i).own - addInvestmentReturn(i).ownSpend + ), + }; + const rent = { + costs: m.map((i) => renting(i).totalSpent), + equity: m.map((i) => renting(i).totalEquity), + investment: m.map((i) => addInvestmentReturn(i).rent - addInvestmentReturn(i).rentSpend), + total: m.map( + (i) => renting(i).net + addInvestmentReturn(i).rent - addInvestmentReturn(i).rentSpend + ), + }; + + const datas = { + Owning: negative(own.total), + Renting: negative(rent.total), + "Owning Costs": own.costs, + "Renting Costs": rent.costs, + "Owning Equity": negative(own.equity), + "Renting Equity": negative(rent.equity), + "Owning Investment": negative(own.investment), + "Renting Investment": negative(rent.investment), + }; + + for (let dataset of myChart.data.datasets) { + dataset.data = datas[dataset.label]; + } + + myChart.update(); +} + +export function hideIntermediateLines(hide = true, isOwn = true) { + if (!myChart) { + setTimeout(() => hideIntermediateLines(hide, isOwn), 100); + return; + } + const data = myChart.data.datasets; + for (let i = isOwn ? 2 : 3; i < data.length; i += 2) { + data[i].hidden = hide; + } + myChart.update(); +} diff --git a/_blog/2023-12-29_house-buying-2/gpt4/index.html b/_blog/2023-12-29_house-buying-2/gpt4/index.html index 16a7819..1311538 100644 --- a/_blog/2023-12-29_house-buying-2/gpt4/index.html +++ b/_blog/2023-12-29_house-buying-2/gpt4/index.html @@ -1,11 +1,14 @@ + Mortgage vs Rent Calculator - + +

Mortgage vs Rent Calculator

@@ -13,57 +16,58 @@

Mortgage vs Rent Calculator

- + - + - + - + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + - + - +
$$
%%
%%
% of House Value% of House Value
% of House Value% of House Value
% of House Value% of House Value
%%
%%
@@ -71,7 +75,7 @@

Mortgage vs Rent Calculator

@@ -85,6 +89,7 @@

Mortgage vs Rent Calculator

Total Cost (Cumulative)

- + - + + \ No newline at end of file diff --git a/_blog/2023-12-29_house-buying-2/gpt4/script.js b/_blog/2023-12-29_house-buying-2/gpt4/script.js index c9971e7..11a38f9 100644 --- a/_blog/2023-12-29_house-buying-2/gpt4/script.js +++ b/_blog/2023-12-29_house-buying-2/gpt4/script.js @@ -1,395 +1,7 @@ -let myChart = null; - -function calculate() { - // Getting input values - const V = parseFloat(document.getElementById("houseValue").value); - const D = parseFloat(document.getElementById("downPayment").value) / 100; - const Ri = parseFloat(document.getElementById("mortgageRate").value) / 100; - const A = parseFloat(document.getElementById("closingPct").value) / 100; - const PC = parseFloat(document.getElementById("maintenancePct").value) / 100; - const Rl = parseFloat(document.getElementById("annualRentPct").value) / 100; - const Ra = parseFloat(document.getElementById("realEstateAppreciation").value) / 100; - const Rm = parseFloat(document.getElementById("investmentReturn").value) / 100; - const T = 30; - - // Calculations - - function m(ri) { - return (ri * Math.pow(1 + ri, 360)) / (Math.pow(1 + ri, 360) - 1); - } - function p(M, ri) { - return ( - Math.pow(1 + ri, M) - - ((Math.pow(1 + ri, M) - 1) / (Math.pow(1 + ri, 360) - 1)) * Math.pow(1 + ri, 360) - ); - } - function OwnCosts(M) { - const T = M / 12; - - const downPayment = V * D; - const closingCosts = V * A; - const principle = V * (1 - D); - - const mortgageMonthlyProp = m(Ri / 12); - const mortgageMonthly = mortgageMonthlyProp * principle; - const maintenanceAnnual = V * PC; - - const upfrontCosts = downPayment + closingCosts; - const ongoingCosts = mortgageMonthly * 12 + maintenanceAnnual; - - const totalEquity = V * Math.pow(1 + Ra, T) * (1 - A) - p(M, Ri / 12) * principle; - const totalSpent = upfrontCosts + ongoingCosts * T; - - const net = totalEquity - totalSpent; - - return { - upfrontCosts, - ongoingCosts, - totalEquity, - totalSpent, - net, - }; - } - - function RentCosts(M) { - const T = M / 12; - - const rentMonthly = (V * Rl) / 12; - - const upfrontCosts = 0; - const ongoingCosts = rentMonthly * 12; - - const totalEquity = 0; - const totalSpent = upfrontCosts + ongoingCosts * T; - - const net = totalEquity - totalSpent; - - return { - upfrontCosts, - ongoingCosts, - totalEquity, - totalSpent, - net, - }; - } - - function addInvestmentReturn(M) { - let own = 0, - ownSpend = 0; - let rent = 0, - rentSpend = 0; - const costAtI = (obj, i) => (i == 0 ? obj.upfrontCosts : obj.ongoingCosts / 12); - for (let i = 0; i <= M; i++) { - const own1 = costAtI(OwnCosts(i), i); - const rent1 = costAtI(RentCosts(i), i); - ownSpend += Math.max(own1, rent1) - own1; - rentSpend += Math.max(own1, rent1) - rent1; - own += (Math.max(own1, rent1) - own1) * Math.pow(1 + Rm, (M - i) / 12); - rent += (Math.max(own1, rent1) - rent1) * Math.pow(1 + Rm, (M - i) / 12); - } - return { own, rent, ownSpend, rentSpend }; - } - - // Update table with results - updateResultsTable(T, OwnCosts, RentCosts, addInvestmentReturn); - - // Update graph - updateGraph(T, OwnCosts, RentCosts, addInvestmentReturn); -} - -function toggleCollapseResultTableRows(s, e) { - const rows = document.getElementById("resultsTable").firstChild.rows; - const hiddenRows = Array.from(rows) - .slice(s, e + 1) - .filter((row) => row.classList.contains("hidden-row")); - if (hiddenRows.length > 0) { - for (let row of hiddenRows) { - row.classList.remove("hidden-row"); - } - rows[s - 1].firstChild.textContent = rows[s - 1].firstChild.textContent.replace("⯈", "⯆"); - hideIntermediateLines(false); - } else { - for (let i = s; i <= e; i++) { - rows[i].classList.add("hidden-row"); - } - rows[s - 1].firstChild.textContent = rows[s - 1].firstChild.textContent.replace("⯆", "⯈"); - hideIntermediateLines(true); - } -} - -function updateResultsTable(timeHorizon, owning, renting, investment, cumulative = false) { - const T = timeHorizon; - - const table = document.createElement("table"); - document.getElementById("resultsTable").appendChild(table); - - const rows = []; - for (let i = 0; i < 10 + 1; ++i) { - rows.push(table.insertRow(i)); - } - - function div(text, style = "") { - return `
${text}
`; - } - - rows[0].insertCell(0).innerHTML = div("Year"); - rows[1].insertCell(0).innerHTML = div("Owning Total (Cumulative) ⯈"); - rows[2].insertCell(0).innerHTML = div("Spend", "text-align: left; padding-left: 10px;"); - rows[3].insertCell(0).innerHTML = div("Equity", "text-align: left; padding-left: 10px;"); - rows[4].insertCell(0).innerHTML = div( - "Investment Spend", - "text-align: left; padding-left: 10px;" - ); - rows[5].insertCell(0).innerHTML = div( - "Investment Equity", - "text-align: left; padding-left: 10px;" - ); - rows[6].insertCell(0).innerHTML = div("Renting Total (Cumulative) ⯈"); - rows[7].insertCell(0).innerHTML = div("Spend", "text-align: left; padding-left: 10px;"); - rows[8].insertCell(0).innerHTML = div("Equity", "text-align: left; padding-left: 10px;"); - rows[9].insertCell(0).innerHTML = div( - "Investment Spend", - "text-align: left; padding-left: 10px;" - ); - rows[10].insertCell(0).innerHTML = div( - "Investment Equity", - "text-align: left; padding-left: 10px;" - ); - - rows[1].cells[0].addEventListener("click", () => toggleCollapseResultTableRows(2, 5)); - rows[6].cells[0].addEventListener("click", () => toggleCollapseResultTableRows(7, 10)); - rows[1].cells[0].style.cursor = "pointer"; - rows[6].cells[0].style.cursor = "pointer"; - rows[1].style.backgroundColor = "#f0f0f0"; - rows[6].style.backgroundColor = "#f0f0f0"; - toggleCollapseResultTableRows(2, 5); - toggleCollapseResultTableRows(7, 10); - - for (let row of rows) { - row.cells[0].tagName = "TH"; - } - - let prevOwn = { - upfrontCosts: 0, - ongoingCosts: 0, - totalEquity: 0, - totalSpent: 0, - net: 0, - }; - let prevRent = { - upfrontCosts: 0, - ongoingCosts: 0, - totalEquity: 0, - totalSpent: 0, - net: 0, - }; - let prevInvest = { - own: 0, - rent: 0, - ownSpend: 0, - rentSpend: 0, - }; - function diff(obj1, obj2) { - return Object.keys(obj1).reduce((diff, key) => { - diff[key] = obj1[key] - obj2[key]; - return diff; - }, {}); - } - function format(num) { - // return num.toFixed(2); - return num.toLocaleString(undefined, { style: "currency", currency: "USD" }); - } - let own, rent, invest; - for (let year = 0; year <= T; ++year) { - const thisOwn = owning(year * 12); - const thisRent = renting(year * 12); - const thisInvest = investment(year * 12); - if (cumulative) { - own = thisOwn; - rent = thisRent; - invest = thisInvest; - } else { - own = diff(thisOwn, prevOwn); - rent = diff(thisRent, prevRent); - invest = diff(thisInvest, prevInvest); - prevOwn = thisOwn; - prevRent = thisRent; - prevInvest = thisInvest; - } - rows[0].insertCell(-1).innerHTML = div(year); - rows[1].insertCell(-1).innerHTML = div( - format(thisOwn.net + thisInvest.own - thisInvest.ownSpend) - ); - rows[2].insertCell(-1).innerHTML = div(format(-own.totalSpent)); - rows[3].insertCell(-1).innerHTML = div(format(own.totalEquity)); - rows[4].insertCell(-1).innerHTML = div(format(-invest.ownSpend)); - rows[5].insertCell(-1).innerHTML = div(format(invest.own)); - rows[6].insertCell(-1).innerHTML = div( - format(thisRent.net + thisInvest.rent - thisInvest.rentSpend) - ); - rows[7].insertCell(-1).innerHTML = div(format(-rent.totalSpent)); - rows[8].insertCell(-1).innerHTML = div(format(rent.totalEquity)); - rows[9].insertCell(-1).innerHTML = div(format(-invest.rentSpend)); - rows[10].insertCell(-1).innerHTML = div(format(invest.rent)); - } - - const colsToKeep = [-1, 0, 1, 2, 3, 4, 5, 10, 15, 20, 25, 30]; - for (let row of rows) { - for (let i = row.cells.length - 1; i >= 0; --i) { - if (!colsToKeep.includes(i - 1)) { - row.deleteCell(i); - } - } - } -} - -function updateGraph(timeHorizon, owning, renting, addInvestmenReturn) { - const T = timeHorizon; - const ctx = document.getElementById("costGraph").getContext("2d"); - - // 0, 1, ..., T - // linspace continuous t = [0, T] with 3600 points - const t = Array.from({ length: T * 12 + 1 }, (_, i) => i / 12); - const m = t.map((t_) => 12 * t_); - - const own = { - costs: m.map((i) => owning(i).totalSpent), - equity: m.map((i) => owning(i).totalEquity), - investment: m.map((i) => addInvestmenReturn(i).own - addInvestmenReturn(i).ownSpend), - total: m.map((i) => owning(i).net + addInvestmenReturn(i).own - addInvestmenReturn(i).ownSpend), - }; - const rent = { - costs: m.map((i) => renting(i).totalSpent), - equity: m.map((i) => renting(i).totalEquity), - investment: m.map((i) => addInvestmenReturn(i).rent - addInvestmenReturn(i).rentSpend), - total: m.map( - (i) => renting(i).net + addInvestmenReturn(i).rent - addInvestmenReturn(i).rentSpend - ), - }; - - function negative(arr) { - return arr.map((x) => -x); - } - - myChart = new Chart(ctx, { - type: "line", - data: { - labels: t, - datasets: [ - { - label: "Owning", - data: negative(own.total), - backgroundColor: "rgba(255, 99, 132, 0.2)", - borderColor: "rgba(255, 99, 132, 1)", - borderWidth: 1, - pointRadius: 0, // No dots on the line - }, - { - label: "Renting", - data: negative(rent.total), - backgroundColor: "rgba(54, 162, 235, 0.2)", - borderColor: "rgba(54, 162, 235, 1)", - borderWidth: 1, - pointRadius: 0, // No dots on the line - }, - { - label: "Owning Costs", - data: own.costs, - backgroundColor: "rgba(255, 99, 132, 0.2)", - borderColor: "rgba(255, 99, 132, 1)", - borderWidth: 1, - borderDash: [5, 5], - pointRadius: 0, // No dots on the line - }, - { - label: "Renting Costs", - data: rent.costs, - backgroundColor: "rgba(54, 162, 235, 0.2)", - borderColor: "rgba(54, 162, 235, 1)", - borderWidth: 1, - borderDash: [5, 5], - pointRadius: 0, // No dots on the line - }, - { - label: "Owning Equity", - data: negative(own.equity), - backgroundColor: "rgba(255, 99, 132, 0.2)", - borderColor: "rgba(255, 99, 132, 1)", - borderWidth: 1, - borderDash: [1, 1], - pointRadius: 0, // No dots on the line - }, - { - label: "Renting Equity", - data: negative(rent.equity), - backgroundColor: "rgba(54, 162, 235, 0.2)", - borderColor: "rgba(54, 162, 235, 1)", - borderWidth: 1, - borderDash: [1, 1], - pointRadius: 0, // No dots on the line - }, - { - label: "Owning Investment", - data: negative(own.investment), - backgroundColor: "rgba(255, 99, 132, 0.2)", - borderColor: "rgba(255, 99, 132, 1)", - borderWidth: 1, - borderDash: [2, 2], - pointRadius: 0, // No dots on the line - }, - { - label: "Renting Investment", - data: negative(rent.investment), - backgroundColor: "rgba(54, 162, 235, 0.2)", - borderColor: "rgba(54, 162, 235, 1)", - borderWidth: 1, - borderDash: [2, 2], - pointRadius: 0, // No dots on the line - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - axis: "x", - intersect: false, // Ensure that the tooltip appears when hovering anywhere on the x-axis - }, - tooltips: { - mode: "index", // Show tooltip for all datasets - intersect: false, // Ensure that the tooltip appears when hovering anywhere on the x-axis - }, - scales: { - x: { - ticks: { - maxTicksLimit: 31, - }, - }, - y: { - beginAtZero: true, - }, - }, - plugins: { - tooltip: { - mode: "index", - intersect: false, - }, - }, - }, - }); -} - -function hideIntermediateLines(hide=true) { - if (!myChart) { - setTimeout(() => hideIntermediateLines(hide), 100); - return; - } - const data = myChart.data.datasets; - for (let i = 2; i < data.length; i++) { - data[i].hidden = hide; - } - myChart.update(); -} +import { calculate } from "./calculate.js"; +import { createResultsTable } from "./table.js"; +import { myChart } from "./graph.js"; +import { rows } from "./table.js"; // Event listener for time horizon slider // document.getElementById("timeHorizon").addEventListener("input", function () { @@ -398,3 +10,10 @@ function hideIntermediateLines(hide=true) { // Initial calculation calculate(); + +window.calculate = calculate; + +const inputs = document.querySelectorAll("input"); +inputs.forEach((input) => { + input.addEventListener("input", calculate); +}); diff --git a/_blog/2023-12-29_house-buying-2/gpt4/table.js b/_blog/2023-12-29_house-buying-2/gpt4/table.js new file mode 100644 index 0000000..bfec0aa --- /dev/null +++ b/_blog/2023-12-29_house-buying-2/gpt4/table.js @@ -0,0 +1,121 @@ +import { hideIntermediateLines } from "./graph.js"; + +function div(text, style = "") { + return `
${text}
`; +} + +const YEARS_TO_SHOW = [0, 1, 2, 3, 4, 5, 10, 15, 20, 25, 30]; + +export function createResultsTable() { + const table = document.createElement("table"); + document.getElementById("resultsTable").appendChild(table); + const rows = []; + for (let i = 0; i < 10 + 1; ++i) { + rows.push(table.insertRow(i)); + } + rows[0].insertCell(0).innerHTML = div("Year"); + rows[1].insertCell(0).innerHTML = div("Owning Total (Cumulative) ⯈"); + rows[2].insertCell(0).innerHTML = div("Spend", "text-align: left; padding-left: 10px;"); + rows[3].insertCell(0).innerHTML = div("Equity", "text-align: left; padding-left: 10px;"); + rows[4].insertCell(0).innerHTML = div( + "Investment Spend", + "text-align: left; padding-left: 10px;" + ); + rows[5].insertCell(0).innerHTML = div( + "Investment Equity", + "text-align: left; padding-left: 10px;" + ); + rows[6].insertCell(0).innerHTML = div("Renting Total (Cumulative) ⯈"); + rows[7].insertCell(0).innerHTML = div("Spend", "text-align: left; padding-left: 10px;"); + rows[8].insertCell(0).innerHTML = div("Equity", "text-align: left; padding-left: 10px;"); + rows[9].insertCell(0).innerHTML = div( + "Investment Spend", + "text-align: left; padding-left: 10px;" + ); + rows[10].insertCell(0).innerHTML = div( + "Investment Equity", + "text-align: left; padding-left: 10px;" + ); + rows[1].cells[0].addEventListener("click", () => toggleCollapseResultTableRows(2, 5)); + rows[6].cells[0].addEventListener("click", () => toggleCollapseResultTableRows(7, 10)); + rows[1].cells[0].style.cursor = "pointer"; + rows[6].cells[0].style.cursor = "pointer"; + rows[1].style.backgroundColor = "#f0f0f0"; + rows[6].style.backgroundColor = "#f0f0f0"; + toggleCollapseResultTableRows(2, 5); + toggleCollapseResultTableRows(7, 10); + + for (let year of YEARS_TO_SHOW) { + rows[0].insertCell(-1).innerHTML = div(year); + for (let i = 1; i <= 10; ++i) { + rows[i].insertCell(-1).innerHTML = div(""); + } + } + + return rows; +} + +function diff(obj1, obj2) { + return Object.keys(obj1).reduce((diff, key) => { + diff[key] = obj1[key] - obj2[key]; + return diff; + }, {}); +} +function marginalYearly(func, year) { + return year == 0 ? func(0) : diff(func(year * 12), func((year - 1) * 12)); +} +function format(num) { + return num.toLocaleString(undefined, { style: "currency", currency: "USD" }); +} + +export function updateResultsTable(rows, owning, renting, investment, cumulative = false) { + for (let ind = 0; ind < YEARS_TO_SHOW.length; ++ind) { + const year = YEARS_TO_SHOW[ind]; + + const thisOwn = owning(year * 12); + const thisRent = renting(year * 12); + const thisInvest = investment(year * 12); + const own = cumulative ? thisOwn : marginalYearly(owning, year); + const rent = cumulative ? thisRent : marginalYearly(renting, year); + const invest = cumulative ? thisInvest : marginalYearly(investment, year); + + rows[1].cells[ind + 1].innerHTML = div( + format(thisOwn.net + thisInvest.own - thisInvest.ownSpend) + ); + rows[2].cells[ind + 1].innerHTML = div(format(-own.totalSpent)); + rows[3].cells[ind + 1].innerHTML = div(format(own.totalEquity)); + rows[4].cells[ind + 1].innerHTML = div(format(-invest.ownSpend)); + rows[5].cells[ind + 1].innerHTML = div(format(invest.own)); + rows[6].cells[ind + 1].innerHTML = div( + format(thisRent.net + thisInvest.rent - thisInvest.rentSpend) + ); + rows[7].cells[ind + 1].innerHTML = div(format(-rent.totalSpent)); + rows[8].cells[ind + 1].innerHTML = div(format(rent.totalEquity)); + rows[9].cells[ind + 1].innerHTML = div(format(-invest.rentSpend)); + rows[10].cells[ind + 1].innerHTML = div(format(invest.rent)); + } +} + +export let rows = createResultsTable(); + +function toggleCollapseResultTableRows(s, e) { + const rows = document.getElementById("resultsTable").firstChild.rows; + const hiddenRows = Array.from(rows) + .slice(s, e + 1) + .filter((row) => row.classList.contains("hidden-row")); + if (hiddenRows.length > 0) { + for (let row of hiddenRows) { + row.classList.remove("hidden-row"); + } + rows[s - 1].firstChild.textContent = rows[s - 1].firstChild.textContent.replace("⯈", "⯆"); + const isOwn = s < 4; + hideIntermediateLines(false, isOwn); + } else { + for (let i = s; i <= e; i++) { + rows[i].classList.add("hidden-row"); + } + rows[s - 1].firstChild.textContent = rows[s - 1].firstChild.textContent.replace("⯆", "⯈"); + const isOwn = s < 4; + hideIntermediateLines(true, isOwn); + } +} From db32263e140f330cd9cded4663c0d7e84811aff0 Mon Sep 17 00:00:00 2001 From: Gerry Chen Date: Mon, 8 Apr 2024 02:30:14 -0400 Subject: [PATCH 3/3] Cleaning up and preliminarily done with blog post --- .../2023-12-29_house-buying-2/gpt4/body.html | 125 ++++++++++++++++++ .../2023-12-29_house-buying-2/gpt4/index.html | 83 +----------- .../2023-12-29_house-buying-2/gpt4/script.js | 28 +++- .../2023-12-29_house-buying-2/gpt4/style.css | 4 + _blog/2023-12-29_house-buying-2/index.md | 47 +++---- _plugins/collapsible.rb | 2 +- 6 files changed, 179 insertions(+), 110 deletions(-) create mode 100644 _blog/2023-12-29_house-buying-2/gpt4/body.html diff --git a/_blog/2023-12-29_house-buying-2/gpt4/body.html b/_blog/2023-12-29_house-buying-2/gpt4/body.html new file mode 100644 index 0000000..940af0b --- /dev/null +++ b/_blog/2023-12-29_house-buying-2/gpt4/body.html @@ -0,0 +1,125 @@ + + + + +

Parameters

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
$
%
%
% of House Value
% of House Value
% of House Value
%
%
+ + + + +
+ + + +
+
+ +

Total Cost (Cumulative)

+ +

Total Cost (Cumulative)

+
+ + \ No newline at end of file diff --git a/_blog/2023-12-29_house-buying-2/gpt4/index.html b/_blog/2023-12-29_house-buying-2/gpt4/index.html index 1311538..e7f3840 100644 --- a/_blog/2023-12-29_house-buying-2/gpt4/index.html +++ b/_blog/2023-12-29_house-buying-2/gpt4/index.html @@ -3,93 +3,14 @@ Mortgage vs Rent Calculator - - -

Mortgage vs Rent Calculator

-
- - - - - - - +

Find details in the main blog post.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$
%
%
% of House Value
% of House Value
% of House Value
%
%
- - -
- - - -
-
- - - -
- -

Total Cost (Cumulative)

- - - + \ No newline at end of file diff --git a/_blog/2023-12-29_house-buying-2/gpt4/script.js b/_blog/2023-12-29_house-buying-2/gpt4/script.js index 11a38f9..a9e179b 100644 --- a/_blog/2023-12-29_house-buying-2/gpt4/script.js +++ b/_blog/2023-12-29_house-buying-2/gpt4/script.js @@ -13,7 +13,33 @@ calculate(); window.calculate = calculate; +function updateSliderLabel(input) { + console.log(input.id + "Label"); + const slider = document.getElementById(input.id); + if (slider.type == "range") { + const label = document.getElementById(input.id + "Label"); + label.value = slider.value; + } +} + +function updateInputNumber(input) { + const slider = document.getElementById(input.id.slice(0, -5)); + slider.value = input.value; +} + const inputs = document.querySelectorAll("input"); inputs.forEach((input) => { - input.addEventListener("input", calculate); + if (input.type == "range") { + input.addEventListener("input", () => updateSliderLabel(input)); + updateSliderLabel(input); + input.addEventListener("input", calculate); + } else if (input.type == "number") { + input.addEventListener("input", () => { + updateInputNumber(input); + calculate(); + }); + updateInputNumber(input); + } else if (input.type == "checkbox") { + input.addEventListener("input", calculate); + } }); diff --git a/_blog/2023-12-29_house-buying-2/gpt4/style.css b/_blog/2023-12-29_house-buying-2/gpt4/style.css index 1c0bbfc..4912a3a 100644 --- a/_blog/2023-12-29_house-buying-2/gpt4/style.css +++ b/_blog/2023-12-29_house-buying-2/gpt4/style.css @@ -33,6 +33,10 @@ body { /* margin-bottom: 10px; */ } +#inputs input[type="range"] { + width: 200px; +} + #inputs input { margin-left: 10px; } diff --git a/_blog/2023-12-29_house-buying-2/index.md b/_blog/2023-12-29_house-buying-2/index.md index 461c281..8b95444 100644 --- a/_blog/2023-12-29_house-buying-2/index.md +++ b/_blog/2023-12-29_house-buying-2/index.md @@ -33,7 +33,7 @@ Kind of related to my previous post on [renting vs buying](/blog/2023-06-07_hous Temporarily, we will ignore the risk differential between the stock market and the housing market, but this is a very important point that we must consider later. -{% collapsible --expanded %} +{% collapsible %} ## Parameters to Vary * General Parameters: * $$R_m$$: Stock Market Return percent (proportion), typically 7% @@ -109,7 +109,7 @@ A landlord should aim for approximately market ($$R_l$$) returns adjusted for ri {% endcollapsible %} -{% collapsible --expanded %} +{% collapsible %} ## Cost Equations ### Owning @@ -184,30 +184,23 @@ Where subscripts $$_o$$ and $$_r$$ denote the owning and renting scenarios respe {% endcollapsible %} +{% collapsible Interactive Graph --expanded %} +

+ (Standalone page available [here](gpt4/index.html).) +

- - - - - - -## Cost Equations 2 - -At time 0: -* For a house, we have: - * Down payment -> upfront cost, goes into real-estate equity at appreciation rate $$R_a$$ - * **Cost:** $$(D+A)V$$ - * **Equity:** $$DV$$ - * **Initial Net Worth:** $$-AV$$ -* For renting, we have no upfront costs, so let's assume: - * "Down payment" instead goes into investment equity at apprecation rate $$R_m$$ - * **Equity:** $$(D+A)V$$ - * **Initial Net Worth:** $$(D+A)V$$ - -In steady state: -* For a house, we have a mortgage which goes partly towards interest (lost) and partly towards principle (equity): - * Mortgage + +{% endcollapsible %} + diff --git a/_plugins/collapsible.rb b/_plugins/collapsible.rb index ea3fdeb..d526496 100644 --- a/_plugins/collapsible.rb +++ b/_plugins/collapsible.rb @@ -50,7 +50,7 @@ def render(context) %{ -
+