diff --git a/calc.js b/calc.js index 013e084..d6c4fea 100644 --- a/calc.js +++ b/calc.js @@ -44,8 +44,12 @@ function run_simulation(p) { const renter_nw_arr = []; const cum_buy_arr = []; const cum_rent_arr = []; + const detail_arr = []; const labels = []; + // yearly accumulators (reset each year) + let yr_interest = 0, yr_principal = 0, yr_upkeep = 0; + let breakeven_month = null; const invest_r_m = p.invest_return / 100 / 12; @@ -65,7 +69,10 @@ function run_simulation(p) { // German context: no mortgage interest tax deduction for private buyers const total_buy_m = monthly_payment + prop_tax_m + maint_m + insurance_m; - cum_buy_cost += total_buy_m; + yr_interest += interest_m; + yr_principal += principal_m; + yr_upkeep += prop_tax_m + maint_m + insurance_m; + cum_buy_cost += total_buy_m; buyer_portfolio *= (1 + invest_r_m); const selling_costs = home_value * (p.closing_sell_pct / 100); @@ -103,6 +110,14 @@ function run_simulation(p) { renter_nw_arr.push(Math.round(portfolio - tax_on_gains)); cum_buy_arr.push(Math.round(cum_buy_cost)); cum_rent_arr.push(Math.round(cum_rent_cost)); + detail_arr.push({ + house_value: Math.round(home_value), + buyer_portfolio: Math.round(buyer_portfolio), + yr_interest: Math.round(yr_interest), + yr_principal: Math.round(yr_principal), + yr_upkeep: Math.round(yr_upkeep), + }); + yr_interest = 0; yr_principal = 0; yr_upkeep = 0; } } @@ -116,6 +131,7 @@ function run_simulation(p) { renter_nw_arr, cum_buy_arr, cum_rent_arr, + detail_arr, monthly_payment: Math.round(monthly_payment), final_buyer_nw, final_renter_nw, diff --git a/style.css b/style.css index eeb90bd..1e7bd1f 100644 --- a/style.css +++ b/style.css @@ -409,6 +409,8 @@ th { tbody tr:hover { background: rgba(255,255,255,0.03); } +td.has-tip { cursor: default; border-bottom-style: dashed; } + tr.winner-row td { font-weight: 600; } /* ===== Footer ===== */ diff --git a/ui.js b/ui.js index ffbe555..a66aa63 100644 --- a/ui.js +++ b/ui.js @@ -140,22 +140,91 @@ const update_chart = (chart, labels, data_buy, data_rent, breakeven_month, years chart.update('none'); }; +// ===== Tooltip ===== + +const tooltip_el = (() => { + const el = document.createElement('div'); + el.id = 'tbl_tooltip'; + el.style.cssText = ` + position:fixed; z-index:1000; pointer-events:none; display:none; + background:rgba(13,17,23,0.96); border:1px solid rgba(255,255,255,0.12); + border-radius:10px; padding:0.7rem 1rem; font-size:0.8rem; line-height:1.8; + color:#e6edf3; white-space:nowrap; box-shadow:0 8px 32px rgba(0,0,0,0.5); + `; + document.body.appendChild(el); + return el; +})(); + +const show_tooltip = (e, html) => { + tooltip_el.innerHTML = html; + tooltip_el.style.display = 'block'; + move_tooltip(e); +}; + +const move_tooltip = (e) => { + const pad = 14; + const tw = tooltip_el.offsetWidth; + const th = tooltip_el.offsetHeight; + let x = e.clientX + pad; + let y = e.clientY + pad; + if (x + tw > window.innerWidth - pad) x = e.clientX - tw - pad; + if (y + th > window.innerHeight - pad) y = e.clientY - th - pad; + tooltip_el.style.left = x + 'px'; + tooltip_el.style.top = y + 'px'; +}; + +const hide_tooltip = () => { tooltip_el.style.display = 'none'; }; + +const tip_row = (label, value, color) => + `${label} ${fmt_eur(value)}`; + // ===== Table ===== const build_table = (result) => { const tbody = document.getElementById('table_body'); tbody.innerHTML = ''; for (let i = 0; i < result.labels.length; i++) { + const d = result.detail_arr[i]; const tr = document.createElement('tr'); - tr.innerHTML = ` - ${result.labels[i]} - ${fmt_eur(result.buyer_nw_arr[i])} - ${fmt_eur(result.renter_nw_arr[i])} - ${fmt_eur(result.cum_buy_arr[i])} - ${fmt_eur(result.cum_rent_arr[i])} - `; + + const td_buy_nw = document.createElement('td'); + td_buy_nw.textContent = fmt_eur(result.buyer_nw_arr[i]); + td_buy_nw.classList.add('has-tip'); + td_buy_nw.addEventListener('mouseenter', (e) => show_tooltip(e, [ + tip_row('Immobilienwert', d.house_value, '#f59e0b'), + tip_row('ETF-Portfolio', d.buyer_portfolio, '#f59e0b'), + ].join('
'))); + + const td_rent_nw = document.createElement('td'); + td_rent_nw.textContent = fmt_eur(result.renter_nw_arr[i]); + td_rent_nw.classList.add('has-tip'); + td_rent_nw.addEventListener('mouseenter', (e) => show_tooltip(e, [ + tip_row('ETF-Portfolio (nach Steuer)', result.renter_nw_arr[i], '#2dd4bf'), + ].join('
'))); + + const td_buy_cost = document.createElement('td'); + td_buy_cost.textContent = fmt_eur(result.cum_buy_arr[i]); + td_buy_cost.classList.add('has-tip'); + td_buy_cost.addEventListener('mouseenter', (e) => show_tooltip(e, [ + `dieses Jahr`, + tip_row('Zinsen', d.yr_interest, '#f59e0b'), + tip_row('Tilgung', d.yr_principal, '#f59e0b'), + tip_row('Nebenkosten', d.yr_upkeep, '#f59e0b'), + ].join('
'))); + + const td_rent_cost = document.createElement('td'); + td_rent_cost.textContent = fmt_eur(result.cum_rent_arr[i]); + + tr.appendChild(document.createElement('td')).textContent = result.labels[i]; + tr.appendChild(td_buy_nw); + tr.appendChild(td_rent_nw); + tr.appendChild(td_buy_cost); + tr.appendChild(td_rent_cost); tbody.appendChild(tr); } + + tbody.addEventListener('mousemove', move_tooltip); + tbody.addEventListener('mouseleave', hide_tooltip); }; // ===== Result cards =====