forskjeller.naiv.no/public/lonnsutvikling/app.js
Ole-Morten Duesund dd8eb4042f Ny visualisering: lønnsutvikling basert på SSB-data 2016–2025
Datadriven side (ingen slidere) som viser dobbel ulikhet i norsk
lønnsutvikling: høytlønte fikk både flere kroner OG høyere
prosentvekst. Data fra SSB tabell 11418 via PxWeb API v2.

Inneholder tre diagrammer: månedslønn over tid, kronevekst vs
prosentvekst (dobbel akse), og alle STYRK-hovedyrkesgrupper.

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

219 lines
7.8 KiB
JavaScript

// SSB tabell 11418: Gjennomsnittlig månedslønn (kr)
// Alle sektorer, begge kjønn, i alt (heltid+deltid)
// Hentet 2026-03-16 via SSB PxWeb API v2
const YEARS = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'];
// Utvalgte yrker for hovedvisualiseringen
const OCCUPATIONS = [
{ name: 'Renholdere i bedrifter', data: [30680, 31370, 32200, 33270, 33570, 34850, 36360, 38620, 40650, 42350], color: '#e74c3c' },
{ name: 'Butikkmedarbeidere', data: [30140, 30870, 31790, 32790, 33760, 35000, 36400, 38100, 39910, 41570], color: '#e67e22' },
{ name: 'Barnehageassistenter', data: [30230, 30960, 31650, 32800, 32980, 34260, 35050, 37710, 39660, 40970], color: '#f39c12' },
{ name: 'Kokker', data: [31820, 32240, 33260, 34580, 35270, 36530, 37990, 39930, 41920, 43160], color: '#d4a017' },
{ name: 'Sykepleiere', data: [42130, 43320, 44730, 46690, 47020, 49440, 51020, 53880, 56710, 58290], color: '#27ae60' },
{ name: 'Systemanalytikere', data: [58140, 59630, 61340, 63590, 64520, 67530, 69940, 73590, 77040, 80120], color: '#2980b9' },
{ name: 'Leger (allmennpraktiserende)', data: [65450, 67880, 69550, 71750, 72730, 75650, 78890, 84690, 88650, 92260], color: '#8e44ad' },
{ name: 'Adm. direktører', data: [74140, 76530, 78490, 80980, 83030, 87240, 93240, 96820,102610,108510], color: '#1a1714' },
];
// Hovedyrkesgrupper (STYRK 1-siffernivå)
const GROUPS = [
{ name: 'Renholdere mv.', data: [31080, 31790, 32790, 33890, 34640, 35860, 37560, 39880, 42060, 43700], color: '#e74c3c' },
{ name: 'Salgs- og serviceyrker', data: [32580, 33350, 34240, 35400, 36080, 37370, 38760, 41090, 43190, 44790], color: '#e67e22' },
{ name: 'Bønder, fiskere mv.', data: [32350, 33580, 34750, 35850, 36920, 38220, 40220, 42720, 45440, 48000], color: '#d4a017' },
{ name: 'Kontoryrker', data: [36770, 37680, 38810, 40080, 40990, 42610, 44610, 47120, 49790, 52110], color: '#27ae60' },
{ name: 'Håndverkere', data: [36570, 37490, 38440, 39810, 40640, 41970, 43930, 46420, 49170, 51390], color: '#2c6e49' },
{ name: 'Operatører, transport mv.',data: [37400, 38090, 39210, 40560, 41280, 42690, 44680, 47040, 49720, 52080], color: '#2980b9' },
{ name: 'Akademiske yrker', data: [49740, 51040, 52550, 54420, 55090, 57670, 59980, 63750, 66850, 69650], color: '#8e44ad' },
{ name: 'Ledere', data: [64020, 65600, 67680, 70100, 71270, 74760, 78330, 82300, 86310, 90510], color: '#1a1714' },
];
function fmtKr(n) {
return Math.round(n).toLocaleString('nb-NO') + '\u202fkr';
}
function fmtShort(n) {
if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(1).replace('.', ',') + '\u202fM';
if (Math.abs(n) >= 1e3) return Math.round(n / 1e3).toLocaleString('nb-NO') + '\u202fk';
return Math.round(n).toString();
}
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 => ' ' + c.dataset.label + ': ' + 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' }, 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: Månedslønn over tid (utvalgte yrker) ---
function buildLegend(containerId, items) {
const container = document.getElementById(containerId);
items.forEach(item => {
const span = document.createElement('span');
const swatch = document.createElement('span');
swatch.className = 'swatch';
swatch.style.background = item.color;
span.appendChild(swatch);
span.appendChild(document.createTextNode(item.name));
container.appendChild(span);
});
}
const chart1Occupations = OCCUPATIONS.filter(o =>
['Renholdere i bedrifter', 'Sykepleiere', 'Systemanalytikere', 'Adm. direktører'].includes(o.name)
);
buildLegend('legend1', chart1Occupations);
new Chart(document.getElementById('chart1'), {
type: 'line',
data: {
labels: YEARS,
datasets: chart1Occupations.map(o => ({
label: o.name,
data: o.data,
borderColor: o.color,
backgroundColor: o.color + '12',
fill: false,
tension: 0.3,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2.5
}))
},
options: baseOpts()
});
// --- Diagram 2: Kronevekst OG prosentvekst (dobbel akse) ---
const barData = OCCUPATIONS.map(o => {
const start = o.data[0];
const end = o.data[o.data.length - 1];
return {
name: o.name,
krGrowth: end - start,
pctGrowth: ((end / start) - 1) * 100,
color: o.color
};
}).sort((a, b) => a.krGrowth - b.krGrowth);
const chart2Opts = baseOpts();
chart2Opts.indexAxis = 'y';
chart2Opts.scales = {
x: {
position: 'bottom',
ticks: { color: TICK_COLOR, font: { size: 11, family: 'DM Sans' }, callback: v => fmtShort(v) },
grid: { color: GRID_COLOR },
border: { dash: [3, 3], color: 'transparent' },
title: { display: true, text: 'Kronevekst (kr/mnd)', font: { size: 11, family: 'DM Sans' }, color: TICK_COLOR }
},
x2: {
position: 'top',
ticks: { color: '#c0392b', font: { size: 11, family: 'DM Sans' }, callback: v => v + ' %' },
grid: { display: false },
border: { color: 'transparent' },
title: { display: true, text: 'Prosentvekst', font: { size: 11, family: 'DM Sans' }, color: '#c0392b' }
},
y: {
ticks: { color: TICK_COLOR, font: { size: 11, family: 'DM Sans' }, autoSkip: false },
grid: { display: false },
border: { color: 'rgba(0,0,0,0.1)' }
}
};
chart2Opts.plugins.tooltip = {
callbacks: {
label: c => {
const item = barData[c.dataIndex];
if (c.dataset.xAxisID === 'x2') {
return ' Prosentvekst: ' + item.pctGrowth.toFixed(1).replace('.', ',') + ' %';
}
return ' Kronevekst: +' + fmtKr(item.krGrowth) + '/mnd';
}
},
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 }
};
new Chart(document.getElementById('chart2'), {
type: 'bar',
data: {
labels: barData.map(d => d.name),
datasets: [
{
label: 'Kronevekst (kr/mnd)',
data: barData.map(d => d.krGrowth),
backgroundColor: barData.map(d => d.color + 'cc'),
borderColor: barData.map(d => d.color),
borderWidth: 1,
borderRadius: 3,
xAxisID: 'x'
},
{
label: 'Prosentvekst',
data: barData.map(d => d.pctGrowth),
backgroundColor: '#c0392b33',
borderColor: '#c0392b',
borderWidth: 2,
borderRadius: 3,
xAxisID: 'x2'
}
]
},
options: chart2Opts
});
// --- Diagram 3: Alle yrkesgrupper over tid ---
buildLegend('legend3', GROUPS);
new Chart(document.getElementById('chart3'), {
type: 'line',
data: {
labels: YEARS,
datasets: GROUPS.map(g => ({
label: g.name,
data: g.data,
borderColor: g.color,
backgroundColor: g.color + '12',
fill: false,
tension: 0.3,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2
}))
},
options: baseOpts()
});