QB Salaries vs Playoff Appearances

NFL tidyverse tables

Interactive tables make bad takes more fun.

Thomas Mock

A recent tweet provided a (IMO) fairly week argument that paying a QB ends up making your team unsuccessful (no Superbowl wins for the 9 QBs below).

Highest cap hits from 2014-19:

Matthew Stafford ($130M)
Ben Roethlisberger ($128M)
Aaron Rodgers ($126M)
Drew Brees ($125M)
Eli Manning ($124M)
Matt Ryan ($118M)
Philip Rivers ($117M)
Joe Flacco ($106M)
Cam Newton ($104M)

None won the Super Bowl during that time.

— Paul Hembekides (@PaulHembo) May 12, 2020

Moo had a good counter-argument (just include the 10th QB) - Tom Brady ruins part of the narrative. Additionally, just include playoff appearances and/or wins.

The thing here is the following:

If you include Brady who is the next in the list ($100M), these 10 QBs combine for 3 of 6 Super Bowl titles and 6 of 12 Super Bowl appearances.

Accounting for 50% is pretty good for 10/32 of the league, isn't it? https://t.co/C18MQAHfL8

— Moo (@PFF_Moo) May 12, 2020

So let’s make a table of salary data vs playoff appearances! We could make this static (screenshot for Twitter) - but just for funs lets stretch a bit and make an interactive table with nice formatting.

reactable - interactive data tables

reactable is an R wrapper for the react table javascript library. Greg Lin at RStudio recently made this package and you can install it from CRAN with install.packages("reactable"). I adapted this table from some examples at the reactable package site.

Raw data comes from: Pro Football Reference & Over the Cap

A very basic reactable table can be created as so:

playoff_salary %>%

More Complex

At the risk of drawing the rest of the *&?$ing owl here is a more complex interactive table using the same data.

Part 2 of this blogpost will go step-by-step into creating more complex tables, but for now…enjoy and consume at your own risk!

(Full code at bottom of this post)

2014-2019 Salary and Playoff Appearances

QBs limited to playoff games where they threw a pass

Raw Code to generate the table

Below is the raw code to generate the table - I’ll do a deeper dive later, but as of now here is the raw code I used, including some HTML and CSS helpers. This is all adapted from the reactable cookbook.

The CSS and HTML helpers do some light-lifting for custom text and borders.

make_color_pal <- function(colors, bias = 1) {
  get_color <- colorRamp(colors, bias = bias)
  function(x) rgb(get_color(x), maxColorValue = 255)

good_color <- make_color_pal(c("#ffffff", "#f2fbd2", "#c9ecb4", "#93d3ab", "#35b0ab"), bias = 2)

tbl <- playoff_salary %>%
  arrange(desc(salary)) %>%
    `Salary Rank` = rank(desc(salary)),
    salary = round(salary, 1)
  ) %>%
  select(`Salary Rank`, player:Superbowl, everything()) %>%
    pagination = FALSE,
    compact = TRUE,
    borderless = FALSE,
    striped = FALSE,
    fullWidth = FALSE,
    theme = reactableTheme(
      headerStyle = list(
        "&:hover[aria-sort]" = list(background = "hsl(0, 0%, 96%)"),
        "&[aria-sort='ascending'], &[aria-sort='descending']" = list(background = "hsl(0, 0%, 96%)"),
        borderColor = "#555"
    defaultColDef = colDef(
      align = "center",
      minWidth = 100
    columns = list(
      salary = colDef(
        name = "Salary",
        style = function(value) {
          normalized <- (value - min(playoff_salary$salary)) / (max(playoff_salary$salary) - min(playoff_salary$salary))
          color <- good_color(normalized)
          list(background = color)
        cell = JS("function(cellInfo) {
                          return cellInfo.value + 'M'}")
      Total = colDef(
        style = function(value) {
          normalized <- (value - min(playoff_salary$Total)) / (max(playoff_salary$Total) - min(playoff_salary$Total))
          color <- good_color(normalized)
          list(background = color)
        class = "border-left"
      player = colDef(
        name = "Name",
        minWidth = 140,
        align = "left"

  class = "salary",
    class = "title",
    h2("2014-2019 Salary and Playoff Appearances"),
    "QBs limited to playoff games where they threw a pass"

tags$link(href = "https://fonts.googleapis.com/css?family=Karla:400,700|Fira+Mono&display=fallback", rel = "stylesheet")

.salary {
  font-family: Karla, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 14px;

.number {
  font-family: "Fira Mono", Consolas, Monaco, monospace;
  font-size: 16px;
  line-height: 30px;
  white-space: pre;

.title {
  margin: 18px 0;
  font-size: 16px;

.title h2 {
  font-size: 20px;
  font-weight: 600;

.header[aria-sort="descending"] {
  background-color: #eee;

.salary-table {
  margin-bottom: 20px;

/* Align header text to the bottom */
.group-header {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;

.header {
  border-bottom-color: #555;
  font-size: 13px;
  font-weight: 400;
  text-transform: uppercase;

/* Highlight headers when sorting */
.header[aria-sort="descending"] {
  background-color: #eee;

.border-left {
  border-left: 2px solid #555;

/* Use box-shadow to create row borders that appear behind vertical borders */
.cell {
  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);


For attribution, please cite this work as

Mock (2020, May 13). The Mockup Blog: QB Salaries vs Playoff Appearances. Retrieved from https://themockup.blog/posts/2020-05-13-qb-salaries-vs-playoff-appearances/

BibTeX citation

  author = {Mock, Thomas},
  title = {The Mockup Blog: QB Salaries vs Playoff Appearances},
  url = {https://themockup.blog/posts/2020-05-13-qb-salaries-vs-playoff-appearances/},
  year = {2020}