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>
219 lines
7.8 KiB
JavaScript
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()
|
|
});
|