From dd8eb4042f45633016b0d16373ad15ceb155c59d Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 16 Mar 2026 19:25:54 +0100 Subject: [PATCH] =?UTF-8?q?Ny=20visualisering:=20l=C3=B8nnsutvikling=20bas?= =?UTF-8?q?ert=20p=C3=A5=20SSB-data=202016=E2=80=932025?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 1 + TODO.md | 3 + public/index.html | 6 + public/lonnsutvikling/app.js | 219 +++++++++++++++++++++++++++++++ public/lonnsutvikling/index.html | 212 ++++++++++++++++++++++++++++++ 5 files changed, 441 insertions(+) create mode 100644 public/lonnsutvikling/app.js create mode 100644 public/lonnsutvikling/index.html diff --git a/CLAUDE.md b/CLAUDE.md index 7ac6b9b..de4e0a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,7 @@ Each calculator directory contains two files following the same pattern: - **`public/sparing/`** — Compound interest: same savings, different returns. Model: `FV = P × ((1+r)^n - 1) / r` (monthly compounding). Stacked bar chart for deposits vs interest. - **`public/bolig/`** — Housing leverage: same price growth, different starting prices. 85% LTV amplifies equity gap ~6.67×. Model: `Equity = Price × (1+g)^n - Loan` - **`public/arv/`** — Inheritance gap: lump sum + equal savings, gap never closes. Model: `Gap = Arv × (1+r)^n`. Has percentage y-axis chart. +- **`public/lonnsutvikling/`** — Data-driven visualization (not a calculator): real SSB wage data showing how equal percentage growth translates to vastly different kroner amounts across occupations. No sliders — hard-coded SSB table 11418 data (2016–2025). **External dependencies** (loaded via CDN, no install needed): Chart.js 4.4.1, Google Fonts (Fraunces + DM Sans). diff --git a/TODO.md b/TODO.md index 4e9fa89..556bf0c 100644 --- a/TODO.md +++ b/TODO.md @@ -3,3 +3,6 @@ ## Nye kalkulatorer - [ ] **BNP per innbygger (internasjonale forskjeller)** — Ny kalkulator i `public/verden/` som viser hvordan forskjeller i BNP per innbygger mellom land vokser over tid med prosentvis vekst. Samme prosentvise vekst gir stadig større absolutt gap mellom rike og fattige land. +- [x] **Lønnsutvikling: prosent vs. kroner** — Visualisering i `public/lonnsutvikling/` basert på reell SSB-statistikk. Viser dobbel ulikhet: høytlønte fikk både flere kroner OG høyere prosentvekst. Data fra SSB tabell 11418, 2016–2025. +- [ ] **Bankens andel av boligen** — Visualisering som viser hvor mye av boligen du faktisk eier vs. bankens andel over tid, gitt ulik egenkapital og nedbetalingsplan. +- [ ] **Store formuer i Norge — arv vs. selvbygd** — Analyse/visualisering av hvordan store formuer i Norge har utviklet seg, og hvor stor andel som er arvet. Krever research og dataarbeid. diff --git a/public/index.html b/public/index.html index 7d7d214..6b6689c 100644 --- a/public/index.html +++ b/public/index.html @@ -175,6 +175,12 @@ Se kalkulatoren → + +

Dobbel ulikhet i lønn

+

Direktørene fikk 3× flere kroner i lønnsvekst enn renholdere — og høyere prosentvekst. Dobbel ulikhet i praksis. Reelle SSB-tall, 2016–2025.

+ Se visualiseringen → +
+ diff --git a/public/lonnsutvikling/app.js b/public/lonnsutvikling/app.js new file mode 100644 index 0000000..aa5b8f6 --- /dev/null +++ b/public/lonnsutvikling/app.js @@ -0,0 +1,219 @@ +// 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() +}); diff --git a/public/lonnsutvikling/index.html b/public/lonnsutvikling/index.html new file mode 100644 index 0000000..cb6cb09 --- /dev/null +++ b/public/lonnsutvikling/index.html @@ -0,0 +1,212 @@ + + + + + +Lønnsutvikling: prosent vs. kroner — SSB-data 2016–2025 + + + + + + + + + + + + + + + + + +
+

Reelle tall fra SSB, 2016–2025

+

Dobbel
ulikhet

+

De høyest lønte fikk ikke bare flere kroner — de fikk også høyere prosentvis vekst. Dobbel ulikhet i praksis.

+
Lønnsfesten
+
+ +
+ Data: SSB tabell 11418 — Gjennomsnittlig månedslønn, alle sektorer, begge kjønn, 2016–2025 +
+ +
+ + +
+
3× kronene, 10 pp mer
+
Administrerende direktører fikk tre ganger så mange kroner i lønnsvekst som renholdere — og 10 prosentpoeng høyere prosentvekst (46 % mot 36 %).
+
+ + +
+
+
Renholdere
+
30 680 → 42 350 kr/mnd
+
+11 670 kr
+
38,0 % vekst
+
+
+
Sykepleiere
+
42 130 → 58 290 kr/mnd
+
+16 160 kr
+
38,4 % vekst
+
+
+
Adm. direktører
+
74 140 → 108 510 kr/mnd
+
+34 370 kr
+
46,4 % vekst
+
+
+ + +
+
+
+
Månedslønn over tid
+
Gjennomsnittlig månedslønn for utvalgte yrker, 2016–2025
+
+
+
+
+
+ +
+ Avstanden mellom linjene øker for hvert år — og det skyldes ikke bare kroneverdien. + De høyest lønte fikk også høyere prosentvis vekst: direktører fikk 46 %, mens barnehageassistenter og kokker fikk rundt 35–36 %. + Forskjellen mellom en renholdsarbeider og en direktør vokste fra 43 460 til 66 160 kr/mnd. +
+ + +
+
+
+
Dobbel ulikhet
+
Høytlønte fikk både flere kroner og høyere prosent
+
+
+ Kronevekst + Prosentvekst +
+
+
+
+ +
+ Prosentveksten varierer fra 36 % (barnehageassistenter) til 46 % (direktører). + I kroner betyr det fra 10 740 kr til 34 370 kr ekstra per måned — en tredobling. + Høytlønte fikk altså både høyere prosent og flere kroner. +
+ + +
+
+
+
Alle hovedyrkesgrupper
+
Gjennomsnittlig månedslønn etter STYRK-yrkesgruppe, 2016–2025
+
+
+
+
+
+ + +
+

Hvorfor er dette viktig?

+

Lønnsoppgjør diskuteres ofte i prosent: «alle fikk 5 % økning». Det høres rettferdig ut. Men 5 % av 30 000 kr er 1 500 kr, mens 5 % av 100 000 kr er 5 000 kr.

+
+ 5 % × 30 000 = 1 500 kr
+ 5 % × 100 000 = 5 000 kr
+ Forskjell: 3 500 kr ekstra til den som tjener mest — hvert eneste år +
+

SSB-tallene viser at dette problemet forsterkes ytterligere: de høyest lønte fikk ikke bare flere kroner, men også høyere prosentvekst. Direktører fikk 46 % lønnsvekst, mens barnehageassistenter fikk 36 % — en forskjell på 10 prosentpoeng i tillegg til kronegapet.

+

Derfor har fagbevegelsen historisk kjempet for kronetillegg i stedet for prosenttillegg — et fast beløp til alle, uavhengig av lønnsnivå.

+

Dataene på denne siden er hentet fra SSB tabell 11418 (gjennomsnittlig månedslønn, alle sektorer, begge kjønn). Tallene er ikke justert for inflasjon.

+
+ +
+ + + + + +