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

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

Acting Appointees Tracker

← Back to KAPI Lab

Explore data on presidential use of acting appointees and vacancies across federal departments and agencies

Under construction. This dashboard is a design preview populated with simulated placeholder data; it does not show real records. The actual dataset will replace it when it is ready for public release. Watch this space.

data = FileAttachment("../data/actings.json").json()
admins = ["All", ...new Set(data.map(d => d.administration))]
depts = ["All", ...new Set(data.map(d => d.department)).values()].sort()
levels = ["All", ...new Set(data.map(d => d.position_level)).values()].sort()
viewof adminFilter = Inputs.select(admins, {label: "Administration", value: "All"})
viewof deptFilter = Inputs.select(depts, {label: "Department", value: "All"})
viewof levelFilter = Inputs.select(levels, {label: "Position Level", value: "All"})
filtered = data.filter(d =>
  (adminFilter === "All" || d.administration === adminFilter) &&
  (deptFilter === "All" || d.department === deptFilter) &&
  (levelFilter === "All" || d.position_level === levelFilter)
)
totalPositions = filtered.length
actingCount = filtered.filter(d => d.status === "Acting").length
vacantCount = filtered.filter(d => d.status === "Vacant").length
avgDays = filtered.length > 0
  ? Math.round(filtered.reduce((sum, d) => sum + d.days_served, 0) / filtered.length)
  : 0
html`<div class="stat-grid">
  <div class="stat-box">
    <div class="stat-number">${totalPositions}</div>
    <div class="stat-label">Total Positions Tracked</div>
  </div>
  <div class="stat-box stat-terracotta">
    <div class="stat-number">${actingCount}</div>
    <div class="stat-label">Filled by Actings</div>
  </div>
  <div class="stat-box stat-rose">
    <div class="stat-number">${vacantCount}</div>
    <div class="stat-label">Left Vacant</div>
  </div>
  <div class="stat-box">
    <div class="stat-number">${avgDays}</div>
    <div class="stat-label">Avg. Days as Acting</div>
  </div>
</div>`
// Aggregate data for stacked bar by department
deptStatusData = {
  const depts = [...new Set(filtered.map(d => d.department))];
  const statuses = ["Acting", "Confirmed", "Vacant"];
  const result = [];
  for (const dept of depts) {
    for (const status of statuses) {
      const count = filtered.filter(d => d.department === dept && d.status === status).length;
      if (count > 0) {
        result.push({ department: dept, status, count });
      }
    }
  }
  return result;
}
// Aggregate data for timeline by quarter
timelineData = {
  const result = [];
  const quarters = {};

  for (const d of filtered) {
    const date = new Date(d.start_date);
    const q = Math.floor(date.getMonth() / 3) + 1;
    const key = `${date.getFullYear()}-Q${q}`;

    if (!quarters[key]) {
      quarters[key] = { quarter: key, date: new Date(date.getFullYear(), (q - 1) * 3, 1), Acting: 0, Vacant: 0 };
    }
    if (d.status === "Acting") quarters[key].Acting++;
    else if (d.status === "Vacant") quarters[key].Vacant++;
  }

  for (const key of Object.keys(quarters).sort()) {
    result.push({ date: quarters[key].date, count: quarters[key].Acting, series: "Acting" });
    result.push({ date: quarters[key].date, count: quarters[key].Vacant, series: "Vacant" });
  }
  return result;
}

Position Status by Department

Plot.plot({
  marginLeft: 160,
  marginRight: 20,
  height: Math.max(300, deptStatusData.length * 8),
  x: { label: "Count" },
  y: { label: null },
  color: {
    domain: ["Acting", "Confirmed", "Vacant"],
    range: ["#C06840", "#6B8F6B", "#B85C5C"],
    legend: true
  },
  marks: [
    Plot.barX(deptStatusData, {
      y: "department",
      x: "count",
      fill: "status",
      sort: { y: "-x" }
    }),
    Plot.ruleX([0])
  ]
})

Acting Appointees Over Time

Plot.plot({
  marginLeft: 50,
  height: 300,
  x: { label: "Quarter", type: "utc" },
  y: { label: "Count" },
  color: {
    domain: ["Acting", "Vacant"],
    range: ["#C06840", "#B85C5C"],
    legend: true
  },
  marks: [
    Plot.line(timelineData, {
      x: "date",
      y: "count",
      stroke: "series",
      strokeWidth: 2
    }),
    Plot.dot(timelineData, {
      x: "date",
      y: "count",
      fill: "series",
      r: 3
    })
  ]
})

Data Table

viewof searchActings = Inputs.text({ placeholder: "Search by name...", width: 300 })
tableFiltered = filtered.filter(d =>
  d.name.toLowerCase().includes(searchActings.toLowerCase())
)
Inputs.table(tableFiltered, {
  columns: ["name", "position_title", "department", "administration", "status", "days_served"],
  header: {
    name: "Name",
    position_title: "Position",
    department: "Department",
    administration: "Administration",
    status: "Status",
    days_served: "Days Served"
  },
  sort: "days_served",
  reverse: true,
  rows: 20
})

Note: Sample data is shown for demonstration purposes. This data is mock/simulated and does not represent actual appointee records.

 

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