forskjeller.naiv.no/public/bolig/app.js
Ole-Morten Duesund 2615e96042 Knytt alle bannerverdier til sliderne i lonn, sparing og bolig
Hardkodede tall i bannerene (lønnsforskjell, sparebeløp,
prosentpoeng, prisforskjell, vekst, antall år) oppdateres nå
dynamisk når brukerne endrer sliderne.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:18:28 +01:00

179 lines
6.8 KiB
JavaScript

// Formateringshjelpere — norsk locale
function fmtKr(n) {
return Math.round(n).toLocaleString('nb-NO') + '\u202fkr';
}
function fmtShort(n) {
if (n >= 1e6) return (n / 1e6).toFixed(1).replace('.', ',') + '\u202fM';
if (n >= 1e3) return Math.round(n / 1e3) + '\u202fk';
return Math.round(n);
}
function getData() {
const pA = +document.getElementById('priceA').value;
const pB = +document.getElementById('priceB').value;
const g = +document.getElementById('growth').value / 100;
const Y = +document.getElementById('yrs').value;
// Lån = 85 % av kjøpspris (forenklet avdragsfritt)
const loanA = pA * 0.85;
const loanB = pB * 0.85;
const labels = [], valA = [], valB = [], valGaps = [];
const eqA = [], eqB = [], eqGaps = [];
for (let y = 0; y <= Y; y++) {
labels.push(y === 0 ? 'I dag' : (y % 5 === 0 || Y <= 10 || y === Y) ? 'År ' + y : '');
const a = pA * Math.pow(1 + g, y);
const b = pB * Math.pow(1 + g, y);
valA.push(a);
valB.push(b);
valGaps.push(b - a);
eqA.push(a - loanA);
eqB.push(b - loanB);
eqGaps.push((b - loanB) - (a - loanA));
}
return { pA, pB, g, Y, loanA, loanB, labels, valA, valB, valGaps, eqA, eqB, eqGaps };
}
const GRID_COLOR = 'rgba(0,0,0,0.06)';
const TICK_COLOR = '#8a857e';
function baseOpts() {
return {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: { label: c => ' ' + fmtKr(c.parsed.y) },
backgroundColor: '#1a1714',
titleColor: '#f5f2eb',
bodyColor: 'rgba(245,242,235,0.7)',
padding: 10,
cornerRadius: 4,
titleFont: { family: 'DM Sans', size: 12 },
bodyFont: { family: 'DM Sans', size: 12 }
}
},
scales: {
x: {
ticks: { color: TICK_COLOR, font: { size: 11, family: 'DM Sans' }, autoSkip: false, maxRotation: 0 },
grid: { display: false },
border: { color: 'rgba(0,0,0,0.1)' }
},
y: {
ticks: { color: TICK_COLOR, font: { size: 11, family: 'DM Sans' }, callback: v => fmtShort(v) },
grid: { color: GRID_COLOR },
border: { dash: [3, 3], color: 'transparent' }
}
}
};
}
// Diagram 1: Boligverdi over tid
const chart1 = new Chart(document.getElementById('chart1'), {
type: 'line',
data: { labels: [], datasets: [
{ data: [], borderColor: '#1a4a8a', backgroundColor: 'rgba(26,74,138,0.07)', fill: true, tension: 0.35, pointRadius: 0, borderWidth: 2 },
{ data: [], borderColor: '#c0392b', backgroundColor: 'rgba(192,57,43,0.07)', fill: true, tension: 0.35, pointRadius: 0, borderWidth: 2 }
]},
options: baseOpts()
});
// Diagram 2: Verdidifferanse
const chart2 = new Chart(document.getElementById('chart2'), {
type: 'line',
data: { labels: [], datasets: [
{ data: [], borderColor: '#4a3a8a', backgroundColor: 'rgba(74,58,138,0.1)', fill: true, tension: 0.35, pointRadius: 0, borderWidth: 2 }
]},
options: baseOpts()
});
// Diagram 3: Egenkapitalvekst (A, B, gap)
const chart3 = new Chart(document.getElementById('chart3'), {
type: 'line',
data: { labels: [], datasets: [
{ data: [], borderColor: '#1a4a8a', backgroundColor: 'rgba(26,74,138,0.07)', fill: true, tension: 0.35, pointRadius: 0, borderWidth: 2 },
{ data: [], borderColor: '#c0392b', backgroundColor: 'rgba(192,57,43,0.07)', fill: true, tension: 0.35, pointRadius: 0, borderWidth: 2 },
{ data: [], borderColor: '#4a3a8a', backgroundColor: 'rgba(74,58,138,0.1)', fill: true, tension: 0.35, pointRadius: 0, borderWidth: 2, borderDash: [6, 4] }
]},
options: baseOpts()
});
// SIKKERHETSNOTAT: Alle verdier er beregnet fra numeriske slider-verdier.
// Ingen bruker-tekstinput brukes i DOM-oppdateringer.
function update() {
const { pA, pB, g, Y, loanA, loanB, labels, valA, valB, valGaps, eqA, eqB, eqGaps } = getData();
// Slider-visning
document.querySelectorAll('.yr-lbl').forEach(el => el.textContent = Y);
document.getElementById('priceA-out').textContent = Math.round(pA).toLocaleString('nb-NO') + '\u202fkr';
document.getElementById('priceB-out').textContent = Math.round(pB).toLocaleString('nb-NO') + '\u202fkr';
document.getElementById('growth-out').textContent = (g * 100).toLocaleString('nb-NO', { minimumFractionDigits: 1 }) + '\u202f%';
document.getElementById('yrs-out').textContent = Y + '\u202får';
// Statistikkort
document.getElementById('st-valA').textContent = fmtKr(valA[Y]);
document.getElementById('st-subA').textContent = 'EK: ' + fmtKr(eqA[Y]);
document.getElementById('st-valB').textContent = fmtKr(valB[Y]);
document.getElementById('st-subB').textContent = 'EK: ' + fmtKr(eqB[Y]);
document.getElementById('st-valdiff').textContent = fmtKr(valGaps[Y]);
document.getElementById('st-valdiff-sub').textContent = 'var ' + fmtKr(pB - pA) + ' ved start';
document.getElementById('st-eqdiff').textContent = fmtKr(eqGaps[Y]);
const startEqGap = (pB * 0.15) - (pA * 0.15);
document.getElementById('st-eqdiff-sub').textContent = 'var ' + fmtKr(startEqGap) + ' ved start';
// Banner
const priceDiff = pB - pA;
if (priceDiff >= 1e6) {
document.getElementById('banner-diff').textContent = (priceDiff / 1e6).toLocaleString('nb-NO', { maximumFractionDigits: 1 }) + ' million' + (priceDiff >= 2e6 ? 'er' : '');
} else {
document.getElementById('banner-diff').textContent = fmtKr(priceDiff);
}
document.getElementById('banner-vekst').textContent = (g * 100).toLocaleString('nb-NO', { maximumFractionDigits: 1 });
document.getElementById('banner-aar').textContent = Y;
document.getElementById('banner-gap').textContent = fmtKr(eqGaps[Y]);
// Innsikt — bruker DOM-metoder for sikkerhet
const insightEl = document.getElementById('insight');
insightEl.textContent = '';
const growthPct = (g * 100).toLocaleString('nb-NO', { minimumFractionDigits: 1 });
const parts = [
{ text: 'Begge boligene steg ' + growthPct + ' % i året. Men B-eieren har ' },
{ text: fmtKr(eqGaps[Y]), bold: true },
{ text: ' mer i egenkapital — fordi gjeldsgraden forsterker forskjellen.' }
];
parts.forEach(p => {
if (p.bold) {
const strong = document.createElement('strong');
strong.textContent = p.text;
insightEl.appendChild(strong);
} else {
insightEl.appendChild(document.createTextNode(p.text));
}
});
// Diagram 1: Boligverdi
chart1.data.labels = labels;
chart1.data.datasets[0].data = valA;
chart1.data.datasets[1].data = valB;
chart1.update('none');
// Diagram 2: Verdidifferanse
chart2.data.labels = labels;
chart2.data.datasets[0].data = valGaps;
chart2.update('none');
// Diagram 3: Egenkapital
chart3.data.labels = labels;
chart3.data.datasets[0].data = eqA;
chart3.data.datasets[1].data = eqB;
chart3.data.datasets[2].data = eqGaps;
chart3.update('none');
}
['priceA', 'priceB', 'growth', 'yrs'].forEach(id =>
document.getElementById(id).addEventListener('input', update)
);
update();