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 A0 = +document.getElementById('salA').value; const B0 = +document.getElementById('salB').value; const pct = +document.getElementById('pct').value / 100; const Y = +document.getElementById('yrs').value; const labels=[], sA=[], sB=[], gaps=[], monthly=[], cumA=[], cumB=[], cumGaps=[]; let rA=0, rB=0; for (let y=0; y<=Y; y++) { labels.push(y===0 ? 'I dag' : (y % 5 === 0 || Y <= 10) ? 'År '+y : (y===Y ? 'År '+y : '')); const a = A0 * Math.pow(1+pct, y); const b = B0 * Math.pow(1+pct, y); sA.push(a); sB.push(b); gaps.push(b-a); monthly.push((b-a)/12); rA += a; rB += b; cumA.push(rA); cumB.push(rB); cumGaps.push(rB-rA); } return {A0,B0,pct,Y,labels,sA,sB,gaps,monthly,cumA,cumB,cumGaps}; } 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' } } } }; } 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() }); 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() }); const chart3 = new Chart(document.getElementById('chart3'), { type: 'line', data: { labels:[], datasets:[ { data:[], borderColor:'#2c6e49', backgroundColor:'rgba(44,110,73,0.1)', fill:true, tension:0.35, pointRadius:0, borderWidth:2 } ]}, options: baseOpts() }); const chart4 = new Chart(document.getElementById('chart4'), { 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:'#b5620a', backgroundColor:'rgba(181,98,10,0.1)', fill:true, tension:0.35, pointRadius:0, borderWidth:2, borderDash:[6,4] } ]}, options: baseOpts() }); // Alle verdier som settes via innerHTML kommer fra numeriske beregninger (slider-verdier), // ikke fra brukerinput-tekst — ingen XSS-risiko. function update() { const {A0,B0,pct,Y,labels,sA,sB,gaps,monthly,cumA,cumB,cumGaps} = getData(); // Oppdater visning document.querySelectorAll('.yr-lbl').forEach(el => el.textContent = Y); document.getElementById('salA-out').textContent = Math.round(A0).toLocaleString('nb-NO') + '\u202fkr'; document.getElementById('salB-out').textContent = Math.round(B0).toLocaleString('nb-NO') + '\u202fkr'; document.getElementById('pct-out').textContent = (+document.getElementById('pct').value).toLocaleString('nb-NO', {minimumFractionDigits:1}) + '\u202f%'; document.getElementById('yrs-out').textContent = Y + '\u202får'; // Statistikk document.getElementById('st-finalA').textContent = fmtKr(sA[Y]); document.getElementById('st-monthA').textContent = fmtKr(sA[Y]/12) + '/mnd'; document.getElementById('st-subA').textContent = '+' + fmtKr(sA[Y]-A0) + ' fra start'; document.getElementById('st-finalB').textContent = fmtKr(sB[Y]); document.getElementById('st-monthB').textContent = fmtKr(sB[Y]/12) + '/mnd'; document.getElementById('st-subB').textContent = '+' + fmtKr(sB[Y]-B0) + ' fra start'; document.getElementById('st-diffyr').textContent = fmtKr(gaps[Y]) + '/år'; document.getElementById('st-diffmnd').textContent = fmtKr(monthly[Y]) + '/mnd'; document.getElementById('st-diffstart').textContent = 'Var ' + fmtKr(B0-A0) + '/år ved start'; document.getElementById('st-cumgap').textContent = fmtKr(cumGaps[Y]); // Banner document.getElementById('banner-gap').textContent = fmtKr(cumGaps[Y]); // Innsikt — innerHTML er trygt her: verdiene er kun formaterte tall fra slidere const monthlyNow = Math.round((B0-A0)/12); const monthlyEnd = Math.round(monthly[Y]); document.getElementById('insight-monthly').innerHTML = `Månedlig gap i dag: ${fmtKr(monthlyNow)} ekstra til B. `+ `Etter ${Y} år er det ${fmtKr(monthlyEnd)} per måned — `+ `en økning på ${fmtKr(monthlyEnd - monthlyNow)} kun pga. prosentvekst.`; // Diagrammer chart1.data.labels = labels; chart1.data.datasets[0].data = sA; chart1.data.datasets[1].data = sB; chart1.update('none'); chart2.data.labels = labels; chart2.data.datasets[0].data = gaps; chart2.update('none'); chart3.data.labels = labels; chart3.data.datasets[0].data = monthly; chart3.update('none'); chart4.data.labels = labels; chart4.data.datasets[0].data = cumA; chart4.data.datasets[1].data = cumB; chart4.data.datasets[2].data = cumGaps; chart4.update('none'); } ['salA','salB','pct','yrs'].forEach(id => document.getElementById(id).addEventListener('input', update) ); update();