forskjeller.naiv.no/public/arv/app.js
Ole-Morten Duesund 781f1075ba Knytt bannerverdier i arvekalkulator til sliderne
Arvebeløp og antall år i banneret var hardkodet — nå oppdateres
de dynamisk når brukeren endrer sliderne.

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

197 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}
// Fremtidig verdi av annuitet med månedlig compounding
function futureValue(P, annualRate, years) {
const n = years * 12;
if (annualRate === 0) return P * n;
const r = annualRate / 12;
return P * (Math.pow(1 + r, n) - 1) / r;
}
function getData() {
const inheritance = +document.getElementById('inheritance').value;
const P = +document.getElementById('monthly').value;
const rate = +document.getElementById('rate').value / 100;
const Y = +document.getElementById('yrs').value;
const labels = [], wealthA = [], wealthB = [], gaps = [], arvPct = [];
for (let y = 0; y <= Y; y++) {
labels.push(y === 0 ? 'I dag' : (y % 5 === 0 || Y <= 10 || y === Y) ? 'År ' + y : '');
// A = ren annuitet (starter fra null)
const a = futureValue(P, rate, y);
// B = arv med rentes rente + samme annuitet
const arvGrown = inheritance * Math.pow(1 + rate, y);
const b = arvGrown + futureValue(P, rate, y);
// Gap = Arv × (1+r)^n — vokser alltid
const gap = arvGrown;
wealthA.push(a);
wealthB.push(b);
gaps.push(gap);
// Arvens andel av Bs formue (prosent)
arvPct.push(b > 0 ? (arvGrown / b) * 100 : 0);
}
return { inheritance, P, rate, Y, labels, wealthA, wealthB, gaps, arvPct };
}
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' }
}
}
};
}
// Tilpasset y-akse for prosentdiagram
function pctOpts() {
const opts = baseOpts();
opts.scales.y.min = 0;
opts.scales.y.max = 100;
opts.scales.y.ticks = {
color: TICK_COLOR,
font: { size: 11, family: 'DM Sans' },
callback: v => v + ' %'
};
opts.plugins.tooltip.callbacks = {
label: c => ' ' + c.parsed.y.toFixed(1) + ' %'
};
return opts;
}
// Diagram 1: Formue 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: Absolutt forskjell (gap)
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: Arvens andel av Bs formue (prosent — tilpasset y-akse)
const chart3 = new Chart(document.getElementById('chart3'), {
type: 'line',
data: { labels: [], datasets: [
{ data: [], borderColor: '#b5620a', backgroundColor: 'rgba(181,98,10,0.1)', fill: true, tension: 0.35, pointRadius: 0, borderWidth: 2 }
]},
options: pctOpts()
});
// SIKKERHETSNOTAT: Alle verdier er beregnet fra numeriske slider-verdier.
// Ingen bruker-tekstinput brukes i DOM-oppdateringer.
function update() {
const { inheritance, P, rate, Y, labels, wealthA, wealthB, gaps, arvPct } = getData();
// Slider-visning
document.querySelectorAll('.yr-lbl').forEach(el => el.textContent = Y);
document.getElementById('inheritance-out').textContent = Math.round(inheritance).toLocaleString('nb-NO') + '\u202fkr';
document.getElementById('monthly-out').textContent = Math.round(P).toLocaleString('nb-NO') + '\u202fkr';
document.getElementById('rate-out').textContent = (rate * 100).toLocaleString('nb-NO', { minimumFractionDigits: 1 }) + '\u202f%';
document.getElementById('yrs-out').textContent = Y + '\u202får';
// Statistikkort
document.getElementById('st-valA').textContent = fmtKr(wealthA[Y]);
document.getElementById('st-valB').textContent = fmtKr(wealthB[Y]);
document.getElementById('st-subB').textContent = 'herav ' + fmtKr(gaps[Y]) + ' fra arv';
document.getElementById('st-gap').textContent = fmtKr(gaps[Y]);
const multiplier = (gaps[Y] / inheritance).toFixed(1).replace('.', ',');
document.getElementById('st-gap-sub').textContent = multiplier + '× opprinnelig arv';
document.getElementById('st-grown').textContent = fmtKr(gaps[Y]);
document.getElementById('st-grown-sub').textContent = 'fra ' + fmtKr(inheritance);
// Banner
document.getElementById('banner-arv').textContent = fmtKr(inheritance);
document.getElementById('banner-gap').textContent = fmtKr(gaps[Y]);
document.getElementById('banner-aar').textContent = Y;
// Innsikt — bruker DOM-metoder for sikkerhet
const insightEl = document.getElementById('insight');
insightEl.textContent = '';
const parts = [
{ text: 'A sparte like mye som B i ' + Y + ' år. Men arven på ' },
{ text: fmtKr(inheritance), bold: true },
{ text: ' har vokst til ' },
{ text: fmtKr(gaps[Y]), bold: true },
{ text: ' — og gapet kan aldri lukkes.' }
];
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: Formue
chart1.data.labels = labels;
chart1.data.datasets[0].data = wealthA;
chart1.data.datasets[1].data = wealthB;
chart1.update('none');
// Diagram 2: Absolutt gap
chart2.data.labels = labels;
chart2.data.datasets[0].data = gaps;
chart2.update('none');
// Diagram 3: Arvens prosentandel
chart3.data.labels = labels;
chart3.data.datasets[0].data = arvPct;
chart3.update('none');
}
['inheritance', 'monthly', 'rate', 'yrs'].forEach(id =>
document.getElementById(id).addEventListener('input', update)
);
update();