Skip to content

Commit ab45e83

Browse files
committed
Fix income tax taper calc
1 parent a43b37d commit ab45e83

File tree

2 files changed

+92
-38
lines changed

2 files changed

+92
-38
lines changed

.DS_Store

0 Bytes
Binary file not shown.

data-story/tax-explorer.html

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,21 @@ <h3>Summary Statistics</h3>
172172
<script>
173173
// Tax configuration data structure
174174
const taxConfig = {
175+
// incomeTax: {
176+
// name: "Income Tax",
177+
// bands: [
178+
// { min: 0, max: 12570, rate: 0 },
179+
// { min: 12570, max: 50270, rate: 0.20 },
180+
// { min: 50270, max: 125140, rate: 0.40 },
181+
// { min: 125140, max: Infinity, rate: 0.45 }
182+
// ],
183+
// personalAllowance: 12570,
184+
// allowanceTaper: {
185+
// start: 100000,
186+
// end: 125140,
187+
// reductionRate: 0.5 // £1 reduction per £2 over threshold
188+
// }
189+
// },
175190
incomeTax: {
176191
name: "Income Tax",
177192
bands: [
@@ -180,12 +195,17 @@ <h3>Summary Statistics</h3>
180195
{ min: 50270, max: 125140, rate: 0.40 },
181196
{ min: 125140, max: Infinity, rate: 0.45 }
182197
],
183-
personalAllowance: 12570,
198+
personalAllowance: 12_570,
184199
allowanceTaper: {
185-
start: 100000,
186-
end: 125140,
187-
reductionRate: 0.5 // £1 reduction per £2 over threshold
188-
}
200+
start: 100_000,
201+
end: 125_140,
202+
reductionRate: 0.5 // £1 PA reduced per £2 over => 0.5 per £1
203+
},
204+
basicRate: 0.20,
205+
higherRate: 0.40,
206+
additionalRate: 0.45,
207+
basicBandWidth: 37_700, // constant width of 20% band
208+
additionalThresholdGross: 125_140 // fixed gross threshold for 45%
189209
},
190210
nationalInsurance: {
191211
name: "National Insurance",
@@ -237,35 +257,52 @@ <h3>Summary Statistics</h3>
237257
}
238258
};
239259

240-
// Calculate income tax with personal allowance tapering
260+
241261
function calculateIncomeTax(income) {
242-
const config = taxConfig.incomeTax;
243-
let personalAllowance = config.personalAllowance;
244-
245-
// Apply tapering
246-
if (income > config.allowanceTaper.start) {
247-
const excess = Math.min(income - config.allowanceTaper.start,
248-
config.allowanceTaper.end - config.allowanceTaper.start);
249-
personalAllowance = Math.max(0, personalAllowance - (excess * config.allowanceTaper.reductionRate));
262+
263+
const cfg = taxConfig.incomeTax;
264+
265+
// 1) Adjust the personal allowance for taper
266+
let pa = cfg.personalAllowance;
267+
if (income > cfg.allowanceTaper.start) {
268+
const excess = income - cfg.allowanceTaper.start;
269+
const reduction = Math.min(excess * cfg.allowanceTaper.reductionRate, cfg.personalAllowance);
270+
pa = Math.max(0, cfg.personalAllowance - reduction);
250271
}
251-
272+
273+
// 2) Taxable income after the adjusted allowance
274+
let taxable = Math.max(0, income - pa);
275+
276+
// 3) Compute taxable thresholds in *taxable-income* space
277+
const basicWidth = cfg.basicBandWidth; // 37,700
278+
const additionalThresholdTaxable = cfg.additionalThresholdGross - pa; // shifts with PA
279+
280+
// 4) Apply bands in taxable space
252281
let tax = 0;
253-
let taxableIncome = Math.max(0, income - personalAllowance);
254-
255-
//
256-
for (const band of config.bands) {
257-
if (taxableIncome <= 0) break;
258-
// Skip first band
259-
if (band.min === 0) continue;
260-
261-
const bandWidth = band.max - band.min;
262-
const taxableInBand = Math.min(taxableIncome, bandWidth);
263-
tax += taxableInBand * band.rate;
264-
taxableIncome -= taxableInBand;
282+
283+
// Basic rate (20%) on first 37,700 of taxable income
284+
const basicTaxable = Math.min(taxable, basicWidth);
285+
tax += basicTaxable * cfg.basicRate;
286+
taxable -= basicTaxable;
287+
288+
if (taxable > 0) {
289+
// Higher-rate ceiling measured from the *start* of taxable bands
290+
// i.e., the portion available at 40% before hitting additional rate
291+
const higherWidth = Math.max(0, additionalThresholdTaxable - basicWidth);
292+
293+
const higherTaxable = Math.min(taxable, higherWidth);
294+
tax += higherTaxable * cfg.higherRate;
295+
taxable -= higherTaxable;
296+
297+
if (taxable > 0) {
298+
// Remainder at additional rate (45%)
299+
tax += taxable * cfg.additionalRate;
300+
}
265301
}
266-
302+
267303
return tax;
268-
}
304+
}
305+
269306

270307
// Calculate National Insurance
271308
function calculateNationalInsurance(income) {
@@ -337,7 +374,7 @@ <h3>Summary Statistics</h3>
337374
}
338375

339376
// Create Vega-Lite specification
340-
function createSpec(data) {
377+
function createSpec(data, summaryIncome) {
341378
// Prepare data for labels - get the last point for each line
342379
const lastPoint = data[data.length - 1];
343380
const labelData = [
@@ -359,6 +396,28 @@ <h3>Summary Statistics</h3>
359396
height: 400,
360397
data: { values: data },
361398
layer: [
399+
{
400+
data: { values: [{ income: summaryIncome }] },
401+
mark: {
402+
type: "rule",
403+
strokeWidth: 2,
404+
// color: "#666",
405+
strokeDash: [3, 3],
406+
opacity: 0.7
407+
},
408+
encoding: {
409+
x: {
410+
field: "income",
411+
type: "quantitative",
412+
// scale: {
413+
// domain: [0, Math.max(...data.map(d => d.income))]
414+
// }
415+
},
416+
tooltip: [
417+
{ field: "income", type: "quantitative", title: "Your Income", format: "$.0f" }
418+
]
419+
}
420+
},
362421
{
363422
mark: { type: "line", strokeWidth: 2, color: "#ff6b6b" },
364423
encoding: {
@@ -452,12 +511,6 @@ <h3>Summary Statistics</h3>
452511
};
453512
}
454513

455-
function loadConfig() {
456-
const config = fetch('config.json').then(response => response.json());
457-
console.log(config);
458-
return config;
459-
}
460-
461514
// Update visualisation
462515
function updateVisualisation() {
463516
const components = {
@@ -468,8 +521,9 @@ <h3>Summary Statistics</h3>
468521
};
469522

470523
const maxIncome = parseInt(document.getElementById('maxIncome').value) || 200000;
524+
const summaryIncome = parseInt(document.getElementById('summaryIncomeInput').value) || 50000;
471525
const data = generateData(maxIncome, components);
472-
const spec = createSpec(data);
526+
const spec = createSpec(data, summaryIncome);
473527

474528

475529
vegaEmbed('#vis', spec, {
@@ -494,7 +548,7 @@ <h3>Summary Statistics</h3>
494548
console.log(spec);
495549

496550
// Update summary statistics
497-
const summaryIncome = parseInt(document.getElementById('summaryIncomeInput').value) || 50000;
551+
// const summaryIncome = parseInt(document.getElementById('summaryIncomeInput').value) || 50000;
498552

499553
const marginal = calculateMarginalRate(summaryIncome, components);
500554
const total = calculateTotalTax(summaryIncome, components);

0 commit comments

Comments
 (0)