159 lines
6.2 KiB
JavaScript
159 lines
6.2 KiB
JavaScript
|
|
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: <strong>${fmtKr(monthlyNow)}</strong> ekstra til B. `+
|
||
|
|
`Etter ${Y} år er det <strong>${fmtKr(monthlyEnd)}</strong> per måned — `+
|
||
|
|
`en økning på <strong>${fmtKr(monthlyEnd - monthlyNow)}</strong> 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();
|