diff --git a/lib/slideshows.ts b/lib/slideshows.ts index 9aa3968..dfea92a 100644 --- a/lib/slideshows.ts +++ b/lib/slideshows.ts @@ -29,8 +29,10 @@ import { overhang2026LightningConfig } from "@/slideshows/overhang-2026-lightnin import { blsTaxsim2026Config } from "@/slideshows/bls-taxsim-2026/config"; import { scottishGovernment2026Config } from "@/slideshows/scottish-government-2026/config"; import { goodGrowthFoundation2026Config } from "@/slideshows/good-growth-foundation-2026/config"; +import { autumnBudget2026Config } from "@/slideshows/autumn-budget-2026/config"; export const slideshows: SlideshowConfig[] = [ + autumnBudget2026Config, goodGrowthFoundation2026Config, scottishGovernment2026Config, blsTaxsim2026Config, diff --git a/next.config.ts b/next.config.ts index c66c230..71f0735 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,16 @@ const nextConfig: NextConfig = { env: { NEXT_PUBLIC_BASE_PATH: '/slides', }, + // The Autumn Budget 2026 webinar deck was introduction-2026 until 28 October. + async redirects() { + return [ + { + source: '/introduction-2026/:path*', + destination: '/autumn-budget-2026/:path*', + permanent: true, + }, + ]; + }, // Pin the workspace root so Turbopack does not silently pick up a stray // lockfile from a parent directory during local development or CI. turbopack: { diff --git a/public/images/autumn-budget-2026/dashboard-qr.svg b/public/images/autumn-budget-2026/dashboard-qr.svg new file mode 100644 index 0000000..7874146 --- /dev/null +++ b/public/images/autumn-budget-2026/dashboard-qr.svg @@ -0,0 +1 @@ + diff --git a/public/logos/funders/nuffield-foundation-white.svg b/public/logos/funders/nuffield-foundation-white.svg new file mode 100644 index 0000000..a53f698 --- /dev/null +++ b/public/logos/funders/nuffield-foundation-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/logos/orgs/good-growth-foundation.png b/public/logos/orgs/good-growth-foundation.png new file mode 100644 index 0000000..fdfec09 Binary files /dev/null and b/public/logos/orgs/good-growth-foundation.png differ diff --git a/slideshows/autumn-budget-2026/README.md b/slideshows/autumn-budget-2026/README.md new file mode 100644 index 0000000..d016199 --- /dev/null +++ b/slideshows/autumn-budget-2026/README.md @@ -0,0 +1,33 @@ +# Analysing the Autumn Budget 2026 with PolicyEngine + +5 slides at `/slides/autumn-budget-2026`, for the 28 October 2026 webinar +(`/slides/introduction-2026` redirects here): +what PolicyEngine is, how we prepared for Budget day, and a slide with the live +[Autumn Budget 2026 dashboard](https://ab2026-local-preview.vercel.app/uk/autumn-budget-2026) +embedded in it, where the analysis is presented. The closing slide links the +same address, with its QR code, and credits the Nuffield Foundation, which +funds our UK Budget analysis. + +- `config.ts` controls the slide order and metadata. +- `content.ts` holds the sources, the three pillars of What PolicyEngine is, + the dates of the Budget-day rehearsals (the mock Budgets) and the + preparation items, written for a general audience. +- `slides/index.tsx` contains the slides. The analysis slide embeds the + dashboard itself (`slides/DashboardEmbed.tsx`): Expand stretches the same + frame over the whole screen, so whatever is open in it is kept, and New tab + opens it in the browser. Buttons in its bar load the Public finances, + Pensioners (By age) and Living standards sub-tabs and the Household + calculator tab into the frame. The deck draws no charts of its own, so nothing + here goes stale when the dashboard's data change. +- `SPEAKER-NOTES.md` gives the presenter track. + +`public/images/autumn-budget-2026/dashboard-qr.svg` encodes +the dashboard's address. + +`public/logos/funders/nuffield-foundation-white.svg` is the Nuffield +Foundation's white logo, downloaded from nuffieldfoundation.org on 22 October +2026 (3,432 bytes, SHA-256 +`b3727385be9295f8daa3938c745f87b2e00e5675130354f6394ae6b3fec5654c`). That day +its communications team asked for "funded" rather than "supported", and for +its logo, the white version on a dark background. Keep the file as +downloaded; don't recolour or redraw it. diff --git a/slideshows/autumn-budget-2026/SPEAKER-NOTES.md b/slideshows/autumn-budget-2026/SPEAKER-NOTES.md new file mode 100644 index 0000000..db82a2e --- /dev/null +++ b/slideshows/autumn-budget-2026/SPEAKER-NOTES.md @@ -0,0 +1,70 @@ +# Speaker notes: Analysing the UK Autumn Budget 2026 with PolicyEngine + +## 1. Title + +Webinar, Wednesday 28 October 2026, 4:30pm UK time. Presenters: Vahid +Ahmadi, Max Ghenis, María Juaristi. + +## 2. What PolicyEngine is + +A brief view: PolicyEngine in one line (a nonprofit building free, +open-source tools, in policyengine.org's own words), then the rules, the +households and the results, and that all the code is public. + +## 3. How we prepared + +The slide is for a general audience: the rehearsals and what they gave us. +The detail below is background, if asked. + +- Last year's Budget (26 November 2025): the dashboard went live on Budget + day, and the other measures and the household calculator followed by 28 + November (the 2025 dashboard repo's commit history). +- Five Budget-day rehearsals, the mock Budgets (28 September; 5, 12, 19 and + 26 October): drill issues #6 (with the drill 1 retrospective), #8, #10, #53 + and #144 in the dashboard repo. +- The model and data: the policyengine package 6.11.1, with policyengine-uk + 2.151.0 and the Microcosm UK 2024-25 households. The live dashboard's + publication manifest records it for every measure and the package, and + main's uv.lock pins it (28 October). +- National results run on Modal, one machine per measure: all ten units (nine + measures and the package) took 8.1 minutes on 26 October (dashboard PR + #178). The household calculator also runs on Modal and is kept warm. +- Mark anything still in review as such on the slide before presenting. + +## 4. The analysis: live dashboard + +The dashboard itself is on the slide; it loads while slide 3 is showing. +Press **Expand** to fill the screen with it and present from there. **Back to +the slide** returns to the deck with the same tab, measures and household +still open. **New tab** opens it in a browser tab, if the embedded copy ever +misbehaves. + +While you are using the dashboard, the arrow keys, Space and a clicker go to +it, not to the deck (the arrows switch its tabs). Press **Back to the slide**, +or click the bar above the dashboard, before moving on. + +The buttons in the bar (Public finances, Pensioners, Living standards, +Household calculator) load that view into the frame; each click reloads it, +so measures or a household chosen in the frame are not kept. Its tabs: + +- **Population impact**, with five sub-tabs: **Public finances** (the + default: revenue by year and the OBR comparison), **All households** (the + constituency map and households by income), **By age** (the Pensioners + button opens it at pensioners), **Living standards** (income by decile and + inequality) and **Poverty**. +- **Household calculator**: run one household live with Calculate, or pick + an example card. +- **Pre-measures forecast**: the outlook before the Budget's measures, with + the OBR forecast named on the tab. +- **Policies**: what each measure does. +- **Methodology**. + +These are the tabs at ab2026-local-preview.vercel.app/uk/autumn-budget-2026 +on 28 October. The frame and the closing slide's link and QR code all open +that address. + +## 5. Close + +The dashboard link, with a QR code for phones, and how to reach us. Thank +the Nuffield Foundation, which funds our UK Budget analysis ("funded by", +not "supported by"). diff --git a/slideshows/autumn-budget-2026/config.ts b/slideshows/autumn-budget-2026/config.ts new file mode 100644 index 0000000..d0f3e01 --- /dev/null +++ b/slideshows/autumn-budget-2026/config.ts @@ -0,0 +1,26 @@ +import type { SlideshowConfig } from "@/lib/types"; +import { + ClosingSlide, + DashboardSlide, + PreparationSlide, + TitleSlide, + WhatSlide, +} from "./slides"; + +export const autumnBudget2026Config: SlideshowConfig = { + id: "autumn-budget-2026", + title: "Analysing the Autumn Budget 2026 with PolicyEngine", + description: + "What PolicyEngine is, how we prepared for Budget day, and the analysis, presented live on the dashboard.", + date: "2026-10-28", + location: "PolicyEngine", + // The slides keep this clear of the viewer's slide counter (deck.module.css). + footerText: "Autumn Budget 2026 webinar · 28 October 2026", + slides: [ + TitleSlide, + WhatSlide, + PreparationSlide, + DashboardSlide, + ClosingSlide, + ], +}; diff --git a/slideshows/autumn-budget-2026/content.ts b/slideshows/autumn-budget-2026/content.ts new file mode 100644 index 0000000..de14782 --- /dev/null +++ b/slideshows/autumn-budget-2026/content.ts @@ -0,0 +1,144 @@ +// Content for the PolicyEngine and Autumn Budget 2026 deck. + +export type Source = { label: string; href: string }; + +export const sources = { + home: { label: "policyengine.org", href: "https://www.policyengine.org/uk" }, + model: { + label: "PolicyEngine UK code", + href: "https://github.com/PolicyEngine/policyengine-uk", + }, + data: { + label: "Microcosm data pipeline", + href: "https://github.com/PolicyEngine/microcosm", + }, + hansard: { + label: "Hansard, 24 February 2026", + href: "https://hansard.parliament.uk/Lords/2026-02-24/debates/A381F7D6-0A3C-48FD-8D9E-67751E25877A/NationalInsuranceContributions(EmployerPensionsContributions)Bill", + }, + tpa: { + label: "Tax Policy Associates, September 2026", + href: "https://taxpolicy.org.uk/2026/09/10/universities-uk-national-insurance-proposal/", + }, + cps: { + label: "Centre for Policy Studies, July 2023", + href: "https://cps.org.uk/research/family-friendly-taxation/", + }, + nuffield: { + label: "Nuffield Foundation project", + href: "https://www.nuffieldfoundation.org/project/enhancing-localising-and-democratising-tax-benefit-policy-analysis", + }, + nef: { + label: "New Economics Foundation, July 2026", + href: "https://neweconomics.org/2026/07/insecure-low-quality-work-a-major-driver-of-uks-neets-and-inactivity-crisis", + }, + no10Post: { + label: "PolicyEngine at No 10", + href: "https://www.policyengine.org/uk/research/policyengine-10-downing-street", + }, + // The dashboard the webinar presents, on the analysis slide and in the + // closing slide's link and QR code. The host alone returns 404. + dashboard: { + label: "Autumn Budget 2026 dashboard", + href: "https://ab2026-local-preview.vercel.app/uk/autumn-budget-2026", + }, + dashboardCode: { + label: "Dashboard code", + href: "https://github.com/PolicyEngine/uk-autumn-budget-dashboard-2026", + }, + dashboard2025Code: { + label: "2025 dashboard code", + href: "https://github.com/PolicyEngine/uk-autumn-budget-dashboard", + }, +}; + +// How policyengine.org/uk/donate describes PolicyEngine. +export const about = + "PolicyEngine is a nonprofit building free, open-source tools that help people understand tax and benefit policies."; + +// The Nuffield Foundation's credit. On 22 October its communications team +// asked for "funded" rather than "supported", and for its logo, the white +// version on a dark background. The logo is the white one from +// nuffieldfoundation.org, kept as downloaded (3,432 bytes, SHA-256 +// b3727385…); its own size is 117.862 by 70. +export const funder = { + label: "Funded by", + name: "Nuffield Foundation", + logo: "/logos/funders/nuffield-foundation-white.svg", + width: 118, + height: 70, +}; + +// What PolicyEngine is: the rules, the households and the results. The rules +// are the main ones in the Good Growth Foundation deck's checked list. +export const pillars = [ + { + title: "Rules", + items: [ + "Income tax and National Insurance", + "Universal Credit and other benefits", + "State Pension and Pension Credit", + "VAT, fuel duty and capital gains tax", + ], + }, + { + title: "Households", + items: [ + "Built from the Family Resources Survey", + "With HMRC tax records and other surveys", + "Weighted to match official statistics", + "Or enter any household yourself", + ], + }, + { + title: "Results", + items: [ + "Cost or saving to the Treasury", + "Who gains and loses, by income decile", + "Poverty and inequality", + "By region and constituency", + ], + }, +]; + +// Last year's Budget, from the 2025 dashboard's commit history: the dashboard +// went live on the day, and the other measures and the household calculator +// followed by 28 November. +export const lastBudget = { + title: "Autumn Budget 2025", + text: "Analysed on the day, 26 Nov", +}; + +// The five Budget-day rehearsals (the mock Budgets): full runs of the day on +// invented measures. +export const mockBudgets = ["28 Sep", "5 Oct", "12 Oct", "19 Oct", "26 Oct"]; + +export type PrepItem = { title: string; text: string; status: "Done" | "In review" }; + +// Written for a general audience: the technical detail is in the speaker +// notes. Each line is checked against PolicyEngine/uk-autumn-budget-dashboard-2026; +// the version is the one in the live dashboard's publication manifest and in +// main's uv.lock on 28 October. +export const preparation: PrepItem[] = [ + { + title: "Fixed model and data", + text: "Every figure comes from PolicyEngine 6.11.1 and the same set of households, so anyone can reproduce it.", + status: "Done", + }, + { + title: "A household calculator", + text: "Enter any household to see how the measures change its income, live on the dashboard.", + status: "Done", + }, + { + title: "Every measure in about 8 minutes", + text: "The cost of each measure, and who gains and loses, worked out all at once rather than one by one.", + status: "Done", + }, + { + title: "Each measure published once reviewed", + text: "A measure goes on the dashboard as soon as its figures have been checked.", + status: "In review", + }, +]; + diff --git a/slideshows/autumn-budget-2026/funder.test.ts b/slideshows/autumn-budget-2026/funder.test.ts new file mode 100644 index 0000000..d927e27 --- /dev/null +++ b/slideshows/autumn-budget-2026/funder.test.ts @@ -0,0 +1,31 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { funder } from "./content"; + +// The white logo as downloaded from nuffieldfoundation.org on 22 October. Keep +// it byte for byte: don't recolour or redraw it. +const DOWNLOADED_SHA256 = + "b3727385be9295f8daa3938c745f87b2e00e5675130354f6394ae6b3fec5654c"; + +describe("the Nuffield Foundation credit on the closing slide", () => { + const file = path.join(__dirname, "../../public", funder.logo); + const bytes = readFileSync(file); + + it("uses the logo exactly as downloaded", () => { + expect(bytes.length).toBe(3432); + expect(createHash("sha256").update(bytes).digest("hex")).toBe(DOWNLOADED_SHA256); + }); + + it("matches the logo's own dimensions", () => { + const svg = bytes.toString("utf8"); + const size = (name: string) => Number(svg.match(new RegExp(`${name}="([\\d.]+)"`))?.[1]); + expect(Math.round(size("width"))).toBe(funder.width); + expect(Math.round(size("height"))).toBe(funder.height); + }); + + it('reads "Funded by"', () => { + expect(funder.label).toBe("Funded by"); + }); +}); diff --git a/slideshows/autumn-budget-2026/slides/ClosingSlide.test.tsx b/slideshows/autumn-budget-2026/slides/ClosingSlide.test.tsx new file mode 100644 index 0000000..a089e51 --- /dev/null +++ b/slideshows/autumn-budget-2026/slides/ClosingSlide.test.tsx @@ -0,0 +1,35 @@ +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { funder, sources } from "../content"; +import { ClosingSlide } from "./index"; + +let container: HTMLDivElement; +let root: Root; + +beforeEach(async () => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + await act(async () => root.render()); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); +}); + +describe("the closing slide's funder credit", () => { + it('reads "Funded by" beside the Nuffield Foundation logo', () => { + const logo = container.querySelector(`img[alt="${funder.name}"]`); + expect(logo).not.toBeNull(); + expect(decodeURIComponent(logo!.getAttribute("src") ?? "")).toContain(funder.logo); + expect(logo!.closest("a")?.textContent).toContain("Funded by"); + }); + + it("links to the Nuffield Foundation's page for the project", () => { + const logo = container.querySelector(`img[alt="${funder.name}"]`); + expect(logo?.closest("a")?.getAttribute("href")).toBe(sources.nuffield.href); + }); +}); diff --git a/slideshows/autumn-budget-2026/slides/DashboardEmbed.tsx b/slideshows/autumn-budget-2026/slides/DashboardEmbed.tsx new file mode 100644 index 0000000..5250d32 --- /dev/null +++ b/slideshows/autumn-budget-2026/slides/DashboardEmbed.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState } from "react"; +import { + IconArrowsMaximize, + IconArrowsMinimize, + IconExternalLink, +} from "@tabler/icons-react"; +import styles from "./deck.module.css"; + +/** + * The dashboard, live on the slide. Expand stretches the same frame over the + * whole screen, so the tab, measures and household it shows are kept when it + * goes back to the slide. Clicks in the frame go to the dashboard, and so do + * the arrow keys until the slide is clicked outside it. The view buttons in + * the bar load one of the dashboard's tabs or sub-tabs into the same frame. + */ +export type DashboardView = { + label: string; + /** The deep link's query string, such as "?tab=household". */ + query: string; +}; + +export default function DashboardEmbed({ + href, + title, + views = [], +}: { + href: string; + title: string; + /** Buttons that open a tab or sub-tab in the frame. */ + views?: DashboardView[]; +}) { + const [expanded, setExpanded] = useState(false); + const [src, setSrc] = useState(href); + // Bumped on every view button, so a second click on the same view reloads it. + const [load, setLoad] = useState(0); + const ToggleIcon = expanded ? IconArrowsMinimize : IconArrowsMaximize; + + return ( +
event.stopPropagation()} + > +
+ {views.length > 0 && ( +
+ {views.map((view) => ( + + ))} +
+ )} + + + +
+