Public
Edited
Mar 2, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
// TODO:
// - separate out marks + annotations plotting in to separate sections from axes definition
// - hence use scale('x').invert/apply etc to improve annotation positioning
const xmax = 75000
const ymax = xmax + 5000

const data_clipped = data.filter(d=>d['Gross income']<=xmax)
const below_threshold = data_clipped.filter(d=>d['Gross income']<=parameters.threshold)
const above_threshold = data_clipped.filter(d=>d['Gross income']>parameters.threshold)
const negative_tax_points = {}
negative_tax_points.top = {x: parameters.threshold/3,
y: parameters.minimum_income + (parameters.threshold-parameters.minimum_income)/3
}
negative_tax_points.mid = {x: negative_tax_points.top.x,
y:(negative_tax_points.top.y + parameters.threshold/3)/2
}
negative_tax_points.annotation = {x: Math.max(negative_tax_points.top.x, 10000),
y: Math.max(negative_tax_points.top.y + 5000, 20000),
text: ['Below the threshold, the negative income tax supplements base pay']
}
negative_tax_points.annotation.bend = (360/(2*Math.PI)) * Math.atan(
(negative_tax_points.mid.x - negative_tax_points.annotation.x)/
(negative_tax_points.mid.y-negative_tax_points.annotation.y))

const threshold_annotation = { x1: parameters.threshold + 7500,
y1: Math.max(parameters.threshold - 7500, 3000),
x2: parameters.threshold,
y2: parameters.threshold,
text: ['At the threshold, the effective tax rate is zero']
}
threshold_annotation.bend = -(360/(2*Math.PI)) * Math.atan(
(threshold_annotation.y2 - threshold_annotation.y1)/
(threshold_annotation.x2 - threshold_annotation.x1))-10




const label_values = data.filter(d=>d['Gross income'] >= xmax)[0]
return Plot.plot({
x: {domain: [0, xmax]},
y: {domain: [0, ymax]},
width: 640,
// height: 640 * ymax/xmax,
insetRight: 50,
marks: [
Plot.axisX({label: 'Income before tax →',
fontSize: 10,
tickFormat: (d, i, _) => (i === _.length - 1 ? `£${d/1000}k` : d/1000)}),
Plot.axisY({label: '↑ Income after tax',
tickSize: 0,
tickFormat: (d, i, _) => (i === _.length - 1 ? `£${d/1000}k` : d/1000)}),
Plot.gridY({interval: 10000, stroke: dark_grey, strokeDasharray:[2,2], strokeOpacity:.2}),
Plot.ruleY([0]),
Plot.line(below_threshold, {y: 'Net income', x: 'Gross income',
stroke: line_color,
strokeWidth:2.5}),
Plot.line(above_threshold, {y: 'Net income', x: 'Gross income',
stroke: dark_grey,
strokeWidth:2.5}),
Plot.line(data_clipped, {y: 'Gross income', x:'Gross income',
strokeDasharray:[2, 4],
stroke: dark_grey,
strokeWidth: 1}),
Plot.area(below_threshold, {x1: 'Gross income', y1: 'Gross income', x2: 'Gross income', y2: 'Net income',
fill: line_color,
fillOpacity:0.1}),
Plot.area(above_threshold, {x1: 'Gross income', y1: 'Gross income', x2: 'Gross income', y2: 'Net income',
fill: dark_grey,
fillOpacity:0.1}),
// Plot.arrow([{x1:parameters.threshold, y1:0, x2:parameters.threshold, y2:parameters.threshold}],
// {x1: 'x1',
// y1: 'y1',
// x2: 'x2',
// y2: 'y2',
// stroke: dark_grey,
// // strokeDasharray: [2,2],
// strokeOpacity: 0.5,
// strokeWidth:1,
// headLength:0,
// inset: 5,
// bend: false}),
Plot.dot([parameters.threshold], {x:d=>d, y:d=>d, fill:line_color}),
Plot.arrow([threshold_annotation],
{x1: 'x1',
y1: 'y1',
x2: 'x2',
y2: 'y2',
bend: threshold_annotation.bend,
insetEnd: 10,
strokeWidth:1}),
Plot.text([threshold_annotation], {
x: d => d.x1,
y: d => d.y1,
text: d => d.text,
lineWidth: 13,
textAnchor: 'start',
lineAnchor: 'middle',
dx: 5
}),
Plot.arrow([negative_tax_points],
{x1: pts => pts.annotation.x,
y1: pts => pts.annotation.y,
x2: pts=>pts.mid.x,
y2: pts=>pts.mid.y,
markerEnd:'none',
bend: negative_tax_points.annotation.bend,
// insetEnd: 10,
strokeWidth:1,
strokeOpacity:0.8}),
Plot.text([negative_tax_points], {
x: pts => pts.annotation.x,
y: pts => pts.annotation.y,
text: pts => pts.annotation.text,
lineWidth: 15,
lineAnchor: 'bottom',
textAnchor: 'middle',
dy: -10,
}),
Plot.text([label_values],
{
x: d => d['Gross income'],
y: d => d['Net income'],
text: ['Income after tax'],
fontWeight: 'bold',
color: dark_grey,
dx: 10,
dy: 0,
lineAnchor: 'middle',
lineWidth: 5,
textAnchor: 'start',
clip: false
}),
Plot.text([label_values],
{
x: d => d['Gross income'],
y: d => d['Gross income'],
text: ['Base pay'],
fontWeight: 'regular',
color: dark_grey,
dx: 10,
dy: 0,
lineAnchor: 'middle',
lineWidth: 5,
textAnchor: 'start',
clip: false
})
],
marginLeft: 60,
paddingRight: 200
})
}
Insert cell
Insert cell
max_min_income = 35000
Insert cell
min_rate = 0.1
Insert cell
viewof line_color = Inputs.color({label: "Line colour", value: "#387a66"})
Insert cell
dark_grey = '#404040'
Insert cell
UK_tax_bands = [{name: "Basic rate", lower: 0, upper: 37700, rate:.30},
{name: "Higher rate", lower: 37700, upper: 150000, rate:.45},
{name: "Additional rate", lower: 150000, upper: null, rate:.6}]
Insert cell
UK_personal_allowance_base = 12570
Insert cell
UK_personal_allowance = income => Math.max(0, UK_personal_allowance_base - Math.max((income-100000),0) / 2)
Insert cell
tax_in_band = (gross_income, band) => (gross_income < band.lower) ? 0 : band.rate*0.01*
((band.upper && (gross_income >= band.upper)) ? (band.upper - band.lower) : (gross_income - band.lower))
Insert cell
tax_in_band_new = (gross_income, band) => {
let tax
if (band.negative) {
tax = gross_income > band.upper ? 0 : band.rate *
(((typeof(band.lower)=='number' && gross_income < band.lower)) ? (band.lower - band.upper) : (gross_income - band.upper))
}
else {
tax = (gross_income < band.lower) ? 0 : band.rate *
(((band.upper && (gross_income >= band.upper)) ? (band.upper - band.lower) : (gross_income - band.lower)))
}
return tax
}
Insert cell
incomes = d3.range(0, 250000, 100)
Insert cell
tax_payable_UK = gross_income => {
const personal_allowance = UK_personal_allowance(gross_income)
const taxable_income = gross_income - personal_allowance
const tax_payable = UK_tax_bands.reduce((tax_payable, band) => tax_payable += tax_in_band(taxable_income, band), 0)
return tax_payable
}
Insert cell
tax_paid_UK = incomes.map(income => tax_payable_UK(income))
Insert cell
net_incomes_UK = incomes.map(income => income - tax_payable_UK(income))
Insert cell
tax_bands_proposed = [{name: "Negative rate", lower: 0, upper: parameters.threshold, rate: rate, negative: true },
{name: "Basic rate", lower: parameters.threshold, upper: Math.max(50000,parameters.threshold), rate:0.20, negative: false },
{name: "Higher rate", lower: Math.max(50000, parameters.threshold), upper: 150000, rate:0.40, negative: false },
{name: "Additional rate", lower: 150000, upper: null, rate:0.5, negative: false }]
Insert cell
tax_proposed = gross_income => tax_bands_proposed.reduce((tax_payable, band) => tax_payable += tax_in_band_new(gross_income, band), 0)
Insert cell
tax_proposed_by_income = incomes.map(tax_proposed)
Insert cell
net_income_proposed = incomes.map(income => income - tax_proposed(income))
Insert cell
data_UK = incomes.map((income, idx) => {return {'Gross income': income, 'Tax':tax_paid_UK[idx], 'Net income': net_incomes_UK[idx]}})
Insert cell
data_proposed = incomes.map((income, idx) => {return {'Gross income': income, 'Tax':tax_proposed_by_income[idx], 'Net income': net_income_proposed[idx]}})
Insert cell
styledRange = function(range, options, styles) {
const styled_range = Inputs.range(range, options)
Object.assign(styled_range[1].style, styles)
return styled_range
}
Insert cell
data = data_proposed
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more