monitoring(uk-payslip): simplify yearly receipt to earned-and-kept view

Replace the 7-stack "where total comp went" decomposition with a 3-stack
"what I actually earned" view: salary (gross), bonus (gross), and RSU
vest after band-aware tax (PAYE+NI withheld via sell-to-cover). Skips
income tax / NI / student loan / pension / RSU offset.

Bar height = real income kept across all components. RSU is net of tax
because it's withheld at source and never hits the bank account; salary
and bonus are gross because they're paid in full and taxes are deducted
elsewhere. This is the income-side view where tax is implicit, not the
deduction waterfall.

Per-year RSU after tax: 2020/21 £18k · 2021/22 £39k · 2022/23 £50k ·
2023/24 £26k · 2024/25 £71k · 2025/26 £73k.
This commit is contained in:
Viktor Barzin 2026-04-25 23:42:20 +00:00
parent a17304f735
commit b2a25775aa

View file

@ -2328,8 +2328,8 @@
},
{
"id": 16,
"title": "Yearly receipt — where total comp went per tax year",
"description": "One stacked bar per tax year. Bar height ≈ total comp (gross_pay + pension salary-sacrifice). Stacks: net pay (bank deposit), cash income tax, RSU tax (band-aware marginal: PAYE+NI), cash NI, student loan, pension via salary-sacrifice, RSU offset (Variant A only). Always shows all years — ignores the time picker.",
"title": "Yearly receipt — what I actually earned per tax year",
"description": "One stacked bar per tax year, showing pay components I keep: salary (gross), bonus (gross), and RSU vests AFTER band-aware tax (PAYE+NI withheld via sell-to-cover). Excludes deductions and taxes — this is the take-home view of earnings, not the gross-deductions waterfall. tax_year axis (text), unit GBP. Always shows all years — ignores the time picker.",
"type": "barchart",
"datasource": {
"type": "grafana-postgresql-datasource",
@ -2368,7 +2368,7 @@
{
"matcher": {
"id": "byName",
"options": "net_pay"
"options": "salary"
},
"properties": [
{
@ -2380,121 +2380,45 @@
},
{
"id": "displayName",
"value": "Net pay (bank deposit)"
"value": "Salary (gross)"
}
]
},
{
"matcher": {
"id": "byName",
"options": "cash_income_tax"
"options": "bonus"
},
"properties": [
{
"id": "color",
"value": {
"mode": "fixed",
"fixedColor": "#C4162A"
"fixedColor": "#FADE2A"
}
},
{
"id": "displayName",
"value": "Income Tax (cash)"
"value": "Bonus (gross)"
}
]
},
{
"matcher": {
"id": "byName",
"options": "rsu_tax_marginal"
"options": "rsu_after_tax"
},
"properties": [
{
"id": "color",
"value": {
"mode": "fixed",
"fixedColor": "#E0652E"
"fixedColor": "#3274D9"
}
},
{
"id": "displayName",
"value": "Tax on RSU vest (band-aware marginal)"
}
]
},
{
"matcher": {
"id": "byName",
"options": "cash_ni"
},
"properties": [
{
"id": "color",
"value": {
"mode": "fixed",
"fixedColor": "orange"
}
},
{
"id": "displayName",
"value": "National Insurance (cash)"
}
]
},
{
"matcher": {
"id": "byName",
"options": "student_loan"
},
"properties": [
{
"id": "color",
"value": {
"mode": "fixed",
"fixedColor": "#8B4513"
}
},
{
"id": "displayName",
"value": "Student Loan"
}
]
},
{
"matcher": {
"id": "byName",
"options": "pension_sacrifice"
},
"properties": [
{
"id": "color",
"value": {
"mode": "fixed",
"fixedColor": "#CE96D8"
}
},
{
"id": "displayName",
"value": "Pension (salary sacrifice)"
}
]
},
{
"matcher": {
"id": "byName",
"options": "rsu_offset"
},
"properties": [
{
"id": "color",
"value": {
"mode": "fixed",
"fixedColor": "#888888"
}
},
{
"id": "displayName",
"value": "RSU Offset (Variant A)"
"value": "RSU vest (after band-aware tax)"
}
]
}
@ -2532,7 +2456,7 @@
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH r AS (SELECT * FROM payslip_ingest.payslip), ani AS (SELECT *, COALESCE(SUM(gross_pay - COALESCE(pension_sacrifice, 0)) OVER (PARTITION BY tax_year ORDER BY pay_date ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) AS ani_prior FROM r), slice AS (SELECT *, ani_prior + gross_pay - COALESCE(rsu_vest, 0) - COALESCE(pension_sacrifice, 0) AS ani_pre, ani_prior + gross_pay - COALESCE(pension_sacrifice, 0) AS ani_post FROM ani), m AS (SELECT *, GREATEST(0, LEAST(ani_post, 12570) - GREATEST(ani_pre, 0)) * 0.00 + GREATEST(0, LEAST(ani_post, 50270) - GREATEST(ani_pre, 12570)) * 0.20 + GREATEST(0, LEAST(ani_post, 100000) - GREATEST(ani_pre, 50270)) * 0.40 + GREATEST(0, LEAST(ani_post, 125140) - GREATEST(ani_pre, 100000)) * 0.60 + GREATEST(0, ani_post - GREATEST(ani_pre, 125140)) * 0.45 AS rsu_paye_marginal, GREATEST(0, LEAST(ani_post, 12570) - GREATEST(ani_pre, 0)) * 0.00 + GREATEST(0, LEAST(ani_post, 50270) - GREATEST(ani_pre, 12570)) * 0.08 + GREATEST(0, ani_post - GREATEST(ani_pre, 50270)) * 0.02 AS rsu_ni_marginal FROM slice) SELECT tax_year, SUM(net_pay) AS net_pay, SUM(GREATEST(0, income_tax - rsu_paye_marginal)) AS cash_income_tax, SUM(rsu_paye_marginal + rsu_ni_marginal) AS rsu_tax_marginal, SUM(GREATEST(0, national_insurance - rsu_ni_marginal)) AS cash_ni, SUM(student_loan) AS student_loan, SUM(COALESCE(pension_sacrifice, 0)) AS pension_sacrifice, SUM(rsu_offset) AS rsu_offset FROM m GROUP BY tax_year ORDER BY tax_year"
"rawSql": "WITH r AS (SELECT * FROM payslip_ingest.payslip), ani AS (SELECT *, COALESCE(SUM(gross_pay - COALESCE(pension_sacrifice, 0)) OVER (PARTITION BY tax_year ORDER BY pay_date ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) AS ani_prior FROM r), slice AS (SELECT *, ani_prior + gross_pay - COALESCE(rsu_vest, 0) - COALESCE(pension_sacrifice, 0) AS ani_pre, ani_prior + gross_pay - COALESCE(pension_sacrifice, 0) AS ani_post FROM ani), m AS (SELECT *, GREATEST(0, LEAST(ani_post, 12570) - GREATEST(ani_pre, 0)) * 0.00 + GREATEST(0, LEAST(ani_post, 50270) - GREATEST(ani_pre, 12570)) * 0.20 + GREATEST(0, LEAST(ani_post, 100000) - GREATEST(ani_pre, 50270)) * 0.40 + GREATEST(0, LEAST(ani_post, 125140) - GREATEST(ani_pre, 100000)) * 0.60 + GREATEST(0, ani_post - GREATEST(ani_pre, 125140)) * 0.45 AS rsu_paye_marginal, GREATEST(0, LEAST(ani_post, 12570) - GREATEST(ani_pre, 0)) * 0.00 + GREATEST(0, LEAST(ani_post, 50270) - GREATEST(ani_pre, 12570)) * 0.08 + GREATEST(0, ani_post - GREATEST(ani_pre, 50270)) * 0.02 AS rsu_ni_marginal FROM slice) SELECT tax_year, SUM(salary) AS salary, SUM(bonus) AS bonus, SUM(rsu_vest - rsu_paye_marginal - rsu_ni_marginal) AS rsu_after_tax FROM m GROUP BY tax_year ORDER BY tax_year"
}
]
},