Skip to contents

Raptors vs. Warriors: Jun 5, 2019

Q1
Q2
Q3
Q4
Final
36
24
36
27
123
29
23
31
26
109
Toronto Raptors
Player
Min
FG
FG%
3P
3P%
FT
FT%
ORB
DRB
REB
AST
STL
BLK
TO
PF
PTS
+/-
38:25
9-17
52.9%
2-6
33.3%
10-11
90.9%
2
5
7
6
2
2
4
3
30
+6
43:22
8-16
50%
5-9
55.6%
2-2
100%
0
4
4
9
1
1
3
3
23
+14
27:12
6-10
60%
6-10
60%
0-0
0%
0
5
5
1
1
1
1
5
18
+11
38:40
8-16
50%
0-3
0%
2-2
100%
1
8
9
6
0
0
2
0
18
+22
26:10
6-11
54.5%
1-4
25%
4-4
100%
0
7
7
4
0
0
2
4
17
+14
33:19
4-8
50%
3-6
50%
0-0
0%
1
2
3
2
3
0
0
3
11
+4
21:50
2-4
50%
0-0
0%
2-2
100%
1
4
5
1
2
6
2
4
6
+0
0:52
0-0
0%
0-0
0%
0-0
0%
0
0
0
0
0
0
0
0
0
+2
0:52
0-0
0%
0-0
0%
0-0
0%
0
0
0
0
0
0
0
0
0
+2
0:52
0-0
0%
0-0
0%
0-0
0%
0
0
0
0
0
0
0
0
0
+2
6:13
0-0
0%
0-0
0%
0-0
0%
0
0
0
0
0
0
0
0
0
-2
2:13
0-0
0%
0-0
0%
0-0
0%
0
0
0
1
0
0
0
0
0
-5
DNP
Golden State Warriors
Player
Min
FG
FG%
3P
3P%
FT
FT%
ORB
DRB
REB
AST
STL
BLK
TO
PF
PTS
+/-
43:15
14-31
45.2%
6-14
42.9%
13-14
92.9%
2
6
8
7
2
0
3
1
47
-8
40:36
6-14
42.9%
2-6
33.3%
3-3
100%
1
6
7
4
2
0
4
4
17
-10
30:38
4-8
50%
2-6
33.3%
1-2
50%
2
4
6
3
1
0
0
0
11
-14
26:38
4-9
44.4%
0-2
0%
1-1
100%
0
1
1
2
1
0
2
2
9
-8
9:51
1-6
16.7%
1-3
33.3%
3-4
75%
2
0
2
1
0
0
0
1
6
+0
21:36
3-4
75%
0-0
0%
0-0
0%
3
4
7
3
1
1
1
3
6
+0
17:20
1-4
25%
0-0
0%
2-2
100%
1
1
2
2
0
0
0
2
4
-14
19:24
1-7
14.3%
0-1
0%
2-3
66.7%
0
3
3
2
1
0
3
2
4
-12
18:05
1-4
25%
1-3
33.3%
0-1
0%
1
2
3
1
0
0
1
4
3
+0
9:23
1-1
100%
0-0
0%
0-0
0%
1
1
2
0
0
2
0
2
2
-2
1:37
0-0
0%
0-0
0%
0-0
0%
0
0
0
0
0
0
0
0
0
-1
1:37
0-3
0%
0-1
0%
0-0
0%
0
0
0
0
0
0
0
0
0
-1
DNP

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;
}