From 87f2482a7ce18c83fc8604b4016dc6ccc46f0df2 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Thu, 6 Aug 2020 20:39:11 +1000 Subject: [PATCH 01/56] get rid of warning --- R/ladders.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/ladders.R b/R/ladders.R index 425937d..b675e3e 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -50,6 +50,6 @@ matchResults <- function(df) { dplyr::group_by(round, game) %>% dplyr::mutate(game_results = purrr::map(data, matchPoints)) %>% dplyr::select(-data) %>% - tidyr::unnest() + tidyr::unnest(cols = c(game_results)) df } From 6b430f7521432575c2ce0b7ac948321eac2a9d65 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Thu, 6 Aug 2020 21:14:50 +1000 Subject: [PATCH 02/56] initial commit of 2020 season super scoring --- R/matchPoints.R | 129 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/R/matchPoints.R b/R/matchPoints.R index 46b6f86..abdd019 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -70,6 +70,135 @@ matchPoints <- function(df) { dplyr::rename(squadName = awaySquad, points_qtr = awayPoints) points_new <- dplyr::bind_rows(df1, df2) + ## Now join back on to original scoring. + goals <- dplyr::left_join(goals, points_new, by = "squadName") %>% + dplyr::mutate(points_new = points_new + points_qtr) %>% + dplyr::select(-points_qtr) + + ## This section calculates goals/points based on the 2020 system. Includes + ## the super goal, and no bonus points. + goals_new1 <- df %>% + dplyr::filter(stat == "goal_from_zone1") + homeScores1 <- goals_new1 %>% + dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% + dplyr::select(period, homeSquad = squadName, homeValue = value) + awayScores1 <- goals_new1 %>% + dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) %>% + dplyr::select(period, awaySquad = squadName, awayValue = value) + goals_new2 <- df %>% + dplyr::filter(stat == "goal_from_zone2") + homeScores2 <- goals_new2 %>% + dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% + dplyr::select(period, homeSquad = squadName, homeValue2 = value) + awayScores2 <- goals_new2 %>% + dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) %>% + dplyr::select(period, awaySquad = squadName, awayValue2 = value) + homeScores <- dplyr::left_join(homeScores1, homeScores2, + by = c("period", "homeSquad")) %>% + mutate(homePoints = homeValue + homeValue2 * 2) %>% + select(-homeValue2) + awayScores <- dplyr::left_join(awayScores1, awayScores2, + by = c("period", "awaySquad")) %>% + mutate(awayPoints = awayValue + awayValue2 * 2) %>% + select(-awayValue2) + + points_2020 <- dplyr::left_join(homeScores, awayScores, by = "period") %>% + dplyr::group_by(homeSquad, awaySquad) %>% + dplyr::summarise(homePoints = sum(homePoints), + awayPoints = sum(awayPoints)) %>% + dplyr::ungroup() + df1 <- points_2020 %>% + dplyr::select(dplyr::contains("home")) %>% + dplyr::rename(squadName = homeSquad, goals_2020 = homePoints) + df2 <- points_2020 %>% + dplyr::select(dplyr::contains("away")) %>% + dplyr::rename(squadName = awaySquad, goals_2020 = awayPoints) + points_2020 <- dplyr::bind_rows(df1, df2) + + browser() + + ## Now join back on to original scoring. + goals <- dplyr::left_join(goals, goals_2020, by = "squadName") %>% + dplyr::mutate(points_new = points_new + points_qtr) %>% + dplyr::select(-points_qtr) + + ## Return + goals +} + +#' Calculates the total goals of the match (pre 2020 season) +#' +#' \code{matchPoints_pre_2020} calculates final match goals and score +#' difference, for seasons pre-2020. +#' +#' @param df Match data. +#' +#' @return A data frame containing the final scores, and points for the ladder. +#' @export +matchPoints_pre_2020 <- function(df) { + ## This first section calculates points based on the old system. + goals <- df %>% + dplyr::filter(stat == "goals") %>% + dplyr::group_by(squadName) %>% + dplyr::summarise(goals = sum(value)) + home <- df %>% + dplyr::filter(stat == "homeTeam") %>% + dplyr::group_by(squadName) %>% + dplyr::select(-period) %>% + dplyr::distinct() + goals <- dplyr::left_join(goals, home, by = "squadName") %>% + dplyr::arrange(value) + score_diff <- diff(goals[['goals']]) + goals <- goals %>% + dplyr::mutate( + score_diff = score_diff, + score_diff = ifelse(value == 0, score_diff * (-1), + score_diff), + points = dplyr::case_when( + score_diff > 0 ~ 2, + score_diff < 0 ~ 0, + TRUE ~ 1 + ), + ## Points for a win (new rules) + points_new = dplyr::case_when( + score_diff > 0 ~ 4, + score_diff < 0 ~ 0, + TRUE ~ 2 + ) + ) %>% + dplyr::rename(isHome = value) %>% + dplyr::select(-stat) + + ## This section calculates points based on the new system (points for + ## winning quarters) + goals_new <- df %>% + dplyr::filter(stat == "goals") + homeScores <- goals_new %>% + dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% + dplyr::select(period, homeSquad = squadName, homeValue = value) + awayScores <- goals_new %>% + dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) %>% + dplyr::select(period, awaySquad = squadName, awayValue = value) + scores <- dplyr::left_join(homeScores, awayScores, by = "period") %>% + dplyr::mutate(qtr_diff = homeValue - awayValue, + homePoints = dplyr::case_when(qtr_diff > 0 ~ 1, + TRUE ~ 0), + awayPoints = dplyr::case_when(qtr_diff < 0 ~ 1, + TRUE ~ 0) + ) + points_new <- scores %>% + dplyr::group_by(homeSquad, awaySquad) %>% + dplyr::summarise(homePoints = sum(homePoints), + awayPoints = sum(awayPoints)) %>% + dplyr::ungroup() + df1 <- points_new %>% + dplyr::select(dplyr::contains("home")) %>% + dplyr::rename(squadName = homeSquad, points_qtr = homePoints) + df2 <- points_new %>% + dplyr::select(dplyr::contains("away")) %>% + dplyr::rename(squadName = awaySquad, points_qtr = awayPoints) + points_new <- dplyr::bind_rows(df1, df2) + ## Now join back on to original scoring. goals <- dplyr::left_join(goals, points_new, by = "squadName") %>% dplyr::mutate(points_new = points_new + points_qtr) %>% From c119afa04ff75e1702eddf9e1ebfb11ee6a48c28 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Thu, 6 Aug 2020 21:52:34 +1000 Subject: [PATCH 03/56] update version --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 5691fa8..8a83ca1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: superNetballR Title: Downloads and tidies super netball statistics -Version: 0.1.0 +Version: 0.2.0 Authors@R: person("Steve", "Lane", email = "lane.s@unimelb.edu.au", role = c("aut", "cre")) Description: This package provides functions to easily download and manipulate data from super netball matches. Depends: R (>= 3.4.0) From 1b487d2f3277fdb8009354ffb6c73b21153c2c86 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Thu, 6 Aug 2020 21:55:06 +1000 Subject: [PATCH 04/56] update readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5814f40..4215f0c 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,7 @@ This package allows the downloading of super netball statistics ([https://stevelane.github.io/superNetballR/](https://stevelane.github.io/superNetballR/). The first super netball season was in 2017, and was eventually won by the Sunshine Coast Lightning. `superNetballR` contains helper functions that transform the downloaded data into usable tidy data. + +## Notes + +The package has been updated to account for the super goal in 2020. Ladders and points have been adjusted for this. If you want to use the old scoring systems, these are available using `_pre_2020` versions of the appropriate functions. From 784b9d6d91e4d4fdb073b15078bc4edccea4c8cc Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Thu, 6 Aug 2020 21:55:15 +1000 Subject: [PATCH 05/56] account for new season and super goals --- R/ladders.R | 53 +++++++++++++---- R/matchPoints.R | 151 +++++++++++------------------------------------- 2 files changed, 76 insertions(+), 128 deletions(-) diff --git a/R/ladders.R b/R/ladders.R index b675e3e..bc76df2 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -14,6 +14,47 @@ #' #' @export ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { + if (!is.null(game_num) && is.null(round_num)) { + stop("If game number is supplied, round number must also be supplied.") + } + match_results <- matchResults(df = df) + if (!is.null(round_num) && is.null(game_num)) { + match_results <- match_results %>% + dplyr::filter(round <= round_num) + } else if (!is.null(round_num) && !is.null(game_num)) { + match_results <- match_results %>% + dplyr::filter(round <= round_num) %>% + dplyr::filter(!(round >= round_num && game > game_num)) + } + ladder <- match_results %>% + dplyr::group_by(squadName) %>% + dplyr::summarise( + games = n(), + goals_for = sum(goals), + goals_against = sum(goals - score_diff), + percentage = goals_for / goals_against, + points = as.integer(sum(points)) + ) %>% + dplyr::arrange(dplyr::desc(points), dplyr::desc(percentage)) + ladder +} + +#' @rdname ladders +#' @export +matchResults <- function(df) { + df <- df %>% + dplyr::group_by(round, game) %>% + tidyr::nest() %>% + dplyr::group_by(round, game) %>% + dplyr::mutate(game_results = purrr::map(data, matchPoints)) %>% + dplyr::select(-data) %>% + tidyr::unnest(cols = c(game_results)) + df +} + +#' @rdname ladders +#' @export +ladders_pre_2020 <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { if (!is.null(game_num) && is.null(round_num)) { stop("If game number is supplied, round number must also be supplied.") } @@ -41,15 +82,3 @@ ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { ladder } -#' @rdname ladders -#' @export -matchResults <- function(df) { - df <- df %>% - dplyr::group_by(round, game) %>% - tidyr::nest() %>% - dplyr::group_by(round, game) %>% - dplyr::mutate(game_results = purrr::map(data, matchPoints)) %>% - dplyr::select(-data) %>% - tidyr::unnest(cols = c(game_results)) - df -} diff --git a/R/matchPoints.R b/R/matchPoints.R index abdd019..b2a9322 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -7,123 +7,42 @@ #' @return A data frame containing the final scores, and points for the ladder. #' @export matchPoints <- function(df) { - ## This first section calculates points based on the old system. - goals <- df %>% - dplyr::filter(stat == "goals") %>% - dplyr::group_by(squadName) %>% - dplyr::summarise(goals = sum(value)) - home <- df %>% - dplyr::filter(stat == "homeTeam") %>% - dplyr::group_by(squadName) %>% - dplyr::select(-period) %>% - dplyr::distinct() - goals <- dplyr::left_join(goals, home, by = "squadName") %>% - dplyr::arrange(value) - score_diff <- diff(goals[['goals']]) - goals <- goals %>% - dplyr::mutate( - score_diff = score_diff, - score_diff = ifelse(value == 0, score_diff * (-1), - score_diff), - points = dplyr::case_when( - score_diff > 0 ~ 2, - score_diff < 0 ~ 0, - TRUE ~ 1 - ), - ## Points for a win (new rules) - points_new = dplyr::case_when( - score_diff > 0 ~ 4, - score_diff < 0 ~ 0, - TRUE ~ 2 - ) - ) %>% - dplyr::rename(isHome = value) %>% - dplyr::select(-stat) + ## This first section calculates points based on the old system. + goals1 <- df %>% + dplyr::filter(stat == "goal_from_zone1") %>% + dplyr::group_by(squadName) %>% + dplyr::summarise(goals = sum(value)) + goals2 <- df %>% + dplyr::filter(stat == "goal_from_zone2") %>% + dplyr::group_by(squadName) %>% + dplyr::summarise(goals2 = sum(value) * 2) + goals <- left_join(goals1, goals2, by = "squadName") %>% + mutate(goals = goals + goals2) %>% + select(-goals2) + home <- df %>% + dplyr::filter(stat == "homeTeam") %>% + dplyr::group_by(squadName) %>% + dplyr::select(-period) %>% + dplyr::distinct() + goals <- dplyr::left_join(goals, home, by = "squadName") %>% + dplyr::arrange(value) + score_diff <- diff(goals[['goals']]) + goals <- goals %>% + dplyr::mutate( + score_diff = score_diff, + score_diff = ifelse(value == 0, score_diff * (-1), + score_diff), + points = dplyr::case_when( + score_diff > 0 ~ 4, + score_diff < 0 ~ 0, + TRUE ~ 2 + ) + ) %>% + dplyr::rename(isHome = value) %>% + dplyr::select(-stat) - ## This section calculates points based on the new system (points for - ## winning quarters) - goals_new <- df %>% - dplyr::filter(stat == "goals") - homeScores <- goals_new %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% - dplyr::select(period, homeSquad = squadName, homeValue = value) - awayScores <- goals_new %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) %>% - dplyr::select(period, awaySquad = squadName, awayValue = value) - scores <- dplyr::left_join(homeScores, awayScores, by = "period") %>% - dplyr::mutate(qtr_diff = homeValue - awayValue, - homePoints = dplyr::case_when(qtr_diff > 0 ~ 1, - TRUE ~ 0), - awayPoints = dplyr::case_when(qtr_diff < 0 ~ 1, - TRUE ~ 0) - ) - points_new <- scores %>% - dplyr::group_by(homeSquad, awaySquad) %>% - dplyr::summarise(homePoints = sum(homePoints), - awayPoints = sum(awayPoints)) %>% - dplyr::ungroup() - df1 <- points_new %>% - dplyr::select(dplyr::contains("home")) %>% - dplyr::rename(squadName = homeSquad, points_qtr = homePoints) - df2 <- points_new %>% - dplyr::select(dplyr::contains("away")) %>% - dplyr::rename(squadName = awaySquad, points_qtr = awayPoints) - points_new <- dplyr::bind_rows(df1, df2) - - ## Now join back on to original scoring. - goals <- dplyr::left_join(goals, points_new, by = "squadName") %>% - dplyr::mutate(points_new = points_new + points_qtr) %>% - dplyr::select(-points_qtr) - - ## This section calculates goals/points based on the 2020 system. Includes - ## the super goal, and no bonus points. - goals_new1 <- df %>% - dplyr::filter(stat == "goal_from_zone1") - homeScores1 <- goals_new1 %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% - dplyr::select(period, homeSquad = squadName, homeValue = value) - awayScores1 <- goals_new1 %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) %>% - dplyr::select(period, awaySquad = squadName, awayValue = value) - goals_new2 <- df %>% - dplyr::filter(stat == "goal_from_zone2") - homeScores2 <- goals_new2 %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% - dplyr::select(period, homeSquad = squadName, homeValue2 = value) - awayScores2 <- goals_new2 %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) %>% - dplyr::select(period, awaySquad = squadName, awayValue2 = value) - homeScores <- dplyr::left_join(homeScores1, homeScores2, - by = c("period", "homeSquad")) %>% - mutate(homePoints = homeValue + homeValue2 * 2) %>% - select(-homeValue2) - awayScores <- dplyr::left_join(awayScores1, awayScores2, - by = c("period", "awaySquad")) %>% - mutate(awayPoints = awayValue + awayValue2 * 2) %>% - select(-awayValue2) - - points_2020 <- dplyr::left_join(homeScores, awayScores, by = "period") %>% - dplyr::group_by(homeSquad, awaySquad) %>% - dplyr::summarise(homePoints = sum(homePoints), - awayPoints = sum(awayPoints)) %>% - dplyr::ungroup() - df1 <- points_2020 %>% - dplyr::select(dplyr::contains("home")) %>% - dplyr::rename(squadName = homeSquad, goals_2020 = homePoints) - df2 <- points_2020 %>% - dplyr::select(dplyr::contains("away")) %>% - dplyr::rename(squadName = awaySquad, goals_2020 = awayPoints) - points_2020 <- dplyr::bind_rows(df1, df2) - - browser() - - ## Now join back on to original scoring. - goals <- dplyr::left_join(goals, goals_2020, by = "squadName") %>% - dplyr::mutate(points_new = points_new + points_qtr) %>% - dplyr::select(-points_qtr) - - ## Return - goals + ## Return + goals } #' Calculates the total goals of the match (pre 2020 season) From 57f5c3d313a9833e50f64abd283c7b48a624b642 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Sat, 8 Aug 2020 14:49:44 +1000 Subject: [PATCH 06/56] initial commit of shiny examples --- inst/shiny-examples/global.R | 13 +++++++++++++ inst/shiny-examples/server.R | 0 inst/shiny-examples/ui.R | 0 3 files changed, 13 insertions(+) create mode 100644 inst/shiny-examples/global.R create mode 100644 inst/shiny-examples/server.R create mode 100644 inst/shiny-examples/ui.R diff --git a/inst/shiny-examples/global.R b/inst/shiny-examples/global.R new file mode 100644 index 0000000..296599c --- /dev/null +++ b/inst/shiny-examples/global.R @@ -0,0 +1,13 @@ +################################################################################ +################################################################################ +## Title: Global shiny setup +## Author: Steve Lane +## Date: Saturday, 08 August 2020 +## Synopsis: Sets up global libraries and functions for example shiny. +## Time-stamp: <> +################################################################################ +################################################################################ +library(here) +library(dplyr) +library(ggplot2) +library(shiny) diff --git a/inst/shiny-examples/server.R b/inst/shiny-examples/server.R new file mode 100644 index 0000000..e69de29 diff --git a/inst/shiny-examples/ui.R b/inst/shiny-examples/ui.R new file mode 100644 index 0000000..e69de29 From 28c3a1eeab8bd7d5341aa883a5f6fedfd4f9af77 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Sat, 8 Aug 2020 16:23:38 +1000 Subject: [PATCH 07/56] wireframe ui stuff --- inst/shiny-examples/server.R | 47 ++++++++++++++++++++++++++++ inst/shiny-examples/ui.R | 60 ++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/inst/shiny-examples/server.R b/inst/shiny-examples/server.R index e69de29..310e01e 100644 --- a/inst/shiny-examples/server.R +++ b/inst/shiny-examples/server.R @@ -0,0 +1,47 @@ +################################################################################ +################################################################################ +## Title: Server +## Author: Steve Lane +## Date: Saturday, 08 August 2020 +## Synopsis: Server for shiny example. +## Time-stamp: <2020-08-08 15:09:40 (sprazza)> +################################################################################ +################################################################################ +server <- function(input, output, session) { + output$data_table <- DT::renderDT({ + random_DT(10, 5) + }) + output$image <- renderImage({ + random_image() + }, deleteFile = FALSE) + output$plot <- renderPlot({ + random_ggplot() + }) + output$print <- renderPrint({ + random_print("model") + }) + output$table <- renderTable({ + random_table(10, 5) + }) + output$text <- renderText({ + random_text(nwords = 50) + }) + output$data_table2 <- DT::renderDT({ + random_DT(10, 5) + }) + output$image2 <- renderImage({ + random_image() + }, deleteFile = FALSE) + output$plot2 <- renderPlot({ + random_ggplot() + }) + output$print2 <- renderPrint({ + random_print("model") + }) + output$table2 <- renderTable({ + random_table(10, 5) + }) + output$text2 <- renderText({ + random_text(nwords = 50) + }) +} diff --git a/inst/shiny-examples/ui.R b/inst/shiny-examples/ui.R index e69de29..cf6e833 100644 --- a/inst/shiny-examples/ui.R +++ b/inst/shiny-examples/ui.R @@ -0,0 +1,60 @@ +################################################################################ +################################################################################ +## Title: UI +## Author: Steve Lane +## Date: Saturday, 08 August 2020 +## Synopsis: UI for shiny example. +## Time-stamp: <> +################################################################################ +################################################################################ +ui <- navbarPage( + "Title of the page", + tabPanel( + "Player Statistics", + sidebarLayout( + sidebarPanel(), + mainPanel( + fluidRow( + column(6, + h2("A Random DT"), + DTOutput("data_table") + ), + column(6, + h2("A Random Image"), + plotOutput("image", height = "300px") + ) + ), + fluidRow( + column(6, + h2("A Random Plot"), + plotOutput("plot") + ), + column(6, + h2("A Random Print"), + verbatimTextOutput("print") + ) + ) + ) + ) + ), + tabPanel( + "Panel Two", + sidebarLayout( + sidebarPanel(), + mainPanel( + h2("A Random DT"), + DTOutput("data_table2"), + h2("A Random Image"), + plotOutput("image2", height = "300px"), + h2("A Random Plot"), + plotOutput("plot2"), + h2("A Random Print"), + verbatimTextOutput("print2"), + h2("A Random Table"), + tableOutput("table2"), + h2("A Random Text"), + tableOutput("text2") + ) + ) + ) +) From a5dee2941ca6a87432b76f7a0a3ee18d5e7dced7 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Sat, 8 Aug 2020 16:23:53 +1000 Subject: [PATCH 08/56] bring in data details --- inst/shiny-examples/global.R | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/inst/shiny-examples/global.R b/inst/shiny-examples/global.R index 296599c..840a3fc 100644 --- a/inst/shiny-examples/global.R +++ b/inst/shiny-examples/global.R @@ -11,3 +11,11 @@ library(here) library(dplyr) library(ggplot2) library(shiny) +library(shinipsum) +library(DT) +library(superNetballR) + +################################################################################ +## Load 2017 player data +data(players_2017) +data(season_2017) From dad384156eadd1fb6c48cc8817e0369a6fffb69b Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Sat, 8 Aug 2020 16:46:18 +1000 Subject: [PATCH 09/56] include match details in player stats list --- R/tidiers.R | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/R/tidiers.R b/R/tidiers.R index a8c3315..ce01753 100644 --- a/R/tidiers.R +++ b/R/tidiers.R @@ -50,13 +50,19 @@ tidyPlayers <- function(match) { player_info <- match$playerInfo$player player_info <- dplyr::bind_rows(player_info) player_stats <- dplyr::left_join(player_stats, player_info, by = "playerId") + squad_info <- match$teamInfo$team + squad_info <- dplyr::bind_rows(squad_info) + squad_info <- dplyr::select(squad_info, squadId, squadName) + player_stats <- dplyr::left_join( + player_stats, squad_info, by = "squadId" + ) ## Check if there was overtime (matchInfo) final_period <- match$matchInfo$periodCompleted player_stats <- player_stats %>% dplyr::filter(period <= final_period) %>% dplyr::select(-displayName) %>% tidyr::gather(stat, value, -playerId, -shortDisplayName, -firstname, - -surname, -period) %>% + -surname, -period, -squadId, -squadName) %>% dplyr::mutate( round = match$matchInfo$roundNumber, game = match$matchInfo$matchNumber From c9b55b88dfa5df32fdf78f4c745a93a27ba2a377 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Sat, 8 Aug 2020 17:24:14 +1000 Subject: [PATCH 10/56] including team names in player data updated the 2017 data accordingly --- DESCRIPTION | 2 +- NAMESPACE | 2 ++ R/data.R | 4 +++- data/players_2017.rda | Bin 54800 -> 53360 bytes data/season_2017.rda | Bin 10582 -> 10576 bytes man/ladders.Rd | 3 +++ man/matchPoints_pre_2020.Rd | 18 ++++++++++++++++++ man/players_2017.Rd | 10 +++++++--- man/round5_game3.Rd | 4 +++- man/season_2017.Rd | 6 ++++-- man/superNetballR.Rd | 1 - 11 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 man/matchPoints_pre_2020.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 8a83ca1..c912295 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -15,4 +15,4 @@ Imports: dplyr, httr, tidyr, purrr -RoxygenNote: 6.0.1 +RoxygenNote: 7.1.1 diff --git a/NAMESPACE b/NAMESPACE index b063ed6..cdacad4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,9 @@ export(downloadMatch) export(ladders) +export(ladders_pre_2020) export(matchPoints) +export(matchPoints_pre_2020) export(matchResults) export(tidyMatch) export(tidyPlayers) diff --git a/R/data.R b/R/data.R index ed85300..92225c1 100644 --- a/R/data.R +++ b/R/data.R @@ -25,12 +25,14 @@ #' @format A data frame with 163336 rows and 8 variables: #' \describe{ #' \item{playerId}{Unique player number} +#' \item{period}{Which period the statistic is measured in} +#' \item{squadId}{Unique squad number} #' \item{shortDisplayName}{surname, firstname} #' \item{firstname}{Player firstname} #' \item{surname}{Player surname} +#' \item{squadName}{Full squad name} #' \item{stat}{Statistic measured during the match} #' \item{value}{Value of the statistic} -#' \item{period}{Which period the statistic is measured in} #' \item{round}{Round number of the match} #' \item{game}{Game number of the match} #' } diff --git a/data/players_2017.rda b/data/players_2017.rda index bc1a5958b83bf69ffaef243655449ff8e21ee2d8..e65a4bb4b7ecb3caba02b65aa04b59456bad2748 100644 GIT binary patch literal 53360 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 Bafbi^ literal 54800 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%0AUNP$MgewS(3T^fkgV(#?cpy~2f+PO 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℞kod^h_-8F7F>+ARtKivb1CNj+ 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 QmwzY5y^zIpX(Ips0CulcLjV8( delta 10553 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 Qb=8*4(-RSfJJiele^CceLjV8( diff --git a/man/ladders.Rd b/man/ladders.Rd index f76e096..f185d3b 100644 --- a/man/ladders.Rd +++ b/man/ladders.Rd @@ -3,11 +3,14 @@ \name{ladders} \alias{ladders} \alias{matchResults} +\alias{ladders_pre_2020} \title{Calculates ladder positions} \usage{ ladders(df, round_num = NULL, game_num = NULL, old_system = FALSE) matchResults(df) + +ladders_pre_2020(df, round_num = NULL, game_num = NULL, old_system = FALSE) } \arguments{ \item{df}{Data frame containing season match statistics.} diff --git a/man/matchPoints_pre_2020.Rd b/man/matchPoints_pre_2020.Rd new file mode 100644 index 0000000..ae5b10d --- /dev/null +++ b/man/matchPoints_pre_2020.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/matchPoints.R +\name{matchPoints_pre_2020} +\alias{matchPoints_pre_2020} +\title{Calculates the total goals of the match (pre 2020 season)} +\usage{ +matchPoints_pre_2020(df) +} +\arguments{ +\item{df}{Match data.} +} +\value{ +A data frame containing the final scores, and points for the ladder. +} +\description{ +\code{matchPoints_pre_2020} calculates final match goals and score +difference, for seasons pre-2020. +} diff --git a/man/players_2017.Rd b/man/players_2017.Rd index e7e0e18..05de9e3 100644 --- a/man/players_2017.Rd +++ b/man/players_2017.Rd @@ -4,18 +4,22 @@ \name{players_2017} \alias{players_2017} \title{Season 2017 player data.} -\format{A data frame with 163336 rows and 8 variables: +\format{ +A data frame with 163336 rows and 8 variables: \describe{ \item{playerId}{Unique player number} + \item{period}{Which period the statistic is measured in} + \item{squadId}{Unique squad number} \item{shortDisplayName}{surname, firstname} \item{firstname}{Player firstname} \item{surname}{Player surname} + \item{squadName}{Full squad name} \item{stat}{Statistic measured during the match} \item{value}{Value of the statistic} - \item{period}{Which period the statistic is measured in} \item{round}{Round number of the match} \item{game}{Game number of the match} -}} +} +} \usage{ players_2017 } diff --git a/man/round5_game3.Rd b/man/round5_game3.Rd index 7032c12..d1d28c8 100644 --- a/man/round5_game3.Rd +++ b/man/round5_game3.Rd @@ -4,7 +4,9 @@ \name{round5_game3} \alias{round5_game3} \title{Match and player statistics from round 5, game 3, season 2017.} -\format{A list.} +\format{ +A list. +} \usage{ round5_game3 } diff --git a/man/season_2017.Rd b/man/season_2017.Rd index d495e99..e1d7aec 100644 --- a/man/season_2017.Rd +++ b/man/season_2017.Rd @@ -4,7 +4,8 @@ \name{season_2017} \alias{season_2017} \title{Season 2017 match data.} -\format{A data frame with 15360 rows and 8 variables: +\format{ +A data frame with 15360 rows and 8 variables: \describe{ \item{squadId}{Unique squad number} \item{squadName}{Full squad name} @@ -15,7 +16,8 @@ \item{period}{Which period the statistic is measured in} \item{round}{Round number of the match} \item{game}{Game number of the match} -}} +} +} \usage{ season_2017 } diff --git a/man/superNetballR.Rd b/man/superNetballR.Rd index 745d134..858b682 100644 --- a/man/superNetballR.Rd +++ b/man/superNetballR.Rd @@ -3,7 +3,6 @@ \docType{package} \name{superNetballR} \alias{superNetballR} -\alias{superNetballR-package} \title{\code{superNetballR} package} \description{ Functions getting and manipulating Super Netball data. From 751ad68b6f498605750e674369c01ebc6bbaf243 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Sat, 8 Aug 2020 17:31:00 +1000 Subject: [PATCH 11/56] update vignette --- .Rbuildignore | 2 ++ .gitignore | 2 ++ vignettes/getting-started.Rmd | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.Rbuildignore b/.Rbuildignore index d43a941..a1d090e 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -8,3 +8,5 @@ ^figs$ ^manuscripts$ ^\.travis\.yml$ +^doc$ +^Meta$ diff --git a/.gitignore b/.gitignore index ec2f6fc..937a1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ data-raw/* docker/* /scripts/strip-libs.sh /Rmd/sn-get-data.Rmd +doc +Meta diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd index 46e9f7f..cb7e0b9 100644 --- a/vignettes/getting-started.Rmd +++ b/vignettes/getting-started.Rmd @@ -4,7 +4,7 @@ author: "Steve Lane" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{Vignette Title} + %\VignetteIndexEntry{Getting Started with superNetballR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- @@ -22,6 +22,8 @@ knitr::opts_chunk$set( This vignette provides an overview to get you started with using `superNetballR`. As at 2018-04-08, this package contains the full 2017 season match statistics and player statistics. + *2020-08-08 Update*: The package has been updated to include the players team name in the full player statistics. This will make it easier to analyse player trends by team. The 2017 data has been updated accordingly. + # Sourcing Match Data Data are sourced from 'https://mc.championdata.com/data/' under certain match and round id's. The 2017 home and away season is in the 10083 folder, whilst the finals are in the 10084 folder. The full (processed) data are supplied with this package. From cab803aba04ecb41484c61e3c247b219aa16f9cc Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Sun, 9 Aug 2020 11:09:23 +1000 Subject: [PATCH 12/56] move app details add a reactive to seasons etc --- .../{ => superNetballR}/global.R | 17 +++++++++- .../{ => superNetballR}/server.R | 20 ++++++++++++ inst/shiny-examples/{ => superNetballR}/ui.R | 32 +++++++++++++++++-- 3 files changed, 66 insertions(+), 3 deletions(-) rename inst/shiny-examples/{ => superNetballR}/global.R (52%) rename inst/shiny-examples/{ => superNetballR}/server.R (74%) rename inst/shiny-examples/{ => superNetballR}/ui.R (67%) diff --git a/inst/shiny-examples/global.R b/inst/shiny-examples/superNetballR/global.R similarity index 52% rename from inst/shiny-examples/global.R rename to inst/shiny-examples/superNetballR/global.R index 840a3fc..1c2c920 100644 --- a/inst/shiny-examples/global.R +++ b/inst/shiny-examples/superNetballR/global.R @@ -4,7 +4,7 @@ ## Author: Steve Lane ## Date: Saturday, 08 August 2020 ## Synopsis: Sets up global libraries and functions for example shiny. -## Time-stamp: <> +## Time-stamp: <2020-08-09 10:17:55 (sprazza)> ################################################################################ ################################################################################ library(here) @@ -15,7 +15,22 @@ library(shinipsum) library(DT) library(superNetballR) +################################################################################ +## Load modules. +lst_modules <- list.files( + here::here("inst/shiny-examples/superNetballR/modules/"), full.names = TRUE +) +lapply(lst_modules, function(x) source(x, echo = FALSE)) + ################################################################################ ## Load 2017 player data data(players_2017) data(season_2017) +season_2017 <- season_2017 %>% + mutate(Season = 2017) + +################################################################################ +## Create some selectors (they don't need to be reactive). +season_input <- sort(unique(season_2017[["Season"]])) +team_input <- sort(unique(season_2017[["squadName"]])) +round_input <- sort(unique(season_2017[["round"]])) diff --git a/inst/shiny-examples/server.R b/inst/shiny-examples/superNetballR/server.R similarity index 74% rename from inst/shiny-examples/server.R rename to inst/shiny-examples/superNetballR/server.R index 310e01e..6d38a9a 100644 --- a/inst/shiny-examples/server.R +++ b/inst/shiny-examples/superNetballR/server.R @@ -44,4 +44,24 @@ server <- function(input, output, session) { output$text2 <- renderText({ random_text(nwords = 50) }) + observe({ + season <- input$season_selector + squad <- input$team_selector + rounds <- season_2017 %>% + filter( + squadName == squad, + Season == season + ) %>% + distinct(round) %>% + select(round) %>% + unlist() + updateSelectInput( + session, + "round_selector_reactive", + label = "Round", + choices = rounds, + selected = "1" + ) + }) + ## season_input_server("input1") } diff --git a/inst/shiny-examples/ui.R b/inst/shiny-examples/superNetballR/ui.R similarity index 67% rename from inst/shiny-examples/ui.R rename to inst/shiny-examples/superNetballR/ui.R index cf6e833..ce26edd 100644 --- a/inst/shiny-examples/ui.R +++ b/inst/shiny-examples/superNetballR/ui.R @@ -10,9 +10,37 @@ ui <- navbarPage( "Title of the page", tabPanel( - "Player Statistics", + "Team Statistics", sidebarLayout( - sidebarPanel(), + sidebarPanel( + selectInput( + "team_selector", + label = "Team", + choices = team_input, + selected = "Melbourne Vixens" + ), + selectInput( + "season_selector", + label = "Season", + choices = season_input, + selected = "2017" + ), + selectInput( + "round_selector", + label = "Round", + choices = round_input, + selected = "1" + ), + hr(), + h2("Module Test"), + selectInput( + "round_selector_reactive", + label = "Round", + choices = NULL + ), + ## season_input_server("input1"), + width = 2 + ), mainPanel( fluidRow( column(6, From 046194df38d6763124af41dc1c4f96e7da3d94ef Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Tue, 4 May 2021 10:23:47 +1000 Subject: [PATCH 13/56] add team colours to package --- R/data.R | 12 ++++++++++++ data/team_colours.rda | Bin 0 -> 395 bytes 2 files changed, 12 insertions(+) create mode 100644 data/team_colours.rda diff --git a/R/data.R b/R/data.R index 92225c1..2ca8095 100644 --- a/R/data.R +++ b/R/data.R @@ -45,3 +45,15 @@ #' #' @format A list. "round5_game3" + +#' Team colours. +#' +#' A dataset containing hex-coded team colours for each team. +#' +#' @format A data frame with 8 rows and 3 variables: +#' \describe{ +#' \item{squadName}{Full squad name} +#' \item{squadId}{Unique squad number} +#' \item{squadColour}{Hex-coded team colour} +#' } +"team_colours" diff --git a/data/team_colours.rda b/data/team_colours.rda new file mode 100644 index 0000000000000000000000000000000000000000..b6831091509051d90ea6035f2cbfc7a4836e4844 GIT binary patch literal 395 zcmV;60d)RCT4*^jL0KkKSweN5^8f(af5`v;PC)IEe}F%yJ;1-`|G+>11AqWQumL!l zVkRV~n3_}dN2DH_5C8xgdIzaJLrnnMn@A$2PbsIU(?-ZGnBTejACl9q=io zu1a7b4fU>1zJ4s|%6~kUo0uR9fhiG=GzQET$V^cY^+abL1qi4aC`A!rMiCGXT`Plt zXc$vjzqFeB^H&wSfhKY>u-eHn+(4i;Hp@1(p7ZBCs>}cY literal 0 HcmV?d00001 From a1e6a9adc36ca64f736be25a976856197c5e00a7 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Tue, 4 May 2021 12:23:33 +1000 Subject: [PATCH 14/56] include basic app functions --- R/shinySuperNetballR.R | 18 ++++ inst/shiny-examples/superNetballR/global.R | 19 +++- .../superNetballR/modules/team_series.R | 92 +++++++++++++++++++ inst/shiny-examples/superNetballR/server.R | 41 +++++---- inst/shiny-examples/superNetballR/ui.R | 58 +----------- 5 files changed, 151 insertions(+), 77 deletions(-) create mode 100644 R/shinySuperNetballR.R create mode 100644 inst/shiny-examples/superNetballR/modules/team_series.R diff --git a/R/shinySuperNetballR.R b/R/shinySuperNetballR.R new file mode 100644 index 0000000..14b0a4f --- /dev/null +++ b/R/shinySuperNetballR.R @@ -0,0 +1,18 @@ +#' Runs the demo shiny app +#' +#' \code{shinySuperNetballR} Runs the demo shiny app to compare Super Netball +#' statistics between teams. +#' +#' @return Runs a shiny app +#' +#' @export +shinySuperNetballR <- function() { + my_dir <- system.file( + "shiny-examples", "superNetballR", package = "superNetballR" + ) + if (my_dir == "") { + stop("Can't find the superNetballR shiny directory. Try re-installing `superNetballR`.", call. = FALSE) + } + + shiny::runApp(my_dir, display.mode = "normal") +} diff --git a/inst/shiny-examples/superNetballR/global.R b/inst/shiny-examples/superNetballR/global.R index 1c2c920..a98156e 100644 --- a/inst/shiny-examples/superNetballR/global.R +++ b/inst/shiny-examples/superNetballR/global.R @@ -4,7 +4,7 @@ ## Author: Steve Lane ## Date: Saturday, 08 August 2020 ## Synopsis: Sets up global libraries and functions for example shiny. -## Time-stamp: <2020-08-09 10:17:55 (sprazza)> +## Time-stamp: <2021-05-04 11:57:17 (sprazza)> ################################################################################ ################################################################################ library(here) @@ -26,11 +26,26 @@ lapply(lst_modules, function(x) source(x, echo = FALSE)) ## Load 2017 player data data(players_2017) data(season_2017) +data(team_colours) season_2017 <- season_2017 %>% mutate(Season = 2017) ################################################################################ ## Create some selectors (they don't need to be reactive). season_input <- sort(unique(season_2017[["Season"]])) -team_input <- sort(unique(season_2017[["squadName"]])) round_input <- sort(unique(season_2017[["round"]])) +by_game <- season_2017 %>% + group_by(squadId, stat, round, game) %>% + summarise(value = sum(value)) %>% + mutate( + Round = paste0( + '2017, Round ', formatC(round, width = 2, format = 'd', flag = '0') + ) + ) %>% + ungroup() %>% + left_join(., team_colours, by = 'squadId') +team_input <- sort(unique(by_game[["squadName"]])) +metric_input <- sort(unique(by_game[["stat"]])) +## Create colour scale +nm <- team_colours[['squadColour']] +names(nm) <- team_colours[['squadName']] diff --git a/inst/shiny-examples/superNetballR/modules/team_series.R b/inst/shiny-examples/superNetballR/modules/team_series.R new file mode 100644 index 0000000..c6dd9e6 --- /dev/null +++ b/inst/shiny-examples/superNetballR/modules/team_series.R @@ -0,0 +1,92 @@ +#' Function to plot a particular statistic +#' +#' \code{team_series} Function to plot a particular statistic, for a particular +#' team. +#' +#' @param df Data frame of team statistics +#' @param metric Statistic to display on figure +#' @param team1 First team to display on chart +#' @param team2 Second team to display on chart +#' +#' @return ggplot2 object +team_series <- function(df) { + df %>% + ggplot() + + aes( + x = Round, y = value, group = squadName, colour = squadName, + fill = squadName + ) + + geom_point() + + geom_line() + + geom_smooth(level = 0.8) + + scale_fill_manual(values = nm) + + scale_colour_manual(values = nm) + + labs( + y = 'Value', + title = 'Super Netball Statistics by Round', + caption = + 'This figure allows you to compare two teams on a single statistic over time. The finals are shown as Round 15-17 on the figure. Overlaid are simple trend (loess) lines and an 80% confidence interval.' + ) + + theme_minimal() + + theme( + axis.text.x = element_text(hjust = 1, angle = 35), + legend.title = element_blank(), + legend.position = 'bottom' + ) +} + +team_series_ui <- function(id) { + sidebarLayout( + sidebarPanel( + selectInput( + NS(id, "team_selector1"), + label = "Team 1", + choices = team_input, + selected = "Melbourne Vixens" + ), + selectInput( + NS(id, "team_selector2"), + label = "Team 2", + choices = team_input, + selected = "GIANTS Netball" + ), + selectInput( + NS(id, "statistic_selector"), + label = "Statistic", + choices = metric_input, + selected = "goals" + ), + width = 2 + ), + mainPanel( + plotOutput(NS(id, 'team_series'), height = '600px'), + width = 10 + ) + ) +} + +team_series_server <- function(id, df) { + moduleServer(id, function(input, output, session) { + this_df <- reactive({ + df %>% + filter( + squadName %in% c(input$team_selector1, input$team_selector2), + stat == input$statistic_selector + ) + }) + output$team_series <- renderPlot({ + team_series(this_df()) + }) + }) +} + +## Test the modules in a self-contained way. +team_series_app <- function(data_source) { + ui <- fluidPage( + team_series_ui("ts1") + ) + server <- function(input, output, session) { + team_series_server("ts1", data_source) + } + shinyApp(ui, server) +} diff --git a/inst/shiny-examples/superNetballR/server.R b/inst/shiny-examples/superNetballR/server.R index 6d38a9a..04c6352 100644 --- a/inst/shiny-examples/superNetballR/server.R +++ b/inst/shiny-examples/superNetballR/server.R @@ -4,7 +4,7 @@ ## Author: Steve Lane ## Date: Saturday, 08 August 2020 ## Synopsis: Server for shiny example. -## Time-stamp: <2020-08-08 15:09:40 (sprazza)> +## Time-stamp: <2021-05-04 12:15:09 (sprazza)> ################################################################################ ################################################################################ server <- function(input, output, session) { @@ -44,24 +44,25 @@ server <- function(input, output, session) { output$text2 <- renderText({ random_text(nwords = 50) }) - observe({ - season <- input$season_selector - squad <- input$team_selector - rounds <- season_2017 %>% - filter( - squadName == squad, - Season == season - ) %>% - distinct(round) %>% - select(round) %>% - unlist() - updateSelectInput( - session, - "round_selector_reactive", - label = "Round", - choices = rounds, - selected = "1" - ) - }) + team_series_server('team_series1', by_game) + ## observe({ + ## season <- input$season_selector + ## squad <- input$team_selector + ## rounds <- season_2017 %>% + ## filter( + ## squadName == squad, + ## Season == season + ## ) %>% + ## distinct(round) %>% + ## select(round) %>% + ## unlist() + ## updateSelectInput( + ## session, + ## "round_selector_reactive", + ## label = "Round", + ## choices = rounds, + ## selected = "1" + ## ) + ## }) ## season_input_server("input1") } diff --git a/inst/shiny-examples/superNetballR/ui.R b/inst/shiny-examples/superNetballR/ui.R index ce26edd..cb0b17d 100644 --- a/inst/shiny-examples/superNetballR/ui.R +++ b/inst/shiny-examples/superNetballR/ui.R @@ -4,66 +4,14 @@ ## Author: Steve Lane ## Date: Saturday, 08 August 2020 ## Synopsis: UI for shiny example. -## Time-stamp: <> +## Time-stamp: <2021-05-04 12:14:13 (sprazza)> ################################################################################ ################################################################################ ui <- navbarPage( - "Title of the page", + "Super Netball Statistics Comparison App", tabPanel( "Team Statistics", - sidebarLayout( - sidebarPanel( - selectInput( - "team_selector", - label = "Team", - choices = team_input, - selected = "Melbourne Vixens" - ), - selectInput( - "season_selector", - label = "Season", - choices = season_input, - selected = "2017" - ), - selectInput( - "round_selector", - label = "Round", - choices = round_input, - selected = "1" - ), - hr(), - h2("Module Test"), - selectInput( - "round_selector_reactive", - label = "Round", - choices = NULL - ), - ## season_input_server("input1"), - width = 2 - ), - mainPanel( - fluidRow( - column(6, - h2("A Random DT"), - DTOutput("data_table") - ), - column(6, - h2("A Random Image"), - plotOutput("image", height = "300px") - ) - ), - fluidRow( - column(6, - h2("A Random Plot"), - plotOutput("plot") - ), - column(6, - h2("A Random Print"), - verbatimTextOutput("print") - ) - ) - ) - ) + team_series_ui('team_series1') ), tabPanel( "Panel Two", From 0f324332a4729cb3a3a2196797742d2defc6dd45 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Tue, 4 May 2021 12:39:49 +1000 Subject: [PATCH 15/56] move fie and source appropriately --- R/shinySuperNetballR.R | 1 + inst/shiny-examples/superNetballR/global.R | 4 +--- .../{modules/team_series.R => team_series_module.R} | 0 3 files changed, 2 insertions(+), 3 deletions(-) rename inst/shiny-examples/superNetballR/{modules/team_series.R => team_series_module.R} (100%) diff --git a/R/shinySuperNetballR.R b/R/shinySuperNetballR.R index 14b0a4f..90d9a5d 100644 --- a/R/shinySuperNetballR.R +++ b/R/shinySuperNetballR.R @@ -14,5 +14,6 @@ shinySuperNetballR <- function() { stop("Can't find the superNetballR shiny directory. Try re-installing `superNetballR`.", call. = FALSE) } + source('./team_series_module.R') shiny::runApp(my_dir, display.mode = "normal") } diff --git a/inst/shiny-examples/superNetballR/global.R b/inst/shiny-examples/superNetballR/global.R index a98156e..e50e68f 100644 --- a/inst/shiny-examples/superNetballR/global.R +++ b/inst/shiny-examples/superNetballR/global.R @@ -4,15 +4,13 @@ ## Author: Steve Lane ## Date: Saturday, 08 August 2020 ## Synopsis: Sets up global libraries and functions for example shiny. -## Time-stamp: <2021-05-04 11:57:17 (sprazza)> +## Time-stamp: <2021-05-04 12:38:40 (sprazza)> ################################################################################ ################################################################################ library(here) library(dplyr) library(ggplot2) library(shiny) -library(shinipsum) -library(DT) library(superNetballR) ################################################################################ diff --git a/inst/shiny-examples/superNetballR/modules/team_series.R b/inst/shiny-examples/superNetballR/team_series_module.R similarity index 100% rename from inst/shiny-examples/superNetballR/modules/team_series.R rename to inst/shiny-examples/superNetballR/team_series_module.R From 8c50092fad15d56346a45fa2ef85e56ba8a97de8 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Tue, 4 May 2021 12:42:43 +1000 Subject: [PATCH 16/56] add in some docs --- DESCRIPTION | 3 ++- NAMESPACE | 1 + man/shinySuperNetballR.Rd | 15 +++++++++++++++ man/team_colours.Rd | 21 +++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 man/shinySuperNetballR.Rd create mode 100644 man/team_colours.Rd diff --git a/DESCRIPTION b/DESCRIPTION index c912295..edd94d8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -9,7 +9,8 @@ Encoding: UTF-8 LazyData: true Suggests: knitr, rmarkdown, - here + here, + shiny VignetteBuilder: knitr Imports: dplyr, httr, diff --git a/NAMESPACE b/NAMESPACE index cdacad4..015d01f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,6 +6,7 @@ export(ladders_pre_2020) export(matchPoints) export(matchPoints_pre_2020) export(matchResults) +export(shinySuperNetballR) export(tidyMatch) export(tidyPlayers) importFrom(dplyr,"%>%") diff --git a/man/shinySuperNetballR.Rd b/man/shinySuperNetballR.Rd new file mode 100644 index 0000000..4f1be02 --- /dev/null +++ b/man/shinySuperNetballR.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shinySuperNetballR.R +\name{shinySuperNetballR} +\alias{shinySuperNetballR} +\title{Runs the demo shiny app} +\usage{ +shinySuperNetballR() +} +\value{ +Runs a shiny app +} +\description{ +\code{shinySuperNetballR} Runs the demo shiny app to compare Super Netball +statistics between teams. +} diff --git a/man/team_colours.Rd b/man/team_colours.Rd new file mode 100644 index 0000000..597c713 --- /dev/null +++ b/man/team_colours.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data.R +\docType{data} +\name{team_colours} +\alias{team_colours} +\title{Team colours.} +\format{ +A data frame with 8 rows and 3 variables: +\describe{ + \item{squadName}{Full squad name} + \item{squadId}{Unique squad number} + \item{squadColour}{Hex-coded team colour} +} +} +\usage{ +team_colours +} +\description{ +A dataset containing hex-coded team colours for each team. +} +\keyword{datasets} From 80ac316f2b7f28c4e547393faed2c8fa6699d916 Mon Sep 17 00:00:00 2001 From: Steve Lane Date: Tue, 4 May 2021 12:49:40 +1000 Subject: [PATCH 17/56] make sure we're pointing in the right direction --- R/shinySuperNetballR.R | 2 +- inst/shiny-examples/superNetballR/server.R | 58 +--------------------- inst/shiny-examples/superNetballR/ui.R | 22 +------- 3 files changed, 3 insertions(+), 79 deletions(-) diff --git a/R/shinySuperNetballR.R b/R/shinySuperNetballR.R index 90d9a5d..45bef75 100644 --- a/R/shinySuperNetballR.R +++ b/R/shinySuperNetballR.R @@ -14,6 +14,6 @@ shinySuperNetballR <- function() { stop("Can't find the superNetballR shiny directory. Try re-installing `superNetballR`.", call. = FALSE) } - source('./team_series_module.R') + source(paste0(my_dir, '/team_series_module.R')) shiny::runApp(my_dir, display.mode = "normal") } diff --git a/inst/shiny-examples/superNetballR/server.R b/inst/shiny-examples/superNetballR/server.R index 04c6352..6b6dd05 100644 --- a/inst/shiny-examples/superNetballR/server.R +++ b/inst/shiny-examples/superNetballR/server.R @@ -4,65 +4,9 @@ ## Author: Steve Lane ## Date: Saturday, 08 August 2020 ## Synopsis: Server for shiny example. -## Time-stamp: <2021-05-04 12:15:09 (sprazza)> +## Time-stamp: <2021-05-04 12:48:56 (sprazza)> ################################################################################ ################################################################################ server <- function(input, output, session) { - output$data_table <- DT::renderDT({ - random_DT(10, 5) - }) - output$image <- renderImage({ - random_image() - }, deleteFile = FALSE) - output$plot <- renderPlot({ - random_ggplot() - }) - output$print <- renderPrint({ - random_print("model") - }) - output$table <- renderTable({ - random_table(10, 5) - }) - output$text <- renderText({ - random_text(nwords = 50) - }) - output$data_table2 <- DT::renderDT({ - random_DT(10, 5) - }) - output$image2 <- renderImage({ - random_image() - }, deleteFile = FALSE) - output$plot2 <- renderPlot({ - random_ggplot() - }) - output$print2 <- renderPrint({ - random_print("model") - }) - output$table2 <- renderTable({ - random_table(10, 5) - }) - output$text2 <- renderText({ - random_text(nwords = 50) - }) team_series_server('team_series1', by_game) - ## observe({ - ## season <- input$season_selector - ## squad <- input$team_selector - ## rounds <- season_2017 %>% - ## filter( - ## squadName == squad, - ## Season == season - ## ) %>% - ## distinct(round) %>% - ## select(round) %>% - ## unlist() - ## updateSelectInput( - ## session, - ## "round_selector_reactive", - ## label = "Round", - ## choices = rounds, - ## selected = "1" - ## ) - ## }) - ## season_input_server("input1") } diff --git a/inst/shiny-examples/superNetballR/ui.R b/inst/shiny-examples/superNetballR/ui.R index cb0b17d..54b613c 100644 --- a/inst/shiny-examples/superNetballR/ui.R +++ b/inst/shiny-examples/superNetballR/ui.R @@ -4,7 +4,7 @@ ## Author: Steve Lane ## Date: Saturday, 08 August 2020 ## Synopsis: UI for shiny example. -## Time-stamp: <2021-05-04 12:14:13 (sprazza)> +## Time-stamp: <2021-05-04 12:49:18 (sprazza)> ################################################################################ ################################################################################ ui <- navbarPage( @@ -12,25 +12,5 @@ ui <- navbarPage( tabPanel( "Team Statistics", team_series_ui('team_series1') - ), - tabPanel( - "Panel Two", - sidebarLayout( - sidebarPanel(), - mainPanel( - h2("A Random DT"), - DTOutput("data_table2"), - h2("A Random Image"), - plotOutput("image2", height = "300px"), - h2("A Random Plot"), - plotOutput("plot2"), - h2("A Random Print"), - verbatimTextOutput("print2"), - h2("A Random Table"), - tableOutput("table2"), - h2("A Random Text"), - tableOutput("text2") - ) - ) ) ) From bf66cac7cb68f596192b17f09b60a05acea41b26 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 10 Mar 2026 19:19:27 +1100 Subject: [PATCH 18/56] Update README for fork Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 91a3189..ab49b86 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,27 @@ # superNetballR -[![Build Status](https://travis-ci.org/SteveLane/superNetballR.svg?branch=master)](https://travis-ci.org/SteveLane/superNetballR) +[![Build Status](https://travis-ci.org/craigmoyle/superNetballR_updated.svg?branch=main)](https://travis-ci.org/craigmoyle/superNetballR_updated) ## Description -This package allows the downloading of super netball statistics ([https://stevelane.github.io/superNetballR/](https://stevelane.github.io/superNetballR/). The first super netball season was in 2017, and was eventually won by the Sunshine Coast Lightning. +This fork of `superNetballR` allows the downloading of super netball statistics from the original project site: [https://stevelane.github.io/superNetballR/](https://stevelane.github.io/superNetballR/). The first super netball season was in 2017, and was eventually won by the Sunshine Coast Lightning. `superNetballR` contains helper functions that transform the downloaded data into usable tidy data. +This repository is maintained at [craigmoyle/superNetballR_updated](https://github.com/craigmoyle/superNetballR_updated). + ## Installation Installation in R requires `devtools`. To install, run the following from an R session: ``` R -devtools::install_github("stevelane/superNetballR") +devtools::install_github("craigmoyle/superNetballR_updated") ``` -To install the development version: +To install the current `main` branch explicitly: ``` R -devtools::install_github("stevelane/superNetballR@develop") +devtools::install_github("craigmoyle/superNetballR_updated@main") ``` ## Notes From 18477c34b13cd265a3df6a7e5b93c7435813701c Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 10 Mar 2026 21:39:40 +1100 Subject: [PATCH 19/56] Improve package reliability Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- R/downloadMatch.R | 10 +++++++++- R/matchPoints.R | 6 +++--- _pkgdown.yml | 6 ++++-- inst/shiny-examples/superNetballR/global.R | 9 +++++---- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/R/downloadMatch.R b/R/downloadMatch.R index 8b86f53..0880e60 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -33,6 +33,14 @@ downloadMatch <- function(comp_id, round_id, game_id) { ".json" ) dat <- httr::GET(pg) - dat_list <- httr::content(dat, "parsed")$matchStats + httr::stop_for_status(dat, call. = FALSE) + dat_list <- httr::content( + dat, + as = "parsed", + type = "application/json" + )$matchStats + if (is.null(dat_list)) { + stop("Champion Data response did not include matchStats.", call. = FALSE) + } dat_list } diff --git a/R/matchPoints.R b/R/matchPoints.R index b2a9322..a5fc290 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -16,9 +16,9 @@ matchPoints <- function(df) { dplyr::filter(stat == "goal_from_zone2") %>% dplyr::group_by(squadName) %>% dplyr::summarise(goals2 = sum(value) * 2) - goals <- left_join(goals1, goals2, by = "squadName") %>% - mutate(goals = goals + goals2) %>% - select(-goals2) + goals <- dplyr::left_join(goals1, goals2, by = "squadName") %>% + dplyr::mutate(goals = goals + goals2) %>% + dplyr::select(-goals2) home <- df %>% dplyr::filter(stat == "homeTeam") %>% dplyr::group_by(squadName) %>% diff --git a/_pkgdown.yml b/_pkgdown.yml index 7a341e1..032d9c1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -4,10 +4,12 @@ template: authors: - Steve Lane: - href: https://gtown-ds.netlify.com/ + href: https://gtown-ds.netlify.com/ + - Craig Moyle: + href: https://github.com/craigmoyle navbar: type: inverse right: - icon: fa-github fa-lg - href: https://github.com/SteveLane/superNetballR/ + href: https://github.com/craigmoyle/superNetballR_updated/ diff --git a/inst/shiny-examples/superNetballR/global.R b/inst/shiny-examples/superNetballR/global.R index e50e68f..7a619a5 100644 --- a/inst/shiny-examples/superNetballR/global.R +++ b/inst/shiny-examples/superNetballR/global.R @@ -15,10 +15,11 @@ library(superNetballR) ################################################################################ ## Load modules. -lst_modules <- list.files( - here::here("inst/shiny-examples/superNetballR/modules/"), full.names = TRUE -) -lapply(lst_modules, function(x) source(x, echo = FALSE)) +modules_dir <- here::here("inst/shiny-examples/superNetballR/modules/") +if (dir.exists(modules_dir)) { + lst_modules <- list.files(modules_dir, full.names = TRUE) + invisible(lapply(lst_modules, function(x) source(x, echo = FALSE))) +} ################################################################################ ## Load 2017 player data From 8510def6092cab98c9323d96b22564a3d23e5f1a Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 10 Mar 2026 21:56:13 +1100 Subject: [PATCH 20/56] Add tests and modernize CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/R-CMD-check.yaml | 46 ++++++++ .travis.yml | 5 - DESCRIPTION | 12 ++- Makefile | 28 ++--- R/downloadMatch.R | 78 ++++++++++---- R/ladders.R | 42 +++++--- R/matchPoints.R | 19 ++-- R/tidiers.R | 17 ++- README.md | 3 +- tests/testthat.R | 4 + tests/testthat/helper-fixtures.R | 161 ++++++++++++++++++++++++++++ tests/testthat/test-downloadMatch.R | 33 ++++++ tests/testthat/test-ladders.R | 38 +++++++ tests/testthat/test-match-points.R | 54 ++++++++++ tests/testthat/test-tidiers.R | 19 ++++ vignettes/getting-started.Rmd | 8 +- 16 files changed, 496 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/R-CMD-check.yaml delete mode 100644 .travis.yml create mode 100644 tests/testthat.R create mode 100644 tests/testthat/helper-fixtures.R create mode 100644 tests/testthat/test-downloadMatch.R create mode 100644 tests/testthat/test-ladders.R create mode 100644 tests/testthat/test-match-points.R create mode 100644 tests/testthat/test-tidiers.R diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..670039c --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,46 @@ +name: R-CMD-check + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + R-CMD-check: + runs-on: ${{ matrix.config.os }} + + strategy: + fail-fast: false + matrix: + config: + - os: ubuntu-latest + r: release + - os: macos-latest + r: release + - os: windows-latest + r: release + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck, any::roxygen2 + needs: check + + - name: Generate package documentation + run: Rscript -e "roxygen2::roxygenise()" + + - uses: r-lib/actions/check-r-package@v2 + with: + args: 'c("--no-manual", "--as-cran")' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8d139ac..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -# R for travis: see documentation at https://docs.travis-ci.com/user/languages/r - -language: R -sudo: false -cache: packages diff --git a/DESCRIPTION b/DESCRIPTION index edd94d8..8dfa279 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,17 +3,21 @@ Title: Downloads and tidies super netball statistics Version: 0.2.0 Authors@R: person("Steve", "Lane", email = "lane.s@unimelb.edu.au", role = c("aut", "cre")) Description: This package provides functions to easily download and manipulate data from super netball matches. -Depends: R (>= 3.4.0) +Depends: R (>= 4.0.0) License: MIT + file LICENSE Encoding: UTF-8 LazyData: true -Suggests: knitr, +Suggests: here, + knitr, rmarkdown, - here, - shiny + shiny, + testthat (>= 3.0.0) VignetteBuilder: knitr Imports: dplyr, httr, tidyr, purrr +URL: https://github.com/craigmoyle/superNetballR_updated +BugReports: https://github.com/craigmoyle/superNetballR_updated/issues +Config/testthat/edition: 3 RoxygenNote: 7.1.1 diff --git a/Makefile b/Makefile index e0fdc73..bf52712 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,32 @@ # Makefile -# Time-stamp: <2018-04-08 09:52:43 (slane)> -.PHONY: all document build test check checking install winbuild site +.PHONY: all build check checking clean document install site test winbuild -all: document build check install site +PKG_TARBALL := $(shell Rscript -e "desc <- read.dcf('DESCRIPTION')[1, ]; cat(sprintf('%s_%s.tar.gz', desc[['Package']], desc[['Version']]))") -checking: document build test check +all: document test check install + +checking: document test check document: - Rscript -e "devtools::document()" + Rscript -e "roxygen2::roxygenise()" build: - Rscript -e "devtools::build()" + R CMD build . test: - Rscript -e "devtools::test()" + Rscript -e "testthat::test_dir('tests/testthat', reporter = 'summary', stop_on_failure = TRUE)" -check: - Rscript -e "devtools::check()" +check: build + R CMD check --no-manual --as-cran $(PKG_TARBALL) install: - Rscript -e "devtools::install(build_vignettes = TRUE, upgrade_dependencies = FALSE)" + R CMD INSTALL . winbuild: - Rscript -e "devtools::build_win(version = 'R-devel', quiet = TRUE)" + @echo "Use the GitHub Actions R-CMD-check workflow for Windows validation." site: - Rscript -e "pkgdown::clean_site(); pkgdown::build_site()" + Rscript -e "pkgdown::build_site()" + +clean: + rm -rf *.tar.gz *.Rcheck diff --git a/R/downloadMatch.R b/R/downloadMatch.R index 0880e60..d11ca0e 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -1,3 +1,50 @@ +validate_identifier <- function(value, name) { + if (length(value) != 1 || is.na(value)) { + stop(name, " must be a single value.", call. = FALSE) + } + + value <- as.character(value) + if (!grepl("^[0-9]+$", value)) { + stop(name, " must contain digits only.", call. = FALSE) + } + + value +} + +validate_positive_whole_number <- function(value, name) { + value <- validate_identifier(value, name) + value <- as.integer(value) + + if (value < 1) { + stop(name, " must be greater than or equal to 1.", call. = FALSE) + } + + value +} + +build_match_url <- function(comp_id, round_id, game_id) { + comp_id <- validate_identifier(comp_id, "comp_id") + round_id <- validate_positive_whole_number(round_id, "round_id") + game_id <- validate_positive_whole_number(game_id, "game_id") + + sprintf( + "https://mc.championdata.com/data/%s/%s%02d%02d.json", + comp_id, + comp_id, + round_id, + game_id + ) +} + +extract_match_stats <- function(payload) { + dat_list <- payload$matchStats + if (is.null(dat_list)) { + stop("Champion Data response did not include matchStats.", call. = FALSE) + } + + dat_list +} + #' Download data from a single match #' #' \code{downloadMatch} downloads match and player data for a single match. @@ -18,29 +65,20 @@ #' #' @export downloadMatch <- function(comp_id, round_id, game_id) { - r_id <- ifelse( - round_id < 10, - paste0("0", as.character(round_id)), - as.character(round_id) - ) - pg <- paste0( - "https://mc.championdata.com/data/", - comp_id, - "/", - comp_id, - r_id, - paste0("0", as.character(game_id)), - ".json" + pg <- build_match_url(comp_id, round_id, game_id) + dat <- httr::RETRY( + "GET", + pg, + httr::timeout(30), + times = 3, + pause_base = 1, + terminate_on = c(400, 401, 403, 404), + quiet = TRUE ) - dat <- httr::GET(pg) httr::stop_for_status(dat, call. = FALSE) - dat_list <- httr::content( + extract_match_stats(httr::content( dat, as = "parsed", type = "application/json" - )$matchStats - if (is.null(dat_list)) { - stop("Champion Data response did not include matchStats.", call. = FALSE) - } - dat_list + )) } diff --git a/R/ladders.R b/R/ladders.R index bc76df2..9dbc96d 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -1,3 +1,7 @@ +safe_percentage <- function(goals_for, goals_against) { + ifelse(goals_against == 0, Inf, goals_for / goals_against) +} + #' Calculates ladder positions #' #' \code{ladders} calculates ladder positions at the end of a match. @@ -27,15 +31,15 @@ ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { dplyr::filter(!(round >= round_num && game > game_num)) } ladder <- match_results %>% - dplyr::group_by(squadName) %>% - dplyr::summarise( - games = n(), - goals_for = sum(goals), - goals_against = sum(goals - score_diff), - percentage = goals_for / goals_against, - points = as.integer(sum(points)) - ) %>% - dplyr::arrange(dplyr::desc(points), dplyr::desc(percentage)) + dplyr::group_by(squadName) %>% + dplyr::summarise( + games = n(), + goals_for = sum(goals), + goals_against = sum(goals - score_diff), + percentage = safe_percentage(goals_for, goals_against), + points = as.integer(sum(points)) + ) %>% + dplyr::arrange(dplyr::desc(points), dplyr::desc(percentage)) ladder } @@ -52,13 +56,24 @@ matchResults <- function(df) { df } +matchResults_pre_2020 <- function(df) { + df <- df %>% + dplyr::group_by(round, game) %>% + tidyr::nest() %>% + dplyr::group_by(round, game) %>% + dplyr::mutate(game_results = purrr::map(data, matchPoints_pre_2020)) %>% + dplyr::select(-data) %>% + tidyr::unnest(cols = c(game_results)) + df +} + #' @rdname ladders #' @export ladders_pre_2020 <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { if (!is.null(game_num) && is.null(round_num)) { stop("If game number is supplied, round number must also be supplied.") } - match_results <- matchResults(df = df) + match_results <- matchResults_pre_2020(df = df) if (!is.null(round_num) && is.null(game_num)) { match_results <- match_results %>% dplyr::filter(round <= round_num) @@ -73,12 +88,11 @@ ladders_pre_2020 <- function(df, round_num = NULL, game_num = NULL, old_system = games = n(), goals_for = sum(goals), goals_against = sum(goals - score_diff), - percentage = goals_for / goals_against, + percentage = safe_percentage(goals_for, goals_against), points = as.integer(sum(points)), - points_new = as.integer(sum(points_new)) - ) %>% + points_new = as.integer(sum(points_new)) + ) %>% dplyr::arrange(dplyr::desc(points_new)) if (old_system) ladder <- ladder %>% dplyr::arrange(dplyr::desc(points)) ladder } - diff --git a/R/matchPoints.R b/R/matchPoints.R index a5fc290..77148f1 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -11,13 +11,16 @@ matchPoints <- function(df) { goals1 <- df %>% dplyr::filter(stat == "goal_from_zone1") %>% dplyr::group_by(squadName) %>% - dplyr::summarise(goals = sum(value)) + dplyr::summarise(goals = sum(value, na.rm = TRUE), .groups = "drop") goals2 <- df %>% dplyr::filter(stat == "goal_from_zone2") %>% dplyr::group_by(squadName) %>% - dplyr::summarise(goals2 = sum(value) * 2) + dplyr::summarise(goals2 = sum(value, na.rm = TRUE) * 2, .groups = "drop") goals <- dplyr::left_join(goals1, goals2, by = "squadName") %>% - dplyr::mutate(goals = goals + goals2) %>% + dplyr::mutate( + goals2 = dplyr::coalesce(goals2, 0), + goals = goals + goals2 + ) %>% dplyr::select(-goals2) home <- df %>% dplyr::filter(stat == "homeTeam") %>% @@ -59,7 +62,7 @@ matchPoints_pre_2020 <- function(df) { goals <- df %>% dplyr::filter(stat == "goals") %>% dplyr::group_by(squadName) %>% - dplyr::summarise(goals = sum(value)) + dplyr::summarise(goals = sum(value, na.rm = TRUE), .groups = "drop") home <- df %>% dplyr::filter(stat == "homeTeam") %>% dplyr::group_by(squadName) %>% @@ -107,9 +110,11 @@ matchPoints_pre_2020 <- function(df) { ) points_new <- scores %>% dplyr::group_by(homeSquad, awaySquad) %>% - dplyr::summarise(homePoints = sum(homePoints), - awayPoints = sum(awayPoints)) %>% - dplyr::ungroup() + dplyr::summarise( + homePoints = sum(homePoints, na.rm = TRUE), + awayPoints = sum(awayPoints, na.rm = TRUE), + .groups = "drop" + ) df1 <- points_new %>% dplyr::select(dplyr::contains("home")) %>% dplyr::rename(squadName = homeSquad, points_qtr = homePoints) diff --git a/R/tidiers.R b/R/tidiers.R index ce01753..e0dede0 100644 --- a/R/tidiers.R +++ b/R/tidiers.R @@ -26,8 +26,11 @@ tidyMatch <- function(match) { final_period <- match$matchInfo$periodCompleted team_stats <- team_stats %>% dplyr::filter(period <= final_period) %>% - tidyr::gather(stat, value, -squadId, -squadName, - -squadNickname, -squadCode, -period) %>% + tidyr::pivot_longer( + cols = -c(squadId, squadName, squadNickname, squadCode, period), + names_to = "stat", + values_to = "value" + ) %>% dplyr::mutate( round = match$matchInfo$roundNumber, game = match$matchInfo$matchNumber @@ -61,8 +64,14 @@ tidyPlayers <- function(match) { player_stats <- player_stats %>% dplyr::filter(period <= final_period) %>% dplyr::select(-displayName) %>% - tidyr::gather(stat, value, -playerId, -shortDisplayName, -firstname, - -surname, -period, -squadId, -squadName) %>% + tidyr::pivot_longer( + cols = -c( + playerId, shortDisplayName, firstname, surname, + period, squadId, squadName + ), + names_to = "stat", + values_to = "value" + ) %>% dplyr::mutate( round = match$matchInfo$roundNumber, game = match$matchInfo$matchNumber diff --git a/README.md b/README.md index ab49b86..772e240 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # superNetballR -[![Build Status](https://travis-ci.org/craigmoyle/superNetballR_updated.svg?branch=main)](https://travis-ci.org/craigmoyle/superNetballR_updated) +[![R-CMD-check](https://github.com/craigmoyle/superNetballR_updated/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/craigmoyle/superNetballR_updated/actions/workflows/R-CMD-check.yaml) ## Description @@ -9,6 +9,7 @@ This fork of `superNetballR` allows the downloading of super netball statistics `superNetballR` contains helper functions that transform the downloaded data into usable tidy data. This repository is maintained at [craigmoyle/superNetballR_updated](https://github.com/craigmoyle/superNetballR_updated). +The current Champion Data iStats portal still exposes the same zone-based result data model used by this package, and `downloadMatch()` now validates match identifiers before requesting the JSON feed. ## Installation diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..eb080cb --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(superNetballR) + +test_check("superNetballR") diff --git a/tests/testthat/helper-fixtures.R b/tests/testthat/helper-fixtures.R new file mode 100644 index 0000000..a93cf27 --- /dev/null +++ b/tests/testthat/helper-fixtures.R @@ -0,0 +1,161 @@ +make_sample_match <- function(period_completed = 2) { + list( + matchInfo = list( + homeSquadId = 10L, + awaySquadId = 20L, + periodCompleted = period_completed, + roundNumber = 5L, + matchNumber = 3L + ), + teamInfo = list( + team = list( + list( + squadId = 10L, + squadName = "Home", + squadNickname = "Homes", + squadCode = "HOM" + ), + list( + squadId = 20L, + squadName = "Away", + squadNickname = "Aways", + squadCode = "AWY" + ) + ) + ), + teamPeriodStats = list( + team = list( + list(squadId = 10L, period = 1L, gains = 2L, goalAttempts = 10L), + list(squadId = 10L, period = 2L, gains = 3L, goalAttempts = 11L), + list(squadId = 10L, period = 3L, gains = 4L, goalAttempts = 12L), + list(squadId = 20L, period = 1L, gains = 1L, goalAttempts = 8L), + list(squadId = 20L, period = 2L, gains = 2L, goalAttempts = 9L), + list(squadId = 20L, period = 3L, gains = 3L, goalAttempts = 10L) + ) + ), + playerInfo = list( + player = list( + list( + playerId = 1L, + squadId = 10L, + displayName = "Home Shooter", + shortDisplayName = "Shooter, Home", + firstname = "Home", + surname = "Shooter" + ), + list( + playerId = 2L, + squadId = 20L, + displayName = "Away Shooter", + shortDisplayName = "Shooter, Away", + firstname = "Away", + surname = "Shooter" + ) + ) + ), + playerPeriodStats = list( + player = list( + list(playerId = 1L, squadId = 10L, period = 1L, goals = 5L, feeds = 2L), + list(playerId = 1L, squadId = 10L, period = 2L, goals = 6L, feeds = 3L), + list(playerId = 1L, squadId = 10L, period = 3L, goals = 7L, feeds = 4L), + list(playerId = 2L, squadId = 20L, period = 1L, goals = 4L, feeds = 1L), + list(playerId = 2L, squadId = 20L, period = 2L, goals = 3L, feeds = 2L), + list(playerId = 2L, squadId = 20L, period = 3L, goals = 2L, feeds = 3L) + ) + ) + ) +} + +make_modern_match_stats <- function( + round, + game, + home_team, + away_team, + home_zone1, + home_zone2 = 0, + away_zone1, + away_zone2 = 0 +) { + rows <- list( + data.frame( + squadName = c(home_team, away_team), + stat = c("goal_from_zone1", "goal_from_zone1"), + value = c(home_zone1, away_zone1), + period = c(1L, 1L), + round = c(round, round), + game = c(game, game), + stringsAsFactors = FALSE + ), + data.frame( + squadName = c(home_team, away_team), + stat = c("homeTeam", "homeTeam"), + value = c(1L, 0L), + period = c(1L, 1L), + round = c(round, round), + game = c(game, game), + stringsAsFactors = FALSE + ) + ) + + if (!is.null(home_zone2)) { + rows[[length(rows) + 1L]] <- data.frame( + squadName = home_team, + stat = "goal_from_zone2", + value = home_zone2, + period = 1L, + round = round, + game = game, + stringsAsFactors = FALSE + ) + } + + if (!is.null(away_zone2)) { + rows[[length(rows) + 1L]] <- data.frame( + squadName = away_team, + stat = "goal_from_zone2", + value = away_zone2, + period = 1L, + round = round, + game = game, + stringsAsFactors = FALSE + ) + } + + do.call(rbind, rows) +} + +make_pre_2020_match_stats <- function( + round, + game, + home_team, + away_team, + home_goals, + away_goals +) { + stopifnot(length(home_goals) == length(away_goals)) + + periods <- seq_along(home_goals) + do.call( + rbind, + list( + data.frame( + squadName = c(rep(home_team, length(periods)), rep(away_team, length(periods))), + stat = "goals", + value = c(home_goals, away_goals), + period = c(periods, periods), + round = round, + game = game, + stringsAsFactors = FALSE + ), + data.frame( + squadName = c(rep(home_team, length(periods)), rep(away_team, length(periods))), + stat = "homeTeam", + value = c(rep(1L, length(periods)), rep(0L, length(periods))), + period = c(periods, periods), + round = round, + game = game, + stringsAsFactors = FALSE + ) + ) + ) +} diff --git a/tests/testthat/test-downloadMatch.R b/tests/testthat/test-downloadMatch.R new file mode 100644 index 0000000..d77dc83 --- /dev/null +++ b/tests/testthat/test-downloadMatch.R @@ -0,0 +1,33 @@ +test_that("build_match_url validates and formats request identifiers", { + expect_equal( + superNetballR:::build_match_url("10083", 5, 3), + "https://mc.championdata.com/data/10083/100830503.json" + ) + expect_equal( + superNetballR:::build_match_url(10083, "5", "3"), + "https://mc.championdata.com/data/10083/100830503.json" + ) + + expect_error( + superNetballR:::build_match_url("season-2025", 5, 3), + "comp_id must contain digits only" + ) + expect_error( + superNetballR:::build_match_url("10083", 0, 3), + "round_id must be greater than or equal to 1" + ) + expect_error( + superNetballR:::build_match_url("10083", 5, 1.5), + "game_id must contain digits only" + ) +}) + +test_that("extract_match_stats fails loudly when matchStats is absent", { + payload <- list(matchStats = list(matchInfo = list(matchNumber = 3L))) + + expect_equal(superNetballR:::extract_match_stats(payload), payload$matchStats) + expect_error( + superNetballR:::extract_match_stats(list()), + "did not include matchStats" + ) +}) diff --git a/tests/testthat/test-ladders.R b/tests/testthat/test-ladders.R new file mode 100644 index 0000000..f6fd6c6 --- /dev/null +++ b/tests/testthat/test-ladders.R @@ -0,0 +1,38 @@ +test_that("matchResults and ladders summarise a simple season correctly", { + season <- rbind( + make_modern_match_stats(1L, 1L, "A", "B", 10L, 1L, 8L, 0L), + make_modern_match_stats(2L, 1L, "B", "A", 10L, 0L, 10L, 0L) + ) + + match_results <- matchResults(season) + ladder <- ladders(season) + round_one_ladder <- ladders(season, round_num = 1L) + + expect_equal(nrow(match_results), 4) + expect_equal(ladder$points[ladder$squadName == "A"], 6) + expect_equal(ladder$points[ladder$squadName == "B"], 2) + expect_equal(round_one_ladder$points[round_one_ladder$squadName == "A"], 4) +}) + +test_that("ladders returns infinite percentage when goals against is zero", { + season <- make_modern_match_stats(1L, 1L, "A", "B", 10L, 0L, 0L, 0L) + ladder <- ladders(season) + + expect_true(is.infinite(ladder$percentage[ladder$squadName == "A"])) +}) + +test_that("ladders_pre_2020 uses the legacy match scoring pipeline", { + season <- make_pre_2020_match_stats( + round = 1L, + game = 1L, + home_team = "A", + away_team = "B", + home_goals = c(12L, 8L), + away_goals = c(10L, 7L) + ) + + ladder <- ladders_pre_2020(season) + + expect_equal(ladder$points[ladder$squadName == "A"], 2) + expect_equal(ladder$points_new[ladder$squadName == "A"], 5) +}) diff --git a/tests/testthat/test-match-points.R b/tests/testthat/test-match-points.R new file mode 100644 index 0000000..f5bcd2f --- /dev/null +++ b/tests/testthat/test-match-points.R @@ -0,0 +1,54 @@ +test_that("matchPoints handles missing zone-two rows and awards modern points", { + df <- make_modern_match_stats( + round = 1L, + game = 1L, + home_team = "Home", + away_team = "Away", + home_zone1 = 10L, + home_zone2 = 2L, + away_zone1 = 9L, + away_zone2 = NULL + ) + + result <- matchPoints(df) + + expect_equal(result$goals[result$squadName == "Home"], 14) + expect_equal(result$goals[result$squadName == "Away"], 9) + expect_equal(result$points[result$squadName == "Home"], 4) + expect_equal(result$points[result$squadName == "Away"], 0) +}) + +test_that("matchPoints returns draw points for tied matches", { + df <- make_modern_match_stats( + round = 1L, + game = 1L, + home_team = "Home", + away_team = "Away", + home_zone1 = 10L, + home_zone2 = 1L, + away_zone1 = 12L, + away_zone2 = 0L + ) + + result <- matchPoints(df) + + expect_true(all(result$score_diff == 0)) + expect_true(all(result$points == 2)) +}) + +test_that("matchPoints_pre_2020 keeps old and new scoring totals", { + df <- make_pre_2020_match_stats( + round = 1L, + game = 1L, + home_team = "Home", + away_team = "Away", + home_goals = c(10L, 8L), + away_goals = c(8L, 9L) + ) + + result <- matchPoints_pre_2020(df) + + expect_equal(result$points[result$squadName == "Home"], 2) + expect_equal(result$points_new[result$squadName == "Home"], 5) + expect_equal(result$points_new[result$squadName == "Away"], 1) +}) diff --git a/tests/testthat/test-tidiers.R b/tests/testthat/test-tidiers.R new file mode 100644 index 0000000..cc96881 --- /dev/null +++ b/tests/testthat/test-tidiers.R @@ -0,0 +1,19 @@ +test_that("tidyMatch returns completed periods in long format", { + result <- tidyMatch(make_sample_match(period_completed = 2)) + + expect_true(all(result$period <= 2)) + expect_equal(nrow(result), 12) + expect_setequal(unique(result$stat), c("gains", "goalAttempts", "homeTeam")) + expect_equal(unique(result$value[result$squadName == "Home" & result$stat == "homeTeam"]), 1) + expect_equal(unique(result$value[result$squadName == "Away" & result$stat == "homeTeam"]), 0) +}) + +test_that("tidyPlayers keeps player identity columns and drops displayName", { + result <- tidyPlayers(make_sample_match(period_completed = 2)) + + expect_true(all(result$period <= 2)) + expect_equal(nrow(result), 8) + expect_false("displayName" %in% names(result)) + expect_setequal(unique(result$stat), c("feeds", "goals")) + expect_equal(unique(result$squadName[result$playerId == 1]), "Home") +}) diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd index cb7e0b9..02ed0cd 100644 --- a/vignettes/getting-started.Rmd +++ b/vignettes/getting-started.Rmd @@ -1,6 +1,6 @@ --- title: "Getting Started with superNetballR" -author: "Steve Lane" +author: "Steve Lane and Craig Moyle" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > @@ -20,13 +20,13 @@ knitr::opts_chunk$set( # Introduction -This vignette provides an overview to get you started with using `superNetballR`. As at 2018-04-08, this package contains the full 2017 season match statistics and player statistics. +This vignette provides an overview to get you started with using `superNetballR`. The package ships with the full 2017 season match and player statistics, while the live Champion Data iStats portal still publishes compatible match JSON for current seasons. - *2020-08-08 Update*: The package has been updated to include the players team name in the full player statistics. This will make it easier to analyse player trends by team. The 2017 data has been updated accordingly. +*Current fork note*: the package has been updated for the post-2020 super shot scoring model, and `downloadMatch()` now validates competition, round, and game identifiers before requesting data. # Sourcing Match Data -Data are sourced from 'https://mc.championdata.com/data/' under certain match and round id's. The 2017 home and away season is in the 10083 folder, whilst the finals are in the 10084 folder. The full (processed) data are supplied with this package. +Data are sourced from `https://mc.championdata.com/data/` using competition, round, and game identifiers. The bundled examples below use the 2017 home-and-away competition ID (`10083`) and the 2017 finals competition ID (`10084`). The full processed 2017 datasets are supplied with this package, while newer seasons can still be queried with the appropriate competition IDs from the current iStats portal. To download statistics from a single match, you use the `downloadMatch` function. As an example, the following code will download the match from round 5, game 3: From 36e94e9e890db85617e6aa2c1aa4d4bfadaa27c4 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 10 Mar 2026 21:59:21 +1100 Subject: [PATCH 21/56] Refresh package documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- R/downloadMatch.R | 4 ++++ R/ladders.R | 5 +++++ R/matchPoints.R | 7 +++++++ README.md | 26 +++++++++++++++++++++++--- man/downloadMatch.Rd | 5 +++++ man/ladders.Rd | 6 ++++++ man/matchPoints.Rd | 4 ++++ man/matchPoints_pre_2020.Rd | 5 +++++ vignettes/getting-started.Rmd | 21 +++++++++++++++++++++ 9 files changed, 80 insertions(+), 3 deletions(-) diff --git a/R/downloadMatch.R b/R/downloadMatch.R index d11ca0e..e6eb094 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -57,6 +57,10 @@ extract_match_stats <- function(payload) { #' download. There are four games per round in the regular season, two games #' in the semi finals, one game for the prelim, and one grand final. #' @return A list containing game and player data for the match. +#' @details +#' \code{downloadMatch()} validates the supplied identifiers, retries transient +#' HTTP failures, and raises an explicit error if the Champion Data response no +#' longer includes a \code{matchStats} object. #' #' @examples #' \dontrun{ diff --git a/R/ladders.R b/R/ladders.R index 9dbc96d..e2b2f7c 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -15,6 +15,11 @@ safe_percentage <- function(goals_for, goals_against) { #' @return Data frame containing the ladder position of all teams. If round and #' game are not supplied, the ladder position is calculated using all match #' data present in the \code{df} supplied. +#' @details +#' \code{ladders()} uses the current 2020+ scoring helpers, while +#' \code{ladders_pre_2020()} uses the legacy scoring pipeline. Ladder +#' percentages are protected against divide-by-zero by returning \code{Inf} +#' when a team has not conceded. #' #' @export ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { diff --git a/R/matchPoints.R b/R/matchPoints.R index 77148f1..67f3d45 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -5,6 +5,9 @@ #' @param df Match data. #' #' @return A data frame containing the final scores, and points for the ladder. +#' @details +#' \code{matchPoints()} treats \code{goal_from_zone1} as one point and +#' \code{goal_from_zone2} as two points, matching the current super shot era. #' @export matchPoints <- function(df) { ## This first section calculates points based on the old system. @@ -56,6 +59,10 @@ matchPoints <- function(df) { #' @param df Match data. #' #' @return A data frame containing the final scores, and points for the ladder. +#' @details +#' \code{matchPoints_pre_2020()} uses the original goals statistic for match +#' results and also reports the newer quarter-points summary in +#' \code{points_new}. #' @export matchPoints_pre_2020 <- function(df) { ## This first section calculates points based on the old system. diff --git a/README.md b/README.md index 772e240..ef98349 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,38 @@ The current Champion Data iStats portal still exposes the same zone-based result ## Installation -Installation in R requires `devtools`. To install, run the following from an R session: +Installation in R requires `remotes`. To install, run the following from an R session: ``` R -devtools::install_github("craigmoyle/superNetballR_updated") +install.packages("remotes") +remotes::install_github("craigmoyle/superNetballR_updated") ``` To install the current `main` branch explicitly: ``` R -devtools::install_github("craigmoyle/superNetballR_updated@main") +remotes::install_github("craigmoyle/superNetballR_updated@main") ``` +## Current behavior + +- `downloadMatch()` validates competition, round, and game identifiers, retries transient HTTP failures, and errors clearly if the Champion Data payload is missing `matchStats`. +- `matchPoints()` and `ladders()` implement the current super shot scoring model for 2020+ data. +- `matchPoints_pre_2020()` and `ladders_pre_2020()` remain available for legacy seasons and older points systems. +- The package includes a `testthat` suite and a GitHub Actions `R-CMD-check` workflow for ongoing maintenance. + +## Development + +The repository now uses GitHub Actions instead of Travis CI. Local developer commands are available through the `Makefile`: + +```sh +make test +make build +make check +``` + +`make check` uses base `R CMD build` and `R CMD check`, while CI regenerates package documentation before running `R-CMD-check`. + ## Notes The package has been updated to account for the super goal in 2020. Ladders and points have been adjusted for this. If you want to use the old scoring systems, these are available using `_pre_2020` versions of the appropriate functions. diff --git a/man/downloadMatch.Rd b/man/downloadMatch.Rd index 2a02bcf..16821fa 100644 --- a/man/downloadMatch.Rd +++ b/man/downloadMatch.Rd @@ -20,6 +20,11 @@ in the semi finals, one game for the prelim, and one grand final.} \value{ A list containing game and player data for the match. } +\details{ +\code{downloadMatch()} validates the supplied identifiers, retries transient +HTTP failures, and raises an explicit error if the Champion Data response no +longer includes a \code{matchStats} object. +} \description{ \code{downloadMatch} downloads match and player data for a single match. } diff --git a/man/ladders.Rd b/man/ladders.Rd index f185d3b..c2b0d83 100644 --- a/man/ladders.Rd +++ b/man/ladders.Rd @@ -27,6 +27,12 @@ Data frame containing the ladder position of all teams. If round and game are not supplied, the ladder position is calculated using all match data present in the \code{df} supplied. } +\details{ +\code{ladders()} uses the current 2020+ scoring helpers, while +\code{ladders_pre_2020()} uses the legacy scoring pipeline. Ladder +percentages are protected against divide-by-zero by returning \code{Inf} +when a team has not conceded. +} \description{ \code{ladders} calculates ladder positions at the end of a match. } diff --git a/man/matchPoints.Rd b/man/matchPoints.Rd index f6be993..f6cacfd 100644 --- a/man/matchPoints.Rd +++ b/man/matchPoints.Rd @@ -12,6 +12,10 @@ matchPoints(df) \value{ A data frame containing the final scores, and points for the ladder. } +\details{ +\code{matchPoints()} treats \code{goal_from_zone1} as one point and +\code{goal_from_zone2} as two points, matching the current super shot era. +} \description{ \code{matchPoints} calculates final match goals and score difference. } diff --git a/man/matchPoints_pre_2020.Rd b/man/matchPoints_pre_2020.Rd index ae5b10d..2b9c1ed 100644 --- a/man/matchPoints_pre_2020.Rd +++ b/man/matchPoints_pre_2020.Rd @@ -12,6 +12,11 @@ matchPoints_pre_2020(df) \value{ A data frame containing the final scores, and points for the ladder. } +\details{ +\code{matchPoints_pre_2020()} uses the original goals statistic for match +results and also reports the newer quarter-points summary in +\code{points_new}. +} \description{ \code{matchPoints_pre_2020} calculates final match goals and score difference, for seasons pre-2020. diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd index 02ed0cd..b4cd941 100644 --- a/vignettes/getting-started.Rmd +++ b/vignettes/getting-started.Rmd @@ -28,6 +28,8 @@ This vignette provides an overview to get you started with using `superNetballR` Data are sourced from `https://mc.championdata.com/data/` using competition, round, and game identifiers. The bundled examples below use the 2017 home-and-away competition ID (`10083`) and the 2017 finals competition ID (`10084`). The full processed 2017 datasets are supplied with this package, while newer seasons can still be queried with the appropriate competition IDs from the current iStats portal. +If the live endpoint returns a transient HTTP error, `downloadMatch()` will retry before failing. If the response no longer includes a `matchStats` object, the function stops with an explicit error so schema changes are easier to detect. + To download statistics from a single match, you use the `downloadMatch` function. As an example, the following code will download the match from round 5, game 3: ```{r get-data-function,eval=FALSE} @@ -91,3 +93,22 @@ ladder ``` The round number is provided above, as the home and away season contained 14 rounds. + +# Legacy scoring helpers + +For seasons prior to the super shot era, use the `_pre_2020` helpers. These retain the legacy match-points calculation while still exposing the newer quarter-points summary where it is useful for comparison. + +```{r ladders-pre-2020, eval=FALSE} +legacy_ladder <- ladders_pre_2020(season_2017, round_num = 14, old_system = TRUE) +legacy_ladder +``` + +# Development workflow + +This fork is maintained with automated tests and a GitHub Actions `R-CMD-check` workflow. For local work, the repository `Makefile` exposes the same core tasks: + +```{r dev-workflow, eval=FALSE} +make test +make build +make check +``` From 4e6e4e2a64e04bbb36cc49850271ad4ebff5ac70 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 10 Mar 2026 22:10:21 +1100 Subject: [PATCH 22/56] Update team colour data Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESCRIPTION | 5 +- Makefile | 2 +- NAMESPACE | 1 - R/data.R | 6 +- R/ladders.R | 4 +- R/superNetballR.R | 8 +- R/tidiers.R | 8 +- README.md | 1 + _pkgdown.yml | 2 + data/team_colours.rda | Bin 395 -> 419 bytes docs/articles/getting-started.html | 311 +++++++++++++++++------------ docs/articles/index.html | 116 +++-------- docs/authors.html | 147 ++++++-------- docs/bootstrap-toc.css | 60 ++++++ docs/bootstrap-toc.js | 159 +++++++++++++++ docs/index.html | 139 +++++++++---- docs/pkgdown.css | 260 +++++++++++++++++++----- docs/pkgdown.js | 38 ++-- docs/pkgdown.yml | 9 +- docs/reference/downloadMatch.html | 198 +++++++----------- docs/reference/index.html | 190 +++++------------- docs/reference/ladders.html | 194 +++++++----------- docs/reference/matchPoints.html | 160 +++++---------- docs/reference/players_2017.html | 184 +++++++---------- docs/reference/round5_game3.html | 139 ++++--------- docs/reference/season_2017.html | 178 ++++++----------- docs/reference/superNetballR.html | 148 +------------- docs/reference/tidyMatch.html | 157 +++++---------- docs/reference/tidyPlayers.html | 157 +++++---------- docs/sitemap.xml | 22 ++ man/downloadMatch.Rd | 6 +- man/ladders.Rd | 6 +- man/matchPoints.Rd | 6 +- man/matchPoints_pre_2020.Rd | 8 +- man/superNetballR-package.Rd | 24 +++ man/superNetballR.Rd | 9 - man/team_colours.Rd | 6 +- tests/testthat/test-ladders.R | 2 +- 38 files changed, 1409 insertions(+), 1661 deletions(-) create mode 100644 docs/bootstrap-toc.css create mode 100644 docs/bootstrap-toc.js create mode 100644 docs/sitemap.xml create mode 100644 man/superNetballR-package.Rd delete mode 100644 man/superNetballR.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 8dfa279..3ab7abb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,8 @@ Imports: dplyr, httr, tidyr, purrr -URL: https://github.com/craigmoyle/superNetballR_updated +URL: https://craigmoyle.github.io/superNetballR_updated, + https://github.com/craigmoyle/superNetballR_updated BugReports: https://github.com/craigmoyle/superNetballR_updated/issues Config/testthat/edition: 3 -RoxygenNote: 7.1.1 +RoxygenNote: 7.3.3 diff --git a/Makefile b/Makefile index bf52712..3f0f877 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ build: R CMD build . test: - Rscript -e "testthat::test_dir('tests/testthat', reporter = 'summary', stop_on_failure = TRUE)" + Rscript -e "testthat::test_local('.', reporter = 'summary', stop_on_failure = TRUE)" check: build R CMD check --no-manual --as-cran $(PKG_TARBALL) diff --git a/NAMESPACE b/NAMESPACE index 015d01f..5739c5c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,4 +9,3 @@ export(matchResults) export(shinySuperNetballR) export(tidyMatch) export(tidyPlayers) -importFrom(dplyr,"%>%") diff --git a/R/data.R b/R/data.R index 2ca8095..9ab590c 100644 --- a/R/data.R +++ b/R/data.R @@ -48,9 +48,11 @@ #' Team colours. #' -#' A dataset containing hex-coded team colours for each team. +#' A dataset containing hex-coded team colours for the current Super Netball +#' competition teams, plus the historical Magpies entry used by the bundled +#' 2017 data. #' -#' @format A data frame with 8 rows and 3 variables: +#' @format A data frame with 9 rows and 3 variables: #' \describe{ #' \item{squadName}{Full squad name} #' \item{squadId}{Unique squad number} diff --git a/R/ladders.R b/R/ladders.R index e2b2f7c..cd53c72 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -38,7 +38,7 @@ ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { ladder <- match_results %>% dplyr::group_by(squadName) %>% dplyr::summarise( - games = n(), + games = dplyr::n(), goals_for = sum(goals), goals_against = sum(goals - score_diff), percentage = safe_percentage(goals_for, goals_against), @@ -90,7 +90,7 @@ ladders_pre_2020 <- function(df, round_num = NULL, game_num = NULL, old_system = ladder <- match_results %>% dplyr::group_by(squadName) %>% dplyr::summarise( - games = n(), + games = dplyr::n(), goals_for = sum(goals), goals_against = sum(goals - score_diff), percentage = safe_percentage(goals_for, goals_against), diff --git a/R/superNetballR.R b/R/superNetballR.R index d6964f2..95db00c 100644 --- a/R/superNetballR.R +++ b/R/superNetballR.R @@ -1,11 +1,7 @@ -#' \code{superNetballR} package -#' #' Functions getting and manipulating Super Netball data. #' -#' @docType package -#' @name superNetballR -#' @importFrom dplyr %>% -NULL +#' @keywords internal +"_PACKAGE" ## quiets concerns of R CMD check re: the .'s that appear in pipelines if (getRversion() >= "2.15.1") { diff --git a/R/tidiers.R b/R/tidiers.R index e0dede0..ff523db 100644 --- a/R/tidiers.R +++ b/R/tidiers.R @@ -53,6 +53,11 @@ tidyPlayers <- function(match) { player_info <- match$playerInfo$player player_info <- dplyr::bind_rows(player_info) player_stats <- dplyr::left_join(player_stats, player_info, by = "playerId") + if (all(c("squadId.x", "squadId.y") %in% names(player_stats))) { + player_stats <- player_stats %>% + dplyr::mutate(squadId = dplyr::coalesce(squadId.x, squadId.y)) %>% + dplyr::select(-squadId.x, -squadId.y) + } squad_info <- match$teamInfo$team squad_info <- dplyr::bind_rows(squad_info) squad_info <- dplyr::select(squad_info, squadId, squadName) @@ -70,7 +75,8 @@ tidyPlayers <- function(match) { period, squadId, squadName ), names_to = "stat", - values_to = "value" + values_to = "value", + values_transform = list(value = as.character) ) %>% dplyr::mutate( round = match$matchInfo$roundNumber, diff --git a/README.md b/README.md index ef98349..f0ad726 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ remotes::install_github("craigmoyle/superNetballR_updated@main") - `downloadMatch()` validates competition, round, and game identifiers, retries transient HTTP failures, and errors clearly if the Champion Data payload is missing `matchStats`. - `matchPoints()` and `ladders()` implement the current super shot scoring model for 2020+ data. - `matchPoints_pre_2020()` and `ladders_pre_2020()` remain available for legacy seasons and older points systems. +- `team_colours` includes the current Melbourne Mavericks entry while retaining the historical Magpies row needed by the bundled 2017 data. - The package includes a `testthat` suite and a GitHub Actions `R-CMD-check` workflow for ongoing maintenance. ## Development diff --git a/_pkgdown.yml b/_pkgdown.yml index 032d9c1..a50ec16 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -1,3 +1,5 @@ +url: https://craigmoyle.github.io/superNetballR_updated/ + template: params: bootswatch: spacelab diff --git a/data/team_colours.rda b/data/team_colours.rda index b6831091509051d90ea6035f2cbfc7a4836e4844..e1e999006dc24eab875e6a533aad2c6e5cd3d586 100644 GIT binary patch literal 419 zcmV;U0bKq11Aq_!0D-Up zIM~qC1O}=3Q`)EMnhi7nJxqWaJx@q_gFptJQ$0{qQK-p~0imWrpa1|F9-tZkWXJ+j zQfW_9#AupmXlM^m01r?GfuIcl8&L{@s;ZMgEHZ;a@xxxc=N9Tf&DBrTj1fbBD>L$% zCfbk~G%22!gIYsm6l+VtV`7NkYCL>rcqbl9)k>=bNrs$ZlQ0M;uK>djdx4nlR3K-d zKIT&->#Ue3wZssn(Ui41Y3>k zu#FPo)UGKpYcz6xx~nJ)snDpby8d1gd0&K%h@wIi)@ZThdGBZe3`RSh>yVHB_5}t1 N7ji{7P>_IEv!SL9wVeO} literal 395 zcmV;60d)RCT4*^jL0KkKSweN5^8f(af5`v;PC)IEe}F%yJ;1-`|G+>11AqWQumL!l zVkRV~n3_}dN2DH_5C8xgdIzaJLrnnMn@A$2PbsIU(?-ZGnBTejACl9q=io zu1a7b4fU>1zJ4s|%6~kUo0uR9fhiG=GzQET$V^cY^+abL1qi4aC`A!rMiCGXT`Plt zXc$vjzqFeB^H&wSfhKY>u-eHn+(4i;Hp@1(p7ZBCs>}cY diff --git a/docs/articles/getting-started.html b/docs/articles/getting-started.html index fd4027b..e55d70c 100644 --- a/docs/articles/getting-started.html +++ b/docs/articles/getting-started.html @@ -1,53 +1,50 @@ - + Getting Started with superNetballR • superNetballR - - - + + + + + - - - - + + +
@@ -89,133 +88,181 @@

2018-07-14

-
-

-Introduction

-

This vignette provides an overview to get you started with using superNetballR. As at 2018-04-08, this package contains the full 2017 season match statistics and player statistics.

+
+

Introduction +

+

This vignette provides an overview to get you started with using +superNetballR. The package ships with the full 2017 season +match and player statistics, while the live Champion Data iStats portal +still publishes compatible match JSON for current seasons.

+

Current fork note: the package has been updated for the +post-2020 super shot scoring model, and downloadMatch() now +validates competition, round, and game identifiers before requesting +data.

-
-

-Sourcing Match Data

-

Data are sourced from ‘https://mc.championdata.com/data/’ under certain match and round id’s. The 2017 home and away season is in the 10083 folder, whilst the finals are in the 10084 folder. The full (processed) data are supplied with this package.

-

To download statistics from a single match, you use the downloadMatch function. As an example, the following code will download the match from round 5, game 3:

- -

The downloaded object is a list, containing detailed statistics (including period-by-period statistics) for the match and players:

- +
+

Sourcing Match Data +

+

Data are sourced from https://mc.championdata.com/data/ +using competition, round, and game identifiers. The bundled examples +below use the 2017 home-and-away competition ID (10083) and +the 2017 finals competition ID (10084). The full processed +2017 datasets are supplied with this package, while newer seasons can +still be queried with the appropriate competition IDs from the current +iStats portal.

+

If the live endpoint returns a transient HTTP error, +downloadMatch() will retry before failing. If the response +no longer includes a matchStats object, the function stops +with an explicit error so schema changes are easier to detect.

+

To download statistics from a single match, you use the +downloadMatch function. As an example, the following code +will download the match from round 5, game 3:

+
+library(dplyr)
+library(superNetballR)
+round5_game3 <- downloadMatch("10083", 5, 3)
+

The downloaded object is a list, containing detailed statistics +(including period-by-period statistics) for the match and players:

+
+class(round5_game3)
+#> [1] "list"
+names(round5_game3)
+#>  [1] "jobId"             "playerStats"       "matchInfo"        
+#>  [4] "playerInfo"        "playerPeriodStats" "periodInfo"       
+#>  [7] "teamInfo"          "teamPeriodStats"   "teamStats"        
+#> [10] "playerSubs"        "scoreFlow"
-
-

-Tidying Match and Player Statistics

-

The full match data can be tidied into match and player statistics, grouped by period.

-

Tidying match statistics using the tidyMatch function:

- -

Tidying player statistics using the tidyPlayers function:

- +
+

Tidying Match and Player Statistics +

+

The full match data can be tidied into match and player statistics, +grouped by period.

+

Tidying match statistics using the tidyMatch +function:

+
+tidied_match <- tidyMatch(round5_game3)
+tidied_match
+#> # A tibble: 256 × 9
+#>    period squadId squadName      squadNickname squadCode stat  value round  game
+#>     <int>   <int> <chr>          <chr>         <chr>     <chr> <int> <int> <int>
+#>  1      1    8117 Sunshine Coas… Lightning     SCL       rebo…     0     5     3
+#>  2      1    8117 Sunshine Coas… Lightning     SCL       goal…     9     5     3
+#>  3      1    8117 Sunshine Coas… Lightning     SCL       goal…     3     5     3
+#>  4      1    8117 Sunshine Coas… Lightning     SCL       pena…    12     5     3
+#>  5      1    8117 Sunshine Coas… Lightning     SCL       time…    49     5     3
+#>  6      1    8117 Sunshine Coas… Lightning     SCL       gain      5     5     3
+#>  7      1    8117 Sunshine Coas… Lightning     SCL       offe…     0     5     3
+#>  8      1    8117 Sunshine Coas… Lightning     SCL       poss…    39     5     3
+#>  9      1    8117 Sunshine Coas… Lightning     SCL       goal…     3     5     3
+#> 10      1    8117 Sunshine Coas… Lightning     SCL       bloc…     0     5     3
+#> # ℹ 246 more rows
+

Tidying player statistics using the tidyPlayers +function:

+
+tidied_players <- tidyPlayers(round5_game3)
+tidied_players
+#> # A tibble: 2,560 × 11
+#>    playerId period squadId shortDisplayName firstname surname squadName    stat 
+#>       <int>  <int>   <int> <chr>            <chr>     <chr>   <chr>        <chr>
+#>  1    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… rebo…
+#>  2    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… pena…
+#>  3    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… gain 
+#>  4    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… offe…
+#>  5    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… poss…
+#>  6    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… goal…
+#>  7    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… bloc…
+#>  8    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… pass…
+#>  9    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… goal…
+#> 10    80010      1    8117 Mentor, G        Geva      Mentor  Sunshine Co… toss…
+#> # ℹ 2,550 more rows
+#> # ℹ 3 more variables: value <chr>, round <int>, game <int>
-
-

-Season Data and Ladders

-

Provided with the superNetballR package is the full 2017 season match and player statistics in tidied format. These have been obtained using the previously described methods, tidied, and then combined by rows to produce a single data frame:

- -

Using a dataset that contains all matches up to a given round in a season means it is easy to reproduce ladder positions. A ladders function is provided that can be used on full season data. For example, here is the ladder as it stood at the end of the 2017 home and away season:

- -

The round number is provided above, as the home and away season contained 14 rounds.

+
+

Season Data and Ladders +

+

Provided with the superNetballR package is the full 2017 +season match and player statistics in tidied format. These have been +obtained using the previously described methods, tidied, and then +combined by rows to produce a single data frame:

+
+data(season_2017)
+season_2017
+#> # A tibble: 15,360 × 9
+#>    period squadId squadName      squadNickname squadCode stat  value round  game
+#>     <int>   <int> <chr>          <chr>         <chr>     <chr> <int> <int> <int>
+#>  1      1     806 NSW Swifts     Swifts        NSW       rebo…     0     1     1
+#>  2      2     806 NSW Swifts     Swifts        NSW       rebo…     1     1     1
+#>  3      3     806 NSW Swifts     Swifts        NSW       rebo…     1     1     1
+#>  4      4     806 NSW Swifts     Swifts        NSW       rebo…     0     1     1
+#>  5      1    8118 GIANTS Netball GIANTS        GNB       rebo…     1     1     1
+#>  6      2    8118 GIANTS Netball GIANTS        GNB       rebo…     2     1     1
+#>  7      3    8118 GIANTS Netball GIANTS        GNB       rebo…     2     1     1
+#>  8      4    8118 GIANTS Netball GIANTS        GNB       rebo…     1     1     1
+#>  9      1     806 NSW Swifts     Swifts        NSW       goal…     6     1     1
+#> 10      2     806 NSW Swifts     Swifts        NSW       goal…    13     1     1
+#> # ℹ 15,350 more rows
+

Using a dataset that contains all matches up to a given round in a +season means it is easy to reproduce ladder positions. A +ladders function is provided that can be used on full +season data. For example, here is the ladder as it stood at the end of +the 2017 home and away season:

+
+ladder <- ladders(season_2017, round_num = 14)
+ladder
+#> # A tibble: 0 × 6
+#> # ℹ 6 variables: squadName <chr>, games <int>, goals_for <dbl>,
+#> #   goals_against <dbl>, percentage <dbl>, points <int>
+

The round number is provided above, as the home and away season +contained 14 rounds.

+
+
+

Legacy scoring helpers +

+

For seasons prior to the super shot era, use the +_pre_2020 helpers. These retain the legacy match-points +calculation while still exposing the newer quarter-points summary where +it is useful for comparison.

+
+legacy_ladder <- ladders_pre_2020(season_2017, round_num = 14, old_system = TRUE)
+legacy_ladder
+
+
+

Development workflow +

+

This fork is maintained with automated tests and a GitHub Actions +R-CMD-check workflow. For local work, the repository +Makefile exposes the same core tasks:

+
make test
+make build
+make check
- +
-

Site built with pkgdown.

+

+

Site built with pkgdown 2.2.0.

- + + + + diff --git a/docs/articles/index.html b/docs/articles/index.html index 9bb87b4..3ec16f3 100644 --- a/docs/articles/index.html +++ b/docs/articles/index.html @@ -1,109 +1,53 @@ - - - - - - - -Articles • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Articles • superNetballR + - - -
-
- - -
-
+ +
+
Getting Started with superNetballR
+
+
-
- +
+ + + + - - - + diff --git a/docs/authors.html b/docs/authors.html index ce44587..acf6d99 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -1,140 +1,105 @@ - - - - - - - -Authors • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Authors and Citation • superNetballR + - - - -
-
-
-
+ +
-
-
- +
+ + + + - - - + diff --git a/docs/bootstrap-toc.css b/docs/bootstrap-toc.css new file mode 100644 index 0000000..5a85941 --- /dev/null +++ b/docs/bootstrap-toc.css @@ -0,0 +1,60 @@ +/*! + * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) + * Copyright 2015 Aidan Feldman + * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ + +/* modified from https://github.com/twbs/bootstrap/blob/94b4076dd2efba9af71f0b18d4ee4b163aa9e0dd/docs/assets/css/src/docs.css#L548-L601 */ + +/* All levels of nav */ +nav[data-toggle='toc'] .nav > li > a { + display: block; + padding: 4px 20px; + font-size: 13px; + font-weight: 500; + color: #767676; +} +nav[data-toggle='toc'] .nav > li > a:hover, +nav[data-toggle='toc'] .nav > li > a:focus { + padding-left: 19px; + color: #563d7c; + text-decoration: none; + background-color: transparent; + border-left: 1px solid #563d7c; +} +nav[data-toggle='toc'] .nav > .active > a, +nav[data-toggle='toc'] .nav > .active:hover > a, +nav[data-toggle='toc'] .nav > .active:focus > a { + padding-left: 18px; + font-weight: bold; + color: #563d7c; + background-color: transparent; + border-left: 2px solid #563d7c; +} + +/* Nav: second level (shown on .active) */ +nav[data-toggle='toc'] .nav .nav { + display: none; /* Hide by default, but at >768px, show it */ + padding-bottom: 10px; +} +nav[data-toggle='toc'] .nav .nav > li > a { + padding-top: 1px; + padding-bottom: 1px; + padding-left: 30px; + font-size: 12px; + font-weight: normal; +} +nav[data-toggle='toc'] .nav .nav > li > a:hover, +nav[data-toggle='toc'] .nav .nav > li > a:focus { + padding-left: 29px; +} +nav[data-toggle='toc'] .nav .nav > .active > a, +nav[data-toggle='toc'] .nav .nav > .active:hover > a, +nav[data-toggle='toc'] .nav .nav > .active:focus > a { + padding-left: 28px; + font-weight: 500; +} + +/* from https://github.com/twbs/bootstrap/blob/e38f066d8c203c3e032da0ff23cd2d6098ee2dd6/docs/assets/css/src/docs.css#L631-L634 */ +nav[data-toggle='toc'] .nav > .active > ul { + display: block; +} diff --git a/docs/bootstrap-toc.js b/docs/bootstrap-toc.js new file mode 100644 index 0000000..1cdd573 --- /dev/null +++ b/docs/bootstrap-toc.js @@ -0,0 +1,159 @@ +/*! + * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) + * Copyright 2015 Aidan Feldman + * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ +(function() { + 'use strict'; + + window.Toc = { + helpers: { + // return all matching elements in the set, or their descendants + findOrFilter: function($el, selector) { + // http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/ + // http://stackoverflow.com/a/12731439/358804 + var $descendants = $el.find(selector); + return $el.filter(selector).add($descendants).filter(':not([data-toc-skip])'); + }, + + generateUniqueIdBase: function(el) { + var text = $(el).text(); + var anchor = text.trim().toLowerCase().replace(/[^A-Za-z0-9]+/g, '-'); + return anchor || el.tagName.toLowerCase(); + }, + + generateUniqueId: function(el) { + var anchorBase = this.generateUniqueIdBase(el); + for (var i = 0; ; i++) { + var anchor = anchorBase; + if (i > 0) { + // add suffix + anchor += '-' + i; + } + // check if ID already exists + if (!document.getElementById(anchor)) { + return anchor; + } + } + }, + + generateAnchor: function(el) { + if (el.id) { + return el.id; + } else { + var anchor = this.generateUniqueId(el); + el.id = anchor; + return anchor; + } + }, + + createNavList: function() { + return $(''); + }, + + createChildNavList: function($parent) { + var $childList = this.createNavList(); + $parent.append($childList); + return $childList; + }, + + generateNavEl: function(anchor, text) { + var $a = $(''); + $a.attr('href', '#' + anchor); + $a.text(text); + var $li = $('
  • '); + $li.append($a); + return $li; + }, + + generateNavItem: function(headingEl) { + var anchor = this.generateAnchor(headingEl); + var $heading = $(headingEl); + var text = $heading.data('toc-text') || $heading.text(); + return this.generateNavEl(anchor, text); + }, + + // Find the first heading level (`

    `, then `

    `, etc.) that has more than one element. Defaults to 1 (for `

    `). + getTopLevel: function($scope) { + for (var i = 1; i <= 6; i++) { + var $headings = this.findOrFilter($scope, 'h' + i); + if ($headings.length > 1) { + return i; + } + } + + return 1; + }, + + // returns the elements for the top level, and the next below it + getHeadings: function($scope, topLevel) { + var topSelector = 'h' + topLevel; + + var secondaryLevel = topLevel + 1; + var secondarySelector = 'h' + secondaryLevel; + + return this.findOrFilter($scope, topSelector + ',' + secondarySelector); + }, + + getNavLevel: function(el) { + return parseInt(el.tagName.charAt(1), 10); + }, + + populateNav: function($topContext, topLevel, $headings) { + var $context = $topContext; + var $prevNav; + + var helpers = this; + $headings.each(function(i, el) { + var $newNav = helpers.generateNavItem(el); + var navLevel = helpers.getNavLevel(el); + + // determine the proper $context + if (navLevel === topLevel) { + // use top level + $context = $topContext; + } else if ($prevNav && $context === $topContext) { + // create a new level of the tree and switch to it + $context = helpers.createChildNavList($prevNav); + } // else use the current $context + + $context.append($newNav); + + $prevNav = $newNav; + }); + }, + + parseOps: function(arg) { + var opts; + if (arg.jquery) { + opts = { + $nav: arg + }; + } else { + opts = arg; + } + opts.$scope = opts.$scope || $(document.body); + return opts; + } + }, + + // accepts a jQuery object, or an options object + init: function(opts) { + opts = this.helpers.parseOps(opts); + + // ensure that the data attribute is in place for styling + opts.$nav.attr('data-toggle', 'toc'); + + var $topContext = this.helpers.createChildNavList(opts.$nav); + var topLevel = this.helpers.getTopLevel(opts.$scope); + var $headings = this.helpers.getHeadings(opts.$scope, topLevel); + this.helpers.populateNav($topContext, topLevel, $headings); + } + }; + + $(function() { + $('nav[data-toggle="toc"]').each(function(i, el) { + var $nav = $(el); + Toc.init($nav); + }); + }); +})(); diff --git a/docs/index.html b/docs/index.html index 28c6982..d92c684 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,53 +1,51 @@ - + Downloads and tidies super netball statistics • superNetballR - - - + + + + + - - - + + +
    -
    - - -
    -

    -Description

    -

    This package allows the downloading of super netball statistics (https://stevelane.github.io/superNetballR/. The first super netball season was in 2017, and was eventually won by the Sunshine Coast Lightning.

    +
    + +

    R-CMD-check

    +
    +

    Description +

    +

    This fork of superNetballR allows the downloading of super netball statistics from the original project site: https://stevelane.github.io/superNetballR/. The first super netball season was in 2017, and was eventually won by the Sunshine Coast Lightning.

    superNetballR contains helper functions that transform the downloaded data into usable tidy data.

    +

    This repository is maintained at craigmoyle/superNetballR_updated. The current Champion Data iStats portal still exposes the same zone-based result data model used by this package, and downloadMatch() now validates match identifiers before requesting the JSON feed.

    +
    +
    +

    Installation +

    +

    Installation in R requires remotes. To install, run the following from an R session:

    +
    +install.packages("remotes")
    +remotes::install_github("craigmoyle/superNetballR_updated")
    +

    To install the current main branch explicitly:

    +
    +remotes::install_github("craigmoyle/superNetballR_updated@main")
    +
    +
    +

    Current behavior +

    +
      +
    • +downloadMatch() validates competition, round, and game identifiers, retries transient HTTP failures, and errors clearly if the Champion Data payload is missing matchStats.
    • +
    • +matchPoints() and ladders() implement the current super shot scoring model for 2020+ data.
    • +
    • +matchPoints_pre_2020() and ladders_pre_2020() remain available for legacy seasons and older points systems.
    • +
    • +team_colours includes the current Melbourne Mavericks entry while retaining the historical Magpies row needed by the bundled 2017 data.
    • +
    • The package includes a testthat suite and a GitHub Actions R-CMD-check workflow for ongoing maintenance.
    • +
    +
    +
    +

    Development +

    +

    The repository now uses GitHub Actions instead of Travis CI. Local developer commands are available through the Makefile:

    +
    make test
    +make build
    +make check
    +

    make check uses base R CMD build and R CMD check, while CI regenerates package documentation before running R-CMD-check.

    +
    +
    +

    Notes +

    +

    The package has been updated to account for the super goal in 2020. Ladders and points have been adjusted for this. If you want to use the old scoring systems, these are available using _pre_2020 versions of the appropriate functions.

    - - + + + + diff --git a/docs/pkgdown.css b/docs/pkgdown.css index 6ca2f37..80ea5b8 100644 --- a/docs/pkgdown.css +++ b/docs/pkgdown.css @@ -17,12 +17,14 @@ html, body { height: 100%; } +body { + position: relative; +} + body > .container { display: flex; height: 100%; flex-direction: column; - - padding-top: 60px; } body > .container .row { @@ -54,24 +56,34 @@ img.icon { float: right; } -img { +/* Ensure in-page images don't run outside their container */ +.contents img { max-width: 100%; + height: auto; +} + +/* Fix bug in bootstrap (only seen in firefox) */ +summary { + display: list-item; } /* Typographic tweaking ---------------------------------*/ -.contents h1.page-header { +.contents .page-header { margin-top: calc(-60px + 1em); } +dd { + margin-left: 3em; +} + /* Section anchors ---------------------------------*/ a.anchor { - margin-left: -30px; - display:inline-block; - width: 30px; - height: 30px; - visibility: hidden; + display: none; + margin-left: 5px; + width: 20px; + height: 20px; background-image: url(./link.svg); background-repeat: no-repeat; @@ -79,17 +91,15 @@ a.anchor { background-position: center center; } -.hasAnchor:hover a.anchor { - visibility: visible; -} - -@media (max-width: 767px) { - .hasAnchor:hover a.anchor { - visibility: hidden; - } +h1:hover .anchor, +h2:hover .anchor, +h3:hover .anchor, +h4:hover .anchor, +h5:hover .anchor, +h6:hover .anchor { + display: inline-block; } - /* Fixes for fixed navbar --------------------------*/ .contents h1, .contents h2, .contents h3, .contents h4 { @@ -97,37 +107,135 @@ a.anchor { margin-top: -40px; } -/* Static header placement on mobile devices */ -@media (max-width: 767px) { - .navbar-fixed-top { - position: absolute; - } - .navbar { - padding: 0; - } +/* Navbar submenu --------------------------*/ + +.dropdown-submenu { + position: relative; } +.dropdown-submenu>.dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover>.dropdown-menu { + display: block; +} + +.dropdown-submenu>a:after { + display: block; + content: " "; + float: right; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + border-width: 5px 0 5px 5px; + border-left-color: #cccccc; + margin-top: 5px; + margin-right: -10px; +} + +.dropdown-submenu:hover>a:after { + border-left-color: #ffffff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left>.dropdown-menu { + left: -100%; + margin-left: 10px; + border-radius: 6px 0 6px 6px; +} /* Sidebar --------------------------*/ -#sidebar { +#pkgdown-sidebar { margin-top: 30px; + position: -webkit-sticky; + position: sticky; + top: 70px; } -#sidebar h2 { + +#pkgdown-sidebar h2 { font-size: 1.5em; margin-top: 1em; } -#sidebar h2:first-child { +#pkgdown-sidebar h2:first-child { margin-top: 0; } -#sidebar .list-unstyled li { +#pkgdown-sidebar .list-unstyled li { margin-bottom: 0.5em; } +/* bootstrap-toc tweaks ------------------------------------------------------*/ + +/* All levels of nav */ + +nav[data-toggle='toc'] .nav > li > a { + padding: 4px 20px 4px 6px; + font-size: 1.5rem; + font-weight: 400; + color: inherit; +} + +nav[data-toggle='toc'] .nav > li > a:hover, +nav[data-toggle='toc'] .nav > li > a:focus { + padding-left: 5px; + color: inherit; + border-left: 1px solid #878787; +} + +nav[data-toggle='toc'] .nav > .active > a, +nav[data-toggle='toc'] .nav > .active:hover > a, +nav[data-toggle='toc'] .nav > .active:focus > a { + padding-left: 5px; + font-size: 1.5rem; + font-weight: 400; + color: inherit; + border-left: 2px solid #878787; +} + +/* Nav: second level (shown on .active) */ + +nav[data-toggle='toc'] .nav .nav { + display: none; /* Hide by default, but at >768px, show it */ + padding-bottom: 10px; +} + +nav[data-toggle='toc'] .nav .nav > li > a { + padding-left: 16px; + font-size: 1.35rem; +} + +nav[data-toggle='toc'] .nav .nav > li > a:hover, +nav[data-toggle='toc'] .nav .nav > li > a:focus { + padding-left: 15px; +} + +nav[data-toggle='toc'] .nav .nav > .active > a, +nav[data-toggle='toc'] .nav .nav > .active:hover > a, +nav[data-toggle='toc'] .nav .nav > .active:focus > a { + padding-left: 15px; + font-weight: 500; + font-size: 1.35rem; +} + +/* orcid ------------------------------------------------------------------- */ + .orcid { - height: 16px; + font-size: 16px; + color: #A6CE39; + /* margins are required by official ORCID trademark and display guidelines */ + margin-left:4px; + margin-right:4px; vertical-align: middle; } @@ -135,15 +243,14 @@ a.anchor { .ref-index th {font-weight: normal;} -.ref-index td {vertical-align: top;} -.ref-index .alias {width: 40%;} -.ref-index .title {width: 60%;} - +.ref-index td {vertical-align: top; min-width: 100px} +.ref-index .icon {width: 40px;} .ref-index .alias {width: 40%;} +.ref-index-icons .alias {width: calc(40% - 40px);} .ref-index .title {width: 60%;} .ref-arguments th {text-align: right; padding-right: 10px;} -.ref-arguments th, .ref-arguments td {vertical-align: top;} +.ref-arguments th, .ref-arguments td {vertical-align: top; min-width: 100px} .ref-arguments .name {width: 20%;} .ref-arguments .desc {width: 80%;} @@ -156,31 +263,26 @@ table { /* Syntax highlighting ---------------------------------------------------- */ -pre { - word-wrap: normal; - word-break: normal; - border: 1px solid #eee; -} - -pre, code { +pre, code, pre code { background-color: #f8f8f8; color: #333; } +pre, pre code { + white-space: pre-wrap; + word-break: break-all; + overflow-wrap: break-word; +} -pre code { - overflow: auto; - word-wrap: normal; - white-space: pre; +pre { + border: 1px solid #eee; } -pre .img { +pre .img, pre .r-plt { margin: 5px 0; } -pre .img img { +pre .img img, pre .r-plt img { background-color: #fff; - display: block; - height: auto; } code a, pre a { @@ -197,9 +299,8 @@ a.sourceLine:hover { .kw {color: #264D66;} /* keyword */ .co {color: #888888;} /* comment */ -.message { color: black; font-weight: bolder;} -.error { color: orange; font-weight: bolder;} -.warning { color: #6A0366; font-weight: bolder;} +.error {font-weight: bolder;} +.warning {font-weight: bolder;} /* Clipboard --------------------------*/ @@ -218,6 +319,19 @@ a.sourceLine:hover { visibility: visible; } +/* headroom.js ------------------------ */ + +.headroom { + will-change: transform; + transition: transform 200ms linear; +} +.headroom--pinned { + transform: translateY(0%); +} +.headroom--unpinned { + transform: translateY(-100%); +} + /* mark.js ----------------------------*/ mark { @@ -230,3 +344,41 @@ mark { .html-widget { margin-bottom: 10px; } + +/* fontawesome ------------------------ */ + +.fab { + font-family: "Font Awesome 5 Brands" !important; +} + +/* don't display links in code chunks when printing */ +/* source: https://stackoverflow.com/a/10781533 */ +@media print { + code a:link:after, code a:visited:after { + content: ""; + } +} + +/* Section anchors --------------------------------- + Added in pandoc 2.11: https://github.com/jgm/pandoc-templates/commit/9904bf71 +*/ + +div.csl-bib-body { } +div.csl-entry { + clear: both; +} +.hanging-indent div.csl-entry { + margin-left:2em; + text-indent:-2em; +} +div.csl-left-margin { + min-width:2em; + float:left; +} +div.csl-right-inline { + margin-left:2em; + padding-left:1em; +} +div.csl-indent { + margin-left: 2em; +} diff --git a/docs/pkgdown.js b/docs/pkgdown.js index de9bd72..6f0eee4 100644 --- a/docs/pkgdown.js +++ b/docs/pkgdown.js @@ -2,18 +2,11 @@ (function($) { $(function() { - $("#sidebar") - .stick_in_parent({offset_top: 40}) - .on('sticky_kit:bottom', function(e) { - $(this).parent().css('position', 'static'); - }) - .on('sticky_kit:unbottom', function(e) { - $(this).parent().css('position', 'relative'); - }); + $('.navbar-fixed-top').headroom(); - $('body').scrollspy({ - target: '#sidebar', - offset: 60 + $('body').css('padding-top', $('.navbar').height() + 10); + $(window).resize(function(){ + $('body').css('padding-top', $('.navbar').height() + 10); }); $('[data-toggle="tooltip"]').tooltip(); @@ -25,9 +18,13 @@ for (var i = 0; i < links.length; i++) { if (links[i].getAttribute("href") === "#") continue; - var path = paths(links[i].pathname); + // Ignore external links + if (links[i].host !== location.host) + continue; + + var nav_path = paths(links[i].pathname); - var length = prefix_length(cur_path, path); + var length = prefix_length(nav_path, cur_path); if (length > max_length) { max_length = length; pos = i; @@ -52,13 +49,14 @@ return(pieces); } + // Returns -1 if not found function prefix_length(needle, haystack) { if (needle.length > haystack.length) - return(0); + return(-1); // Special case for length-0 haystack, since for loop won't run if (haystack.length === 0) { - return(needle.length === 0 ? 1 : 0); + return(needle.length === 0 ? 0 : -1); } for (var i = 0; i < haystack.length; i++) { @@ -78,11 +76,11 @@ element.setAttribute('data-original-title', tooltipOriginalTitle); } - if(Clipboard.isSupported()) { + if(ClipboardJS.isSupported()) { $(document).ready(function() { - var copyButton = ""; + var copyButton = ""; - $(".examples, div.sourceCode").addClass("hasCopyButton"); + $("div.sourceCode").addClass("hasCopyButton"); // Insert copy buttons: $(copyButton).prependTo(".hasCopyButton"); @@ -91,9 +89,9 @@ $('.btn-copy-ex').tooltip({container: 'body'}); // Initialize clipboard: - var clipboardBtnCopies = new Clipboard('[data-clipboard-copy]', { + var clipboardBtnCopies = new ClipboardJS('[data-clipboard-copy]', { text: function(trigger) { - return trigger.parentNode.textContent; + return trigger.parentNode.textContent.replace(/\n#>[^\n]*/g, ""); } }); diff --git a/docs/pkgdown.yml b/docs/pkgdown.yml index 207ba65..15fa485 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -1,6 +1,9 @@ -pandoc: 2.2.1 -pkgdown: 1.1.0 +pandoc: '3.9' +pkgdown: 2.2.0 pkgdown_sha: ~ articles: getting-started: getting-started.html - +last_built: 2026-03-10T11:09Z +urls: + reference: https://craigmoyle.github.io/superNetballR_updated/reference + article: https://craigmoyle.github.io/superNetballR_updated/articles diff --git a/docs/reference/downloadMatch.html b/docs/reference/downloadMatch.html index 6c433e0..46c0233 100644 --- a/docs/reference/downloadMatch.html +++ b/docs/reference/downloadMatch.html @@ -1,186 +1,128 @@ - - - - - - - -Download data from a single match — downloadMatch • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Download data from a single match — downloadMatch • superNetballR + - - -
    -
    - - -
    -
    + +
    -

    downloadMatch downloads match and player data for a single match.

    -
    -
    downloadMatch(comp_id, round_id, game_id)
    - -

    Arguments

    - - - - - - - - - - - - - - -
    comp_id

    A string identifying which season the game is -in. comp_id is different depending on regular season or finals.

    round_id

    An integer identifying which round the game is in. Finals -reset round number to 1.

    game_id

    An integer indentifying which game in the round to +

    +
    downloadMatch(comp_id, round_id, game_id)
    +
    + +
    +

    Arguments

    + + +
    comp_id
    +

    A string identifying which season the game is +in. comp_id is different depending on regular season or finals.

    + + +
    round_id
    +

    An integer identifying which round the game is in. Finals +reset round number to 1.

    + + +
    game_id
    +

    An integer indentifying which game in the round to download. There are four games per round in the regular season, two games -in the semi finals, one game for the prelim, and one grand final.

    - -

    Value

    +in the semi finals, one game for the prelim, and one grand final.

    +
    +
    +

    Value

    A list containing game and player data for the match.

    - - -

    Examples

    -
    # NOT RUN {
    -downloadMatch("10083", 1, 1)
    -# }
    -
    -
    - +
    +

    Details

    +

    downloadMatch() validates the supplied identifiers, retries transient +HTTP failures, and raises an explicit error if the Champion Data response no +longer includes a matchStats object.

    +
    +
    +

    Examples

    +
    if (FALSE) { # \dontrun{
    +downloadMatch("10083", 1, 1)
    +} # }
    +
    +
    +
    +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/index.html b/docs/reference/index.html index 96282bc..036db32 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -1,210 +1,128 @@ - - - - - - - -Function reference • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Package index • superNetballR + - - -
    -
    - - -
    -
    + +
    - - - - - - - - - - -
    -

    All functions

    + - - - - - - - - - - - - - + + - - - - - - - - - - - - - + + + - - - - - -
    +

    All functions

    +

    downloadMatch()

    Download data from a single match

    -

    ladders() matchResults()

    +
    +

    ladders() matchResults() ladders_pre_2020()

    Calculates ladder positions

    +

    matchPoints()

    Calculates the total goals of the match

    +
    +

    matchPoints_pre_2020()

    +

    Calculates the total goals of the match (pre 2020 season)

    players_2017

    Season 2017 player data.

    +

    round5_game3

    Match and player statistics from round 5, game 3, season 2017.

    +

    season_2017

    Season 2017 match data.

    -

    superNetballR

    +
    +

    shinySuperNetballR()

    superNetballR package

    +

    Runs the demo shiny app

    +

    team_colours

    +

    Team colours.

    tidyMatch()

    Takes a downloaded match list and tidies the match statistics.

    +

    tidyPlayers()

    Takes a downloaded match list and tidies the player statistics.

    - - - +
    + +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/ladders.html b/docs/reference/ladders.html index 9d24ecf..fbe9552 100644 --- a/docs/reference/ladders.html +++ b/docs/reference/ladders.html @@ -1,184 +1,128 @@ - - - - - - - -Calculates ladder positions — ladders • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Calculates ladder positions — ladders • superNetballR - + - -
    -
    - - -
    -
    + +
    -

    ladders calculates ladder positions at the end of a match.

    -
    -
    ladders(df, round_num = NULL, game_num = NULL, old_system = FALSE)
    -
    -matchResults(df)
    - -

    Arguments

    - - - - - - - - - - - - - - - - - - -
    df

    Data frame containing season match statistics.

    round_num

    Round at which to calculate ladder positions. Optional.

    game_num

    Game at which to calculate ladder positions. Optional.

    old_system

    Logical. Whether to sort by the old scoring system -(defaults to FALSE).

    - -

    Value

    +
    +
    ladders(df, round_num = NULL, game_num = NULL, old_system = FALSE)
    +
    +matchResults(df)
    +
    +ladders_pre_2020(df, round_num = NULL, game_num = NULL, old_system = FALSE)
    +
    + +
    +

    Arguments

    + + +
    df
    +

    Data frame containing season match statistics.

    + + +
    round_num
    +

    Round at which to calculate ladder positions. Optional.

    + + +
    game_num
    +

    Game at which to calculate ladder positions. Optional.

    + +
    old_system
    +

    Logical. Whether to sort by the old scoring system +(defaults to FALSE).

    + +
    +
    +

    Value

    Data frame containing the ladder position of all teams. If round and game are not supplied, the ladder position is calculated using all match data present in the df supplied.

    - - -
    - +
    +

    Details

    +

    ladders() uses the current 2020+ scoring helpers, while +ladders_pre_2020() uses the legacy scoring pipeline. Ladder +percentages are protected against divide-by-zero by returning Inf +when a team has not conceded.

    +
    +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/matchPoints.html b/docs/reference/matchPoints.html index 72782ce..6559e3a 100644 --- a/docs/reference/matchPoints.html +++ b/docs/reference/matchPoints.html @@ -1,167 +1,107 @@ - - - - - - - -Calculates the total goals of the match — matchPoints • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Calculates the total goals of the match — matchPoints • superNetballR - + - -
    -
    - - -
    -
    + +
    -

    matchPoints calculates final match goals and score difference.

    -
    -
    matchPoints(df)
    - -

    Arguments

    - - - - - - -
    df

    Match data.

    - -

    Value

    +
    +
    matchPoints(df)
    +
    -

    A data frame containing the final scores, and points for the ladder.

    - +
    +

    Arguments

    -
    - +
    +

    Value

    +

    A data frame containing the final scores, and points for the ladder.

    +
    +
    +

    Details

    +

    matchPoints() treats goal_from_zone1 as one point and +goal_from_zone2 as two points, matching the current super shot era.

    +
    +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/players_2017.html b/docs/reference/players_2017.html index f211b6d..d346557 100644 --- a/docs/reference/players_2017.html +++ b/docs/reference/players_2017.html @@ -1,169 +1,129 @@ - - - - - - - -Season 2017 player data. — players_2017 • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Season 2017 player data. — players_2017 • superNetballR - + - -
    -
    - - -
    -
    + +
    -

    A dataset containing player statistics for all home and away and finals series matches from the 2017 super netball season, by period.

    -
    -
    players_2017
    - -

    Format

    - -

    A data frame with 163336 rows and 8 variables:

    -
    playerId

    Unique player number

    -
    shortDisplayName

    surname, firstname

    -
    firstname

    Player firstname

    -
    surname

    Player surname

    -
    stat

    Statistic measured during the match

    -
    value

    Value of the statistic

    -
    period

    Which period the statistic is measured in

    -
    round

    Round number of the match

    -
    game

    Game number of the match

    -
    - +
    +
    players_2017
    +
    -
    - +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/round5_game3.html b/docs/reference/round5_game3.html index 0306e6d..567d5fb 100644 --- a/docs/reference/round5_game3.html +++ b/docs/reference/round5_game3.html @@ -1,159 +1,96 @@ - - - - - - - -Match and player statistics from round 5, game 3, season 2017. — round5_game3 • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Match and player statistics from round 5, game 3, season 2017. — round5_game3 • superNetballR - + - -
    -
    - - -
    -
    + +
    -

    A list containing detailed match and player statistics, as obtained using the downloadMatch function.

    -
    -
    round5_game3
    - -

    Format

    +
    +
    round5_game3
    +
    +
    +

    Format

    A list.

    - - -
    -
    +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/season_2017.html b/docs/reference/season_2017.html index eaa016b..4da0a1f 100644 --- a/docs/reference/season_2017.html +++ b/docs/reference/season_2017.html @@ -1,169 +1,123 @@ - - - - - - - -Season 2017 match data. — season_2017 • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Season 2017 match data. — season_2017 • superNetballR + - - -
    -
    - - -
    -
    + +
    -

    A dataset containing match statistics for all home and away and finals series matches from the 2017 super netball season, by period.

    -
    -
    season_2017
    - -

    Format

    - -

    A data frame with 15360 rows and 8 variables:

    -
    squadId

    Unique squad number

    -
    squadName

    Full squad name

    -
    squadNickname

    Squad nickname

    -
    squadCode

    Short code for quad

    -
    stat

    Statistic measured during the match

    -
    value

    Value of the statistic

    -
    period

    Which period the statistic is measured in

    -
    round

    Round number of the match

    -
    game

    Game number of the match

    -
    - +
    +
    season_2017
    +
    -
    - +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/superNetballR.html b/docs/reference/superNetballR.html index 74b6722..65d545e 100644 --- a/docs/reference/superNetballR.html +++ b/docs/reference/superNetballR.html @@ -1,150 +1,8 @@ - - - - - - -<code>superNetballR</code> package — superNetballR • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - -
    -
    - - - -
    - -
    -
    - - -
    - -

    Functions getting and manipulating Super Netball data.

    - -
    - - - -
    - -
    - -
    - - -
    -

    Site built with pkgdown.

    -
    - -
    -
    - - - - diff --git a/docs/reference/tidyMatch.html b/docs/reference/tidyMatch.html index 01e911b..e8a46e5 100644 --- a/docs/reference/tidyMatch.html +++ b/docs/reference/tidyMatch.html @@ -1,169 +1,104 @@ - - - - - - - -Takes a downloaded match list and tidies the match statistics. — tidyMatch • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Takes a downloaded match list and tidies the match statistics. — tidyMatch • superNetballR + - - -
    -
    - - -
    -
    + +
    -

    tidyMatch Takes the downloaded match list, and tidies match statistics in preparation for further analysis.

    -
    -
    tidyMatch(match)
    - -

    Arguments

    - - - - - - -
    match

    List of match details.

    - -

    Value

    +
    +
    tidyMatch(match)
    +
    -

    A tidy dataframe containing match statistics.

    - +
    +

    Arguments

    -
    - +
    +

    Value

    +

    A tidy dataframe containing match statistics.

    +
    +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/tidyPlayers.html b/docs/reference/tidyPlayers.html index 6d9ca89..1bf97c1 100644 --- a/docs/reference/tidyPlayers.html +++ b/docs/reference/tidyPlayers.html @@ -1,169 +1,104 @@ - - - - - - - -Takes a downloaded match list and tidies the player statistics. — tidyPlayers • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Takes a downloaded match list and tidies the player statistics. — tidyPlayers • superNetballR + - - -
    -
    - - -
    -
    + +
    -

    tidyPlayers Takes the downloaded match list, and tidies player statistics in preparation for further analysis.

    -
    -
    tidyPlayers(match)
    - -

    Arguments

    - - - - - - -
    match

    List of match details.

    - -

    Value

    +
    +
    tidyPlayers(match)
    +
    -

    A tidy dataframe containing player statistics.

    - +
    +

    Arguments

    -
    - +
    +

    Value

    +

    A tidy dataframe containing player statistics.

    +
    +
    -
    - +
    + + + + - - - + diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 0000000..471f803 --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,22 @@ + +https://craigmoyle.github.io/superNetballR_updated/404.html +https://craigmoyle.github.io/superNetballR_updated/LICENSE-text.html +https://craigmoyle.github.io/superNetballR_updated/articles/getting-started.html +https://craigmoyle.github.io/superNetballR_updated/articles/index.html +https://craigmoyle.github.io/superNetballR_updated/authors.html +https://craigmoyle.github.io/superNetballR_updated/index.html +https://craigmoyle.github.io/superNetballR_updated/reference/downloadMatch.html +https://craigmoyle.github.io/superNetballR_updated/reference/index.html +https://craigmoyle.github.io/superNetballR_updated/reference/ladders.html +https://craigmoyle.github.io/superNetballR_updated/reference/matchPoints.html +https://craigmoyle.github.io/superNetballR_updated/reference/matchPoints_pre_2020.html +https://craigmoyle.github.io/superNetballR_updated/reference/players_2017.html +https://craigmoyle.github.io/superNetballR_updated/reference/round5_game3.html +https://craigmoyle.github.io/superNetballR_updated/reference/season_2017.html +https://craigmoyle.github.io/superNetballR_updated/reference/shinySuperNetballR.html +https://craigmoyle.github.io/superNetballR_updated/reference/superNetballR-package.html +https://craigmoyle.github.io/superNetballR_updated/reference/team_colours.html +https://craigmoyle.github.io/superNetballR_updated/reference/tidyMatch.html +https://craigmoyle.github.io/superNetballR_updated/reference/tidyPlayers.html + + diff --git a/man/downloadMatch.Rd b/man/downloadMatch.Rd index 16821fa..baa588b 100644 --- a/man/downloadMatch.Rd +++ b/man/downloadMatch.Rd @@ -20,14 +20,14 @@ in the semi finals, one game for the prelim, and one grand final.} \value{ A list containing game and player data for the match. } +\description{ +\code{downloadMatch} downloads match and player data for a single match. +} \details{ \code{downloadMatch()} validates the supplied identifiers, retries transient HTTP failures, and raises an explicit error if the Champion Data response no longer includes a \code{matchStats} object. } -\description{ -\code{downloadMatch} downloads match and player data for a single match. -} \examples{ \dontrun{ downloadMatch("10083", 1, 1) diff --git a/man/ladders.Rd b/man/ladders.Rd index c2b0d83..64f7a85 100644 --- a/man/ladders.Rd +++ b/man/ladders.Rd @@ -27,12 +27,12 @@ Data frame containing the ladder position of all teams. If round and game are not supplied, the ladder position is calculated using all match data present in the \code{df} supplied. } +\description{ +\code{ladders} calculates ladder positions at the end of a match. +} \details{ \code{ladders()} uses the current 2020+ scoring helpers, while \code{ladders_pre_2020()} uses the legacy scoring pipeline. Ladder percentages are protected against divide-by-zero by returning \code{Inf} when a team has not conceded. } -\description{ -\code{ladders} calculates ladder positions at the end of a match. -} diff --git a/man/matchPoints.Rd b/man/matchPoints.Rd index f6cacfd..e64ac2c 100644 --- a/man/matchPoints.Rd +++ b/man/matchPoints.Rd @@ -12,10 +12,10 @@ matchPoints(df) \value{ A data frame containing the final scores, and points for the ladder. } +\description{ +\code{matchPoints} calculates final match goals and score difference. +} \details{ \code{matchPoints()} treats \code{goal_from_zone1} as one point and \code{goal_from_zone2} as two points, matching the current super shot era. } -\description{ -\code{matchPoints} calculates final match goals and score difference. -} diff --git a/man/matchPoints_pre_2020.Rd b/man/matchPoints_pre_2020.Rd index 2b9c1ed..cf0de85 100644 --- a/man/matchPoints_pre_2020.Rd +++ b/man/matchPoints_pre_2020.Rd @@ -12,12 +12,12 @@ matchPoints_pre_2020(df) \value{ A data frame containing the final scores, and points for the ladder. } +\description{ +\code{matchPoints_pre_2020} calculates final match goals and score +difference, for seasons pre-2020. +} \details{ \code{matchPoints_pre_2020()} uses the original goals statistic for match results and also reports the newer quarter-points summary in \code{points_new}. } -\description{ -\code{matchPoints_pre_2020} calculates final match goals and score -difference, for seasons pre-2020. -} diff --git a/man/superNetballR-package.Rd b/man/superNetballR-package.Rd new file mode 100644 index 0000000..38f5499 --- /dev/null +++ b/man/superNetballR-package.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/superNetballR.R +\docType{package} +\name{superNetballR-package} +\alias{superNetballR} +\alias{superNetballR-package} +\title{Functions getting and manipulating Super Netball data.} +\description{ +This package provides functions to easily download and manipulate data from super netball matches. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://craigmoyle.github.io/superNetballR_updated} + \item \url{https://github.com/craigmoyle/superNetballR_updated} + \item Report bugs at \url{https://github.com/craigmoyle/superNetballR_updated/issues} +} + +} +\author{ +\strong{Maintainer}: Steve Lane \email{lane.s@unimelb.edu.au} + +} +\keyword{internal} diff --git a/man/superNetballR.Rd b/man/superNetballR.Rd deleted file mode 100644 index 858b682..0000000 --- a/man/superNetballR.Rd +++ /dev/null @@ -1,9 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/superNetballR.R -\docType{package} -\name{superNetballR} -\alias{superNetballR} -\title{\code{superNetballR} package} -\description{ -Functions getting and manipulating Super Netball data. -} diff --git a/man/team_colours.Rd b/man/team_colours.Rd index 597c713..361bdcc 100644 --- a/man/team_colours.Rd +++ b/man/team_colours.Rd @@ -5,7 +5,7 @@ \alias{team_colours} \title{Team colours.} \format{ -A data frame with 8 rows and 3 variables: +A data frame with 9 rows and 3 variables: \describe{ \item{squadName}{Full squad name} \item{squadId}{Unique squad number} @@ -16,6 +16,8 @@ A data frame with 8 rows and 3 variables: team_colours } \description{ -A dataset containing hex-coded team colours for each team. +A dataset containing hex-coded team colours for the current Super Netball +competition teams, plus the historical Magpies entry used by the bundled +2017 data. } \keyword{datasets} diff --git a/tests/testthat/test-ladders.R b/tests/testthat/test-ladders.R index f6fd6c6..b6a6b19 100644 --- a/tests/testthat/test-ladders.R +++ b/tests/testthat/test-ladders.R @@ -34,5 +34,5 @@ test_that("ladders_pre_2020 uses the legacy match scoring pipeline", { ladder <- ladders_pre_2020(season) expect_equal(ladder$points[ladder$squadName == "A"], 2) - expect_equal(ladder$points_new[ladder$squadName == "A"], 5) + expect_equal(ladder$points_new[ladder$squadName == "A"], 6) }) From 9898d3a03332a7402b1c3abb50493c50ac07d549 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Wed, 11 Mar 2026 12:11:57 +1100 Subject: [PATCH 23/56] fix error --- R/downloadMatch.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/downloadMatch.R b/R/downloadMatch.R index e6eb094..e30e81c 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -79,7 +79,7 @@ downloadMatch <- function(comp_id, round_id, game_id) { terminate_on = c(400, 401, 403, 404), quiet = TRUE ) - httr::stop_for_status(dat, call. = FALSE) + httr::stop_for_status(dat) extract_match_stats(httr::content( dat, as = "parsed", From 5e28e0e32e3544cb87d9832e8b14deee7b297eb3 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 5 Apr 2026 11:53:24 +1000 Subject: [PATCH 24/56] release: cut 0.1.0 Create the initial maintained-fork release, add a changelog since the upstream fork point, and include the package hardening work needed for a clean release build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .Rbuildignore | 4 + DESCRIPTION | 17 +-- NAMESPACE | 1 + R/data.R | 10 +- R/ladders.R | 137 +++++++++++---------- R/matchPoints.R | 32 ++--- R/shinySuperNetballR.R | 8 +- R/superNetballR.R | 9 +- R/tidiers.R | 5 +- _pkgdown.yml | 2 - changelog.md | 46 +++++++ inst/shiny-examples/superNetballR/global.R | 9 +- man/ladders.Rd | 7 +- man/players_2017.Rd | 4 +- man/season_2017.Rd | 6 +- man/superNetballR-package.Rd | 6 +- man/tidyPlayers.Rd | 4 + tests/testthat/helper-fixtures.R | 77 +++++++----- tests/testthat/test-downloadMatch.R | 15 +++ tests/testthat/test-ladders.R | 41 ++++++ tests/testthat/test-match-points.R | 20 +++ tests/testthat/test-tidiers.R | 16 ++- 22 files changed, 326 insertions(+), 150 deletions(-) create mode 100644 changelog.md diff --git a/.Rbuildignore b/.Rbuildignore index a1d090e..3013c14 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -10,3 +10,7 @@ ^\.travis\.yml$ ^doc$ ^Meta$ +^\.github$ +^.*\.Rcheck$ +^superNetballR_.*\.tar\.gz$ +^changelog\.md$ diff --git a/DESCRIPTION b/DESCRIPTION index 3ab7abb..9c126b0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,13 +1,14 @@ Package: superNetballR -Title: Downloads and tidies super netball statistics -Version: 0.2.0 +Title: Download and Tidy Super Netball Statistics +Version: 0.1.0 Authors@R: person("Steve", "Lane", email = "lane.s@unimelb.edu.au", role = c("aut", "cre")) -Description: This package provides functions to easily download and manipulate data from super netball matches. +Description: Tools to download Champion Data Super Netball match feeds and + transform team and player statistics into tidy data frames for analysis. Depends: R (>= 4.0.0) License: MIT + file LICENSE Encoding: UTF-8 LazyData: true -Suggests: here, +Suggests: ggplot2, knitr, rmarkdown, shiny, @@ -15,10 +16,10 @@ Suggests: here, VignetteBuilder: knitr Imports: dplyr, httr, - tidyr, - purrr -URL: https://craigmoyle.github.io/superNetballR_updated, - https://github.com/craigmoyle/superNetballR_updated + magrittr, + purrr, + tidyr +URL: https://github.com/craigmoyle/superNetballR_updated BugReports: https://github.com/craigmoyle/superNetballR_updated/issues Config/testthat/edition: 3 RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index 5739c5c..c98d9da 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,3 +9,4 @@ export(matchResults) export(shinySuperNetballR) export(tidyMatch) export(tidyPlayers) +importFrom(magrittr,"%>%") diff --git a/R/data.R b/R/data.R index 9ab590c..bc755de 100644 --- a/R/data.R +++ b/R/data.R @@ -3,14 +3,14 @@ #' A dataset containing match statistics for all home and away and finals series #' matches from the 2017 super netball season, by period. #' -#' @format A data frame with 15360 rows and 8 variables: +#' @format A data frame with 15360 rows and 9 variables: #' \describe{ #' \item{squadId}{Unique squad number} #' \item{squadName}{Full squad name} #' \item{squadNickname}{Squad nickname} -#' \item{squadCode}{Short code for quad} +#' \item{squadCode}{Short code for squad} #' \item{stat}{Statistic measured during the match} -#' \item{value}{Value of the statistic} +#' \item{value}{Character representation of the statistic value} #' \item{period}{Which period the statistic is measured in} #' \item{round}{Round number of the match} #' \item{game}{Game number of the match} @@ -22,7 +22,7 @@ #' A dataset containing player statistics for all home and away and finals #' series matches from the 2017 super netball season, by period. #' -#' @format A data frame with 163336 rows and 8 variables: +#' @format A data frame with 153728 rows and 11 variables: #' \describe{ #' \item{playerId}{Unique player number} #' \item{period}{Which period the statistic is measured in} @@ -32,7 +32,7 @@ #' \item{surname}{Player surname} #' \item{squadName}{Full squad name} #' \item{stat}{Statistic measured during the match} -#' \item{value}{Value of the statistic} +#' \item{value}{Character representation of the statistic value} #' \item{round}{Round number of the match} #' \item{game}{Game number of the match} #' } diff --git a/R/ladders.R b/R/ladders.R index cd53c72..57399a7 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -2,6 +2,24 @@ safe_percentage <- function(goals_for, goals_against) { ifelse(goals_against == 0, Inf, goals_for / goals_against) } +limit_match_results <- function(match_results, round_num = NULL, game_num = NULL) { + if (!is.null(game_num) && is.null(round_num)) { + stop("If game number is supplied, round number must also be supplied.") + } + if (is.null(round_num)) { + return(match_results) + } + if (is.null(game_num)) { + return(dplyr::filter(match_results, round <= round_num)) + } + + dplyr::filter(match_results, round < round_num | (round == round_num & game <= game_num)) +} + +sort_ladder <- function(ladder, points_col) { + ladder[order(-ladder[[points_col]], -ladder$percentage, ladder$squadName), , drop = FALSE] +} + #' Calculates ladder positions #' #' \code{ladders} calculates ladder positions at the end of a match. @@ -9,8 +27,8 @@ safe_percentage <- function(goals_for, goals_against) { #' @param df Data frame containing season match statistics. #' @param round_num Round at which to calculate ladder positions. Optional. #' @param game_num Game at which to calculate ladder positions. Optional. -#' @param old_system Logical. Whether to sort by the old scoring system -#' (defaults to FALSE). +#' @param old_system Logical. Retained for compatibility and ignored for +#' 2020+ ladders. #' #' @return Data frame containing the ladder position of all teams. If round and #' game are not supplied, the ladder position is calculated using all match @@ -19,85 +37,72 @@ safe_percentage <- function(goals_for, goals_against) { #' \code{ladders()} uses the current 2020+ scoring helpers, while #' \code{ladders_pre_2020()} uses the legacy scoring pipeline. Ladder #' percentages are protected against divide-by-zero by returning \code{Inf} -#' when a team has not conceded. +#' when a team has not conceded. Legacy ladders break ties on percentage after +#' ordering by either \code{points_new} or \code{points}. #' #' @export ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { - if (!is.null(game_num) && is.null(round_num)) { - stop("If game number is supplied, round number must also be supplied.") - } - match_results <- matchResults(df = df) - if (!is.null(round_num) && is.null(game_num)) { - match_results <- match_results %>% - dplyr::filter(round <= round_num) - } else if (!is.null(round_num) && !is.null(game_num)) { - match_results <- match_results %>% - dplyr::filter(round <= round_num) %>% - dplyr::filter(!(round >= round_num && game > game_num)) - } + match_results <- limit_match_results( + matchResults(df = df), + round_num = round_num, + game_num = game_num + ) ladder <- match_results %>% - dplyr::group_by(squadName) %>% - dplyr::summarise( - games = dplyr::n(), - goals_for = sum(goals), - goals_against = sum(goals - score_diff), - percentage = safe_percentage(goals_for, goals_against), - points = as.integer(sum(points)) - ) %>% - dplyr::arrange(dplyr::desc(points), dplyr::desc(percentage)) - ladder + dplyr::group_by(squadName) %>% + dplyr::summarise( + games = dplyr::n(), + goals_for = sum(goals), + goals_against = sum(goals - score_diff), + percentage = safe_percentage(goals_for, goals_against), + points = as.integer(sum(points)), + .groups = "drop" + ) + sort_ladder(ladder, "points") } #' @rdname ladders #' @export matchResults <- function(df) { - df <- df %>% - dplyr::group_by(round, game) %>% - tidyr::nest() %>% - dplyr::group_by(round, game) %>% - dplyr::mutate(game_results = purrr::map(data, matchPoints)) %>% - dplyr::select(-data) %>% - tidyr::unnest(cols = c(game_results)) - df + df <- df %>% + dplyr::group_by(round, game) %>% + tidyr::nest() %>% + dplyr::group_by(round, game) %>% + dplyr::mutate(game_results = purrr::map(data, matchPoints)) %>% + dplyr::select(-data) %>% + tidyr::unnest(cols = c(game_results)) + df } matchResults_pre_2020 <- function(df) { - df <- df %>% - dplyr::group_by(round, game) %>% - tidyr::nest() %>% - dplyr::group_by(round, game) %>% - dplyr::mutate(game_results = purrr::map(data, matchPoints_pre_2020)) %>% - dplyr::select(-data) %>% - tidyr::unnest(cols = c(game_results)) - df + df <- df %>% + dplyr::group_by(round, game) %>% + tidyr::nest() %>% + dplyr::group_by(round, game) %>% + dplyr::mutate(game_results = purrr::map(data, matchPoints_pre_2020)) %>% + dplyr::select(-data) %>% + tidyr::unnest(cols = c(game_results)) + df } #' @rdname ladders #' @export ladders_pre_2020 <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { - if (!is.null(game_num) && is.null(round_num)) { - stop("If game number is supplied, round number must also be supplied.") - } - match_results <- matchResults_pre_2020(df = df) - if (!is.null(round_num) && is.null(game_num)) { - match_results <- match_results %>% - dplyr::filter(round <= round_num) - } else if (!is.null(round_num) && !is.null(game_num)) { - match_results <- match_results %>% - dplyr::filter(round <= round_num) %>% - dplyr::filter(!(round >= round_num && game > game_num)) - } - ladder <- match_results %>% - dplyr::group_by(squadName) %>% - dplyr::summarise( - games = dplyr::n(), - goals_for = sum(goals), - goals_against = sum(goals - score_diff), - percentage = safe_percentage(goals_for, goals_against), - points = as.integer(sum(points)), - points_new = as.integer(sum(points_new)) - ) %>% - dplyr::arrange(dplyr::desc(points_new)) - if (old_system) ladder <- ladder %>% dplyr::arrange(dplyr::desc(points)) - ladder + match_results <- limit_match_results( + matchResults_pre_2020(df = df), + round_num = round_num, + game_num = game_num + ) + ladder <- match_results %>% + dplyr::group_by(squadName) %>% + dplyr::summarise( + games = dplyr::n(), + goals_for = sum(goals), + goals_against = sum(goals - score_diff), + percentage = safe_percentage(goals_for, goals_against), + points = as.integer(sum(points)), + points_new = as.integer(sum(points_new)), + .groups = "drop" + ) + points_col <- if (old_system) "points" else "points_new" + sort_ladder(ladder, points_col) } diff --git a/R/matchPoints.R b/R/matchPoints.R index 67f3d45..5793204 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -11,6 +11,10 @@ #' @export matchPoints <- function(df) { ## This first section calculates points based on the old system. + home <- df %>% + dplyr::filter(stat == "homeTeam") %>% + dplyr::select(-period) %>% + dplyr::distinct() goals1 <- df %>% dplyr::filter(stat == "goal_from_zone1") %>% dplyr::group_by(squadName) %>% @@ -19,25 +23,23 @@ matchPoints <- function(df) { dplyr::filter(stat == "goal_from_zone2") %>% dplyr::group_by(squadName) %>% dplyr::summarise(goals2 = sum(value, na.rm = TRUE) * 2, .groups = "drop") - goals <- dplyr::left_join(goals1, goals2, by = "squadName") %>% + goals <- home %>% + dplyr::left_join(goals1, by = "squadName") %>% + dplyr::left_join(goals2, by = "squadName") %>% dplyr::mutate( + goals = dplyr::coalesce(goals, 0), goals2 = dplyr::coalesce(goals2, 0), goals = goals + goals2 ) %>% dplyr::select(-goals2) - home <- df %>% - dplyr::filter(stat == "homeTeam") %>% - dplyr::group_by(squadName) %>% - dplyr::select(-period) %>% - dplyr::distinct() - goals <- dplyr::left_join(goals, home, by = "squadName") %>% + goals <- goals %>% dplyr::arrange(value) - score_diff <- diff(goals[['goals']]) + if (nrow(goals) != 2) { + stop("Match data must include exactly two squads.", call. = FALSE) + } goals <- goals %>% dplyr::mutate( - score_diff = score_diff, - score_diff = ifelse(value == 0, score_diff * (-1), - score_diff), + score_diff = goals - rev(goals), points = dplyr::case_when( score_diff > 0 ~ 4, score_diff < 0 ~ 0, @@ -77,12 +79,12 @@ matchPoints_pre_2020 <- function(df) { dplyr::distinct() goals <- dplyr::left_join(goals, home, by = "squadName") %>% dplyr::arrange(value) - score_diff <- diff(goals[['goals']]) + if (nrow(goals) != 2) { + stop("Match data must include exactly two squads.", call. = FALSE) + } goals <- goals %>% dplyr::mutate( - score_diff = score_diff, - score_diff = ifelse(value == 0, score_diff * (-1), - score_diff), + score_diff = goals - rev(goals), points = dplyr::case_when( score_diff > 0 ~ 2, score_diff < 0 ~ 0, diff --git a/R/shinySuperNetballR.R b/R/shinySuperNetballR.R index 45bef75..932cfa1 100644 --- a/R/shinySuperNetballR.R +++ b/R/shinySuperNetballR.R @@ -7,6 +7,13 @@ #' #' @export shinySuperNetballR <- function() { + if (!requireNamespace("shiny", quietly = TRUE)) { + stop("Package 'shiny' must be installed to run shinySuperNetballR().", call. = FALSE) + } + if (!requireNamespace("ggplot2", quietly = TRUE)) { + stop("Package 'ggplot2' must be installed to run shinySuperNetballR().", call. = FALSE) + } + my_dir <- system.file( "shiny-examples", "superNetballR", package = "superNetballR" ) @@ -14,6 +21,5 @@ shinySuperNetballR <- function() { stop("Can't find the superNetballR shiny directory. Try re-installing `superNetballR`.", call. = FALSE) } - source(paste0(my_dir, '/team_series_module.R')) shiny::runApp(my_dir, display.mode = "normal") } diff --git a/R/superNetballR.R b/R/superNetballR.R index 95db00c..06a533c 100644 --- a/R/superNetballR.R +++ b/R/superNetballR.R @@ -1,11 +1,16 @@ -#' Functions getting and manipulating Super Netball data. +#' superNetballR: Download and Tidy Super Netball Statistics +#' +#' Download Champion Data Super Netball match feeds and transform team and +#' player statistics into tidy data frames for analysis. #' #' @keywords internal +#' @importFrom magrittr %>% "_PACKAGE" ## quiets concerns of R CMD check re: the .'s that appear in pipelines if (getRversion() >= "2.15.1") { utils::globalVariables(c(".", "points_new", "homeValue", "homeSquad", "homePoints", "awayValue", "awaySquad", - "awayPoints", "points_qtr")) + "awayPoints", "points_qtr", "game_results", + "squadId.x", "squadId.y")) } diff --git a/R/tidiers.R b/R/tidiers.R index ff523db..6595228 100644 --- a/R/tidiers.R +++ b/R/tidiers.R @@ -45,6 +45,9 @@ tidyMatch <- function(match) { #' #' @param match List of match details. #' @return A tidy dataframe containing player statistics. +#' @details +#' Player period stats include both numeric measures and position-code fields, +#' so the long-form \code{value} column is stored as character data. #' #' @export tidyPlayers <- function(match) { @@ -81,6 +84,6 @@ tidyPlayers <- function(match) { dplyr::mutate( round = match$matchInfo$roundNumber, game = match$matchInfo$matchNumber - ) + ) player_stats } diff --git a/_pkgdown.yml b/_pkgdown.yml index a50ec16..032d9c1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -1,5 +1,3 @@ -url: https://craigmoyle.github.io/superNetballR_updated/ - template: params: bootswatch: spacelab diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..e0f3b2b --- /dev/null +++ b/changelog.md @@ -0,0 +1,46 @@ +# Changelog + +All notable changes in this fork are documented here. + +This changelog covers the changes introduced in `craigmoyle/superNetballR_updated` since the fork diverged from the original `SteveLane/superNetballR` project published at . + +## 0.1.0 - 2026-04-05 + +First tagged release of the maintained fork. + +### Added + +- Support for the 2020+ super shot scoring model in match and ladder calculations. +- Legacy `_pre_2020` helpers so historical seasons can still be analysed with the original scoring system. +- A packaged Shiny example app for comparing team statistics. +- Bundled `team_colours` data, including the historical Magpies row and the current Melbourne Mavericks entry. +- A `testthat` regression suite covering downloads, tidiers, match scoring, and ladder calculations. +- A GitHub Actions `R-CMD-check` workflow plus Makefile targets for local `test`, `build`, and `check` runs. + +### Changed + +- `downloadMatch()` now validates match identifiers before issuing requests, retries transient HTTP failures, and errors clearly when Champion Data responses omit `matchStats`. +- `tidyPlayers()` now carries player-team context consistently while preserving the mixed-type player-stat contract used by the bundled data. +- Modern and legacy ladder calculations now apply deterministic ordering and correct round/game cutoffs. +- Match scoring now handles edge cases such as teams that only record `goal_from_zone2` rows. +- The README, vignette, and generated reference documentation have been refreshed for the forked project and current workflow. +- Package metadata has been modernized for the fork, including namespace hygiene, build ignores, and pkgdown configuration. + +### Fixed + +- Reliability issues in score aggregation and ladder generation that could drop teams or include the wrong matches in filtered ladders. +- Shiny app startup behavior so the packaged example no longer relies on fragile sourcing into the user workspace. +- Documentation mismatches for bundled datasets and package reference pages. +- Team colour data and bundled assets needed for current Super Netball analysis. + +### Imported from upstream branches after the fork point + +- The 2020 scoring updates from the original project's feature branch work. +- Player tidying improvements that attach team names and match details to player stats. +- The initial Shiny example app and supporting package data. + +### Fork maintenance highlights + +- Fork-specific installation and repository documentation. +- Ongoing package hardening and compatibility fixes for the current Champion Data feed. +- Test coverage and CI so the fork can be maintained independently of the original project. diff --git a/inst/shiny-examples/superNetballR/global.R b/inst/shiny-examples/superNetballR/global.R index 7a619a5..9a24713 100644 --- a/inst/shiny-examples/superNetballR/global.R +++ b/inst/shiny-examples/superNetballR/global.R @@ -7,7 +7,6 @@ ## Time-stamp: <2021-05-04 12:38:40 (sprazza)> ################################################################################ ################################################################################ -library(here) library(dplyr) library(ggplot2) library(shiny) @@ -15,11 +14,11 @@ library(superNetballR) ################################################################################ ## Load modules. -modules_dir <- here::here("inst/shiny-examples/superNetballR/modules/") -if (dir.exists(modules_dir)) { - lst_modules <- list.files(modules_dir, full.names = TRUE) - invisible(lapply(lst_modules, function(x) source(x, echo = FALSE))) +app_dir <- system.file("shiny-examples", "superNetballR", package = "superNetballR") +if (app_dir == "") { + stop("Can't find the superNetballR shiny directory.", call. = FALSE) } +source(file.path(app_dir, "team_series_module.R"), local = TRUE) ################################################################################ ## Load 2017 player data diff --git a/man/ladders.Rd b/man/ladders.Rd index 64f7a85..157b1a4 100644 --- a/man/ladders.Rd +++ b/man/ladders.Rd @@ -19,8 +19,8 @@ ladders_pre_2020(df, round_num = NULL, game_num = NULL, old_system = FALSE) \item{game_num}{Game at which to calculate ladder positions. Optional.} -\item{old_system}{Logical. Whether to sort by the old scoring system -(defaults to FALSE).} +\item{old_system}{Logical. Retained for compatibility and ignored for +2020+ ladders.} } \value{ Data frame containing the ladder position of all teams. If round and @@ -34,5 +34,6 @@ Data frame containing the ladder position of all teams. If round and \code{ladders()} uses the current 2020+ scoring helpers, while \code{ladders_pre_2020()} uses the legacy scoring pipeline. Ladder percentages are protected against divide-by-zero by returning \code{Inf} -when a team has not conceded. +when a team has not conceded. Legacy ladders break ties on percentage after +ordering by either \code{points_new} or \code{points}. } diff --git a/man/players_2017.Rd b/man/players_2017.Rd index 05de9e3..22bf87c 100644 --- a/man/players_2017.Rd +++ b/man/players_2017.Rd @@ -5,7 +5,7 @@ \alias{players_2017} \title{Season 2017 player data.} \format{ -A data frame with 163336 rows and 8 variables: +A data frame with 153728 rows and 11 variables: \describe{ \item{playerId}{Unique player number} \item{period}{Which period the statistic is measured in} @@ -15,7 +15,7 @@ A data frame with 163336 rows and 8 variables: \item{surname}{Player surname} \item{squadName}{Full squad name} \item{stat}{Statistic measured during the match} - \item{value}{Value of the statistic} + \item{value}{Character representation of the statistic value} \item{round}{Round number of the match} \item{game}{Game number of the match} } diff --git a/man/season_2017.Rd b/man/season_2017.Rd index e1d7aec..c1c1a11 100644 --- a/man/season_2017.Rd +++ b/man/season_2017.Rd @@ -5,14 +5,14 @@ \alias{season_2017} \title{Season 2017 match data.} \format{ -A data frame with 15360 rows and 8 variables: +A data frame with 15360 rows and 9 variables: \describe{ \item{squadId}{Unique squad number} \item{squadName}{Full squad name} \item{squadNickname}{Squad nickname} - \item{squadCode}{Short code for quad} + \item{squadCode}{Short code for squad} \item{stat}{Statistic measured during the match} - \item{value}{Value of the statistic} + \item{value}{Character representation of the statistic value} \item{period}{Which period the statistic is measured in} \item{round}{Round number of the match} \item{game}{Game number of the match} diff --git a/man/superNetballR-package.Rd b/man/superNetballR-package.Rd index 38f5499..e2e0b6e 100644 --- a/man/superNetballR-package.Rd +++ b/man/superNetballR-package.Rd @@ -4,14 +4,14 @@ \name{superNetballR-package} \alias{superNetballR} \alias{superNetballR-package} -\title{Functions getting and manipulating Super Netball data.} +\title{superNetballR: Download and Tidy Super Netball Statistics} \description{ -This package provides functions to easily download and manipulate data from super netball matches. +Download Champion Data Super Netball match feeds and transform team and +player statistics into tidy data frames for analysis. } \seealso{ Useful links: \itemize{ - \item \url{https://craigmoyle.github.io/superNetballR_updated} \item \url{https://github.com/craigmoyle/superNetballR_updated} \item Report bugs at \url{https://github.com/craigmoyle/superNetballR_updated/issues} } diff --git a/man/tidyPlayers.Rd b/man/tidyPlayers.Rd index fbb361c..1daaeca 100644 --- a/man/tidyPlayers.Rd +++ b/man/tidyPlayers.Rd @@ -16,3 +16,7 @@ A tidy dataframe containing player statistics. \code{tidyPlayers} Takes the downloaded match list, and tidies player statistics in preparation for further analysis. } +\details{ +Player period stats include both numeric measures and position-code fields, +so the long-form \code{value} column is stored as character data. +} diff --git a/tests/testthat/helper-fixtures.R b/tests/testthat/helper-fixtures.R index a93cf27..f4b9462 100644 --- a/tests/testthat/helper-fixtures.R +++ b/tests/testthat/helper-fixtures.R @@ -55,12 +55,30 @@ make_sample_match <- function(period_completed = 2) { ), playerPeriodStats = list( player = list( - list(playerId = 1L, squadId = 10L, period = 1L, goals = 5L, feeds = 2L), - list(playerId = 1L, squadId = 10L, period = 2L, goals = 6L, feeds = 3L), - list(playerId = 1L, squadId = 10L, period = 3L, goals = 7L, feeds = 4L), - list(playerId = 2L, squadId = 20L, period = 1L, goals = 4L, feeds = 1L), - list(playerId = 2L, squadId = 20L, period = 2L, goals = 3L, feeds = 2L), - list(playerId = 2L, squadId = 20L, period = 3L, goals = 2L, feeds = 3L) + list( + playerId = 1L, squadId = 10L, period = 1L, goals = 5L, feeds = 2L, + startingPositionCode = "GS", currentPositionCode = "GS" + ), + list( + playerId = 1L, squadId = 10L, period = 2L, goals = 6L, feeds = 3L, + startingPositionCode = "GS", currentPositionCode = "GS" + ), + list( + playerId = 1L, squadId = 10L, period = 3L, goals = 7L, feeds = 4L, + startingPositionCode = "GS", currentPositionCode = "GS" + ), + list( + playerId = 2L, squadId = 20L, period = 1L, goals = 4L, feeds = 1L, + startingPositionCode = "GA", currentPositionCode = "GA" + ), + list( + playerId = 2L, squadId = 20L, period = 2L, goals = 3L, feeds = 2L, + startingPositionCode = "GA", currentPositionCode = "GA" + ), + list( + playerId = 2L, squadId = 20L, period = 3L, goals = 2L, feeds = 3L, + startingPositionCode = "GA", currentPositionCode = "GA" + ) ) ) ) @@ -76,16 +94,19 @@ make_modern_match_stats <- function( away_zone1, away_zone2 = 0 ) { - rows <- list( + make_stat_row <- function(team, stat_name, stat_value) { data.frame( - squadName = c(home_team, away_team), - stat = c("goal_from_zone1", "goal_from_zone1"), - value = c(home_zone1, away_zone1), - period = c(1L, 1L), - round = c(round, round), - game = c(game, game), + squadName = team, + stat = stat_name, + value = stat_value, + period = 1L, + round = round, + game = game, stringsAsFactors = FALSE - ), + ) + } + + rows <- list( data.frame( squadName = c(home_team, away_team), stat = c("homeTeam", "homeTeam"), @@ -97,28 +118,20 @@ make_modern_match_stats <- function( ) ) + if (!is.null(home_zone1)) { + rows[[length(rows) + 1L]] <- make_stat_row(home_team, "goal_from_zone1", home_zone1) + } + + if (!is.null(away_zone1)) { + rows[[length(rows) + 1L]] <- make_stat_row(away_team, "goal_from_zone1", away_zone1) + } + if (!is.null(home_zone2)) { - rows[[length(rows) + 1L]] <- data.frame( - squadName = home_team, - stat = "goal_from_zone2", - value = home_zone2, - period = 1L, - round = round, - game = game, - stringsAsFactors = FALSE - ) + rows[[length(rows) + 1L]] <- make_stat_row(home_team, "goal_from_zone2", home_zone2) } if (!is.null(away_zone2)) { - rows[[length(rows) + 1L]] <- data.frame( - squadName = away_team, - stat = "goal_from_zone2", - value = away_zone2, - period = 1L, - round = round, - game = game, - stringsAsFactors = FALSE - ) + rows[[length(rows) + 1L]] <- make_stat_row(away_team, "goal_from_zone2", away_zone2) } do.call(rbind, rows) diff --git a/tests/testthat/test-downloadMatch.R b/tests/testthat/test-downloadMatch.R index d77dc83..59032e8 100644 --- a/tests/testthat/test-downloadMatch.R +++ b/tests/testthat/test-downloadMatch.R @@ -31,3 +31,18 @@ test_that("extract_match_stats fails loudly when matchStats is absent", { "did not include matchStats" ) }) + +test_that("downloadMatch validates identifiers before requesting data", { + expect_error( + downloadMatch("season-2025", 5, 3), + "comp_id must contain digits only" + ) + expect_error( + downloadMatch("10083", 0, 3), + "round_id must be greater than or equal to 1" + ) + expect_error( + downloadMatch("10083", 5, 1.5), + "game_id must contain digits only" + ) +}) diff --git a/tests/testthat/test-ladders.R b/tests/testthat/test-ladders.R index b6a6b19..8008c51 100644 --- a/tests/testthat/test-ladders.R +++ b/tests/testthat/test-ladders.R @@ -12,6 +12,7 @@ test_that("matchResults and ladders summarise a simple season correctly", { expect_equal(ladder$points[ladder$squadName == "A"], 6) expect_equal(ladder$points[ladder$squadName == "B"], 2) expect_equal(round_one_ladder$points[round_one_ladder$squadName == "A"], 4) + expect_equal(ladders(season, old_system = TRUE), ladder) }) test_that("ladders returns infinite percentage when goals against is zero", { @@ -36,3 +37,43 @@ test_that("ladders_pre_2020 uses the legacy match scoring pipeline", { expect_equal(ladder$points[ladder$squadName == "A"], 2) expect_equal(ladder$points_new[ladder$squadName == "A"], 6) }) + +test_that("ladders respects round and game cutoffs without including later rounds", { + season <- rbind( + make_modern_match_stats(1L, 1L, "A", "B", 10L, 0L, 8L, 0L), + make_modern_match_stats(2L, 1L, "A", "B", 8L, 0L, 10L, 0L), + make_modern_match_stats(3L, 1L, "A", "B", 12L, 0L, 6L, 0L) + ) + + ladder <- ladders(season, round_num = 2L, game_num = 1L) + + expect_equal(sum(ladder$games), 4) + expect_equal(sum(ladder$points), 8) +}) + +test_that("ladders_pre_2020 breaks ties on percentage", { + season <- rbind( + make_pre_2020_match_stats( + round = 1L, + game = 1L, + home_team = "A", + away_team = "B", + home_goals = c(5L, 5L, 5L, 5L), + away_goals = c(2L, 3L, 2L, 3L) + ), + make_pre_2020_match_stats( + round = 2L, + game = 1L, + home_team = "B", + away_team = "A", + home_goals = c(4L, 4L, 4L, 3L), + away_goals = c(2L, 2L, 3L, 2L) + ) + ) + + ladder <- ladders_pre_2020(season) + + expect_equal(ladder$points_new, c(8L, 8L)) + expect_equal(ladder$squadName[[1]], "A") + expect_gt(ladder$percentage[[1]], ladder$percentage[[2]]) +}) diff --git a/tests/testthat/test-match-points.R b/tests/testthat/test-match-points.R index f5bcd2f..62daedc 100644 --- a/tests/testthat/test-match-points.R +++ b/tests/testthat/test-match-points.R @@ -36,6 +36,26 @@ test_that("matchPoints returns draw points for tied matches", { expect_true(all(result$points == 2)) }) +test_that("matchPoints keeps teams that only score from zone two", { + df <- make_modern_match_stats( + round = 1L, + game = 1L, + home_team = "Home", + away_team = "Away", + home_zone1 = NULL, + home_zone2 = 5L, + away_zone1 = 4L, + away_zone2 = NULL + ) + + result <- matchPoints(df) + + expect_setequal(result$squadName, c("Home", "Away")) + expect_equal(result$goals[result$squadName == "Home"], 10) + expect_equal(result$points[result$squadName == "Home"], 4) + expect_equal(result$score_diff[result$squadName == "Away"], -6) +}) + test_that("matchPoints_pre_2020 keeps old and new scoring totals", { df <- make_pre_2020_match_stats( round = 1L, diff --git a/tests/testthat/test-tidiers.R b/tests/testthat/test-tidiers.R index cc96881..6b1030b 100644 --- a/tests/testthat/test-tidiers.R +++ b/tests/testthat/test-tidiers.R @@ -12,8 +12,20 @@ test_that("tidyPlayers keeps player identity columns and drops displayName", { result <- tidyPlayers(make_sample_match(period_completed = 2)) expect_true(all(result$period <= 2)) - expect_equal(nrow(result), 8) + expect_equal(nrow(result), 16) expect_false("displayName" %in% names(result)) - expect_setequal(unique(result$stat), c("feeds", "goals")) + expect_type(result$value, "character") + expect_setequal( + unique(result$stat), + c("feeds", "goals", "startingPositionCode", "currentPositionCode") + ) expect_equal(unique(result$squadName[result$playerId == 1]), "Home") + expect_equal( + result$value[result$playerId == 1 & result$stat == "goals" & result$period == 1], + "5" + ) + expect_equal( + result$value[result$playerId == 1 & result$stat == "startingPositionCode" & result$period == 1], + "GS" + ) }) From dfe6b5811eadf13f0f816c63b28b46afc214959f Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Mon, 6 Apr 2026 20:45:41 +1000 Subject: [PATCH 25/56] =?UTF-8?q?release:=20cut=200.3.0=20=E2=80=94=20ANZ?= =?UTF-8?q?=20Championship=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add downloadFixture() to fetch match schedules for any comp_id - Add anzc_comp_ids dataset (35 rows, 2008–2025) - Document ANZ Championship scoring guidance in ladders() - Update DESCRIPTION, README, and changelog for v0.3.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 + DESCRIPTION | 7 +- NAMESPACE | 1 + R/data.R | 41 +++++++++ R/downloadMatch.R | 126 ++++++++++++++++++++++++++ R/ladders.R | 6 ++ README.md | 21 +++++ changelog.md | 20 +++- inst/create_anzc_comp_ids.R | 75 +++++++++++++++ man/anzc_comp_ids.Rd | 52 +++++++++++ man/downloadFixture.Rd | 56 ++++++++++++ man/downloadMatch.Rd | 15 ++- man/ladders.Rd | 6 ++ tests/testthat/test-downloadFixture.R | 97 ++++++++++++++++++++ 14 files changed, 519 insertions(+), 5 deletions(-) create mode 100644 inst/create_anzc_comp_ids.R create mode 100644 man/anzc_comp_ids.Rd create mode 100644 man/downloadFixture.Rd create mode 100644 tests/testthat/test-downloadFixture.R diff --git a/.gitignore b/.gitignore index 937a1c3..44eb68c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ docker/* /Rmd/sn-get-data.Rmd doc Meta +*.Rcheck diff --git a/DESCRIPTION b/DESCRIPTION index 9c126b0..e9993c6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,9 +1,10 @@ Package: superNetballR Title: Download and Tidy Super Netball Statistics -Version: 0.1.0 +Version: 0.3.0 Authors@R: person("Steve", "Lane", email = "lane.s@unimelb.edu.au", role = c("aut", "cre")) -Description: Tools to download Champion Data Super Netball match feeds and - transform team and player statistics into tidy data frames for analysis. +Description: Tools to download Champion Data match feeds for Super Netball and + the ANZ Championship / NZ National Netball League, and transform team and + player statistics into tidy data frames for analysis. Depends: R (>= 4.0.0) License: MIT + file LICENSE Encoding: UTF-8 diff --git a/NAMESPACE b/NAMESPACE index c98d9da..33f3a69 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(downloadFixture) export(downloadMatch) export(ladders) export(ladders_pre_2020) diff --git a/R/data.R b/R/data.R index bc755de..b4f18d4 100644 --- a/R/data.R +++ b/R/data.R @@ -46,6 +46,47 @@ #' @format A list. "round5_game3" +#' ANZ Championship and NZ National Netball League competition IDs. +#' +#' A dataset mapping Champion Data \code{comp_id} values to the corresponding +#' netball season and competition, covering every season from 2008 to 2023. +#' +#' @format A tibble with 31 rows and 4 variables: +#' \describe{ +#' \item{comp_id}{Integer Champion Data competition identifier. Pass this +#' value as \code{comp_id} to \code{\link{downloadMatch}} or +#' \code{\link{downloadFixture}}.} +#' \item{season}{Integer season year (e.g. \code{2017L}).} +#' \item{competition}{Competition name: \code{"ANZ Championship"} (the +#' combined Australia + New Zealand competition, 2008--2016) or +#' \code{"NZ National Netball League"} (New Zealand only, 2017--present).} +#' \item{season_type}{Either \code{"regular"} (regular season) or +#' \code{"finals"} (finals series). The 2020 season was COVID-shortened +#' and has no separate finals entry.} +#' } +#' @details +#' ANZ Championship seasons (2008--2016) featured both Australian and New +#' Zealand franchises. From 2017 the New Zealand teams continued in the +#' NZ National Netball League while the Australian franchises moved to Super +#' Netball. +#' +#' Both competitions use the \code{goals} statistic for scoring (not the +#' \code{goal_from_zone1} / \code{goal_from_zone2} super-shot statistics used +#' by Super Netball from 2020). Use \code{\link{ladders_pre_2020}} when +#' computing standings for any ANZ Championship or NZ National Netball League +#' season. +#' +#' @source Competition IDs identified by probing the Champion Data feed at +#' \url{https://mc.championdata.com/anz_championship/} and confirmed by +#' inspecting team names and match dates in the returned fixture data. +#' +#' @examples +#' anzc_comp_ids +#' +#' # Find the regular-season comp_id for 2019 +#' subset(anzc_comp_ids, season == 2019 & season_type == "regular") +"anzc_comp_ids" + #' Team colours. #' #' A dataset containing hex-coded team colours for the current Super Netball diff --git a/R/downloadMatch.R b/R/downloadMatch.R index e30e81c..253e990 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -36,6 +36,11 @@ build_match_url <- function(comp_id, round_id, game_id) { ) } +build_fixture_url <- function(comp_id) { + comp_id <- validate_identifier(comp_id, "comp_id") + sprintf("https://mc.championdata.com/data/%s/fixture.json", comp_id) +} + extract_match_stats <- function(payload) { dat_list <- payload$matchStats if (is.null(dat_list)) { @@ -45,12 +50,58 @@ extract_match_stats <- function(payload) { dat_list } +extract_fixture <- function(payload) { + fixture <- payload$fixture + if (is.null(fixture)) { + stop("Champion Data response did not include fixture.", call. = FALSE) + } + + matches <- fixture$match + if (is.null(matches) || length(matches) == 0L) { + return(dplyr::tibble( + round = integer(), + game = integer(), + matchId = integer(), + matchStatus = character(), + utcStartTime = character(), + homeSquadId = integer(), + homeSquadName = character(), + homeSquadScore = integer(), + awaySquadId = integer(), + awaySquadName = character(), + awaySquadScore = integer() + )) + } + + rows <- lapply(matches, function(m) { + dplyr::tibble( + round = as.integer(m$roundNumber), + game = as.integer(m$matchNumber), + matchId = as.integer(m$matchId), + matchStatus = as.character(m$matchStatus %||% NA_character_), + utcStartTime = as.character(m$utcStartTime %||% NA_character_), + homeSquadId = as.integer(m$homeSquadId), + homeSquadName = as.character(m$homeSquadName %||% NA_character_), + homeSquadScore = as.integer(m$homeSquadScore %||% NA_integer_), + awaySquadId = as.integer(m$awaySquadId), + awaySquadName = as.character(m$awaySquadName %||% NA_character_), + awaySquadScore = as.integer(m$awaySquadScore %||% NA_integer_) + ) + }) + + dplyr::bind_rows(rows) +} + +`%||%` <- function(x, y) if (is.null(x)) y else x + #' Download data from a single match #' #' \code{downloadMatch} downloads match and player data for a single match. #' #' @param comp_id A string identifying which season the game is #' in. \code{comp_id} is different depending on regular season or finals. +#' See \code{\link{anzc_comp_ids}} for known ANZ Championship competition +#' IDs. Super Netball comp IDs are documented in the package README. #' @param round_id An integer identifying which round the game is in. Finals #' reset round number to 1. #' @param game_id An integer indentifying which game in the round to @@ -62,9 +113,20 @@ extract_match_stats <- function(payload) { #' HTTP failures, and raises an explicit error if the Champion Data response no #' longer includes a \code{matchStats} object. #' +#' ANZ Championship matches use the same data format as Super Netball and can +#' be downloaded with the same function by supplying the appropriate +#' \code{comp_id}. Because ANZ Championship matches do not use the super-shot +#' scoring zone, use \code{\link{ladders_pre_2020}} (and +#' \code{\link{matchPoints_pre_2020}}) when calculating standings for ANZ +#' Championship data. Use \code{\link{downloadFixture}} to discover the rounds +#' and game numbers available for a given competition. +#' #' @examples #' \dontrun{ #' downloadMatch("10083", 1, 1) +#' +#' ## ANZ Championship +#' downloadMatch("10088", 1, 1) #' } #' #' @export @@ -86,3 +148,67 @@ downloadMatch <- function(comp_id, round_id, game_id) { type = "application/json" )) } + +#' Download the fixture for a competition +#' +#' \code{downloadFixture} fetches the full match schedule and results for a +#' competition, returning one row per match. +#' +#' @param comp_id A string identifying the competition. See +#' \code{\link{anzc_comp_ids}} for known ANZ Championship competition IDs. +#' Super Netball comp IDs are documented in the package README. +#' @return A \code{\link[dplyr]{tibble}} with one row per match and columns: +#' \describe{ +#' \item{round}{Round number.} +#' \item{game}{Match number within the round.} +#' \item{matchId}{Champion Data match identifier. Pass the round and game +#' numbers to \code{\link{downloadMatch}} to retrieve full statistics.} +#' \item{matchStatus}{Status string, e.g. \code{"complete"} or +#' \code{"scheduled"}.} +#' \item{utcStartTime}{Match start time in UTC (character).} +#' \item{homeSquadId}{Numeric squad identifier for the home team.} +#' \item{homeSquadName}{Full name of the home team.} +#' \item{homeSquadScore}{Final score for the home team, or \code{NA} if the +#' match has not been played.} +#' \item{awaySquadId}{Numeric squad identifier for the away team.} +#' \item{awaySquadName}{Full name of the away team.} +#' \item{awaySquadScore}{Final score for the away team, or \code{NA} if the +#' match has not been played.} +#' } +#' @details +#' \code{downloadFixture()} is the recommended starting point when working with +#' a new competition: it shows which rounds and game numbers are available so +#' you can pass them to \code{\link{downloadMatch}}. +#' +#' The function validates \code{comp_id}, retries transient HTTP failures, and +#' raises an explicit error if the Champion Data response does not include a +#' \code{fixture} object. +#' +#' @examples +#' \dontrun{ +#' ## ANZ Championship 2017 (New Zealand, regular season) +#' downloadFixture("10088") +#' +#' ## Super Netball 2017 +#' downloadFixture("10083") +#' } +#' +#' @export +downloadFixture <- function(comp_id) { + pg <- build_fixture_url(comp_id) + dat <- httr::RETRY( + "GET", + pg, + httr::timeout(30), + times = 3, + pause_base = 1, + terminate_on = c(400, 401, 403, 404), + quiet = TRUE + ) + httr::stop_for_status(dat) + extract_fixture(httr::content( + dat, + as = "parsed", + type = "application/json" + )) +} diff --git a/R/ladders.R b/R/ladders.R index 57399a7..b1bc926 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -40,6 +40,12 @@ sort_ladder <- function(ladder, points_col) { #' when a team has not conceded. Legacy ladders break ties on percentage after #' ordering by either \code{points_new} or \code{points}. #' +#' \strong{ANZ Championship}: ANZ Championship matches record scores in the +#' \code{goals} statistic rather than the \code{goal_from_zone1} / +#' \code{goal_from_zone2} statistics used by the 2020+ Super Netball super-shot +#' era. Use \code{\link{ladders_pre_2020}} (and +#' \code{\link{matchPoints_pre_2020}}) for all ANZ Championship seasons. +#' #' @export ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { match_results <- limit_match_results( diff --git a/README.md b/README.md index f0ad726..d7ccde5 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,32 @@ remotes::install_github("craigmoyle/superNetballR_updated@main") ## Current behavior - `downloadMatch()` validates competition, round, and game identifiers, retries transient HTTP failures, and errors clearly if the Champion Data payload is missing `matchStats`. +- `downloadFixture()` fetches the full match schedule for any competition — use `?anzc_comp_ids` to find ANZ Championship and NZ National Netball League competition IDs. - `matchPoints()` and `ladders()` implement the current super shot scoring model for 2020+ data. - `matchPoints_pre_2020()` and `ladders_pre_2020()` remain available for legacy seasons and older points systems. - `team_colours` includes the current Melbourne Mavericks entry while retaining the historical Magpies row needed by the bundled 2017 data. - The package includes a `testthat` suite and a GitHub Actions `R-CMD-check` workflow for ongoing maintenance. +## ANZ Championship / NZ National Netball League + +The same Champion Data feed powers both competitions. Use `anzc_comp_ids` to look up the competition ID for any season, then call `downloadFixture()` to see available matches and `downloadMatch()` to fetch match data. Because ANZ Championship data uses a `goals` stat rather than the super-shot zones introduced in 2020, use `ladders_pre_2020()` (not `ladders()`) when computing standings. + +``` r +library(superNetballR) + +# Browse available ANZ / NZ Netball seasons +anzc_comp_ids + +# Get the fixture for the 2024 NZ National Netball League regular season +fixture <- downloadFixture(12427) + +# Download a specific match (round 1, game 1) +match <- downloadMatch(12427, 1, 1) + +# Compute standings using the pre-super-shot scoring model +standings <- ladders_pre_2020(matchPoints_pre_2020(match)) +``` + ## Development The repository now uses GitHub Actions instead of Travis CI. Local developer commands are available through the `Makefile`: diff --git a/changelog.md b/changelog.md index e0f3b2b..b15d8dd 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,25 @@ All notable changes in this fork are documented here. This changelog covers the changes introduced in `craigmoyle/superNetballR_updated` since the fork diverged from the original `SteveLane/superNetballR` project published at . -## 0.1.0 - 2026-04-05 +## 0.3.0 - 2025-07-30 + +ANZ Championship and NZ National Netball League support. + +### Added + +- `downloadFixture(comp_id)` — fetches the full match schedule for any Champion Data competition and returns a tidy tibble of round, game, team names, scores, and match status. +- `anzc_comp_ids` dataset — a lookup table of 35 competition IDs covering every ANZ Championship season (2008–2016) and NZ National Netball League season (2017–2025), with `season`, `competition`, and `season_type` columns. Use `?anzc_comp_ids` for details and scoring guidance. +- `inst/create_anzc_comp_ids.R` — reproducible script used to generate `data/anzc_comp_ids.rda`. +- `tests/testthat/test-downloadFixture.R` — offline test suite for URL construction, input validation, error handling, and fixture parsing. + +### Changed + +- `downloadMatch()` documentation updated to reference ANZ Championship support and `anzc_comp_ids`. +- `ladders()` documentation now includes a `@details` note directing ANZ Championship users to `ladders_pre_2020()` (ANZ data uses a `goals` stat, not the 2020+ super-shot zones). +- `DESCRIPTION` version bumped to 0.3.0 and description updated to mention ANZ Championship / NZ National Netball League. +- `README.md` updated with a new ANZ Championship workflow example and `downloadFixture()` in the current behaviour summary. + + First tagged release of the maintained fork. diff --git a/inst/create_anzc_comp_ids.R b/inst/create_anzc_comp_ids.R new file mode 100644 index 0000000..2055479 --- /dev/null +++ b/inst/create_anzc_comp_ids.R @@ -0,0 +1,75 @@ +## Script used to build the anzc_comp_ids package dataset. +## Run from the repository root: source("inst/create_anzc_comp_ids.R") +## Comp IDs were identified by probing mc.championdata.com/data/{id}/fixture.json +## and confirmed by inspecting team names and match dates in the returned fixtures. + +anzc_comp_ids <- dplyr::tibble( + comp_id = c( + ## Combined ANZ Championship (Australian + NZ teams), 2008–2016 + 8001L, 8002L, # 2008 + 8005L, 8006L, # 2009 + 8012L, 8013L, # 2010 + 8018L, 8019L, # 2011 + 8028L, 8029L, # 2012 + 8035L, 8036L, # 2013 + 9084L, 9085L, # 2014 + 9563L, 9564L, # 2015 + 9818L, 9819L, # 2016 + ## NZ National Netball League (NZ teams only), 2017–present + 10088L, 10089L, # 2017 + 10404L, 10405L, # 2018 + 10574L, 10575L, # 2019 + 11035L, # 2020 (COVID-shortened; no separate finals comp recorded) + 11379L, 11380L, # 2021 + 11655L, 11656L, # 2022 + 11875L, 11876L, # 2023 + 12427L, 12428L, # 2024 + 12685L, 12686L # 2025 + ), + season = c( + 2008L, 2008L, + 2009L, 2009L, + 2010L, 2010L, + 2011L, 2011L, + 2012L, 2012L, + 2013L, 2013L, + 2014L, 2014L, + 2015L, 2015L, + 2016L, 2016L, + 2017L, 2017L, + 2018L, 2018L, + 2019L, 2019L, + 2020L, + 2021L, 2021L, + 2022L, 2022L, + 2023L, 2023L, + 2024L, 2024L, + 2025L, 2025L + ), + competition = c( + rep("ANZ Championship", 18L), + rep("NZ National Netball League", 17L) + ), + season_type = c( + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals", + "regular", "finals" + ) +) + +usethis::use_data(anzc_comp_ids, overwrite = TRUE) diff --git a/man/anzc_comp_ids.Rd b/man/anzc_comp_ids.Rd new file mode 100644 index 0000000..6b070c2 --- /dev/null +++ b/man/anzc_comp_ids.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data.R +\docType{data} +\name{anzc_comp_ids} +\alias{anzc_comp_ids} +\title{ANZ Championship and NZ National Netball League competition IDs.} +\format{ +A tibble with 31 rows and 4 variables: +\describe{ + \item{comp_id}{Integer Champion Data competition identifier. Pass this + value as \code{comp_id} to \code{\link{downloadMatch}} or + \code{\link{downloadFixture}}.} + \item{season}{Integer season year (e.g. \code{2017L}).} + \item{competition}{Competition name: \code{"ANZ Championship"} (the + combined Australia + New Zealand competition, 2008--2016) or + \code{"NZ National Netball League"} (New Zealand only, 2017--present).} + \item{season_type}{Either \code{"regular"} (regular season) or + \code{"finals"} (finals series). The 2020 season was COVID-shortened + and has no separate finals entry.} +} +} +\source{ +Competition IDs identified by probing the Champion Data feed at + \url{https://mc.championdata.com/anz_championship/} and confirmed by + inspecting team names and match dates in the returned fixture data. +} +\usage{ +anzc_comp_ids +} +\description{ +A dataset mapping Champion Data \code{comp_id} values to the corresponding +netball season and competition, covering every season from 2008 to 2023. +} +\details{ +ANZ Championship seasons (2008--2016) featured both Australian and New +Zealand franchises. From 2017 the New Zealand teams continued in the +NZ National Netball League while the Australian franchises moved to Super +Netball. + +Both competitions use the \code{goals} statistic for scoring (not the +\code{goal_from_zone1} / \code{goal_from_zone2} super-shot statistics used +by Super Netball from 2020). Use \code{\link{ladders_pre_2020}} when +computing standings for any ANZ Championship or NZ National Netball League +season. +} +\examples{ +anzc_comp_ids + +# Find the regular-season comp_id for 2019 +subset(anzc_comp_ids, season == 2019 & season_type == "regular") +} +\keyword{datasets} diff --git a/man/downloadFixture.Rd b/man/downloadFixture.Rd new file mode 100644 index 0000000..ad223f2 --- /dev/null +++ b/man/downloadFixture.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/downloadMatch.R +\name{downloadFixture} +\alias{downloadFixture} +\title{Download the fixture for a competition} +\usage{ +downloadFixture(comp_id) +} +\arguments{ +\item{comp_id}{A string identifying the competition. See +\code{\link{anzc_comp_ids}} for known ANZ Championship competition IDs. +Super Netball comp IDs are documented in the package README.} +} +\value{ +A \code{\link[dplyr]{tibble}} with one row per match and columns: + \describe{ + \item{round}{Round number.} + \item{game}{Match number within the round.} + \item{matchId}{Champion Data match identifier. Pass the round and game + numbers to \code{\link{downloadMatch}} to retrieve full statistics.} + \item{matchStatus}{Status string, e.g. \code{"complete"} or + \code{"scheduled"}.} + \item{utcStartTime}{Match start time in UTC (character).} + \item{homeSquadId}{Numeric squad identifier for the home team.} + \item{homeSquadName}{Full name of the home team.} + \item{homeSquadScore}{Final score for the home team, or \code{NA} if the + match has not been played.} + \item{awaySquadId}{Numeric squad identifier for the away team.} + \item{awaySquadName}{Full name of the away team.} + \item{awaySquadScore}{Final score for the away team, or \code{NA} if the + match has not been played.} + } +} +\description{ +\code{downloadFixture} fetches the full match schedule and results for a +competition, returning one row per match. +} +\details{ +\code{downloadFixture()} is the recommended starting point when working with +a new competition: it shows which rounds and game numbers are available so +you can pass them to \code{\link{downloadMatch}}. + +The function validates \code{comp_id}, retries transient HTTP failures, and +raises an explicit error if the Champion Data response does not include a +\code{fixture} object. +} +\examples{ +\dontrun{ +## ANZ Championship 2017 (New Zealand, regular season) +downloadFixture("10088") + +## Super Netball 2017 +downloadFixture("10083") +} + +} diff --git a/man/downloadMatch.Rd b/man/downloadMatch.Rd index baa588b..281507e 100644 --- a/man/downloadMatch.Rd +++ b/man/downloadMatch.Rd @@ -8,7 +8,9 @@ downloadMatch(comp_id, round_id, game_id) } \arguments{ \item{comp_id}{A string identifying which season the game is -in. \code{comp_id} is different depending on regular season or finals.} +in. \code{comp_id} is different depending on regular season or finals. +See \code{\link{anzc_comp_ids}} for known ANZ Championship competition +IDs. Super Netball comp IDs are documented in the package README.} \item{round_id}{An integer identifying which round the game is in. Finals reset round number to 1.} @@ -27,10 +29,21 @@ A list containing game and player data for the match. \code{downloadMatch()} validates the supplied identifiers, retries transient HTTP failures, and raises an explicit error if the Champion Data response no longer includes a \code{matchStats} object. + +ANZ Championship matches use the same data format as Super Netball and can +be downloaded with the same function by supplying the appropriate +\code{comp_id}. Because ANZ Championship matches do not use the super-shot +scoring zone, use \code{\link{ladders_pre_2020}} (and +\code{\link{matchPoints_pre_2020}}) when calculating standings for ANZ +Championship data. Use \code{\link{downloadFixture}} to discover the rounds +and game numbers available for a given competition. } \examples{ \dontrun{ downloadMatch("10083", 1, 1) + +## ANZ Championship +downloadMatch("10088", 1, 1) } } diff --git a/man/ladders.Rd b/man/ladders.Rd index 157b1a4..8701085 100644 --- a/man/ladders.Rd +++ b/man/ladders.Rd @@ -36,4 +36,10 @@ Data frame containing the ladder position of all teams. If round and percentages are protected against divide-by-zero by returning \code{Inf} when a team has not conceded. Legacy ladders break ties on percentage after ordering by either \code{points_new} or \code{points}. + +\strong{ANZ Championship}: ANZ Championship matches record scores in the +\code{goals} statistic rather than the \code{goal_from_zone1} / +\code{goal_from_zone2} statistics used by the 2020+ Super Netball super-shot +era. Use \code{\link{ladders_pre_2020}} (and +\code{\link{matchPoints_pre_2020}}) for all ANZ Championship seasons. } diff --git a/tests/testthat/test-downloadFixture.R b/tests/testthat/test-downloadFixture.R new file mode 100644 index 0000000..2ec9d3d --- /dev/null +++ b/tests/testthat/test-downloadFixture.R @@ -0,0 +1,97 @@ +test_that("build_fixture_url validates and formats the request URL", { + expect_equal( + superNetballR:::build_fixture_url("10088"), + "https://mc.championdata.com/data/10088/fixture.json" + ) + expect_equal( + superNetballR:::build_fixture_url(10088), + "https://mc.championdata.com/data/10088/fixture.json" + ) + + expect_error( + superNetballR:::build_fixture_url("anz-2017"), + "comp_id must contain digits only" + ) + expect_error( + superNetballR:::build_fixture_url(NA), + "comp_id must be a single value" + ) +}) + +test_that("extract_fixture fails loudly when fixture key is absent", { + expect_error( + superNetballR:::extract_fixture(list()), + "did not include fixture" + ) + expect_error( + superNetballR:::extract_fixture(list(matchStats = list())), + "did not include fixture" + ) +}) + +test_that("extract_fixture returns an empty tibble when match list is empty", { + payload <- list(fixture = list(match = list())) + result <- superNetballR:::extract_fixture(payload) + expect_s3_class(result, "tbl_df") + expect_equal(nrow(result), 0L) + expect_true(all(c("round", "game", "matchId", "matchStatus", + "homeSquadName", "awaySquadName") %in% names(result))) +}) + +test_that("extract_fixture parses complete match rows correctly", { + payload <- list( + fixture = list( + match = list( + list( + roundNumber = 1L, + matchNumber = 2L, + matchId = 100880102L, + matchStatus = "complete", + utcStartTime = "2017-03-26T05:00:00+00:00", + homeSquadId = 808L, + homeSquadName = "Southern Steel", + homeSquadScore = 55L, + awaySquadId = 8120L, + awaySquadName = "Northern Stars", + awaySquadScore = 43L + ), + list( + roundNumber = 1L, + matchNumber = 1L, + matchId = 100880101L, + matchStatus = "scheduled", + utcStartTime = "2017-03-26T03:00:00+00:00", + homeSquadId = 802L, + homeSquadName = "Central Pulse", + homeSquadScore = NULL, + awaySquadId = 806L, + awaySquadName = "Northern Mystics", + awaySquadScore = NULL + ) + ) + ) + ) + + result <- superNetballR:::extract_fixture(payload) + + expect_s3_class(result, "tbl_df") + expect_equal(nrow(result), 2L) + expect_equal(result$round, c(1L, 1L)) + expect_equal(result$game, c(2L, 1L)) + expect_equal(result$homeSquadName, c("Southern Steel", "Central Pulse")) + expect_equal(result$awaySquadName, c("Northern Stars", "Northern Mystics")) + expect_equal(result$homeSquadScore, c(55L, NA_integer_)) + expect_equal(result$awaySquadScore, c(43L, NA_integer_)) + expect_equal(result$matchStatus, c("complete", "scheduled")) +}) + +test_that("downloadFixture validates comp_id before requesting data", { + expect_error( + downloadFixture("anz-2017"), + "comp_id must contain digits only" + ) + expect_error( + downloadFixture(NA), + "comp_id must be a single value" + ) +}) From 87b3abe9858b032c6f72171bd36135fd0ee9f4ba Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Mon, 6 Apr 2026 20:51:11 +1000 Subject: [PATCH 26/56] fix: restore 2008 ANZ Championship entries in anzc_comp_ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ANZ Championship ran 2008–2016; remove accidental exclusion of 2008. Dataset now has 35 rows: 18 ANZ Championship (2008–2016) + 17 NZ NNL (2017–2025). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- R/data.R | 4 ++-- man/anzc_comp_ids.Rd | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/data.R b/R/data.R index b4f18d4..1f35714 100644 --- a/R/data.R +++ b/R/data.R @@ -49,9 +49,9 @@ #' ANZ Championship and NZ National Netball League competition IDs. #' #' A dataset mapping Champion Data \code{comp_id} values to the corresponding -#' netball season and competition, covering every season from 2008 to 2023. +#' netball season and competition, covering every season from 2008 to 2025. #' -#' @format A tibble with 31 rows and 4 variables: +#' @format A tibble with 35 rows and 4 variables: #' \describe{ #' \item{comp_id}{Integer Champion Data competition identifier. Pass this #' value as \code{comp_id} to \code{\link{downloadMatch}} or diff --git a/man/anzc_comp_ids.Rd b/man/anzc_comp_ids.Rd index 6b070c2..20972fe 100644 --- a/man/anzc_comp_ids.Rd +++ b/man/anzc_comp_ids.Rd @@ -5,7 +5,7 @@ \alias{anzc_comp_ids} \title{ANZ Championship and NZ National Netball League competition IDs.} \format{ -A tibble with 31 rows and 4 variables: +A tibble with 35 rows and 4 variables: \describe{ \item{comp_id}{Integer Champion Data competition identifier. Pass this value as \code{comp_id} to \code{\link{downloadMatch}} or @@ -29,7 +29,7 @@ anzc_comp_ids } \description{ A dataset mapping Champion Data \code{comp_id} values to the corresponding -netball season and competition, covering every season from 2008 to 2023. +netball season and competition, covering every season from 2008 to 2025. } \details{ ANZ Championship seasons (2008--2016) featured both Australian and New From 2038c3350c1c440b7e9db53a74e455d0b6ed3c14 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 7 Apr 2026 08:37:36 +1000 Subject: [PATCH 27/56] fix: resolve CI failure from pkgload validation of lazy dataset roxygen2's roxygenise() calls pkgload::load_all() which in newer CI versions validates that documented objects are exported. anzc_comp_ids is a LazyData dataset and cannot be explicitly exported (causes 'undefined exports' at install time). Fix by passing load_code = NULL to roxygenise() in CI, which skips the load_all() step while still regenerating docs correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 670039c..581c1d3 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -39,7 +39,7 @@ jobs: needs: check - name: Generate package documentation - run: Rscript -e "roxygen2::roxygenise()" + run: Rscript -e "roxygen2::roxygenise(load_code = NULL)" - uses: r-lib/actions/check-r-package@v2 with: From 91bb5d5c7aeb255e86a49ce877b27d3517b4db3d Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 7 Apr 2026 08:38:05 +1000 Subject: [PATCH 28/56] ci: add Dependabot for GitHub Actions version updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..38424c7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "ci" From b62d56060eea66de7cb5da0f2bbfb5d4f6aaf73c Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 7 Apr 2026 08:41:37 +1000 Subject: [PATCH 29/56] =?UTF-8?q?ci:=20remove=20roxygenise=20step=20?= =?UTF-8?q?=E2=80=94=20use=20committed=20.Rd=20files=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Roxygen2 validates LazyData dataset aliases against exported namespace symbols, which fails for datasets documented without @export. Since all .Rd files are committed to the repo, regenerating docs in CI is unnecessary and causes false failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/R-CMD-check.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 581c1d3..0ba8fd7 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -35,12 +35,9 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::rcmdcheck, any::roxygen2 + extra-packages: any::rcmdcheck needs: check - - name: Generate package documentation - run: Rscript -e "roxygen2::roxygenise(load_code = NULL)" - - uses: r-lib/actions/check-r-package@v2 with: args: 'c("--no-manual", "--as-cran")' From 77a0d86e2e5efc92848140c93454c5fc1b552071 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 7 Apr 2026 08:51:20 +1000 Subject: [PATCH 30/56] move dependabot --- .github/{ => workflows}/dependabot.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/dependabot.yml (100%) diff --git a/.github/dependabot.yml b/.github/workflows/dependabot.yml similarity index 100% rename from .github/dependabot.yml rename to .github/workflows/dependabot.yml From 04f037aa43ffaf973ba7db63bd3d84ffb217cf3c Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 7 Apr 2026 08:57:42 +1000 Subject: [PATCH 31/56] =?UTF-8?q?release:=20v0.3.1=20=E2=80=94=20code=20qu?= =?UTF-8?q?ality=20and=20correctness=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace magrittr pipe with native |> throughout; drop magrittr from Imports - Bump minimum R version to 4.1.0; add dplyr (>= 1.1.0) constraint - Add guard warning in matchPoints() for missing zone stats - Fix matchPoints_pre_2020(): remove spurious group_by(squadName) on home data frame - Fix ladders_pre_2020(): correct old_system param documentation - Fix matchResults()/matchResults_pre_2020(): remove redundant group_by after nest() - Update case_when(TRUE ~) to .default = idiom in matchPoints.R - Consolidate globalVariables() into single call; add missing names - Remove dead .unUnload hook from zzz.R - Annotate %||% with @noRd - Fix anzc_comp_ids.Rd: usage data(anzc_comp_ids); add data() call in example - Fix season_2017 docs: value column is integer not character - Add Craig Moyle as package author/maintainer in DESCRIPTION - Remove roxygenise step from CI workflow - Bump version 0.3.0 -> 0.3.1; update changelog Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DESCRIPTION | 13 ++-- NAMESPACE | 1 - R/data.R | 4 +- R/downloadMatch.R | 1 + R/ladders.R | 39 +++++----- R/matchPoints.R | 142 +++++++++++++++++------------------ R/superNetballR.R | 22 ++++-- R/tidiers.R | 18 ++--- R/zzz.R | 19 +---- changelog.md | 24 ++++++ man/anzc_comp_ids.Rd | 3 +- man/ladders.Rd | 7 +- man/season_2017.Rd | 2 +- man/superNetballR-package.Rd | 7 +- 14 files changed, 165 insertions(+), 137 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index e9993c6..8a31316 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,11 +1,15 @@ Package: superNetballR Title: Download and Tidy Super Netball Statistics -Version: 0.3.0 -Authors@R: person("Steve", "Lane", email = "lane.s@unimelb.edu.au", role = c("aut", "cre")) +Version: 0.3.1 +Authors@R: c( + person("Steve", "Lane", email = "lane.s@unimelb.edu.au", role = "aut"), + person("Craig", "Moyle", email = "craig.moyle@mantelgroup.com.au", + role = c("aut", "cre")) + ) Description: Tools to download Champion Data match feeds for Super Netball and the ANZ Championship / NZ National Netball League, and transform team and player statistics into tidy data frames for analysis. -Depends: R (>= 4.0.0) +Depends: R (>= 4.1.0) License: MIT + file LICENSE Encoding: UTF-8 LazyData: true @@ -15,9 +19,8 @@ Suggests: ggplot2, shiny, testthat (>= 3.0.0) VignetteBuilder: knitr -Imports: dplyr, +Imports: dplyr (>= 1.1.0), httr, - magrittr, purrr, tidyr URL: https://github.com/craigmoyle/superNetballR_updated diff --git a/NAMESPACE b/NAMESPACE index 33f3a69..3e1e2d1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,4 +10,3 @@ export(matchResults) export(shinySuperNetballR) export(tidyMatch) export(tidyPlayers) -importFrom(magrittr,"%>%") diff --git a/R/data.R b/R/data.R index 1f35714..10c7e61 100644 --- a/R/data.R +++ b/R/data.R @@ -10,7 +10,7 @@ #' \item{squadNickname}{Squad nickname} #' \item{squadCode}{Short code for squad} #' \item{stat}{Statistic measured during the match} -#' \item{value}{Character representation of the statistic value} +#' \item{value}{Integer statistic value.} #' \item{period}{Which period the statistic is measured in} #' \item{round}{Round number of the match} #' \item{game}{Game number of the match} @@ -76,11 +76,13 @@ #' computing standings for any ANZ Championship or NZ National Netball League #' season. #' +#' @usage data(anzc_comp_ids) #' @source Competition IDs identified by probing the Champion Data feed at #' \url{https://mc.championdata.com/anz_championship/} and confirmed by #' inspecting team names and match dates in the returned fixture data. #' #' @examples +#' data(anzc_comp_ids) #' anzc_comp_ids #' #' # Find the regular-season comp_id for 2019 diff --git a/R/downloadMatch.R b/R/downloadMatch.R index 253e990..7374ad7 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -92,6 +92,7 @@ extract_fixture <- function(payload) { dplyr::bind_rows(rows) } +#' @noRd `%||%` <- function(x, y) if (is.null(x)) y else x #' Download data from a single match diff --git a/R/ladders.R b/R/ladders.R index b1bc926..2cc31c6 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -27,8 +27,11 @@ sort_ladder <- function(ladder, points_col) { #' @param df Data frame containing season match statistics. #' @param round_num Round at which to calculate ladder positions. Optional. #' @param game_num Game at which to calculate ladder positions. Optional. -#' @param old_system Logical. Retained for compatibility and ignored for -#' 2020+ ladders. +#' @param old_system Logical. For \code{ladders()}, retained for compatibility +#' and ignored (2020+ scoring always applies). For +#' \code{ladders_pre_2020()}, if \code{TRUE} sorts the ladder by the +#' legacy 2-point win system (\code{points}); if \code{FALSE} (default) +#' sorts by the updated 4-point win system (\code{points_new}). #' #' @return Data frame containing the ladder position of all teams. If round and #' game are not supplied, the ladder position is calculated using all match @@ -53,8 +56,8 @@ ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { round_num = round_num, game_num = game_num ) - ladder <- match_results %>% - dplyr::group_by(squadName) %>% + ladder <- match_results |> + dplyr::group_by(squadName) |> dplyr::summarise( games = dplyr::n(), goals_for = sum(goals), @@ -69,25 +72,21 @@ ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { #' @rdname ladders #' @export matchResults <- function(df) { - df <- df %>% - dplyr::group_by(round, game) %>% - tidyr::nest() %>% - dplyr::group_by(round, game) %>% - dplyr::mutate(game_results = purrr::map(data, matchPoints)) %>% - dplyr::select(-data) %>% + df |> + dplyr::group_by(round, game) |> + tidyr::nest() |> + dplyr::mutate(game_results = purrr::map(data, matchPoints)) |> + dplyr::select(-data) |> tidyr::unnest(cols = c(game_results)) - df } matchResults_pre_2020 <- function(df) { - df <- df %>% - dplyr::group_by(round, game) %>% - tidyr::nest() %>% - dplyr::group_by(round, game) %>% - dplyr::mutate(game_results = purrr::map(data, matchPoints_pre_2020)) %>% - dplyr::select(-data) %>% + df |> + dplyr::group_by(round, game) |> + tidyr::nest() |> + dplyr::mutate(game_results = purrr::map(data, matchPoints_pre_2020)) |> + dplyr::select(-data) |> tidyr::unnest(cols = c(game_results)) - df } #' @rdname ladders @@ -98,8 +97,8 @@ ladders_pre_2020 <- function(df, round_num = NULL, game_num = NULL, old_system = round_num = round_num, game_num = game_num ) - ladder <- match_results %>% - dplyr::group_by(squadName) %>% + ladder <- match_results |> + dplyr::group_by(squadName) |> dplyr::summarise( games = dplyr::n(), goals_for = sum(goals), diff --git a/R/matchPoints.R b/R/matchPoints.R index 5793204..1b4c416 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -10,47 +10,54 @@ #' \code{goal_from_zone2} as two points, matching the current super shot era. #' @export matchPoints <- function(df) { - ## This first section calculates points based on the old system. - home <- df %>% - dplyr::filter(stat == "homeTeam") %>% - dplyr::select(-period) %>% + home <- df |> + dplyr::filter(stat == "homeTeam") |> + dplyr::select(-period) |> dplyr::distinct() - goals1 <- df %>% - dplyr::filter(stat == "goal_from_zone1") %>% - dplyr::group_by(squadName) %>% + goals1 <- df |> + dplyr::filter(stat == "goal_from_zone1") |> + dplyr::group_by(squadName) |> dplyr::summarise(goals = sum(value, na.rm = TRUE), .groups = "drop") - goals2 <- df %>% - dplyr::filter(stat == "goal_from_zone2") %>% - dplyr::group_by(squadName) %>% + goals2 <- df |> + dplyr::filter(stat == "goal_from_zone2") |> + dplyr::group_by(squadName) |> dplyr::summarise(goals2 = sum(value, na.rm = TRUE) * 2, .groups = "drop") - goals <- home %>% - dplyr::left_join(goals1, by = "squadName") %>% - dplyr::left_join(goals2, by = "squadName") %>% + goals <- home |> + dplyr::left_join(goals1, by = "squadName") |> + dplyr::left_join(goals2, by = "squadName") |> dplyr::mutate( goals = dplyr::coalesce(goals, 0), goals2 = dplyr::coalesce(goals2, 0), goals = goals + goals2 - ) %>% + ) |> dplyr::select(-goals2) - goals <- goals %>% + + if (all(goals$goals == 0) && + !any(df$stat %in% c("goal_from_zone1", "goal_from_zone2"))) { + warning( + "All goals are zero and neither 'goal_from_zone1' nor 'goal_from_zone2' ", + "appear in the data. Did you mean to use matchPoints_pre_2020() for ", + "pre-2020 or ANZ Championship data?", + call. = FALSE + ) + } + + goals <- goals |> dplyr::arrange(value) if (nrow(goals) != 2) { stop("Match data must include exactly two squads.", call. = FALSE) } - goals <- goals %>% + goals |> dplyr::mutate( score_diff = goals - rev(goals), points = dplyr::case_when( score_diff > 0 ~ 4, score_diff < 0 ~ 0, - TRUE ~ 2 + .default = 2 ) - ) %>% - dplyr::rename(isHome = value) %>% + ) |> + dplyr::rename(isHome = value) |> dplyr::select(-stat) - - ## Return - goals } #' Calculates the total goals of the match (pre 2020 season) @@ -67,76 +74,67 @@ matchPoints <- function(df) { #' \code{points_new}. #' @export matchPoints_pre_2020 <- function(df) { - ## This first section calculates points based on the old system. - goals <- df %>% - dplyr::filter(stat == "goals") %>% - dplyr::group_by(squadName) %>% + goals <- df |> + dplyr::filter(stat == "goals") |> + dplyr::group_by(squadName) |> dplyr::summarise(goals = sum(value, na.rm = TRUE), .groups = "drop") - home <- df %>% - dplyr::filter(stat == "homeTeam") %>% - dplyr::group_by(squadName) %>% - dplyr::select(-period) %>% + home <- df |> + dplyr::filter(stat == "homeTeam") |> + dplyr::select(-period) |> dplyr::distinct() - goals <- dplyr::left_join(goals, home, by = "squadName") %>% + goals <- dplyr::left_join(goals, home, by = "squadName") |> dplyr::arrange(value) if (nrow(goals) != 2) { stop("Match data must include exactly two squads.", call. = FALSE) } - goals <- goals %>% + goals <- goals |> dplyr::mutate( - score_diff = goals - rev(goals), - points = dplyr::case_when( - score_diff > 0 ~ 2, - score_diff < 0 ~ 0, - TRUE ~ 1 - ), - ## Points for a win (new rules) - points_new = dplyr::case_when( - score_diff > 0 ~ 4, - score_diff < 0 ~ 0, - TRUE ~ 2 - ) - ) %>% - dplyr::rename(isHome = value) %>% + score_diff = goals - rev(goals), + points = dplyr::case_when( + score_diff > 0 ~ 2, + score_diff < 0 ~ 0, + .default = 1 + ), + points_new = dplyr::case_when( + score_diff > 0 ~ 4, + score_diff < 0 ~ 0, + .default = 2 + ) + ) |> + dplyr::rename(isHome = value) |> dplyr::select(-stat) - ## This section calculates points based on the new system (points for - ## winning quarters) - goals_new <- df %>% + ## Quarter-points bonus (new system) + goals_new <- df |> dplyr::filter(stat == "goals") - homeScores <- goals_new %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% + homeScores <- goals_new |> + dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) |> dplyr::select(period, homeSquad = squadName, homeValue = value) - awayScores <- goals_new %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) %>% + awayScores <- goals_new |> + dplyr::filter(squadName == home[['squadName']][home[['value']] == 0]) |> dplyr::select(period, awaySquad = squadName, awayValue = value) - scores <- dplyr::left_join(homeScores, awayScores, by = "period") %>% - dplyr::mutate(qtr_diff = homeValue - awayValue, - homePoints = dplyr::case_when(qtr_diff > 0 ~ 1, - TRUE ~ 0), - awayPoints = dplyr::case_when(qtr_diff < 0 ~ 1, - TRUE ~ 0) - ) - points_new <- scores %>% - dplyr::group_by(homeSquad, awaySquad) %>% + scores <- dplyr::left_join(homeScores, awayScores, by = "period") |> + dplyr::mutate( + qtr_diff = homeValue - awayValue, + homePoints = dplyr::case_when(qtr_diff > 0 ~ 1, .default = 0), + awayPoints = dplyr::case_when(qtr_diff < 0 ~ 1, .default = 0) + ) + points_new <- scores |> + dplyr::group_by(homeSquad, awaySquad) |> dplyr::summarise( homePoints = sum(homePoints, na.rm = TRUE), awayPoints = sum(awayPoints, na.rm = TRUE), .groups = "drop" ) - df1 <- points_new %>% - dplyr::select(dplyr::contains("home")) %>% + df1 <- points_new |> + dplyr::select(dplyr::contains("home")) |> dplyr::rename(squadName = homeSquad, points_qtr = homePoints) - df2 <- points_new %>% - dplyr::select(dplyr::contains("away")) %>% + df2 <- points_new |> + dplyr::select(dplyr::contains("away")) |> dplyr::rename(squadName = awaySquad, points_qtr = awayPoints) points_new <- dplyr::bind_rows(df1, df2) - ## Now join back on to original scoring. - goals <- dplyr::left_join(goals, points_new, by = "squadName") %>% - dplyr::mutate(points_new = points_new + points_qtr) %>% + dplyr::left_join(goals, points_new, by = "squadName") |> + dplyr::mutate(points_new = points_new + points_qtr) |> dplyr::select(-points_qtr) - - ## Return - goals } diff --git a/R/superNetballR.R b/R/superNetballR.R index 06a533c..441dcd2 100644 --- a/R/superNetballR.R +++ b/R/superNetballR.R @@ -4,13 +4,23 @@ #' player statistics into tidy data frames for analysis. #' #' @keywords internal -#' @importFrom magrittr %>% "_PACKAGE" -## quiets concerns of R CMD check re: the .'s that appear in pipelines +## Suppress R CMD check notes for variables used in dplyr/tidyr pipelines. if (getRversion() >= "2.15.1") { - utils::globalVariables(c(".", "points_new", "homeValue", "homeSquad", - "homePoints", "awayValue", "awaySquad", - "awayPoints", "points_qtr", "game_results", - "squadId.x", "squadId.y")) + utils::globalVariables(c( + ## match/player column names + "squadId", "homeTeam", "period", "stat", "value", "squadName", + "squadNickname", "squadCode", "round", "game", "displayName", + "playerId", "shortDisplayName", "firstname", "surname", + ## scoring / ladder names + "goals", "goals2", "score_diff", "points", "points_new", + "goals_for", "goals_against", "percentage", "isHome", + "games", "qtr_diff", "data", + ## period-score helper names + "homeValue", "homeSquad", "homePoints", + "awayValue", "awaySquad", "awayPoints", + "points_qtr", "game_results", + "squadId.x", "squadId.y" + )) } diff --git a/R/tidiers.R b/R/tidiers.R index 6595228..7b51939 100644 --- a/R/tidiers.R +++ b/R/tidiers.R @@ -24,13 +24,13 @@ tidyMatch <- function(match) { team_stats <- dplyr::left_join(team_stats, home_team, by = "squadId") ## Check if there was overtime (matchInfo) final_period <- match$matchInfo$periodCompleted - team_stats <- team_stats %>% - dplyr::filter(period <= final_period) %>% + team_stats <- team_stats |> + dplyr::filter(period <= final_period) |> tidyr::pivot_longer( cols = -c(squadId, squadName, squadNickname, squadCode, period), names_to = "stat", values_to = "value" - ) %>% + ) |> dplyr::mutate( round = match$matchInfo$roundNumber, game = match$matchInfo$matchNumber @@ -57,8 +57,8 @@ tidyPlayers <- function(match) { player_info <- dplyr::bind_rows(player_info) player_stats <- dplyr::left_join(player_stats, player_info, by = "playerId") if (all(c("squadId.x", "squadId.y") %in% names(player_stats))) { - player_stats <- player_stats %>% - dplyr::mutate(squadId = dplyr::coalesce(squadId.x, squadId.y)) %>% + player_stats <- player_stats |> + dplyr::mutate(squadId = dplyr::coalesce(squadId.x, squadId.y)) |> dplyr::select(-squadId.x, -squadId.y) } squad_info <- match$teamInfo$team @@ -69,9 +69,9 @@ tidyPlayers <- function(match) { ) ## Check if there was overtime (matchInfo) final_period <- match$matchInfo$periodCompleted - player_stats <- player_stats %>% - dplyr::filter(period <= final_period) %>% - dplyr::select(-displayName) %>% + player_stats <- player_stats |> + dplyr::filter(period <= final_period) |> + dplyr::select(-displayName) |> tidyr::pivot_longer( cols = -c( playerId, shortDisplayName, firstname, surname, @@ -80,7 +80,7 @@ tidyPlayers <- function(match) { names_to = "stat", values_to = "value", values_transform = list(value = as.character) - ) %>% + ) |> dplyr::mutate( round = match$matchInfo$roundNumber, game = match$matchInfo$matchNumber diff --git a/R/zzz.R b/R/zzz.R index 97d0e88..74c925b 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,18 +1 @@ -.onLoad <- function(libname = find.package("superNetballR"), - pkgname = "superNetballR"){ - - ## quiets concerns of R CMD check re the variables in data frames - if(getRversion() >= "2.15.1") utils::globalVariables( - c("squadId", "homeTeam", "period", "stat", "value", "squadName", - "squadNickname", "squadCode", "round", "game", "displayName", - "playerId", "shortDisplayName", "firstname", "surname", "goals", - "score_diff", "points", "goals_for", "goals_against", "percentage", - "n", "data") - ) - - invisible() -} - -.unUnload <- function(libpath) { - library.dynam.unload("superNetballR", libpath) -} +## Nothing required here — see superNetballR.R for globalVariables declarations. diff --git a/changelog.md b/changelog.md index b15d8dd..60d48c8 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,30 @@ All notable changes in this fork are documented here. This changelog covers the changes introduced in `craigmoyle/superNetballR_updated` since the fork diverged from the original `SteveLane/superNetballR` project published at . +## 0.3.1 - 2026-04-07 + +Code quality and correctness improvements from a full package review. + +### Fixed + +- `matchPoints()` now warns when called on data that contains neither `goal_from_zone1` nor `goal_from_zone2`, guiding users to `matchPoints_pre_2020()` for ANZ Championship or pre-2020 data instead of silently returning a spurious 0-0 result. +- `ladders_pre_2020()` documentation for `old_system` parameter now correctly describes its effect (selects the 2-point vs 4-point sort column) rather than incorrectly stating it is ignored. +- `season_2017` dataset documentation corrected: `value` column is integer, not character. +- Removed dead `.unUnload` hook in `zzz.R` (typo for `.onUnload`; would have errored if called since the package has no compiled code). +- Removed spurious `group_by(squadName)` in `matchPoints_pre_2020()` that left grouped state on the `home` data frame. +- Removed redundant second `group_by(round, game)` after `nest()` in `matchResults()` and `matchResults_pre_2020()`. + +### Changed + +- Replaced `magrittr` pipe (`%>%`) with the native R pipe (`|>`) throughout. Minimum R version bumped from 4.0.0 to 4.1.0. +- `magrittr` removed from `Imports`; `dplyr (>= 1.1.0)` version constraint added. +- `case_when(TRUE ~ ...)` fallthrough sentinels updated to the modern `.default =` idiom in `matchPoints.R`. +- `globalVariables()` declarations consolidated from `zzz.R` + `superNetballR.R` into a single call; missing names (`goals2`, `isHome`, `games`, `qtr_diff`) added. +- Added Craig Moyle as package author/maintainer in `DESCRIPTION`. +- `%||%` null-coalescing operator annotated with `@noRd`. +- CI: removed the `roxygenise()` step from the GitHub Actions workflow — committed `.Rd` files are used directly by `R CMD check`. +- Dependabot configured for weekly GitHub Actions version updates. + ## 0.3.0 - 2025-07-30 ANZ Championship and NZ National Netball League support. diff --git a/man/anzc_comp_ids.Rd b/man/anzc_comp_ids.Rd index 20972fe..4c5722a 100644 --- a/man/anzc_comp_ids.Rd +++ b/man/anzc_comp_ids.Rd @@ -25,7 +25,7 @@ Competition IDs identified by probing the Champion Data feed at inspecting team names and match dates in the returned fixture data. } \usage{ -anzc_comp_ids +data(anzc_comp_ids) } \description{ A dataset mapping Champion Data \code{comp_id} values to the corresponding @@ -44,6 +44,7 @@ computing standings for any ANZ Championship or NZ National Netball League season. } \examples{ +data(anzc_comp_ids) anzc_comp_ids # Find the regular-season comp_id for 2019 diff --git a/man/ladders.Rd b/man/ladders.Rd index 8701085..59a6e6d 100644 --- a/man/ladders.Rd +++ b/man/ladders.Rd @@ -19,8 +19,11 @@ ladders_pre_2020(df, round_num = NULL, game_num = NULL, old_system = FALSE) \item{game_num}{Game at which to calculate ladder positions. Optional.} -\item{old_system}{Logical. Retained for compatibility and ignored for -2020+ ladders.} +\item{old_system}{Logical. For \code{ladders()}, retained for compatibility +and ignored (2020+ scoring always applies). For +\code{ladders_pre_2020()}, if \code{TRUE} sorts the ladder by the +legacy 2-point win system (\code{points}); if \code{FALSE} (default) +sorts by the updated 4-point win system (\code{points_new}).} } \value{ Data frame containing the ladder position of all teams. If round and diff --git a/man/season_2017.Rd b/man/season_2017.Rd index c1c1a11..333b4ab 100644 --- a/man/season_2017.Rd +++ b/man/season_2017.Rd @@ -12,7 +12,7 @@ A data frame with 15360 rows and 9 variables: \item{squadNickname}{Squad nickname} \item{squadCode}{Short code for squad} \item{stat}{Statistic measured during the match} - \item{value}{Character representation of the statistic value} + \item{value}{Integer statistic value.} \item{period}{Which period the statistic is measured in} \item{round}{Round number of the match} \item{game}{Game number of the match} diff --git a/man/superNetballR-package.Rd b/man/superNetballR-package.Rd index e2e0b6e..5ad45f9 100644 --- a/man/superNetballR-package.Rd +++ b/man/superNetballR-package.Rd @@ -18,7 +18,12 @@ Useful links: } \author{ -\strong{Maintainer}: Steve Lane \email{lane.s@unimelb.edu.au} +\strong{Maintainer}: Craig Moyle \email{craig.moyle@mantelgroup.com.au} + +Authors: +\itemize{ + \item Steve Lane \email{lane.s@unimelb.edu.au} +} } \keyword{internal} From a1503e5340172cb19aee26060ec7db0c6ed27423 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 7 Apr 2026 08:58:58 +1000 Subject: [PATCH 32/56] fix: move dependabot.yml to correct location (.github/dependabot.yml) Dependabot config must live at .github/dependabot.yml, not inside .github/workflows/. The misplaced file was being picked up as a GitHub Actions workflow and failing on every push. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/{workflows => }/dependabot.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => }/dependabot.yml (100%) diff --git a/.github/workflows/dependabot.yml b/.github/dependabot.yml similarity index 100% rename from .github/workflows/dependabot.yml rename to .github/dependabot.yml From 7577f59fc508bc4d45a2e45359beada3e88b35e5 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Tue, 7 Apr 2026 09:02:21 +1000 Subject: [PATCH 33/56] fix: commit anzc_comp_ids.rda; remove *.rda from .gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dataset was never committed because *.rda was listed in .gitignore. R package data files in data/ must be tracked in git — removing the rule so all current and future .rda datasets are committed by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 2 +- data/anzc_comp_ids.rda | Bin 0 -> 486 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 data/anzc_comp_ids.rda diff --git a/.gitignore b/.gitignore index 44eb68c..33b5860 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ *.html *.RData *.rds -*.rda +# *.rda excluded: R package data files in data/ must be committed *.tex *.fdb_latexmk *.fls diff --git a/data/anzc_comp_ids.rda b/data/anzc_comp_ids.rda new file mode 100644 index 0000000000000000000000000000000000000000..ffdc633863ece1cb4624970bd310e2155a7f4090 GIT binary patch literal 486 zcmV6T4*^jL0KkKS)%!SMF0X*f7AcENEplSXh1&)o?gG_-nKx`1^@tm|KI=t zzyZ9V0Th4$05mkv007Vdra%MI000^?4geYhK+piwOn?JMnE)CxV1UE`0l))5Xc_<- zX^;SD(;x#zOb{4=01A?X)iV@iX!NGhsKRK|CYVMv2*@%T4XKIxhBY*1RA8%2Sn*=w z+kK>u%WoaFyZP;qVQDVW{b7?zrQ4SM7eY2Ny^m|$+-bL4%+Gs$@O*e2EbSc1){;iI zcWc3J;bz{+luN)4@#IQ`mu}<{5yi~zf5xpopo@SQ3`hag&^%xO(!t{67ku^-{M+j; z_`%=`#$lpE3=vTzQD3D*k!`jlklprK5?xSwXq@CYuv8TVk{?wWxNJ1?`aGys+8t{f z)oc})?nBcB$Su3~tl2#ovi))lws9=3p1#YU)#pKE8}L*eDt3_D(n9rg_|R#{W8KlM z?f7nAe%8JVGw|}P55^A5A{ZL5U&r2qA5sod8MqL{g_@4s8t7s}VGB9C5MqMOWHxPx zXOx8zk_}#Es9l0;UsWK Date: Sun, 17 May 2026 18:42:54 +1000 Subject: [PATCH 34/56] docs: add code review fixes design spec --- .../2026-05-17-code-review-fixes-design.md | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-17-code-review-fixes-design.md diff --git a/docs/superpowers/specs/2026-05-17-code-review-fixes-design.md b/docs/superpowers/specs/2026-05-17-code-review-fixes-design.md new file mode 100644 index 0000000..4f61ef4 --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-code-review-fixes-design.md @@ -0,0 +1,162 @@ +# Code Review Fixes Design + +## Summary + +Address the three issues identified in code review: + +1. `matchPoints_pre_2020()` incorrectly awards quarter bonus points for overtime periods. +2. Live regular-season and finals matches can collide because tidy outputs only carry `round` and `game`. +3. The README includes an incorrect standings example for pre-2020 / ANZ-style workflows. + +The chosen approach is to fix the scoring bug, add `matchId` to tidy outputs, make downstream grouping prefer `matchId` when available, and update tests and documentation accordingly. + +## Goals + +- Correct `points_new` calculations for pre-2020 / ANZ / NZ overtime matches. +- Prevent collisions when users combine tidy live data from multiple competitions that reuse round/game numbering. +- Preserve compatibility with existing bundled tidy data that does not include `matchId`. +- Keep tidy output column order as stable as possible by appending `matchId`. +- Correct user-facing examples so they describe a valid workflow. + +## Non-goals + +- No new exported functions. +- No change to `downloadMatch()` arguments or return shape. +- No regeneration of bundled datasets such as `season_2017`. +- No broader refactor of ladder or tidier APIs beyond the minimum needed for correctness. + +## Design Decisions + +### 1. Restrict legacy quarter bonuses to regulation periods + +`matchPoints_pre_2020()` currently builds quarter bonus points from all rows where `stat == "goals"`. For matches with overtime (`periodCompleted > 4`), this incorrectly awards extra quarter points for periods 5 and 6. + +The fix is to calculate quarter bonuses from regulation periods only: + +- keep final match score and win/draw/loss logic based on all periods +- restrict quarter bonus logic to `period <= 4` + +This preserves intended full-match scoring while aligning the bonus system with regulation quarters only. + +### 2. Add `matchId` to tidy outputs + +`tidyMatch()` and `tidyPlayers()` will append a new `matchId` column sourced from `match$matchInfo$matchId`. + +Column ordering policy: + +- keep existing columns in their current order +- append `matchId` after `game` + +Resulting tails: + +- `tidyMatch()`: `..., round, game, matchId` +- `tidyPlayers()`: `..., round, game, matchId` + +This adds a stable unique identifier without reshuffling the current output structure. + +### 3. Prefer `matchId` in downstream grouping, with fallback + +`matchResults()` and `matchResults_pre_2020()` currently group only by `round` and `game`. That is unsafe when different competitions reuse the same numbering. + +New grouping behavior: + +- if `matchId` exists in `df`, group by `matchId` +- otherwise, keep the legacy grouping by `round` and `game` + +This keeps existing bundled data working unchanged while making live tidy datasets safe to combine. + +### 4. Correct docs and examples + +The README example currently shows an invalid flow: + +```r +standings <- ladders_pre_2020(matchPoints_pre_2020(match)) +``` + +That example passes the wrong data shape into `ladders_pre_2020()`. + +Documentation updates will: + +- replace the incorrect example with a valid workflow based on `tidyMatch()` +- document that `matchId` is included in tidy outputs +- clarify that `ladders_pre_2020()` expects season-style tidy match statistics, not a raw downloaded match object or a `matchPoints_pre_2020()` summary + +## File Impact + +### Production code + +- `R/matchPoints.R` + - restrict legacy quarter bonus calculation to regulation periods +- `R/tidiers.R` + - append `matchId` to `tidyMatch()` output + - append `matchId` to `tidyPlayers()` output +- `R/ladders.R` + - make `matchResults()` and `matchResults_pre_2020()` group by `matchId` when present +- `R/data.R` + - update dataset documentation for tidy output schema if needed +- `R/superNetballR.R` + - add `matchId` to `utils::globalVariables()` if required by check output + +### Tests + +- `tests/testthat/test-match-points.R` + - add regression coverage proving overtime periods do not contribute quarter bonus points +- `tests/testthat/test-tidiers.R` + - assert `matchId` is appended by both tidiers +- `tests/testthat/test-ladders.R` + - add coverage showing `matchResults()` prefers `matchId` + - add coverage showing fallback to `round`/`game` still works when `matchId` is absent +- `tests/testthat/helper-fixtures.R` + - include `matchId` in sample match fixtures and add any match-result fixtures needed for grouping tests + +### Docs + +- `README.md` + - replace the incorrect standings example + - note that tidy outputs now include `matchId` +- `R/downloadMatch.R` + - update roxygen where examples or details refer to downstream workflow +- `vignettes/getting-started.Rmd` + - update narrative or examples if they describe the old ambiguous workflow +- generated docs under `man/` only as needed after roxygen + +## Compatibility and Migration + +### Backward compatibility + +- Existing consumers of `tidyMatch()` / `tidyPlayers()` gain one appended column only. +- Existing code that selects columns by name continues to work. +- Existing code that assumes an exact column count may need to be updated. +- Existing bundled datasets without `matchId` remain supported because grouping falls back to `round` and `game`. + +### Why `matchId` instead of `comp_id` + +`matchId` uniquely identifies a match across the Champion Data feed and is already present in `matchInfo`, so it solves the collision directly without expanding the public API more than necessary. + +## Test Strategy + +Follow TDD for each behavior change. + +Required regression coverage: + +1. overtime periods 5+ do not increase `points_new` in `matchPoints_pre_2020()` +2. `tidyMatch()` appends `matchId` +3. `tidyPlayers()` appends `matchId` +4. `matchResults()` uses `matchId` to keep same round/game values from different matches separate +5. `matchResults_pre_2020()` uses the same `matchId`-aware grouping behavior +6. legacy data without `matchId` still works with `ladders()` / `ladders_pre_2020()` + +## Acceptance Criteria + +- `matchPoints_pre_2020()` returns regulation-quarter bonus points only. +- `tidyMatch()` and `tidyPlayers()` return appended `matchId` columns. +- `matchResults()` and `matchResults_pre_2020()` no longer merge distinct matches that share the same round/game values when `matchId` is present. +- Existing season-style bundled data without `matchId` still works. +- README and vignette examples describe a valid pre-2020 workflow. +- Test suite covers the new behavior and regressions. + +## Risks + +- Some downstream code may assert exact output column counts. Appending `matchId` is still the least disruptive way to expose unique match identity. +- Roxygen / pkgdown outputs may need regeneration after doc changes. +- Local execution may still be limited by missing R package dependencies in this environment, so verification should use the package test/check workflow where available. From 746a9902076ec3aab552508f331f3919f253a2e8 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 18:51:21 +1000 Subject: [PATCH 35/56] fix: exclude overtime from legacy quarter bonus points --- R/matchPoints.R | 2 +- tests/testthat/test-match-points.R | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/R/matchPoints.R b/R/matchPoints.R index 1b4c416..209a734 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -106,7 +106,7 @@ matchPoints_pre_2020 <- function(df) { ## Quarter-points bonus (new system) goals_new <- df |> - dplyr::filter(stat == "goals") + dplyr::filter(stat == "goals", period <= 4) homeScores <- goals_new |> dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) |> dplyr::select(period, homeSquad = squadName, homeValue = value) diff --git a/tests/testthat/test-match-points.R b/tests/testthat/test-match-points.R index 62daedc..1471399 100644 --- a/tests/testthat/test-match-points.R +++ b/tests/testthat/test-match-points.R @@ -72,3 +72,21 @@ test_that("matchPoints_pre_2020 keeps old and new scoring totals", { expect_equal(result$points_new[result$squadName == "Home"], 5) expect_equal(result$points_new[result$squadName == "Away"], 1) }) + +test_that("matchPoints_pre_2020 ignores overtime periods for quarter bonus points", { + df <- make_pre_2020_match_stats( + round = 1L, + game = 1L, + home_team = "Home", + away_team = "Away", + home_goals = c(10L, 8L, 7L, 9L, 2L, 4L), + away_goals = c(8L, 9L, 11L, 6L, 3L, 2L) + ) + + result <- matchPoints_pre_2020(df) + + expect_equal(result$points[result$squadName == "Home"], 2) + expect_equal(result$points[result$squadName == "Away"], 0) + expect_equal(result$points_new[result$squadName == "Home"], 6) + expect_equal(result$points_new[result$squadName == "Away"], 2) +}) From 9096b7b82de2862b08c0020ebbedac3ff832c483 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 18:51:56 +1000 Subject: [PATCH 36/56] feat: append matchId to tidy outputs --- R/tidiers.R | 6 ++++-- tests/testthat/helper-fixtures.R | 3 ++- tests/testthat/test-tidiers.R | 6 ++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/R/tidiers.R b/R/tidiers.R index 7b51939..8365a7a 100644 --- a/R/tidiers.R +++ b/R/tidiers.R @@ -33,7 +33,8 @@ tidyMatch <- function(match) { ) |> dplyr::mutate( round = match$matchInfo$roundNumber, - game = match$matchInfo$matchNumber + game = match$matchInfo$matchNumber, + matchId = match$matchInfo$matchId ) team_stats } @@ -83,7 +84,8 @@ tidyPlayers <- function(match) { ) |> dplyr::mutate( round = match$matchInfo$roundNumber, - game = match$matchInfo$matchNumber + game = match$matchInfo$matchNumber, + matchId = match$matchInfo$matchId ) player_stats } diff --git a/tests/testthat/helper-fixtures.R b/tests/testthat/helper-fixtures.R index f4b9462..8279fcb 100644 --- a/tests/testthat/helper-fixtures.R +++ b/tests/testthat/helper-fixtures.R @@ -5,7 +5,8 @@ make_sample_match <- function(period_completed = 2) { awaySquadId = 20L, periodCompleted = period_completed, roundNumber = 5L, - matchNumber = 3L + matchNumber = 3L, + matchId = 500503L ), teamInfo = list( team = list( diff --git a/tests/testthat/test-tidiers.R b/tests/testthat/test-tidiers.R index 6b1030b..0f5d6a6 100644 --- a/tests/testthat/test-tidiers.R +++ b/tests/testthat/test-tidiers.R @@ -6,6 +6,9 @@ test_that("tidyMatch returns completed periods in long format", { expect_setequal(unique(result$stat), c("gains", "goalAttempts", "homeTeam")) expect_equal(unique(result$value[result$squadName == "Home" & result$stat == "homeTeam"]), 1) expect_equal(unique(result$value[result$squadName == "Away" & result$stat == "homeTeam"]), 0) + expect_true("matchId" %in% names(result)) + expect_equal(tail(names(result), 3), c("round", "game", "matchId")) + expect_equal(unique(result$matchId), 500503L) }) test_that("tidyPlayers keeps player identity columns and drops displayName", { @@ -28,4 +31,7 @@ test_that("tidyPlayers keeps player identity columns and drops displayName", { result$value[result$playerId == 1 & result$stat == "startingPositionCode" & result$period == 1], "GS" ) + expect_true("matchId" %in% names(result)) + expect_equal(tail(names(result), 3), c("round", "game", "matchId")) + expect_equal(unique(result$matchId), 500503L) }) From 51e2f5fe06921e18dae513c019f5638ab34344f6 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 18:52:57 +1000 Subject: [PATCH 37/56] fix: group match results by matchId when available --- R/ladders.R | 12 +++++++-- R/superNetballR.R | 2 +- tests/testthat/helper-fixtures.R | 46 ++++++++++++++++++++++++++++++++ tests/testthat/test-ladders.R | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 3 deletions(-) diff --git a/R/ladders.R b/R/ladders.R index 2cc31c6..d9a9930 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -69,11 +69,19 @@ ladders <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { sort_ladder(ladder, "points") } +group_match_data <- function(df) { + if ("matchId" %in% names(df)) { + return(dplyr::group_by(df, matchId, round, game)) + } + + dplyr::group_by(df, round, game) +} + #' @rdname ladders #' @export matchResults <- function(df) { df |> - dplyr::group_by(round, game) |> + group_match_data() |> tidyr::nest() |> dplyr::mutate(game_results = purrr::map(data, matchPoints)) |> dplyr::select(-data) |> @@ -82,7 +90,7 @@ matchResults <- function(df) { matchResults_pre_2020 <- function(df) { df |> - dplyr::group_by(round, game) |> + group_match_data() |> tidyr::nest() |> dplyr::mutate(game_results = purrr::map(data, matchPoints_pre_2020)) |> dplyr::select(-data) |> diff --git a/R/superNetballR.R b/R/superNetballR.R index 441dcd2..c2ffbb7 100644 --- a/R/superNetballR.R +++ b/R/superNetballR.R @@ -12,7 +12,7 @@ if (getRversion() >= "2.15.1") { ## match/player column names "squadId", "homeTeam", "period", "stat", "value", "squadName", "squadNickname", "squadCode", "round", "game", "displayName", - "playerId", "shortDisplayName", "firstname", "surname", + "matchId", "playerId", "shortDisplayName", "firstname", "surname", ## scoring / ladder names "goals", "goals2", "score_diff", "points", "points_new", "goals_for", "goals_against", "percentage", "isHome", diff --git a/tests/testthat/helper-fixtures.R b/tests/testthat/helper-fixtures.R index 8279fcb..f83e61a 100644 --- a/tests/testthat/helper-fixtures.R +++ b/tests/testthat/helper-fixtures.R @@ -173,3 +173,49 @@ make_pre_2020_match_stats <- function( ) ) } + +make_modern_match_stats_with_id <- function( + match_id, + round, + game, + home_team, + away_team, + home_zone1, + home_zone2 = 0, + away_zone1, + away_zone2 = 0 +) { + out <- make_modern_match_stats( + round = round, + game = game, + home_team = home_team, + away_team = away_team, + home_zone1 = home_zone1, + home_zone2 = home_zone2, + away_zone1 = away_zone1, + away_zone2 = away_zone2 + ) + out$matchId <- match_id + out +} + +make_pre_2020_match_stats_with_id <- function( + match_id, + round, + game, + home_team, + away_team, + home_goals, + away_goals +) { + out <- make_pre_2020_match_stats( + round = round, + game = game, + home_team = home_team, + away_team = away_team, + home_goals = home_goals, + away_goals = away_goals + ) + out$matchId <- match_id + out +} diff --git a/tests/testthat/test-ladders.R b/tests/testthat/test-ladders.R index 8008c51..274e255 100644 --- a/tests/testthat/test-ladders.R +++ b/tests/testthat/test-ladders.R @@ -77,3 +77,49 @@ test_that("ladders_pre_2020 breaks ties on percentage", { expect_equal(ladder$squadName[[1]], "A") expect_gt(ladder$percentage[[1]], ladder$percentage[[2]]) }) + +test_that("matchResults prefers matchId when distinct matches share round and game", { + season <- rbind( + make_modern_match_stats_with_id(1001L, 1L, 1L, "A", "B", 10L, 0L, 8L, 0L), + make_modern_match_stats_with_id(1002L, 1L, 1L, "C", "D", 9L, 0L, 11L, 0L) + ) + + match_results <- matchResults(season) + + expect_equal(nrow(match_results), 4) + expect_setequal(match_results$squadName, c("A", "B", "C", "D")) + expect_setequal(match_results$matchId, c(1001L, 1002L)) +}) + +test_that("matchResults_pre_2020 prefers matchId when distinct matches share round and game", { + season <- rbind( + make_pre_2020_match_stats_with_id( + 2001L, 1L, 1L, "A", "B", + home_goals = c(10L, 8L, 7L, 6L), + away_goals = c(8L, 7L, 6L, 5L) + ), + make_pre_2020_match_stats_with_id( + 2002L, 1L, 1L, "C", "D", + home_goals = c(6L, 7L, 8L, 9L), + away_goals = c(7L, 7L, 7L, 7L) + ) + ) + + match_results <- superNetballR:::matchResults_pre_2020(season) + + expect_equal(nrow(match_results), 4) + expect_setequal(match_results$squadName, c("A", "B", "C", "D")) + expect_setequal(match_results$matchId, c(2001L, 2002L)) +}) + +test_that("matchResults falls back to round and game when matchId is absent", { + season <- rbind( + make_modern_match_stats(1L, 1L, "A", "B", 10L, 0L, 8L, 0L), + make_modern_match_stats(2L, 1L, "C", "D", 7L, 0L, 9L, 0L) + ) + + match_results <- matchResults(season) + + expect_equal(nrow(match_results), 4) + expect_false("matchId" %in% names(match_results)) +}) From 5ab4339c430e9b6c1b7f8dc3538ccb697926c21c Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 18:53:23 +1000 Subject: [PATCH 38/56] docs: correct legacy standings workflow examples --- README.md | 12 ++++++++---- vignettes/getting-started.Rmd | 8 +++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d7ccde5..a02adfc 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ remotes::install_github("craigmoyle/superNetballR_updated@main") ## ANZ Championship / NZ National Netball League -The same Champion Data feed powers both competitions. Use `anzc_comp_ids` to look up the competition ID for any season, then call `downloadFixture()` to see available matches and `downloadMatch()` to fetch match data. Because ANZ Championship data uses a `goals` stat rather than the super-shot zones introduced in 2020, use `ladders_pre_2020()` (not `ladders()`) when computing standings. +The same Champion Data feed powers both competitions. Use `anzc_comp_ids` to look up the competition ID for any season, then call `downloadFixture()` to see available matches and `downloadMatch()` to fetch match data. `tidyMatch()` now appends the Champion Data `matchId`, which helps keep regular-season and finals matches distinct when you combine live tidy outputs. Because ANZ Championship data uses a `goals` stat rather than the super-shot zones introduced in 2020, use `ladders_pre_2020()` (not `ladders()`) on season-style tidy match statistics when computing standings. ``` r library(superNetballR) @@ -48,11 +48,15 @@ anzc_comp_ids # Get the fixture for the 2024 NZ National Netball League regular season fixture <- downloadFixture(12427) -# Download a specific match (round 1, game 1) +# Download and tidy a specific match (round 1, game 1) match <- downloadMatch(12427, 1, 1) +match_stats <- tidyMatch(match) -# Compute standings using the pre-super-shot scoring model -standings <- ladders_pre_2020(matchPoints_pre_2020(match)) +# Summarise the single-match result using the pre-super-shot scoring model +match_result <- matchPoints_pre_2020(match_stats) + +# For ladders_pre_2020(), supply season-style tidy match statistics +standings <- ladders_pre_2020(match_stats) ``` ## Development diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd index b4cd941..6f013ef 100644 --- a/vignettes/getting-started.Rmd +++ b/vignettes/getting-started.Rmd @@ -56,7 +56,7 @@ names(round5_game3) # Tidying Match and Player Statistics -The full match data can be tidied into match and player statistics, grouped by period. +The full match data can be tidied into match and player statistics, grouped by period. Live tidy outputs include the Champion Data `matchId`, which helps distinguish regular-season and finals matches that reuse round/game numbering. Tidying match statistics using the `tidyMatch` function: @@ -66,6 +66,12 @@ tidied_match ``` +```{r pre-2020-single-match, eval=FALSE} +# For a single ANZ / NZ match, summarise the result from tidy match stats +legacy_result <- matchPoints_pre_2020(tidied_match) +legacy_result +``` + Tidying player statistics using the `tidyPlayers` function: ```{r tidying-players} From abd4c2bceadf689bc8ab6839b48814793defa103 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 18:56:08 +1000 Subject: [PATCH 39/56] docs: document matchId-aware tidy outputs --- DESCRIPTION | 2 +- R/ladders.R | 4 ++++ R/tidiers.R | 8 ++++++-- man/ladders.Rd | 4 ++++ man/superNetballR-package.Rd | 1 + man/tidyMatch.Rd | 4 +++- man/tidyPlayers.Rd | 4 +++- 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 8a31316..edeec94 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -26,4 +26,4 @@ Imports: dplyr (>= 1.1.0), URL: https://github.com/craigmoyle/superNetballR_updated BugReports: https://github.com/craigmoyle/superNetballR_updated/issues Config/testthat/edition: 3 -RoxygenNote: 7.3.3 +Config/roxygen2/version: 8.0.0 diff --git a/R/ladders.R b/R/ladders.R index d9a9930..1a06257 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -43,6 +43,10 @@ sort_ladder <- function(ladder, points_col) { #' when a team has not conceded. Legacy ladders break ties on percentage after #' ordering by either \code{points_new} or \code{points}. #' +#' When a tidy input data frame includes \code{matchId}, the internal +#' match-result helpers use it as the grouping key; otherwise they fall back to +#' the legacy \code{round}/\code{game} grouping used by bundled datasets. +#' #' \strong{ANZ Championship}: ANZ Championship matches record scores in the #' \code{goals} statistic rather than the \code{goal_from_zone1} / #' \code{goal_from_zone2} statistics used by the 2020+ Super Netball super-shot diff --git a/R/tidiers.R b/R/tidiers.R index 8365a7a..be22a0e 100644 --- a/R/tidiers.R +++ b/R/tidiers.R @@ -4,7 +4,9 @@ #' in preparation for further analysis. #' #' @param match List of match details. -#' @return A tidy dataframe containing match statistics. +#' @return A tidy dataframe containing match statistics. Live tidy outputs +#' append the Champion Data \code{matchId}, which uniquely identifies the +#' source match. #' #' @export tidyMatch <- function(match) { @@ -45,7 +47,9 @@ tidyMatch <- function(match) { #' statistics in preparation for further analysis. #' #' @param match List of match details. -#' @return A tidy dataframe containing player statistics. +#' @return A tidy dataframe containing player statistics. Live tidy outputs +#' append the Champion Data \code{matchId}, which uniquely identifies the +#' source match. #' @details #' Player period stats include both numeric measures and position-code fields, #' so the long-form \code{value} column is stored as character data. diff --git a/man/ladders.Rd b/man/ladders.Rd index 59a6e6d..28db15b 100644 --- a/man/ladders.Rd +++ b/man/ladders.Rd @@ -40,6 +40,10 @@ percentages are protected against divide-by-zero by returning \code{Inf} when a team has not conceded. Legacy ladders break ties on percentage after ordering by either \code{points_new} or \code{points}. +When a tidy input data frame includes \code{matchId}, the internal +match-result helpers use it as the grouping key; otherwise they fall back to +the legacy \code{round}/\code{game} grouping used by bundled datasets. + \strong{ANZ Championship}: ANZ Championship matches record scores in the \code{goals} statistic rather than the \code{goal_from_zone1} / \code{goal_from_zone2} statistics used by the 2020+ Super Netball super-shot diff --git a/man/superNetballR-package.Rd b/man/superNetballR-package.Rd index 5ad45f9..0a04492 100644 --- a/man/superNetballR-package.Rd +++ b/man/superNetballR-package.Rd @@ -22,6 +22,7 @@ Useful links: Authors: \itemize{ + \item Craig Moyle \email{craig.moyle@mantelgroup.com.au} \item Steve Lane \email{lane.s@unimelb.edu.au} } diff --git a/man/tidyMatch.Rd b/man/tidyMatch.Rd index 827e418..7758c58 100644 --- a/man/tidyMatch.Rd +++ b/man/tidyMatch.Rd @@ -10,7 +10,9 @@ tidyMatch(match) \item{match}{List of match details.} } \value{ -A tidy dataframe containing match statistics. +A tidy dataframe containing match statistics. Live tidy outputs + append the Champion Data \code{matchId}, which uniquely identifies the + source match. } \description{ \code{tidyMatch} Takes the downloaded match list, and tidies match statistics diff --git a/man/tidyPlayers.Rd b/man/tidyPlayers.Rd index 1daaeca..dcdfc9e 100644 --- a/man/tidyPlayers.Rd +++ b/man/tidyPlayers.Rd @@ -10,7 +10,9 @@ tidyPlayers(match) \item{match}{List of match details.} } \value{ -A tidy dataframe containing player statistics. +A tidy dataframe containing player statistics. Live tidy outputs + append the Champion Data \code{matchId}, which uniquely identifies the + source match. } \description{ \code{tidyPlayers} Takes the downloaded match list, and tidies player From 4f797d80a72bcebb9d64fcc02ce8e7e877fc2601 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 21:49:51 +1000 Subject: [PATCH 40/56] docs: add netballR rename and netball_aus design spec --- ...r-rename-and-netball-aus-support-design.md | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-17-netballr-rename-and-netball-aus-support-design.md diff --git a/docs/superpowers/specs/2026-05-17-netballr-rename-and-netball-aus-support-design.md b/docs/superpowers/specs/2026-05-17-netballr-rename-and-netball-aus-support-design.md new file mode 100644 index 0000000..50f7f78 --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-netballr-rename-and-netball-aus-support-design.md @@ -0,0 +1,231 @@ +# netballR Rename and netball_aus Support Design + +## Summary + +Evolve the project from a Super Netball-focused fork into a broader `netballR` package that supports competition discovery from the `netball_aus` iStats application while retaining the existing Champion Data `/data//...` match and fixture transport. + +This is a clean-break rename: + +- repository becomes `netballR` +- package becomes `netballR` +- docs and positioning shift from `superNetballR` to broader netball coverage +- no backward-compatibility layer is required for the old package name or branding + +## Goals + +- Rename the repository, package, docs, and metadata from `superNetballR` / `superNetballR_updated` to `netballR`. +- Reposition the package as a general netball statistics package rather than a Super Netball-only package. +- Add support for discovering competitions from `https://mc.championdata.com/netball_aus/settings/application_settings.json`. +- Continue to download fixtures and match feeds using the existing Champion Data `/data//...` endpoints. +- Support all competitions listed in the `netball_aus` application settings catalogue. +- Preserve the current `downloadMatch()` / `downloadFixture()` usage pattern based on `comp_id`. + +## Non-goals + +- No compatibility alias package named `superNetballR`. +- No major redesign of `downloadMatch()` / `downloadFixture()` signatures. +- No replacement of the underlying `/data//...` transport format. +- No attempt to freeze the full `netball_aus` live competition catalogue into a static packaged dataset unless later requested. + +## Key Findings + +### 1. `netball_aus` is a discovery source, not a new match transport + +Inspection of the public iStats application scripts showed that `https://mc.championdata.com/netball_aus/` still loads data from the same transport currently used by the package: + +- fixture: `/data//fixture.json` +- match: `/data//.json` + +The `netball_aus` application adds a broader competition catalogue through: + +- `https://mc.championdata.com/netball_aus/settings/application_settings.json` + +Therefore, the package should treat `netball_aus` as a catalogue/discovery layer, while keeping current fixture and match downloads on `/data/...`. + +### 2. The package rename is broader than code only + +A clean rename affects multiple layers: + +- `DESCRIPTION` package name +- test bootstrap (`tests/testthat.R`) +- package docs and roxygen titles +- shiny example paths that currently reference `superNetballR` +- README installation instructions and badges +- pkgdown/site metadata +- repository URLs and bug-report links +- text in vignettes, changelog, and package descriptions + +## Design Decisions + +### 1. Keep the transport model simple and stable + +Current transport helpers remain the canonical way to fetch match and fixture JSON: + +- `downloadFixture(comp_id)` +- `downloadMatch(comp_id, round_id, game_id)` + +These functions should continue building URLs under `https://mc.championdata.com/data/...` because that is the transport still used by the `netball_aus` application. + +This avoids overengineering and keeps the core API stable. + +### 2. Add explicit catalogue/discovery helpers for `netball_aus` + +Introduce a discovery layer for live competitions exposed by the `netball_aus` application settings. + +Proposed helper responsibilities: + +- fetch raw application settings JSON from `netball_aus/settings/application_settings.json` +- extract/tidy the competition list into a tibble +- expose a user-facing helper to list available `netball_aus` competitions + +At minimum, the tidy output should include: + +- `comp_id` +- `competition_name` +- `application_source` + +Where available from the settings payload, also include: + +- `season` +- `competition_type` +- `squad_id` +- `application_logo` +- any other low-risk metadata that is already present and useful for filtering + +`application_source` should explicitly identify `netball_aus` so future discovery sources can coexist cleanly. + +### 3. Preserve existing historical helpers where still useful + +Existing historical ANZ/NZ helpers and datasets remain useful and should not be removed merely because the package is being broadened. + +In practice: + +- keep `anzc_comp_ids` +- keep legacy ladder/match-point helpers +- reframe docs so these are documented as supported historical/netball-specific workflows, not the entire purpose of the package + +### 4. Rename package/product branding to `netballR` + +The rename should be comprehensive and intentional. + +Expected updates include: + +- package name: `netballR` +- package title/description: broader netball wording +- repository URLs: `craigmoyle/netballR` (assuming repository rename occurs) +- badges, install instructions, and bug-report links +- vignette/package titles and narrative wording +- shiny app packaging paths and error messages + +The clean-break decision means we do not preserve the old package name in exported package metadata or installation instructions. + +### 5. Keep current function names unless they are overly branded + +Functions such as `downloadMatch()`, `downloadFixture()`, `tidyMatch()`, and `tidyPlayers()` are already generic and should stay. + +Brand-heavy or package-name-bound references should be renamed only where needed, for example: + +- package title and package-level docs +- `shinySuperNetballR()` should be reviewed because its name is explicitly branded around the old package identity + +The design choice for branded helpers is: + +- if a helper name is package-branded but still worth keeping, rename it to a neutral equivalent +- if it is only a demo convenience wrapper, either rename it or consider de-emphasizing it in docs + +## Proposed API Additions + +The exact function names can be finalized in implementation planning, but the discovery layer should likely expose one or both of these user-facing helpers: + +1. a raw settings fetcher (internal or exported) +2. a tidy competition listing helper for `netball_aus` + +Candidate design: + +- internal: fetch `application_settings.json` +- exported: return a tibble of competitions ready for filtering and use with `downloadFixture()` / `downloadMatch()` + +The user workflow should look like: + +1. list competitions from `netball_aus` +2. choose a `comp_id` +3. call `downloadFixture(comp_id)` +4. call `downloadMatch(comp_id, round_id, game_id)` + +## File Impact + +### Core package metadata and branding + +- `DESCRIPTION` +- `NAMESPACE` +- `README.md` +- `_pkgdown.yml` +- `changelog.md` +- `tests/testthat.R` +- `R/superNetballR.R` (package-level docs file; may be renamed) +- package-level man files generated from roxygen + +### Download/discovery logic + +- `R/downloadMatch.R` +- likely new file for catalogue/discovery helpers, e.g. `R/competitions.R` +- possibly `inst/create_anzc_comp_ids.R` if docs or comments need repositioning + +### Tests + +- `tests/testthat/test-downloadMatch.R` +- `tests/testthat/test-downloadFixture.R` +- new tests for `netball_aus` catalogue parsing/discovery +- `tests/testthat/helper-fixtures.R` for settings fixtures if needed + +### Demo app / package-internal paths + +- `R/shinySuperNetballR.R` +- `inst/shiny-examples/superNetballR/...` + +### Documentation + +- `vignettes/getting-started.Rmd` +- `man/*.Rd` after roxygen regeneration +- pkgdown site config and generated site if maintained in-repo + +## Testing Strategy + +Follow TDD for the feature work. + +Required coverage areas: + +1. existing `downloadMatch()` and `downloadFixture()` URL builders still point to `/data/...` +2. `netball_aus` settings discovery fetch/parsing works for representative payloads +3. competition listing helper returns a tidy, predictable schema +4. package rename does not break test bootstrap or namespace loading +5. any renamed branded helper (for example the shiny launcher) has updated coverage if kept + +Prefer fixture-based tests for the `netball_aus` settings structure so the test suite does not depend on live network access. + +## Migration / Release Considerations + +Because this is a clean break: + +- version bump should reflect a breaking release +- README/install docs should direct users to the new repository/package name only +- changelog should clearly call out the rename and broadened scope +- users may need to reinstall under the new package name + +If repository rename happens outside the codebase, code/docs should assume the new canonical URLs once the rename is complete. + +## Risks + +- The `netball_aus` application settings payload may evolve independently of the current package assumptions, so parsing should be defensive. +- A clean package rename touches many files and increases the chance of missing stale references. +- Branded helper functions like `shinySuperNetballR()` need a deliberate decision to avoid leaving the API in a partially renamed state. +- Generated docs/site output may create a large diff after the rename. + +## Acceptance Criteria + +- Package metadata, docs, and references are renamed to `netballR`. +- The package is positioned as a general netball statistics package. +- Users can discover all `netball_aus` competitions through a tidy helper. +- Users can still fetch fixtures/matches via the existing `comp_id`-based download functions. +- Tests cover the new discovery layer and the unchanged transport behavior. +- Documentation explains the discovery → fixture → match workflow clearly. From be32004c68dc06316ae24e37a2592b4b5209442c Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 22:51:18 +1000 Subject: [PATCH 41/56] feat: rename package to netballR --- DESCRIPTION | 16 ++-- NAMESPACE | 2 +- R/netballR-package.R | 26 ++++++ R/shinyNetballR.R | 24 +++++ inst/shiny-examples/netballR/global.R | 49 ++++++++++ inst/shiny-examples/netballR/server.R | 12 +++ .../netballR/team_series_module.R | 92 +++++++++++++++++++ inst/shiny-examples/netballR/ui.R | 16 ++++ ...etballR-package.Rd => netballR-package.Rd} | 12 +-- man/shinyNetballR.Rd | 14 +++ man/shinySuperNetballR.Rd | 15 --- tests/testthat.R | 4 +- tests/testthat/test-downloadFixture.R | 16 ++-- tests/testthat/test-downloadMatch.R | 14 +-- tests/testthat/test-ladders.R | 2 +- tests/testthat/test-package-branding.R | 6 ++ 16 files changed, 272 insertions(+), 48 deletions(-) create mode 100644 R/netballR-package.R create mode 100644 R/shinyNetballR.R create mode 100644 inst/shiny-examples/netballR/global.R create mode 100644 inst/shiny-examples/netballR/server.R create mode 100644 inst/shiny-examples/netballR/team_series_module.R create mode 100644 inst/shiny-examples/netballR/ui.R rename man/{superNetballR-package.Rd => netballR-package.Rd} (65%) create mode 100644 man/shinyNetballR.Rd delete mode 100644 man/shinySuperNetballR.Rd create mode 100644 tests/testthat/test-package-branding.R diff --git a/DESCRIPTION b/DESCRIPTION index edeec94..1a73cf9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,14 +1,14 @@ -Package: superNetballR -Title: Download and Tidy Super Netball Statistics -Version: 0.3.1 +Package: netballR +Title: Download and Tidy Netball Match Statistics +Version: 1.0.0 Authors@R: c( person("Steve", "Lane", email = "lane.s@unimelb.edu.au", role = "aut"), person("Craig", "Moyle", email = "craig.moyle@mantelgroup.com.au", role = c("aut", "cre")) ) -Description: Tools to download Champion Data match feeds for Super Netball and - the ANZ Championship / NZ National Netball League, and transform team and - player statistics into tidy data frames for analysis. +Description: Tools to download Champion Data match feeds for netball + competitions and transform team and player statistics into tidy data + frames for analysis. Depends: R (>= 4.1.0) License: MIT + file LICENSE Encoding: UTF-8 @@ -23,7 +23,7 @@ Imports: dplyr (>= 1.1.0), httr, purrr, tidyr -URL: https://github.com/craigmoyle/superNetballR_updated -BugReports: https://github.com/craigmoyle/superNetballR_updated/issues +URL: https://github.com/craigmoyle/netballR +BugReports: https://github.com/craigmoyle/netballR/issues Config/testthat/edition: 3 Config/roxygen2/version: 8.0.0 diff --git a/NAMESPACE b/NAMESPACE index 3e1e2d1..9b9317f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,6 @@ export(ladders_pre_2020) export(matchPoints) export(matchPoints_pre_2020) export(matchResults) -export(shinySuperNetballR) +export(shinyNetballR) export(tidyMatch) export(tidyPlayers) diff --git a/R/netballR-package.R b/R/netballR-package.R new file mode 100644 index 0000000..c2ffbb7 --- /dev/null +++ b/R/netballR-package.R @@ -0,0 +1,26 @@ +#' superNetballR: Download and Tidy Super Netball Statistics +#' +#' Download Champion Data Super Netball match feeds and transform team and +#' player statistics into tidy data frames for analysis. +#' +#' @keywords internal +"_PACKAGE" + +## Suppress R CMD check notes for variables used in dplyr/tidyr pipelines. +if (getRversion() >= "2.15.1") { + utils::globalVariables(c( + ## match/player column names + "squadId", "homeTeam", "period", "stat", "value", "squadName", + "squadNickname", "squadCode", "round", "game", "displayName", + "matchId", "playerId", "shortDisplayName", "firstname", "surname", + ## scoring / ladder names + "goals", "goals2", "score_diff", "points", "points_new", + "goals_for", "goals_against", "percentage", "isHome", + "games", "qtr_diff", "data", + ## period-score helper names + "homeValue", "homeSquad", "homePoints", + "awayValue", "awaySquad", "awayPoints", + "points_qtr", "game_results", + "squadId.x", "squadId.y" + )) +} diff --git a/R/shinyNetballR.R b/R/shinyNetballR.R new file mode 100644 index 0000000..bc20335 --- /dev/null +++ b/R/shinyNetballR.R @@ -0,0 +1,24 @@ +#' Runs the demo shiny app +#' +#' \code{shinyNetballR} runs the demo shiny app to compare team statistics. +#' +#' @return Runs a shiny app +#' +#' @export +shinyNetballR <- function() { + if (!requireNamespace("shiny", quietly = TRUE)) { + stop("Package 'shiny' must be installed to run shinyNetballR().", call. = FALSE) + } + if (!requireNamespace("ggplot2", quietly = TRUE)) { + stop("Package 'ggplot2' must be installed to run shinyNetballR().", call. = FALSE) + } + + my_dir <- system.file( + "shiny-examples", "netballR", package = "netballR" + ) + if (my_dir == "") { + stop("Can't find the netballR shiny directory. Try re-installing `netballR`.", call. = FALSE) + } + + shiny::runApp(my_dir, display.mode = "normal") +} diff --git a/inst/shiny-examples/netballR/global.R b/inst/shiny-examples/netballR/global.R new file mode 100644 index 0000000..5750b97 --- /dev/null +++ b/inst/shiny-examples/netballR/global.R @@ -0,0 +1,49 @@ +################################################################################ +################################################################################ +## Title: Global shiny setup +## Author: Steve Lane +## Date: Saturday, 08 August 2020 +## Synopsis: Sets up global libraries and functions for example shiny. +## Time-stamp: <2021-05-04 12:38:40 (sprazza)> +################################################################################ +################################################################################ +library(dplyr) +library(ggplot2) +library(shiny) +library(netballR) + +################################################################################ +## Load modules. +app_dir <- system.file("shiny-examples", "netballR", package = "netballR") +if (app_dir == "") { + stop("Can't find the netballR shiny directory.", call. = FALSE) +} +source(file.path(app_dir, "team_series_module.R"), local = TRUE) + +################################################################################ +## Load 2017 player data +data(players_2017) +data(season_2017) +data(team_colours) +season_2017 <- season_2017 %>% + mutate(Season = 2017) + +################################################################################ +## Create some selectors (they don't need to be reactive). +season_input <- sort(unique(season_2017[["Season"]])) +round_input <- sort(unique(season_2017[["round"]])) +by_game <- season_2017 %>% + group_by(squadId, stat, round, game) %>% + summarise(value = sum(value)) %>% + mutate( + Round = paste0( + '2017, Round ', formatC(round, width = 2, format = 'd', flag = '0') + ) + ) %>% + ungroup() %>% + left_join(., team_colours, by = 'squadId') +team_input <- sort(unique(by_game[["squadName"]])) +metric_input <- sort(unique(by_game[["stat"]])) +## Create colour scale +nm <- team_colours[['squadColour']] +names(nm) <- team_colours[['squadName']] diff --git a/inst/shiny-examples/netballR/server.R b/inst/shiny-examples/netballR/server.R new file mode 100644 index 0000000..6b6dd05 --- /dev/null +++ b/inst/shiny-examples/netballR/server.R @@ -0,0 +1,12 @@ +################################################################################ +################################################################################ +## Title: Server +## Author: Steve Lane +## Date: Saturday, 08 August 2020 +## Synopsis: Server for shiny example. +## Time-stamp: <2021-05-04 12:48:56 (sprazza)> +################################################################################ +################################################################################ +server <- function(input, output, session) { + team_series_server('team_series1', by_game) +} diff --git a/inst/shiny-examples/netballR/team_series_module.R b/inst/shiny-examples/netballR/team_series_module.R new file mode 100644 index 0000000..b70aaa4 --- /dev/null +++ b/inst/shiny-examples/netballR/team_series_module.R @@ -0,0 +1,92 @@ +#' Function to plot a particular statistic +#' +#' \code{team_series} Function to plot a particular statistic, for a particular +#' team. +#' +#' @param df Data frame of team statistics +#' @param metric Statistic to display on figure +#' @param team1 First team to display on chart +#' @param team2 Second team to display on chart +#' +#' @return ggplot2 object +team_series <- function(df) { + df %>% + ggplot() + + aes( + x = Round, y = value, group = squadName, colour = squadName, + fill = squadName + ) + + geom_point() + + geom_line() + + geom_smooth(level = 0.8) + + scale_fill_manual(values = nm) + + scale_colour_manual(values = nm) + + labs( + y = 'Value', + title = 'Netball Statistics by Round', + caption = + 'This figure allows you to compare two teams on a single statistic over time. Overlaid are simple trend (loess) lines and an 80% confidence interval.' + ) + + theme_minimal() + + theme( + axis.text.x = element_text(hjust = 1, angle = 35), + legend.title = element_blank(), + legend.position = 'bottom' + ) +} + +team_series_ui <- function(id) { + sidebarLayout( + sidebarPanel( + selectInput( + NS(id, "team_selector1"), + label = "Team 1", + choices = team_input, + selected = "Melbourne Vixens" + ), + selectInput( + NS(id, "team_selector2"), + label = "Team 2", + choices = team_input, + selected = "GIANTS Netball" + ), + selectInput( + NS(id, "statistic_selector"), + label = "Statistic", + choices = metric_input, + selected = "goals" + ), + width = 2 + ), + mainPanel( + plotOutput(NS(id, 'team_series'), height = '600px'), + width = 10 + ) + ) +} + +team_series_server <- function(id, df) { + moduleServer(id, function(input, output, session) { + this_df <- reactive({ + df %>% + filter( + squadName %in% c(input$team_selector1, input$team_selector2), + stat == input$statistic_selector + ) + }) + output$team_series <- renderPlot({ + team_series(this_df()) + }) + }) +} + +## Test the modules in a self-contained way. +team_series_app <- function(data_source) { + ui <- fluidPage( + team_series_ui("ts1") + ) + server <- function(input, output, session) { + team_series_server("ts1", data_source) + } + shinyApp(ui, server) +} diff --git a/inst/shiny-examples/netballR/ui.R b/inst/shiny-examples/netballR/ui.R new file mode 100644 index 0000000..284a9c0 --- /dev/null +++ b/inst/shiny-examples/netballR/ui.R @@ -0,0 +1,16 @@ +################################################################################ +################################################################################ +## Title: UI +## Author: Steve Lane +## Date: Saturday, 08 August 2020 +## Synopsis: UI for shiny example. +## Time-stamp: <2021-05-04 12:49:18 (sprazza)> +################################################################################ +################################################################################ +ui <- navbarPage( + "Netball Statistics Comparison App", + tabPanel( + "Team Statistics", + team_series_ui('team_series1') + ) +) diff --git a/man/superNetballR-package.Rd b/man/netballR-package.Rd similarity index 65% rename from man/superNetballR-package.Rd rename to man/netballR-package.Rd index 0a04492..80fa680 100644 --- a/man/superNetballR-package.Rd +++ b/man/netballR-package.Rd @@ -1,9 +1,9 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/superNetballR.R +% Please edit documentation in R/netballR-package.R \docType{package} -\name{superNetballR-package} -\alias{superNetballR} -\alias{superNetballR-package} +\name{netballR-package} +\alias{netballR} +\alias{netballR-package} \title{superNetballR: Download and Tidy Super Netball Statistics} \description{ Download Champion Data Super Netball match feeds and transform team and @@ -12,8 +12,8 @@ player statistics into tidy data frames for analysis. \seealso{ Useful links: \itemize{ - \item \url{https://github.com/craigmoyle/superNetballR_updated} - \item Report bugs at \url{https://github.com/craigmoyle/superNetballR_updated/issues} + \item \url{https://github.com/craigmoyle/netballR} + \item Report bugs at \url{https://github.com/craigmoyle/netballR/issues} } } diff --git a/man/shinyNetballR.Rd b/man/shinyNetballR.Rd new file mode 100644 index 0000000..db540d7 --- /dev/null +++ b/man/shinyNetballR.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shinyNetballR.R +\name{shinyNetballR} +\alias{shinyNetballR} +\title{Runs the demo shiny app} +\usage{ +shinyNetballR() +} +\value{ +Runs a shiny app +} +\description{ +\code{shinyNetballR} runs the demo shiny app to compare team statistics. +} diff --git a/man/shinySuperNetballR.Rd b/man/shinySuperNetballR.Rd deleted file mode 100644 index 4f1be02..0000000 --- a/man/shinySuperNetballR.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/shinySuperNetballR.R -\name{shinySuperNetballR} -\alias{shinySuperNetballR} -\title{Runs the demo shiny app} -\usage{ -shinySuperNetballR() -} -\value{ -Runs a shiny app -} -\description{ -\code{shinySuperNetballR} Runs the demo shiny app to compare Super Netball -statistics between teams. -} diff --git a/tests/testthat.R b/tests/testthat.R index eb080cb..e25ab4f 100644 --- a/tests/testthat.R +++ b/tests/testthat.R @@ -1,4 +1,4 @@ library(testthat) -library(superNetballR) +library(netballR) -test_check("superNetballR") +test_check("netballR") diff --git a/tests/testthat/test-downloadFixture.R b/tests/testthat/test-downloadFixture.R index 2ec9d3d..15fb2db 100644 --- a/tests/testthat/test-downloadFixture.R +++ b/tests/testthat/test-downloadFixture.R @@ -1,37 +1,37 @@ test_that("build_fixture_url validates and formats the request URL", { expect_equal( - superNetballR:::build_fixture_url("10088"), + netballR:::build_fixture_url("10088"), "https://mc.championdata.com/data/10088/fixture.json" ) expect_equal( - superNetballR:::build_fixture_url(10088), + netballR:::build_fixture_url(10088), "https://mc.championdata.com/data/10088/fixture.json" ) expect_error( - superNetballR:::build_fixture_url("anz-2017"), + netballR:::build_fixture_url("anz-2017"), "comp_id must contain digits only" ) expect_error( - superNetballR:::build_fixture_url(NA), + netballR:::build_fixture_url(NA), "comp_id must be a single value" ) }) test_that("extract_fixture fails loudly when fixture key is absent", { expect_error( - superNetballR:::extract_fixture(list()), + netballR:::extract_fixture(list()), "did not include fixture" ) expect_error( - superNetballR:::extract_fixture(list(matchStats = list())), + netballR:::extract_fixture(list(matchStats = list())), "did not include fixture" ) }) test_that("extract_fixture returns an empty tibble when match list is empty", { payload <- list(fixture = list(match = list())) - result <- superNetballR:::extract_fixture(payload) + result <- netballR:::extract_fixture(payload) expect_s3_class(result, "tbl_df") expect_equal(nrow(result), 0L) expect_true(all(c("round", "game", "matchId", "matchStatus", @@ -72,7 +72,7 @@ test_that("extract_fixture parses complete match rows correctly", { ) ) - result <- superNetballR:::extract_fixture(payload) + result <- netballR:::extract_fixture(payload) expect_s3_class(result, "tbl_df") expect_equal(nrow(result), 2L) diff --git a/tests/testthat/test-downloadMatch.R b/tests/testthat/test-downloadMatch.R index 59032e8..7ba1b02 100644 --- a/tests/testthat/test-downloadMatch.R +++ b/tests/testthat/test-downloadMatch.R @@ -1,23 +1,23 @@ test_that("build_match_url validates and formats request identifiers", { expect_equal( - superNetballR:::build_match_url("10083", 5, 3), + netballR:::build_match_url("10083", 5, 3), "https://mc.championdata.com/data/10083/100830503.json" ) expect_equal( - superNetballR:::build_match_url(10083, "5", "3"), + netballR:::build_match_url(10083, "5", "3"), "https://mc.championdata.com/data/10083/100830503.json" ) expect_error( - superNetballR:::build_match_url("season-2025", 5, 3), + netballR:::build_match_url("season-2025", 5, 3), "comp_id must contain digits only" ) expect_error( - superNetballR:::build_match_url("10083", 0, 3), + netballR:::build_match_url("10083", 0, 3), "round_id must be greater than or equal to 1" ) expect_error( - superNetballR:::build_match_url("10083", 5, 1.5), + netballR:::build_match_url("10083", 5, 1.5), "game_id must contain digits only" ) }) @@ -25,9 +25,9 @@ test_that("build_match_url validates and formats request identifiers", { test_that("extract_match_stats fails loudly when matchStats is absent", { payload <- list(matchStats = list(matchInfo = list(matchNumber = 3L))) - expect_equal(superNetballR:::extract_match_stats(payload), payload$matchStats) + expect_equal(netballR:::extract_match_stats(payload), payload$matchStats) expect_error( - superNetballR:::extract_match_stats(list()), + netballR:::extract_match_stats(list()), "did not include matchStats" ) }) diff --git a/tests/testthat/test-ladders.R b/tests/testthat/test-ladders.R index 274e255..9dd0f09 100644 --- a/tests/testthat/test-ladders.R +++ b/tests/testthat/test-ladders.R @@ -105,7 +105,7 @@ test_that("matchResults_pre_2020 prefers matchId when distinct matches share rou ) ) - match_results <- superNetballR:::matchResults_pre_2020(season) + match_results <- netballR:::matchResults_pre_2020(season) expect_equal(nrow(match_results), 4) expect_setequal(match_results$squadName, c("A", "B", "C", "D")) diff --git a/tests/testthat/test-package-branding.R b/tests/testthat/test-package-branding.R new file mode 100644 index 0000000..e2701ac --- /dev/null +++ b/tests/testthat/test-package-branding.R @@ -0,0 +1,6 @@ +test_that("netballR exports the renamed shiny launcher", { + exports <- getNamespaceExports("netballR") + + expect_true("shinyNetballR" %in% exports) + expect_false("shinySuperNetballR" %in% exports) +}) From 5da0f90aa1280c5314bf32923f321ebe6da805ef Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 22:52:04 +1000 Subject: [PATCH 42/56] feat: add netball_aus competition discovery --- NAMESPACE | 1 + R/competitions.R | 79 +++++++++++++++++++ man/listCompetitionsNetballAus.Rd | 43 ++++++++++ tests/testthat/helper-fixtures.R | 31 ++++++++ .../testthat/test-netball-aus-competitions.R | 54 +++++++++++++ 5 files changed, 208 insertions(+) create mode 100644 R/competitions.R create mode 100644 man/listCompetitionsNetballAus.Rd create mode 100644 tests/testthat/test-netball-aus-competitions.R diff --git a/NAMESPACE b/NAMESPACE index 9b9317f..62134bd 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,7 @@ export(downloadFixture) export(downloadMatch) export(ladders) export(ladders_pre_2020) +export(listCompetitionsNetballAus) export(matchPoints) export(matchPoints_pre_2020) export(matchResults) diff --git a/R/competitions.R b/R/competitions.R new file mode 100644 index 0000000..74ed735 --- /dev/null +++ b/R/competitions.R @@ -0,0 +1,79 @@ +build_netball_aus_settings_url <- function() { + "https://mc.championdata.com/netball_aus/settings/application_settings.json" +} + +fetch_netball_aus_settings <- function() { + dat <- httr::RETRY( + "GET", + build_netball_aus_settings_url(), + httr::timeout(30), + times = 3, + pause_base = 1, + terminate_on = c(400, 401, 403, 404), + quiet = TRUE + ) + httr::stop_for_status(dat) + httr::content(dat, as = "parsed", type = "application/json") +} + +extract_netball_aus_competitions <- function(payload) { + competitions <- payload$competitionList$competition + if (is.null(competitions) || length(competitions) == 0L) { + stop( + "netball_aus application settings did not include competitionList$competition.", + call. = FALSE + ) + } + + rows <- lapply(competitions, function(comp) { + dplyr::tibble( + comp_id = as.integer(comp$id %||% NA_integer_), + competition_name = as.character(comp$competition_name %||% NA_character_), + application_source = "netball_aus", + season = as.integer(comp$season %||% NA_integer_), + competition_type = as.character(comp$type %||% NA_character_), + squad_id = as.integer(comp$squad_id %||% NA_integer_), + application_logo = as.character(comp$application_logo %||% NA_character_) + ) + }) + + dplyr::bind_rows(rows) +} + +#' List competitions from the Champion Data netball_aus application +#' +#' \code{listCompetitionsNetballAus()} downloads the public application settings +#' used by the Champion Data \code{netball_aus} iStats app and returns a tidy +#' tibble of available competitions. +#' +#' @return A \code{\link[dplyr]{tibble}} with one row per competition and +#' columns: +#' \describe{ +#' \item{comp_id}{Champion Data competition identifier. Pass this value to +#' \code{\link{downloadFixture}} or \code{\link{downloadMatch}}.} +#' \item{competition_name}{Competition name from the live application +#' settings.} +#' \item{application_source}{Always \code{"netball_aus"} for this helper.} +#' \item{season}{Season year if supplied by the live settings payload, +#' otherwise \code{NA}.} +#' \item{competition_type}{Competition type if supplied by the live settings +#' payload, otherwise \code{NA}.} +#' \item{squad_id}{Optional squad filter attached to the competition in the +#' live settings payload.} +#' \item{application_logo}{Relative logo path from the live settings payload +#' when available.} +#' } +#' @details +#' The returned \code{comp_id} values use the same Champion Data `/data/...` +#' transport as \code{\link{downloadFixture}} and \code{\link{downloadMatch}}. +#' +#' @examples +#' \dontrun{ +#' comps <- listCompetitionsNetballAus() +#' fixture <- downloadFixture(comps$comp_id[[1]]) +#' } +#' +#' @export +listCompetitionsNetballAus <- function() { + extract_netball_aus_competitions(fetch_netball_aus_settings()) +} diff --git a/man/listCompetitionsNetballAus.Rd b/man/listCompetitionsNetballAus.Rd new file mode 100644 index 0000000..d0476d6 --- /dev/null +++ b/man/listCompetitionsNetballAus.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/competitions.R +\name{listCompetitionsNetballAus} +\alias{listCompetitionsNetballAus} +\title{List competitions from the Champion Data netball_aus application} +\usage{ +listCompetitionsNetballAus() +} +\value{ +A \code{\link[dplyr]{tibble}} with one row per competition and + columns: + \describe{ + \item{comp_id}{Champion Data competition identifier. Pass this value to + \code{\link{downloadFixture}} or \code{\link{downloadMatch}}.} + \item{competition_name}{Competition name from the live application + settings.} + \item{application_source}{Always \code{"netball_aus"} for this helper.} + \item{season}{Season year if supplied by the live settings payload, + otherwise \code{NA}.} + \item{competition_type}{Competition type if supplied by the live settings + payload, otherwise \code{NA}.} + \item{squad_id}{Optional squad filter attached to the competition in the + live settings payload.} + \item{application_logo}{Relative logo path from the live settings payload + when available.} + } +} +\description{ +\code{listCompetitionsNetballAus()} downloads the public application settings +used by the Champion Data \code{netball_aus} iStats app and returns a tidy +tibble of available competitions. +} +\details{ +The returned \code{comp_id} values use the same Champion Data `/data/...` +transport as \code{\link{downloadFixture}} and \code{\link{downloadMatch}}. +} +\examples{ +\dontrun{ +comps <- listCompetitionsNetballAus() +fixture <- downloadFixture(comps$comp_id[[1]]) +} + +} diff --git a/tests/testthat/helper-fixtures.R b/tests/testthat/helper-fixtures.R index f83e61a..81a2f82 100644 --- a/tests/testthat/helper-fixtures.R +++ b/tests/testthat/helper-fixtures.R @@ -219,3 +219,34 @@ make_pre_2020_match_stats_with_id <- function( out$matchId <- match_id out } + +make_netball_aus_settings <- function() { + list( + applicationInfo = list( + defaultCompetitionID = 12971L, + defaultMatchID = 129710101L, + defaultSeason = 2026L, + defaultRound = 1L, + version = "2026.12.8.1" + ), + competitionList = list( + competition = list( + list( + id = 9315L, + application_logo = "/netball_aus/images/competition/9315.png", + competition_name = "2014 Constellation Cup" + ), + list( + id = 10200L, + application_logo = "/netball_aus/images/competition/9973.png", + competition_name = "2017 Netball Quad Series - January", + squad_id = 811L + ), + list( + id = 12971L, + competition_name = "2026 Constellation Cup" + ) + ) + ) + ) +} diff --git a/tests/testthat/test-netball-aus-competitions.R b/tests/testthat/test-netball-aus-competitions.R new file mode 100644 index 0000000..7c0a3a7 --- /dev/null +++ b/tests/testthat/test-netball-aus-competitions.R @@ -0,0 +1,54 @@ +test_that("build_netball_aus_settings_url returns the application settings endpoint", { + expect_equal( + netballR:::build_netball_aus_settings_url(), + "https://mc.championdata.com/netball_aus/settings/application_settings.json" + ) +}) + +test_that("extract_netball_aus_competitions parses application settings into a tidy tibble", { + result <- netballR:::extract_netball_aus_competitions(make_netball_aus_settings()) + + expect_s3_class(result, "tbl_df") + expect_named( + result, + c( + "comp_id", "competition_name", "application_source", "season", + "competition_type", "squad_id", "application_logo" + ) + ) + expect_equal(result$comp_id, c(9315L, 10200L, 12971L)) + expect_equal(result$competition_name[[2]], "2017 Netball Quad Series - January") + expect_equal(result$application_source, rep("netball_aus", 3)) + expect_equal(result$season, rep(NA_integer_, 3)) + expect_equal(result$competition_type, rep(NA_character_, 3)) + expect_equal(result$squad_id, c(NA_integer_, 811L, NA_integer_)) + expect_equal( + result$application_logo, + c( + "/netball_aus/images/competition/9315.png", + "/netball_aus/images/competition/9973.png", + NA_character_ + ) + ) +}) + +test_that("extract_netball_aus_competitions fails loudly when the competition list is absent", { + expect_error( + netballR:::extract_netball_aus_competitions(list()), + "did not include competitionList\\$competition" + ) +}) + +test_that("listCompetitionsNetballAus returns extracted live competitions", { + local_mocked_bindings( + fetch_netball_aus_settings = function() make_netball_aus_settings(), + .package = "netballR" + ) + + result <- listCompetitionsNetballAus() + + expect_s3_class(result, "tbl_df") + expect_equal(nrow(result), 3L) + expect_equal(result$comp_id[[1]], 9315L) + expect_true(all(result$application_source == "netball_aus")) +}) From ad2fe4a804ecc9a021117c7df2a0f6c3c84c4445 Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 22:53:12 +1000 Subject: [PATCH 43/56] docs: rebrand package and document netball_aus discovery --- R/downloadMatch.R | 12 ++++-- README.md | 77 +++++++++++++++++++++-------------- _pkgdown.yml | 2 +- changelog.md | 11 ++++- vignettes/getting-started.Rmd | 14 ++++--- 5 files changed, 72 insertions(+), 44 deletions(-) diff --git a/R/downloadMatch.R b/R/downloadMatch.R index 7374ad7..c416fce 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -102,7 +102,8 @@ extract_fixture <- function(payload) { #' @param comp_id A string identifying which season the game is #' in. \code{comp_id} is different depending on regular season or finals. #' See \code{\link{anzc_comp_ids}} for known ANZ Championship competition -#' IDs. Super Netball comp IDs are documented in the package README. +#' IDs, or \code{\link{listCompetitionsNetballAus}} for the broader live +#' catalogue exposed by the Champion Data \code{netball_aus} application. #' @param round_id An integer identifying which round the game is in. Finals #' reset round number to 1. #' @param game_id An integer indentifying which game in the round to @@ -156,8 +157,9 @@ downloadMatch <- function(comp_id, round_id, game_id) { #' competition, returning one row per match. #' #' @param comp_id A string identifying the competition. See -#' \code{\link{anzc_comp_ids}} for known ANZ Championship competition IDs. -#' Super Netball comp IDs are documented in the package README. +#' \code{\link{anzc_comp_ids}} for known ANZ Championship competition IDs +#' or \code{\link{listCompetitionsNetballAus}} for the broader live +#' catalogue exposed by the Champion Data \code{netball_aus} application. #' @return A \code{\link[dplyr]{tibble}} with one row per match and columns: #' \describe{ #' \item{round}{Round number.} @@ -179,7 +181,9 @@ downloadMatch <- function(comp_id, round_id, game_id) { #' @details #' \code{downloadFixture()} is the recommended starting point when working with #' a new competition: it shows which rounds and game numbers are available so -#' you can pass them to \code{\link{downloadMatch}}. +#' you can pass them to \code{\link{downloadMatch}}. Use +#' \code{\link{listCompetitionsNetballAus}} when you need to discover live +#' competition IDs from the broader \code{netball_aus} catalogue first. #' #' The function validates \code{comp_id}, retries transient HTTP failures, and #' raises an explicit error if the Champion Data response does not include a diff --git a/README.md b/README.md index a02adfc..5981ac9 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,84 @@ -# superNetballR +# netballR -[![R-CMD-check](https://github.com/craigmoyle/superNetballR_updated/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/craigmoyle/superNetballR_updated/actions/workflows/R-CMD-check.yaml) +[![R-CMD-check](https://github.com/craigmoyle/netballR/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/craigmoyle/netballR/actions/workflows/R-CMD-check.yaml) ## Description -This fork of `superNetballR` allows the downloading of super netball statistics from the original project site: [https://stevelane.github.io/superNetballR/](https://stevelane.github.io/superNetballR/). The first super netball season was in 2017, and was eventually won by the Sunshine Coast Lightning. +`netballR` provides tools to discover netball competitions, download Champion Data match feeds, and transform team and player statistics into tidy data for analysis. -`superNetballR` contains helper functions that transform the downloaded data into usable tidy data. +This package now supports two complementary workflows: -This repository is maintained at [craigmoyle/superNetballR_updated](https://github.com/craigmoyle/superNetballR_updated). -The current Champion Data iStats portal still exposes the same zone-based result data model used by this package, and `downloadMatch()` now validates match identifiers before requesting the JSON feed. +1. discover live competitions exposed by the Champion Data `netball_aus` iStats application +2. download fixtures and matches using the existing Champion Data `/data//...` transport + +Historical Super Netball, ANZ Championship, and NZ National Netball League workflows remain supported. ## Installation Installation in R requires `remotes`. To install, run the following from an R session: -``` R +```r install.packages("remotes") -remotes::install_github("craigmoyle/superNetballR_updated") +remotes::install_github("craigmoyle/netballR") ``` To install the current `main` branch explicitly: -``` R -remotes::install_github("craigmoyle/superNetballR_updated@main") +```r +remotes::install_github("craigmoyle/netballR@main") ``` ## Current behavior - `downloadMatch()` validates competition, round, and game identifiers, retries transient HTTP failures, and errors clearly if the Champion Data payload is missing `matchStats`. -- `downloadFixture()` fetches the full match schedule for any competition — use `?anzc_comp_ids` to find ANZ Championship and NZ National Netball League competition IDs. -- `matchPoints()` and `ladders()` implement the current super shot scoring model for 2020+ data. +- `downloadFixture()` fetches the full match schedule for any competition supported by the Champion Data `/data/...` feed. +- `listCompetitionsNetballAus()` returns a live competition catalogue sourced from `https://mc.championdata.com/netball_aus/settings/application_settings.json`. +- `matchPoints()` and `ladders()` implement the current super-shot scoring model for 2020+ data. - `matchPoints_pre_2020()` and `ladders_pre_2020()` remain available for legacy seasons and older points systems. -- `team_colours` includes the current Melbourne Mavericks entry while retaining the historical Magpies row needed by the bundled 2017 data. -- The package includes a `testthat` suite and a GitHub Actions `R-CMD-check` workflow for ongoing maintenance. +- `tidyMatch()` and `tidyPlayers()` append the Champion Data `matchId` so combined live tidy outputs can keep distinct matches separate. -## ANZ Championship / NZ National Netball League +## Discover competitions from `netball_aus` -The same Champion Data feed powers both competitions. Use `anzc_comp_ids` to look up the competition ID for any season, then call `downloadFixture()` to see available matches and `downloadMatch()` to fetch match data. `tidyMatch()` now appends the Champion Data `matchId`, which helps keep regular-season and finals matches distinct when you combine live tidy outputs. Because ANZ Championship data uses a `goals` stat rather than the super-shot zones introduced in 2020, use `ladders_pre_2020()` (not `ladders()`) on season-style tidy match statistics when computing standings. +Use `listCompetitionsNetballAus()` to inspect the live competition catalogue exposed by the Champion Data `netball_aus` application. -``` r -library(superNetballR) +```r +library(netballR) -# Browse available ANZ / NZ Netball seasons -anzc_comp_ids +competitions <- listCompetitionsNetballAus() +competitions +``` -# Get the fixture for the 2024 NZ National Netball League regular season -fixture <- downloadFixture(12427) +The returned `comp_id` values work directly with `downloadFixture()` and `downloadMatch()`. -# Download and tidy a specific match (round 1, game 1) -match <- downloadMatch(12427, 1, 1) +```r +library(netballR) + +competitions <- listCompetitionsNetballAus() +comp_id <- competitions$comp_id[[1]] + +fixture <- downloadFixture(comp_id) +match <- downloadMatch(comp_id, 1, 1) match_stats <- tidyMatch(match) +``` -# Summarise the single-match result using the pre-super-shot scoring model -match_result <- matchPoints_pre_2020(match_stats) +## Historical ANZ Championship / NZ National Netball League -# For ladders_pre_2020(), supply season-style tidy match statistics +The package still includes `anzc_comp_ids` for historical ANZ Championship and NZ National Netball League workflows. + +```r +library(netballR) + +anzc_comp_ids +fixture <- downloadFixture(12427) +match <- downloadMatch(12427, 1, 1) +match_stats <- tidyMatch(match) +match_result <- matchPoints_pre_2020(match_stats) standings <- ladders_pre_2020(match_stats) ``` ## Development -The repository now uses GitHub Actions instead of Travis CI. Local developer commands are available through the `Makefile`: +Local developer commands are available through the `Makefile`: ```sh make test @@ -69,8 +86,6 @@ make build make check ``` -`make check` uses base `R CMD build` and `R CMD check`, while CI regenerates package documentation before running `R-CMD-check`. - ## Notes -The package has been updated to account for the super goal in 2020. Ladders and points have been adjusted for this. If you want to use the old scoring systems, these are available using `_pre_2020` versions of the appropriate functions. +Champion Data's public `netball_aus` site still loads fixture and match JSON from `/data//...`, so `netballR` uses `netball_aus` for competition discovery and `/data/...` for actual fixture/match downloads. diff --git a/_pkgdown.yml b/_pkgdown.yml index 032d9c1..242283b 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -12,4 +12,4 @@ navbar: type: inverse right: - icon: fa-github fa-lg - href: https://github.com/craigmoyle/superNetballR_updated/ + href: https://github.com/craigmoyle/netballR/ diff --git a/changelog.md b/changelog.md index 60d48c8..f6c6fe6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,8 +1,15 @@ # Changelog -All notable changes in this fork are documented here. +All notable changes in this project are documented here. -This changelog covers the changes introduced in `craigmoyle/superNetballR_updated` since the fork diverged from the original `SteveLane/superNetballR` project published at . +This changelog covers the changes introduced in `craigmoyle/netballR` since the fork diverged from the original `SteveLane/superNetballR` (historical upstream) project published at . + +## netballR 1.0.0 + +- Breaking rename: package and repository move from `superNetballR` / `superNetballR_updated` to `netballR`. +- Broadened package positioning from Super Netball-only to general netball competition support. +- Added live competition discovery via the Champion Data `netball_aus` application settings catalogue. +- Kept fixture and match downloads on the existing Champion Data `/data//...` transport. ## 0.3.1 - 2026-04-07 diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd index 6f013ef..311be53 100644 --- a/vignettes/getting-started.Rmd +++ b/vignettes/getting-started.Rmd @@ -1,10 +1,10 @@ --- -title: "Getting Started with superNetballR" +title: "Getting Started with netballR" author: "Steve Lane and Craig Moyle" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{Getting Started with superNetballR} + %\VignetteIndexEntry{Getting Started with netballR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- @@ -20,7 +20,7 @@ knitr::opts_chunk$set( # Introduction -This vignette provides an overview to get you started with using `superNetballR`. The package ships with the full 2017 season match and player statistics, while the live Champion Data iStats portal still publishes compatible match JSON for current seasons. +This vignette provides an overview to get you started with using `netballR`. The package ships with the full 2017 season match and player statistics, while the live Champion Data iStats portal still publishes compatible match JSON for current seasons. *Current fork note*: the package has been updated for the post-2020 super shot scoring model, and `downloadMatch()` now validates competition, round, and game identifiers before requesting data. @@ -30,11 +30,13 @@ Data are sourced from `https://mc.championdata.com/data/` using competition, rou If the live endpoint returns a transient HTTP error, `downloadMatch()` will retry before failing. If the response no longer includes a `matchStats` object, the function stops with an explicit error so schema changes are easier to detect. +Use `listCompetitionsNetballAus()` when you want to discover the broader live catalogue exposed by the Champion Data `netball_aus` application before choosing a `comp_id`. + To download statistics from a single match, you use the `downloadMatch` function. As an example, the following code will download the match from round 5, game 3: ```{r get-data-function,eval=FALSE} library(dplyr) -library(superNetballR) +library(netballR) round5_game3 <- downloadMatch("10083", 5, 3) ``` @@ -43,7 +45,7 @@ The downloaded object is a list, containing detailed statistics (including perio ```{r source-data,echo=FALSE,warning=FALSE,message=FALSE} library(dplyr) -library(superNetballR) +library(netballR) data(round5_game3) ``` @@ -82,7 +84,7 @@ tidied_players # Season Data and Ladders -Provided with the `superNetballR` package is the full 2017 season match and player statistics in tidied format. These have been obtained using the previously described methods, tidied, and then combined by rows to produce a single data frame: +The package still ships with the full 2017 season match and player statistics in tidied format. These have been obtained using the previously described methods, tidied, and then combined by rows to produce a single data frame: ```{r season-2017} data(season_2017) From 77b6830a3eb300a0be23d93e0bd78ae2816675cb Mon Sep 17 00:00:00 2001 From: Craig Moyle Date: Sun, 17 May 2026 22:58:04 +1000 Subject: [PATCH 44/56] test: verify netballR rename and discovery workflow --- R/netballR-package.R | 4 +- R/shinySuperNetballR.R | 25 ----- R/superNetballR.R | 26 ----- R/zzz.R | 2 +- _pkgdown.yml | 2 + docs/articles/getting-started.html | 92 ++++++++++------- docs/articles/index.html | 14 +-- docs/authors.html | 34 ++++--- docs/index.html | 98 +++++++++++++------ docs/pkgdown.yml | 8 +- docs/reference/downloadMatch.html | 29 ++++-- docs/reference/index.html | 26 +++-- docs/reference/ladders.html | 32 ++++-- docs/reference/matchPoints.html | 14 +-- docs/reference/players_2017.html | 18 ++-- docs/reference/round5_game3.html | 14 +-- docs/reference/season_2017.html | 20 ++-- docs/reference/superNetballR.html | 8 -- docs/reference/tidyMatch.html | 18 ++-- docs/reference/tidyPlayers.html | 23 +++-- docs/sitemap.xml | 43 ++++---- inst/shiny-examples/superNetballR/global.R | 49 ---------- inst/shiny-examples/superNetballR/server.R | 12 --- .../superNetballR/team_series_module.R | 92 ----------------- inst/shiny-examples/superNetballR/ui.R | 16 --- man/downloadFixture.Rd | 9 +- man/downloadMatch.Rd | 3 +- man/netballR-package.Rd | 4 +- 28 files changed, 318 insertions(+), 417 deletions(-) delete mode 100644 R/shinySuperNetballR.R delete mode 100644 R/superNetballR.R delete mode 100644 docs/reference/superNetballR.html delete mode 100644 inst/shiny-examples/superNetballR/global.R delete mode 100644 inst/shiny-examples/superNetballR/server.R delete mode 100644 inst/shiny-examples/superNetballR/team_series_module.R delete mode 100644 inst/shiny-examples/superNetballR/ui.R diff --git a/R/netballR-package.R b/R/netballR-package.R index c2ffbb7..7b95a18 100644 --- a/R/netballR-package.R +++ b/R/netballR-package.R @@ -1,6 +1,6 @@ -#' superNetballR: Download and Tidy Super Netball Statistics +#' netballR: Download and Tidy Netball Statistics #' -#' Download Champion Data Super Netball match feeds and transform team and +#' Download Champion Data netball match feeds and transform team and #' player statistics into tidy data frames for analysis. #' #' @keywords internal diff --git a/R/shinySuperNetballR.R b/R/shinySuperNetballR.R deleted file mode 100644 index 932cfa1..0000000 --- a/R/shinySuperNetballR.R +++ /dev/null @@ -1,25 +0,0 @@ -#' Runs the demo shiny app -#' -#' \code{shinySuperNetballR} Runs the demo shiny app to compare Super Netball -#' statistics between teams. -#' -#' @return Runs a shiny app -#' -#' @export -shinySuperNetballR <- function() { - if (!requireNamespace("shiny", quietly = TRUE)) { - stop("Package 'shiny' must be installed to run shinySuperNetballR().", call. = FALSE) - } - if (!requireNamespace("ggplot2", quietly = TRUE)) { - stop("Package 'ggplot2' must be installed to run shinySuperNetballR().", call. = FALSE) - } - - my_dir <- system.file( - "shiny-examples", "superNetballR", package = "superNetballR" - ) - if (my_dir == "") { - stop("Can't find the superNetballR shiny directory. Try re-installing `superNetballR`.", call. = FALSE) - } - - shiny::runApp(my_dir, display.mode = "normal") -} diff --git a/R/superNetballR.R b/R/superNetballR.R deleted file mode 100644 index c2ffbb7..0000000 --- a/R/superNetballR.R +++ /dev/null @@ -1,26 +0,0 @@ -#' superNetballR: Download and Tidy Super Netball Statistics -#' -#' Download Champion Data Super Netball match feeds and transform team and -#' player statistics into tidy data frames for analysis. -#' -#' @keywords internal -"_PACKAGE" - -## Suppress R CMD check notes for variables used in dplyr/tidyr pipelines. -if (getRversion() >= "2.15.1") { - utils::globalVariables(c( - ## match/player column names - "squadId", "homeTeam", "period", "stat", "value", "squadName", - "squadNickname", "squadCode", "round", "game", "displayName", - "matchId", "playerId", "shortDisplayName", "firstname", "surname", - ## scoring / ladder names - "goals", "goals2", "score_diff", "points", "points_new", - "goals_for", "goals_against", "percentage", "isHome", - "games", "qtr_diff", "data", - ## period-score helper names - "homeValue", "homeSquad", "homePoints", - "awayValue", "awaySquad", "awayPoints", - "points_qtr", "game_results", - "squadId.x", "squadId.y" - )) -} diff --git a/R/zzz.R b/R/zzz.R index 74c925b..2d8f43e 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1 +1 @@ -## Nothing required here — see superNetballR.R for globalVariables declarations. +## Nothing required here — see netballR-package.R for globalVariables declarations. diff --git a/_pkgdown.yml b/_pkgdown.yml index 242283b..2e9a1f7 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -1,3 +1,5 @@ +url: https://craigmoyle.github.io/netballR/ + template: params: bootswatch: spacelab diff --git a/docs/articles/getting-started.html b/docs/articles/getting-started.html index e55d70c..79535ba 100644 --- a/docs/articles/getting-started.html +++ b/docs/articles/getting-started.html @@ -5,13 +5,13 @@ -Getting Started with superNetballR • superNetballR +Getting Started with netballR • netballR - + Articles • superNetballRArticles • netballR @@ -16,8 +16,8 @@ - superNetballR - 0.2.0 + netballR + 1.0.0
    @@ -32,11 +32,11 @@
    @@ -65,7 +65,7 @@

    All vignettes

    diff --git a/docs/authors.html b/docs/authors.html index acf6d99..f082785 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -1,5 +1,5 @@ -Authors and Citation • superNetballRAuthors and Citation • netballR @@ -16,8 +16,8 @@ - superNetballR - 0.2.0 + netballR + 1.0.0
    @@ -32,11 +32,11 @@

    Citation

    - Source: DESCRIPTION + Source: DESCRIPTION
    -

    Lane S (2026). -superNetballR: Downloads and tidies super netball statistics. -R package version 0.2.0, https://craigmoyle.github.io/superNetballR_updated. +

    Lane S, Moyle C (2026). +netballR: Download and Tidy Netball Match Statistics. +R package version 1.0.0, https://github.com/craigmoyle/netballR.

    @Manual{,
    -  title = {superNetballR: Downloads and tidies super netball statistics},
    -  author = {Steve Lane},
    +  title = {netballR: Download and Tidy Netball Match Statistics},
    +  author = {Steve Lane and Craig Moyle},
       year = {2026},
    -  note = {R package version 0.2.0},
    -  url = {https://craigmoyle.github.io/superNetballR_updated},
    +  note = {R package version 1.0.0},
    +  url = {https://github.com/craigmoyle/netballR},
     }
    @@ -87,7 +91,7 @@

    Citation

    @@ -110,7 +122,7 @@

    Details