// 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 // FV = P × ((1+r)^n - 1) / r der r = årsrate/12, n = år×12 // Spesialtilfelle: r=0 → FV = P × n 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 P = +document.getElementById('monthly').value; const rA = +document.getElementById('rateA').value / 100; const rB = +document.getElementById('rateB').value / 100; const Y = +document.getElementById('yrs').value; const labels = [], valA = [], valB = [], gaps = []; const deposits = [], interestA = [], interestB = []; for (let y = 0; y <= Y; y++) { labels.push(y === 0 ? 'I dag' : (y % 5 === 0 || Y <= 10 || y === Y) ? 'År ' + y : ''); const a = futureValue(P, rA, y); const b = futureValue(P, rB, y); const dep = P * 12 * y; valA.push(a); valB.push(b); gaps.push(b - a); deposits.push(dep); interestA.push(a - dep); interestB.push(b - dep); } return { P, rA, rB, Y, labels, valA, valB, gaps, deposits, interestA, interestB }; } 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: Formue over tid (to linjer) 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: Forskjell i formue (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: Stacked bar — innskudd + renter for A og B const chart3Opts = baseOpts(); chart3Opts.scales.x.stacked = true; chart3Opts.scales.y.stacked = true; const chart3 = new Chart(document.getElementById('chart3'), { type: 'bar', data: { labels: [], datasets: [ { label: 'Innskudd', data: [], backgroundColor: '#1a4a8a', stack: 'A' }, { label: 'Renter A', data: [], backgroundColor: 'rgba(26,74,138,0.35)', stack: 'A' }, { label: 'Innskudd', data: [], backgroundColor: '#c0392b', stack: 'B' }, { label: 'Renter B', data: [], backgroundColor: 'rgba(192,57,43,0.35)', stack: 'B' } ]}, options: chart3Opts }); // SIKKERHETSNOTAT: Alle verdier i innerHTML-kall er utelukkende beregnet fra // numeriske slider-verdier (range inputs). Ingen bruker-tekstinput brukes. function update() { const { P, rA, rB, Y, labels, valA, valB, gaps, deposits, interestA, interestB } = getData(); // Oppdater slider-visning document.querySelectorAll('.yr-lbl').forEach(el => el.textContent = Y); document.getElementById('monthly-out').textContent = Math.round(P).toLocaleString('nb-NO') + '\u202fkr'; document.getElementById('rateA-out').textContent = (rA * 100).toLocaleString('nb-NO', { minimumFractionDigits: 1 }) + '\u202f%'; document.getElementById('rateB-out').textContent = (rB * 100).toLocaleString('nb-NO', { minimumFractionDigits: 1 }) + '\u202f%'; document.getElementById('yrs-out').textContent = Y + '\u202får'; const totalPaid = P * 12 * Y; // Statistikkort document.getElementById('st-valA').textContent = fmtKr(valA[Y]); document.getElementById('st-subA').textContent = 'herav ' + fmtKr(interestA[Y]) + ' i renter'; document.getElementById('st-valB').textContent = fmtKr(valB[Y]); document.getElementById('st-subB').textContent = 'herav ' + fmtKr(interestB[Y]) + ' i renter'; document.getElementById('st-diff').textContent = fmtKr(gaps[Y]); const ppDiff = Math.abs(rB * 100 - rA * 100); document.getElementById('st-diff-sub').textContent = 'pga. ' + ppDiff.toLocaleString('nb-NO', { minimumFractionDigits: 1 }) + ' pp forskjell'; document.getElementById('st-paid').textContent = fmtKr(totalPaid); // Banner document.getElementById('banner-gap').textContent = fmtKr(gaps[Y]); // Innsikt const insightEl = document.getElementById('insight'); insightEl.textContent = ''; const parts = [ { text: 'Dere betalte inn like mye: ' }, { text: fmtKr(totalPaid), bold: true }, { text: '. Men B har ' }, { text: fmtKr(gaps[Y]), bold: true }, { text: ' mer — kun pga. ' + ppDiff.toLocaleString('nb-NO', { minimumFractionDigits: 1 }) + ' prosentpoeng høyere avkastning.' } ]; 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 over tid chart1.data.labels = labels; chart1.data.datasets[0].data = valA; chart1.data.datasets[1].data = valB; chart1.update('none'); // Diagram 2: Gap chart2.data.labels = labels; chart2.data.datasets[0].data = gaps; chart2.update('none'); // Diagram 3: Stacked bar — sample hvert 5. år for lesbarhet const barLabels = [], barDep = [], barIntA = [], barDepB = [], barIntB = []; for (let y = 0; y <= Y; y++) { if (y === 0 || y % 5 === 0 || y === Y) { barLabels.push(y === 0 ? 'I dag' : 'År ' + y); barDep.push(deposits[y]); barIntA.push(interestA[y]); barDepB.push(deposits[y]); barIntB.push(interestB[y]); } } chart3.data.labels = barLabels; chart3.data.datasets[0].data = barDep; chart3.data.datasets[1].data = barIntA; chart3.data.datasets[2].data = barDepB; chart3.data.datasets[3].data = barIntB; chart3.update('none'); } ['monthly', 'rateA', 'rateB', 'yrs'].forEach(id => document.getElementById(id).addEventListener('input', update) ); update();