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

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

FEC Partisanship Data

← Back to KAPI Lab

Examining partisan dimensions of Federal Election Commission enforcement actions

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/fec.json").json()
yearRanges = ["All", "2020-2024", "2015-2019", "2010-2014", "2005-2009", "2000-2004"]
complaintTypes = ["All", ...new Set(data.map(d => d.complaint_type)).values()].sort()
parties = ["All", ...new Set(data.map(d => d.target_party)).values()].sort()
viewof yearFilter = Inputs.select(yearRanges, {label: "Year Range", value: "All"})
viewof typeFilter = Inputs.select(complaintTypes, {label: "Complaint Type", value: "All"})
viewof partyFilter = Inputs.select(parties, {label: "Target Party", value: "All"})
filtered = data.filter(d => {
  let yearOk = true;
  if (yearFilter !== "All") {
    const [startY, endY] = yearFilter.split("-").map(Number);
    yearOk = d.year >= startY && d.year <= endY;
  }
  const typeOk = typeFilter === "All" || d.complaint_type === typeFilter;
  const partyOk = partyFilter === "All" || d.target_party === partyFilter;
  return yearOk && typeOk && partyOk;
})
totalClaims = filtered.length
demPct = totalClaims > 0
  ? Math.round(filtered.filter(d => d.target_party === "Democratic").length / totalClaims * 100)
  : 0
repPct = totalClaims > 0
  ? Math.round(filtered.filter(d => d.target_party === "Republican").length / totalClaims * 100)
  : 0
nonPct = totalClaims > 0
  ? Math.round(filtered.filter(d => d.target_party === "Non-Partisan").length / totalClaims * 100)
  : 0
html`<div class="stat-grid">
  <div class="stat-box">
    <div class="stat-number">${totalClaims}</div>
    <div class="stat-label">Total Claims Coded</div>
  </div>
  <div class="stat-box stat-blue">
    <div class="stat-number">${demPct}%</div>
    <div class="stat-label">Targeting Democrats</div>
  </div>
  <div class="stat-box stat-rose">
    <div class="stat-number">${repPct}%</div>
    <div class="stat-label">Targeting Republicans</div>
  </div>
  <div class="stat-box stat-muted">
    <div class="stat-number">${nonPct}%</div>
    <div class="stat-label">Non-Partisan</div>
  </div>
</div>`
// Time series data by party
timeData = {
  const years = [...new Set(filtered.map(d => d.year))].sort();
  const partyCounts = {};
  for (const d of filtered) {
    const key = `${d.year}-${d.target_party}`;
    partyCounts[key] = (partyCounts[key] || 0) + 1;
  }
  const result = [];
  for (const year of years) {
    for (const party of ["Democratic", "Republican", "Non-Partisan"]) {
      result.push({
        year: year,
        party: party,
        count: partyCounts[`${year}-${party}`] || 0
      });
    }
  }
  return result;
}
// Grouped data: complaint type by party
typePartyData = {
  const types = [...new Set(filtered.map(d => d.complaint_type))].sort();
  const result = [];
  for (const type of types) {
    for (const party of ["Democratic", "Republican", "Non-Partisan"]) {
      const count = filtered.filter(d => d.complaint_type === type && d.target_party === party).length;
      result.push({ complaint_type: type, party, count });
    }
  }
  return result;
}

Claims by Target Party Over Time

Plot.plot({
  marginLeft: 50,
  height: 300,
  x: { label: "Year" },
  y: { label: "Number of Claims" },
  color: {
    domain: ["Democratic", "Republican", "Non-Partisan"],
    range: ["#5B7FA5", "#B85C5C", "#7A8290"],
    legend: true
  },
  marks: [
    Plot.line(timeData, {
      x: "year",
      y: "count",
      stroke: "party",
      strokeWidth: 2,
      strokeDasharray: d => d.party === "Non-Partisan" ? "4 3" : undefined
    }),
    Plot.dot(timeData, {
      x: "year",
      y: "count",
      fill: "party",
      r: 3
    })
  ]
})

Claim Types by Target Party

Plot.plot({
  marginLeft: 130,
  height: 300,
  x: { label: "Count" },
  y: { label: null },
  color: {
    domain: ["Democratic", "Republican", "Non-Partisan"],
    range: ["#5B7FA5", "#B85C5C", "#7A8290"],
    legend: true
  },
  marks: [
    Plot.barX(typePartyData, Plot.groupY(
      { x: "sum" },
      {
        y: "complaint_type",
        x: "count",
        fill: "party",
        sort: { y: "-x" }
      }
    )),
    Plot.ruleX([0])
  ]
})
unanimousPct = totalClaims > 0
  ? Math.round(filtered.filter(d => d.vote_pattern === "Unanimous").length / totalClaims * 100)
  : 0
splitPct = totalClaims > 0
  ? Math.round(filtered.filter(d => d.vote_pattern === "Party Line Split").length / totalClaims * 100)
  : 0
deadlockedPct = totalClaims > 0
  ? Math.round(filtered.filter(d => d.vote_pattern === "Deadlocked").length / totalClaims * 100)
  : 0
html`<div class="vote-pattern-grid">
  <div class="vote-pattern-box vote-unanimous">
    <div class="pattern-pct">${unanimousPct}%</div>
    <div class="pattern-label">Unanimous to Proceed</div>
  </div>
  <div class="vote-pattern-box vote-split">
    <div class="pattern-pct">${splitPct}%</div>
    <div class="pattern-label">Split Along Party Lines</div>
  </div>
  <div class="vote-pattern-box vote-deadlocked">
    <div class="pattern-pct">${deadlockedPct}%</div>
    <div class="pattern-label">Deadlocked / No Action</div>
  </div>
</div>`

Data Table

Inputs.table(filtered, {
  columns: ["mur_number", "year", "respondent", "complaint_type", "target_party", "outcome"],
  header: {
    mur_number: "MUR #",
    year: "Year",
    respondent: "Respondent",
    complaint_type: "Type",
    target_party: "Target Party",
    outcome: "Outcome"
  },
  sort: "year",
  reverse: true,
  rows: 20
})

Still building — this dashboard is under construction. The data shown is placeholder while the coding pipeline is being finalized.

 

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