Christina M. Kinane
  • Research
  • Teaching
  • In the News
  • CV
  • KAPI Lab
    • Lab Overview

    • Acting Appointees Data
    • Nominations Tracker
    • FEC Partisanship Data
    • IRC Rulemaking Tracker

Nominations Tracker

← Back to KAPI Lab

Track the progress of presidential nominations from submission through Senate confirmation

data = FileAttachment("../data/nominations.json").json()
admins = ["All", ...new Set(data.map(d => d.administration))]
statuses = ["All", ...new Set(data.map(d => d.status))]
viewof adminFilter = Inputs.select(admins, {label: "Administration", value: "All"})
viewof statusFilter = Inputs.select(statuses, {label: "Status", value: "All"})
viewof nameSearch = Inputs.text({placeholder: "Search by nominee name...", label: "Search", width: 250})
filtered = data.filter(d =>
  (adminFilter === "All" || d.administration === adminFilter) &&
  (statusFilter === "All" || d.status === statusFilter) &&
  d.nominee_name.toLowerCase().includes(nameSearch.toLowerCase())
)
totalNoms = filtered.length
confirmedCount = filtered.filter(d => d.status === "Confirmed").length
withdrawnCount = filtered.filter(d => d.status === "Withdrawn").length
confirmedOnly = filtered.filter(d => d.status === "Confirmed" && d.days_to_resolution != null)
avgDaysConfirm = confirmedOnly.length > 0
  ? Math.round(confirmedOnly.reduce((sum, d) => sum + d.days_to_resolution, 0) / confirmedOnly.length)
  : 0
html`<div class="stat-grid">
  <div class="stat-box">
    <div class="stat-number">${totalNoms}</div>
    <div class="stat-label">Total Nominations</div>
  </div>
  <div class="stat-box stat-sage">
    <div class="stat-number">${confirmedCount}</div>
    <div class="stat-label">Confirmed</div>
  </div>
  <div class="stat-box stat-rose">
    <div class="stat-number">${withdrawnCount}</div>
    <div class="stat-label">Withdrawn</div>
  </div>
  <div class="stat-box">
    <div class="stat-number">${avgDaysConfirm}</div>
    <div class="stat-label">Avg. Days to Confirm</div>
  </div>
</div>`
outcomeData = {
  const counts = {};
  for (const d of filtered) {
    const s = d.status.startsWith("Pending") ? "Pending" : d.status;
    counts[s] = (counts[s] || 0) + 1;
  }
  return Object.entries(counts).map(([status, count]) => ({status, count}));
}

doughnutColors = {
  return {
    "Confirmed": "#6B8F6B",
    "Withdrawn": "#B85C5C",
    "Returned": "#7A8290",
    "Pending": "#5B7FA5"
  };
}
// Average days to confirmation by administration
avgByAdmin = {
  const admins = [...new Set(data.map(d => d.administration))];
  const result = [];
  for (const admin of admins) {
    const confirmed = filtered.filter(d => d.administration === admin && d.status === "Confirmed" && d.days_to_resolution != null);
    if (confirmed.length > 0) {
      const avg = Math.round(confirmed.reduce((s, d) => s + d.days_to_resolution, 0) / confirmed.length);
      result.push({ administration: admin, avg_days: avg });
    }
  }
  return result;
}

Nomination Outcomes

{
  const width = 300;
  const height = 300;
  const radius = Math.min(width, height) / 2;
  const innerRadius = radius * 0.55;

  const svg = d3.create("svg")
    .attr("viewBox", [-width / 2, -height / 2, width, height])
    .attr("width", width)
    .attr("height", height)
    .style("font-family", "Nunito Sans, sans-serif");

  const color = d3.scaleOrdinal()
    .domain(["Confirmed", "Withdrawn", "Returned", "Pending"])
    .range(["#6B8F6B", "#B85C5C", "#7A8290", "#5B7FA5"]);

  const pie = d3.pie().value(d => d.count).sort(null).padAngle(0.02);
  const arc = d3.arc().innerRadius(innerRadius).outerRadius(radius - 2);
  const labelArc = d3.arc().innerRadius(radius * 0.78).outerRadius(radius * 0.78);

  const arcs = pie(outcomeData);

  svg.selectAll("path")
    .data(arcs)
    .join("path")
    .attr("fill", d => color(d.data.status))
    .attr("d", arc)
    .attr("stroke", "#FEFCF8")
    .attr("stroke-width", 2);

  svg.selectAll("text")
    .data(arcs)
    .join("text")
    .attr("transform", d => `translate(${labelArc.centroid(d)})`)
    .attr("text-anchor", "middle")
    .attr("font-size", "11px")
    .attr("font-weight", "600")
    .attr("fill", "#1E2D4F")
    .text(d => d.data.count > 5 ? `${d.data.status} (${d.data.count})` : "");

  // Center label
  svg.append("text")
    .attr("text-anchor", "middle")
    .attr("font-size", "14px")
    .attr("font-weight", "700")
    .attr("fill", "#1E2D4F")
    .attr("dy", "-0.2em")
    .text(totalNoms);
  svg.append("text")
    .attr("text-anchor", "middle")
    .attr("font-size", "10px")
    .attr("fill", "#7A8290")
    .attr("dy", "1.2em")
    .text("Total");

  return svg.node();
}

Avg. Days to Confirmation by Administration

Plot.plot({
  marginLeft: 80,
  height: 300,
  x: { label: "Days" },
  y: { label: null },
  color: { range: ["#C06840"] },
  marks: [
    Plot.barX(avgByAdmin, {
      y: "administration",
      x: "avg_days",
      fill: "#C06840",
      sort: { y: "-x" }
    }),
    Plot.text(avgByAdmin, {
      y: "administration",
      x: "avg_days",
      text: d => `${d.avg_days}d`,
      dx: 5,
      fill: "#4F5B6E",
      fontSize: 11,
      textAnchor: "start"
    }),
    Plot.ruleX([0])
  ]
})
// Pipeline data
pipelineData = {
  const total = filtered.length;
  const confirmed = filtered.filter(d => d.status === "Confirmed").length;
  const withdrawn = filtered.filter(d => d.status === "Withdrawn").length;
  const returned = filtered.filter(d => d.status === "Returned").length;
  const pendingCommittee = filtered.filter(d => d.status === "Pending - Committee").length;
  const pendingFloor = filtered.filter(d => d.status === "Pending - Floor Vote").length;

  // Simulate pipeline stages
  const submitted = total;
  const reachedCommittee = total - withdrawn;
  const reachedFloor = reachedCommittee - returned - pendingCommittee;
  const finalConfirmed = confirmed;

  return {
    stages: [
      { label: "Submitted", count: submitted },
      { label: "Committee", count: Math.max(reachedCommittee, 0) },
      { label: "Floor Vote", count: Math.max(reachedFloor, 0) },
      { label: "Confirmed", count: Math.max(finalConfirmed, 0) }
    ],
    dropouts: [
      { label: `${withdrawn} withdrawn`, between: "0-1" },
      { label: `${returned + pendingCommittee} returned/pending`, between: "1-2" },
      { label: `${pendingFloor} pending floor`, between: "2-3" }
    ],
    total: submitted
  };
}

Nomination Pipeline

{
  const total = pipelineData.total || 1;
  const stages = pipelineData.stages;
  const colors = ["#5B7FA5", "#C06840", "#B8923E", "#6B8F6B"];

  const container = d3.create("div");

  // Bar
  const bar = container.append("div")
    .style("display", "flex")
    .style("width", "100%")
    .style("border-radius", "8px")
    .style("overflow", "hidden")
    .style("height", "48px");

  stages.forEach((stage, i) => {
    const pct = (stage.count / total) * 100;
    bar.append("div")
      .style("width", `${pct}%`)
      .style("background", colors[i])
      .style("display", "flex")
      .style("align-items", "center")
      .style("justify-content", "center")
      .style("color", "#fff")
      .style("font-size", "0.78rem")
      .style("font-weight", "700")
      .style("padding", "0 6px")
      .style("min-width", "40px")
      .text(`${stage.label} (${stage.count})`);
  });

  // Dropout labels
  const dropouts = container.append("div")
    .style("display", "flex")
    .style("width", "100%")
    .style("margin-top", "6px");

  pipelineData.dropouts.forEach((d, i) => {
    const pct = (stages[i].count / total) * 100;
    dropouts.append("div")
      .style("width", `${pct}%`)
      .style("text-align", "center")
      .style("font-size", "0.72rem")
      .style("color", "#B85C5C")
      .style("font-style", "italic")
      .text(`↓ ${d.label}`);
  });

  return container.node();
}

Data Table

Inputs.table(filtered, {
  columns: ["nominee_name", "position", "department", "date_submitted", "status", "days_to_resolution"],
  header: {
    nominee_name: "Nominee",
    position: "Position",
    department: "Department",
    date_submitted: "Date Submitted",
    status: "Status",
    days_to_resolution: "Days"
  },
  sort: "date_submitted",
  reverse: true,
  rows: 20
})
 

© 2026 Christina M. Kinane · Yale University · Department of Political Science
Email · Google Scholar · Twitter/X · Yale Profile