{
function parseUKDate(dateStr) {
if (!dateStr) return null;
let parts;
if (dateStr.includes("/")) {
parts = dateStr.split("/");
} else if (dateStr.includes("-")) {
parts = dateStr.split("-");
} else {
return null;
}
if (parts.length === 3) {
const day = parts[0].padStart(2, "0");
const month = parts[1].padStart(2, "0");
let year = parts[2];
if (year.length === 2) {
year = parseInt(year) < 50 ? `20${year}` : `19${year}`;
}
try {
return new Date(`${year}-${month}-${day}`);
} catch (e) {
console.error(`Failed to parse date: ${dateStr}`, e);
return null;
}
}
return null;
}
const pmStartDates = {
Thatcher: new Date("1979-05-04"),
Major: new Date("1990-11-28"),
Blair: new Date("1997-05-02"),
Brown: new Date("2007-06-27"),
Cameron: new Date("2010-05-11"),
May: new Date("2016-07-13"),
Johnson: new Date("2019-07-24"),
Truss: new Date("2022-09-06"),
Sunak: new Date("2022-10-25"),
Starmer: new Date("2024-07-05")
};
const cleanData = data.filter((d) => d.PM && d.Date);
const byPM = d3.group(cleanData, (d) => d.PM);
const plotData = [];
const pmLabels = [];
const pmColor = {
Thatcher: "grey",
Major: "grey",
Blair: "grey",
Brown: "grey",
Cameron: "grey",
May: "grey",
Johnson: "grey",
Truss: "grey",
Sunak: "grey",
Starmer: "#E05D5D"
};
byPM.forEach((resignations, pm) => {
if (!resignations.length) return;
if (!pmStartDates[pm]) {
console.warn(`No start date defined for PM: ${pm}`);
return;
}
const pmStartDate = pmStartDates[pm];
const validResignations = resignations
.map((r) => ({
...r,
parsedDate: parseUKDate(r.Date)
}))
.filter((r) => r.parsedDate);
if (!validResignations.length) return;
validResignations.sort((a, b) => a.parsedDate - b.parsedDate);
plotData.push({
pm,
years: 0,
cumulativeCount: 0
});
validResignations.forEach((d, i) => {
const years =
(d.parsedDate - pmStartDate) / (1000 * 60 * 60 * 24 * 365.25);
const adjustedYears = Math.max(years, 0.01);
plotData.push({
pm,
years: adjustedYears,
cumulativeCount: i + 1
});
if (i < validResignations.length - 1) {
const nextDate = validResignations[i + 1].parsedDate;
const nextYears =
(nextDate - pmStartDate) / (1000 * 60 * 60 * 24 * 365.25);
if (nextYears - adjustedYears > 0.01) {
plotData.push({
pm,
years: nextYears - 0.0001,
cumulativeCount: i + 1
});
}
}
});
const lastResignation = validResignations[validResignations.length - 1];
const years =
(lastResignation.parsedDate - pmStartDate) /
(1000 * 60 * 60 * 24 * 365.25);
pmLabels.push({
pm,
x: years,
y: validResignations.length,
text: pm
});
});
return Plot.plot({
title: "GET IN THE BIN KEITH",
subtitle:
"Ministerial resignations outside reshuffles by prime minister, 1979 to Feb 2025",
caption:
"Source: Institute for Government, ministerial resignations google sheet",
marginRight: 40,
marginTop: 30,
x: {
label: "Years as Prime Minister",
grid: true,
domain: [0, 12]
},
y: {
label: "Cumulative number of resignations",
grid: true,
domain: [0, 50]
},
marks: [
Plot.line(plotData, {
x: "years",
y: "cumulativeCount",
stroke: "white",
strokeWidth: 5,
curve: "step",
z: "pm"
}),
Plot.line(plotData, {
x: "years",
y: "cumulativeCount",
stroke: (d) => pmColor[d.pm] || "#999",
strokeWidth: 2.5,
curve: "step",
z: "pm",
markerEnd: "dot",
tip: true,
strokeOpacity: (d) => (d.pm === "Starmer" ? 1 : 0.7)
}),
Plot.text(pmLabels, {
x: "x",
y: "y",
text: "text",
fill: (d) => pmColor[d.pm] || "#999",
fontWeight: "bold",
fontSize: 12,
dx: -5,
dy: -7,
stroke: "white",
textAnchor: "end",
opacity: (d) => (d.pm === "Starmer" ? 1 : 0.7)
}),
Plot.ruleY([0])
]
});
}