Public
Edited
Feb 28
Insert cell
Insert cell
// Complete ministerial resignations chart in a single cell
{
// Function to correctly parse UK date format (DD/MM/YYYY) with error handling
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) {
// Add padding to ensure two digits for day and month
const day = parts[0].padStart(2, "0");
const month = parts[1].padStart(2, "0");
let year = parts[2];

// Handle two-digit years
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;
}

// Define PM start dates (when they took office)
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")
};

// Filter data to remove any records with null/empty PM or Date
const cleanData = data.filter((d) => d.PM && d.Date);

// Group by PM
const byPM = d3.group(cleanData, (d) => d.PM);

// For each PM, calculate cumulative count and years since first resignation
const plotData = [];
const pmLabels = [];

// Color mapping for PMs - grey everyone except keith
const pmColor = {
Thatcher: "grey", //"#B3CFDC",
Major: "grey", //"#B3CFDC",
Blair: "grey", //"#E05D5D",
Brown: "grey", //"#E9967A",
Cameron: "grey", //"#B3CFDC",
May: "grey", //"#4682B4",
Johnson: "grey", //"#00008B",
Truss: "grey", //"#00008B",
Sunak: "grey", //"#00008B",
Starmer: "#E05D5D" // Red (Labour color)
};

// Process each PM's data
byPM.forEach((resignations, pm) => {
// Skip if there's no data for this PM
if (!resignations.length) return;

// Skip if no start date for this PM
if (!pmStartDates[pm]) {
console.warn(`No start date defined for PM: ${pm}`);
return;
}

const pmStartDate = pmStartDates[pm];

// Filter out any records with unparseable dates
const validResignations = resignations
.map((r) => ({
...r,
parsedDate: parseUKDate(r.Date)
}))
.filter((r) => r.parsedDate);

// Skip if no valid dates after filtering
if (!validResignations.length) return;

// Sort by date
validResignations.sort((a, b) => a.parsedDate - b.parsedDate);

// Add an origin point (0,0) for each PM
plotData.push({
pm,
years: 0,
cumulativeCount: 0
});

// Calculate years and cumulative counts
validResignations.forEach((d, i) => {
// Calculate years since taking office (not since first resignation)
const years =
(d.parsedDate - pmStartDate) / (1000 * 60 * 60 * 24 * 365.25);

// Ensure we don't have negative years (in case of data errors)
const adjustedYears = Math.max(years, 0.01);

plotData.push({
pm,
years: adjustedYears,
cumulativeCount: i + 1
});

// For step line rendering, add an additional point right before the next point
// This ensures the step appearance is maintained even with gaps
if (i < validResignations.length - 1) {
const nextDate = validResignations[i + 1].parsedDate;
const nextYears =
(nextDate - pmStartDate) / (1000 * 60 * 60 * 24 * 365.25);

// Only add the intermediate point if there's a gap
if (nextYears - adjustedYears > 0.01) {
plotData.push({
pm,
years: nextYears - 0.0001, // Just before the next point
cumulativeCount: i + 1 // Same count as current point
});
}
}
});

// Add label at the end of each line
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
});
});

// Create and return the plot
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",
// width: 1000,
// height: 600,
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: [
// Add white background lines (slightly thicker than the colored lines) - nah this won't work because grouped - cba to break it out
Plot.line(plotData, {
x: "years",
y: "cumulativeCount",
stroke: "white",
strokeWidth: 5, // Thicker than the colored lines
curve: "step",
z: "pm"
}),
// Colored lines on top
Plot.line(plotData, {
x: "years",
y: "cumulativeCount",
stroke: (d) => pmColor[d.pm] || "#999",
strokeWidth: 2.5,
curve: "step", // Step curve for the lines
z: "pm",
markerEnd: "dot",
tip: true,
strokeOpacity: (d) => (d.pm === "Starmer" ? 1 : 0.7) // Full opacity for Starmer, 0.8 for others
}),
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) // Match the opacity of the lines
}),
Plot.ruleY([0])
]
});
}
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more