hover tooltips on Übersicht table with breakdown details

This commit is contained in:
Johannes
2026-03-20 02:17:12 +01:00
parent 14ae4810f2
commit 1b7029681e
3 changed files with 95 additions and 8 deletions

16
calc.js
View File

@@ -44,8 +44,12 @@ function run_simulation(p) {
const renter_nw_arr = []; const renter_nw_arr = [];
const cum_buy_arr = []; const cum_buy_arr = [];
const cum_rent_arr = []; const cum_rent_arr = [];
const detail_arr = [];
const labels = []; const labels = [];
// yearly accumulators (reset each year)
let yr_interest = 0, yr_principal = 0, yr_upkeep = 0;
let breakeven_month = null; let breakeven_month = null;
const invest_r_m = p.invest_return / 100 / 12; const invest_r_m = p.invest_return / 100 / 12;
@@ -65,6 +69,9 @@ function run_simulation(p) {
// German context: no mortgage interest tax deduction for private buyers // German context: no mortgage interest tax deduction for private buyers
const total_buy_m = monthly_payment + prop_tax_m + maint_m + insurance_m; const total_buy_m = monthly_payment + prop_tax_m + maint_m + insurance_m;
yr_interest += interest_m;
yr_principal += principal_m;
yr_upkeep += prop_tax_m + maint_m + insurance_m;
cum_buy_cost += total_buy_m; cum_buy_cost += total_buy_m;
buyer_portfolio *= (1 + invest_r_m); buyer_portfolio *= (1 + invest_r_m);
@@ -103,6 +110,14 @@ function run_simulation(p) {
renter_nw_arr.push(Math.round(portfolio - tax_on_gains)); renter_nw_arr.push(Math.round(portfolio - tax_on_gains));
cum_buy_arr.push(Math.round(cum_buy_cost)); cum_buy_arr.push(Math.round(cum_buy_cost));
cum_rent_arr.push(Math.round(cum_rent_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, renter_nw_arr,
cum_buy_arr, cum_buy_arr,
cum_rent_arr, cum_rent_arr,
detail_arr,
monthly_payment: Math.round(monthly_payment), monthly_payment: Math.round(monthly_payment),
final_buyer_nw, final_buyer_nw,
final_renter_nw, final_renter_nw,

View File

@@ -409,6 +409,8 @@ th {
tbody tr:hover { background: rgba(255,255,255,0.03); } 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; } tr.winner-row td { font-weight: 600; }
/* ===== Footer ===== */ /* ===== Footer ===== */

83
ui.js
View File

@@ -140,22 +140,91 @@ const update_chart = (chart, labels, data_buy, data_rent, breakeven_month, years
chart.update('none'); 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) =>
`<span style="color:${color || '#8b949e'}">${label}</span> <b>${fmt_eur(value)}</b>`;
// ===== Table ===== // ===== Table =====
const build_table = (result) => { const build_table = (result) => {
const tbody = document.getElementById('table_body'); const tbody = document.getElementById('table_body');
tbody.innerHTML = ''; tbody.innerHTML = '';
for (let i = 0; i < result.labels.length; i++) { for (let i = 0; i < result.labels.length; i++) {
const d = result.detail_arr[i];
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.innerHTML = `
<td>${result.labels[i]}</td> const td_buy_nw = document.createElement('td');
<td>${fmt_eur(result.buyer_nw_arr[i])}</td> td_buy_nw.textContent = fmt_eur(result.buyer_nw_arr[i]);
<td>${fmt_eur(result.renter_nw_arr[i])}</td> td_buy_nw.classList.add('has-tip');
<td>${fmt_eur(result.cum_buy_arr[i])}</td> td_buy_nw.addEventListener('mouseenter', (e) => show_tooltip(e, [
<td>${fmt_eur(result.cum_rent_arr[i])}</td> tip_row('Immobilienwert', d.house_value, '#f59e0b'),
`; tip_row('ETF-Portfolio', d.buyer_portfolio, '#f59e0b'),
].join('<br>')));
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('<br>')));
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, [
`<span style="color:#8b949e;font-size:0.72rem;text-transform:uppercase;letter-spacing:.05em">dieses Jahr</span>`,
tip_row('Zinsen', d.yr_interest, '#f59e0b'),
tip_row('Tilgung', d.yr_principal, '#f59e0b'),
tip_row('Nebenkosten', d.yr_upkeep, '#f59e0b'),
].join('<br>')));
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.appendChild(tr);
} }
tbody.addEventListener('mousemove', move_tooltip);
tbody.addEventListener('mouseleave', hide_tooltip);
}; };
// ===== Result cards ===== // ===== Result cards =====