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

18
calc.js
View File

@@ -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,

View File

@@ -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 ===== */

83
ui.js
View File

@@ -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) =>
`<span style="color:${color || '#8b949e'}">${label}</span> <b>${fmt_eur(value)}</b>`;
// ===== 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 = `
<td>${result.labels[i]}</td>
<td>${fmt_eur(result.buyer_nw_arr[i])}</td>
<td>${fmt_eur(result.renter_nw_arr[i])}</td>
<td>${fmt_eur(result.cum_buy_arr[i])}</td>
<td>${fmt_eur(result.cum_rent_arr[i])}</td>
`;
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('<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.addEventListener('mousemove', move_tooltip);
tbody.addEventListener('mouseleave', hide_tooltip);
};
// ===== Result cards =====