Source: NBA.com
Raw data: line_score.csv
,
player_stats.csv
, team_stats.csv
Source Code
library(reactable)
library(htmltools)
player_stats <- read.csv("player_stats.csv", stringsAsFactors = FALSE)
team_stats <- read.csv("team_stats.csv", stringsAsFactors = FALSE)
line_score <- read.csv("line_score.csv", stringsAsFactors = FALSE)
line_score <- line_score[, c("TEAM_ID", "TEAM_CITY_NAME", "TEAM_NICKNAME", "TEAM_WINS_LOSSES",
"PTS_QTR1", "PTS_QTR2", "PTS_QTR3", "PTS_QTR4", "PTS")]
line_score_tbl <- reactable(
line_score,
sortable = FALSE,
defaultColDef = colDef(headerClass = "line-score-header", align = "center", minWidth = 50),
columns = list(
TEAM_ID = colDef(show = FALSE),
TEAM_CITY_NAME = colDef(
name = "",
align = "left",
minWidth = 250,
cell = function(value, index) {
team_url <- sprintf("https://stats.nba.com/team/%s/traditional", line_score[index, "TEAM_ID"])
team_name <- paste(value, line_score[index, "TEAM_NICKNAME"])
team_record <- line_score[index, "TEAM_WINS_LOSSES"]
tagList(
tags$a(class = "team-name", href = team_url, target = "_blank", team_name),
span(class = "team-record", team_record)
)
}
),
TEAM_NICKNAME = colDef(show = FALSE),
TEAM_WINS_LOSSES = colDef(show = FALSE),
PTS_QTR1 = colDef(name = "Q1"),
PTS_QTR2 = colDef(name = "Q2"),
PTS_QTR3 = colDef(name = "Q3"),
PTS_QTR4 = colDef(name = "Q4"),
PTS = colDef(name = "Final", class = "line-score-final")
),
class = "line-score-tbl"
)
box_score_tbl <- function(player_stats, team_stats, team) {
# Convert M:SS strings to datetimes for proper sorting
player_stats$MIN_STR <- player_stats$MIN
player_stats$MIN <- strptime(player_stats$MIN, format = "%M:%S")
cols <- c("PLAYER_ID", "PLAYER_NAME", "START_POSITION", "MIN", "MIN_STR",
"FGM", "FGA", "FG_PCT", "FG3M", "FG3A", "FG3_PCT", "FTM", "FTA",
"FT_PCT", "OREB", "DREB", "REB", "AST", "STL", "BLK", "TO", "PF",
"PTS", "PLUS_MINUS")
stats <- player_stats[player_stats$TEAM_ABBREVIATION == team, cols]
team_stats <- team_stats[team_stats$TEAM_ABBREVIATION == team, ]
reactable(
stats,
pagination = FALSE,
defaultSortOrder = "desc",
defaultSorted = "PTS",
defaultColDef = colDef(
sortNALast = TRUE,
minWidth = 45,
class = JS("function(rowInfo, column, state) {
// Highlight sorted columns
for (let i = 0; i < state.sorted.length; i++) {
if (state.sorted[i].id === column.id) {
return 'sorted'
}
}
}"),
headerClass = "box-score-header",
footer = function(values, name) {
value <- team_stats[[name]]
# Format shots made-attempted
if (name %in% c("FGM", "FG3M", "FTM")) {
attempted_name <- c(FGM = "FGA", FG3M = "FG3A", FTM = "FTA")[name]
value <- sprintf("%s-%s", value, team_stats[[attempted_name]])
}
# Format percentages
if (name %in% c("FG_PCT", "FG3_PCT", "FT_PCT")) {
value <- paste0(value * 100, "%")
}
# Format +/-
if (name == "PLUS_MINUS") {
value <- sprintf("%+d", value)
}
value
}
),
columns = list(
PLAYER_ID = colDef(show = FALSE),
PLAYER_NAME = colDef(
name = "Player",
defaultSortOrder = "asc",
width = 130,
cell = function(value, index) {
player_id <- stats[index, "PLAYER_ID"]
player_url <- sprintf("https://stats.nba.com/player/%s", player_id)
start_position <- stats[index, "START_POSITION"]
if (start_position != "") {
value <- tagList(value, " ", tags$sup(start_position))
}
tags$a(href = player_url, target = "_blank", value)
},
footer = span(class = "box-score-total", "Totals")
),
START_POSITION = colDef(show = FALSE),
MIN = colDef(name = "Min", minWidth = 60, align = "right", cell = function(value, index) {
if (!is.na(value)) stats[index, "MIN_STR"] else "DNP"
}),
MIN_STR = colDef(show = FALSE),
FGM = colDef(name = "FG", minWidth = 55, cell = function(value, index) {
if (!is.na(value)) sprintf("%s-%s", value, stats[index, "FGA"])
}),
FGA = colDef(show = FALSE),
FG_PCT = colDef(name = "FG%", minWidth = 55, format = colFormat(percent = TRUE)),
FG3M = colDef(name = "3P", minWidth = 55, cell = function(value, index) {
if (!is.na(value)) sprintf("%s-%s", value, stats[index, "FG3A"])
}),
FG3A = colDef(name = "3PA", show = FALSE),
FG3_PCT = colDef(name = "3P%", minWidth = 55, format = colFormat(percent = TRUE)),
FTM = colDef(name = "FT", minWidth = 55, cell = function(value, index) {
if (!is.na(value)) sprintf("%s-%s", value, stats[index, "FTA"])
}),
FTA = colDef(show = FALSE),
FT_PCT = colDef(name = "FT%", minWidth = 55, format = colFormat(percent = TRUE)),
OREB = colDef(name = "ORB"),
DREB = colDef(name = "DRB"),
PLUS_MINUS = colDef(name = "+/-", cell = function(value) {
if (is.na(value)) "" else sprintf("%+d", value)
})
),
showSortIcon = FALSE,
highlight = TRUE,
striped = TRUE,
class = "box-score-tbl",
theme = reactableTheme(cellPadding = "8px")
)
}
div(class = "box-score",
h2(class = "header", "Raptors vs. Warriors:",
tags$a(class = "game-date", href="https://stats.nba.com/game/0041800403", target = "_blank", "Jun 5, 2019")),
div(class = "line-score", line_score_tbl),
div(class = "box-score-title", "Toronto Raptors"),
box_score_tbl(player_stats, team_stats, "TOR"),
div(class = "box-score-title", "Golden State Warriors"),
box_score_tbl(player_stats, team_stats, "GSW")
)
htmltools::tags$link(href = "https://fonts.googleapis.com/css?family=Roboto:400,500&display=fallback", rel = "stylesheet")
.box-score {
font-family: 'Roboto', Helvetica, Arial, sans-serif;
}
.box-score a {
color: #337ab7;
text-decoration: none;
}
.box-score a:hover,
.box-score a:focus {
text-decoration: underline;
text-decoration-thickness: max(1px, 0.0625rem);
}
.header {
text-align: center;
font-size: 1.25rem;
}
.game-date {
font-size: 1rem;
}
.line-score {
margin-top: 1.5rem;
text-align: center;
}
.line-score-tbl {
margin: 0 auto;
max-width: 32rem;
font-size: 0.9375rem;
}
.line-score-header {
font-size: 0.8125rem;
font-weight: 400;
}
.line-score-final {
font-weight: 500;
}
.team-name {
font-weight: 500;
}
.team-record {
margin-left: 0.375rem;
color: hsl(0, 0%, 45%);
font-size: 0.75rem;
}
.box-score-title {
margin-top: 1.5rem;
padding: 0.5rem;
background-color: hsl(205, 100%, 36%);
color: hsl(0, 0%, 98%);
font-size: 0.9375rem;
font-weight: 400;
}
.box-score-tbl {
font-size: 0.75rem;
letter-spacing: 0.2px;
}
.box-score-header {
border-bottom-width: 1px;
background-color: hsl(205, 93%, 16%);
color: hsl(0, 0%, 98%);
font-weight: 400;
font-size: 0.7rem;
text-transform: uppercase;
transition: box-shadow 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.box-score-header:hover,
.box-score-header:focus,
.box-score-header[aria-sort="ascending"],
.box-score-header[aria-sort="descending"] {
background-color: hsl(205, 100%, 36%);
}
.box-score-header[aria-sort="ascending"] {
box-shadow: inset 0 10px 0 -6px #efaa10;
}
.box-score-header[aria-sort="descending"] {
box-shadow: inset 0 -10px 0 -6px #efaa10;
}
.sorted {
background-color: hsla(0, 0%, 60%, 0.1);
}
.box-score-total {
font-size: 0.8125rem;
font-weight: 500;
}