diff --git a/.Rbuildignore b/.Rbuildignore index d43a941..3013c14 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -8,3 +8,9 @@ ^figs$ ^manuscripts$ ^\.travis\.yml$ +^doc$ +^Meta$ +^\.github$ +^.*\.Rcheck$ +^superNetballR_.*\.tar\.gz$ +^changelog\.md$ diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c974b37 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,82 @@ +# netballR — Copilot Instructions + +## Package overview + +`netballR` is an R package (R ≥ 4.1.0) that downloads Champion Data netball match feeds and transforms them into tidy data frames. It wraps the Champion Data `/data//...` HTTP API, which requires no authentication. + +## Build, test, and lint commands + +```sh +make document # regenerate docs + NAMESPACE via roxygen2 — run after any roxygen change +make test # run test suite (testthat, stops on failure) +make check # build tarball then R CMD check --as-cran +make install # install package locally +``` + +Run a single test file: +```sh +Rscript -e "testthat::test_file('tests/testthat/test-downloadMatch.R')" +``` + +After adding or renaming exported functions, always run `make document` before `make check` — NAMESPACE is generated by roxygen2 and must not be edited manually. + +## Architecture + +Two discovery pathways feed the same download workflow: + +``` +listCompetitions(source) ─┐ + listCompetitionsNetballAus() ─ "netball_aus" │ + listCompetitionsNetballNZ() ─ "netball_nz" │ + listCompetitionsEnglandNetball()─"england_netball"├─→ downloadFixture(comp_id) + listCompetitionsWorldCup(year) ─ "nwc20XX" │ ↓ + listAllCompetitions() ─ all sources ─┘ downloadMatch(comp_id, round_id, game_id) + ↓ +anzc_comp_ids (dataset) ──────────────────────────→ tidyMatch() / tidyPlayers() + ↓ + matchPoints() / ladders() # 2020+ super-shot + matchPoints_pre_2020() / ladders_pre_2020() # pre-2020 / ANZ +``` + +- All catalogue functions share the same underlying generic: `listCompetitions(source)` → `fetch_application_settings(path)` → `extract_competitions(payload, source)`. +- Source names (`"netball_aus"`, `"nwc2019"`, etc.) are friendly identifiers that map to Champion Data application paths via `.application_source_map`. `"nwc2019"` maps to `"VitalityNetballWorldCup2019"` internally. +- World Cup catalogues (`nwc2015`, `nwc2019`, `nwc2023`) return `NA` for `competition_name` — their payloads have `id` + `full_names` only. +- The same `comp_id` can appear in multiple catalogues (e.g. Constellation Cup in both `netball_aus` and `netball_nz`). `listAllCompetitions(deduplicate = TRUE)` handles this; source order in `sources` determines which row wins. + +## Key conventions + +### Naming +- **Exported functions**: `camelCase` (`downloadMatch`, `tidyMatch`, `matchPoints`, `listCompetitionsNetballAus`, `listAllCompetitions`). +- **Internal helpers**: `snake_case` (`build_application_settings_url`, `extract_competitions`, `fetch_application_settings`, `resolve_application_source`). +- **Adding a new catalogue source**: add an entry to `.application_source_map` in `R/competitions.R` and add a named wrapper function. The friendly key (e.g. `"nwc2019"`) becomes the public-facing `source` argument and appears in the `application_source` column of results. + +### Documentation +- All exported functions have roxygen2 comments with `@param`, `@return`, `@details`, and `\dontrun{}` examples. +- Column names in `@return` use `\describe{}`/`\item{}`. +- After edits to roxygen blocks, run `make document`. +- `globalVariables()` declarations for all dplyr/tidyr column names live in `R/netballR-package.R` — add new column names there to silence `R CMD check` notes. + +### HTTP pattern +All HTTP calls use `httr::RETRY("GET", ..., times = 3, pause_base = 1, terminate_on = c(400, 401, 403, 404), quiet = TRUE)` followed by `httr::stop_for_status()`. Never use bare `httr::GET`. + +### Error handling +User-facing errors use `stop(..., call. = FALSE)`. Validation helpers (`validate_identifier`, `validate_positive_whole_number`) live in `R/downloadMatch.R` and should be reused for any new ID parameters. + +### Scoring era split +- 2020+ Super Netball uses `goal_from_zone1` (1 pt) and `goal_from_zone2` (2 pts) — handled by `matchPoints()` / `ladders()`. +- Pre-2020 and ANZ Championship use `goals` — handled by `matchPoints_pre_2020()` / `ladders_pre_2020()`. +- Adding new scoring logic? Add it as a new function pair, not by changing the existing ones. + +### Pipe +Use the native pipe `|>` (R 4.1+). Do not use `%>%` from magrittr. + +### Null coalescing +The `%||%` operator is defined in `R/downloadMatch.R` (`#' @noRd`) and is available across the package. + +## Testing + +- Tests live in `tests/testthat/test-*.R`, one file per source file. +- Shared test fixtures are in `tests/testthat/helper-fixtures.R` as factory functions (`make_sample_match()`, `make_modern_match_stats()`, etc.). +- Internal (unexported) functions are tested via `netballR:::function_name` triple-colon access. +- Tests do **not** make real HTTP calls — validation is tested at the URL-builder and extractor layer. +- Use `testthat::expect_error(..., regexp)` to assert specific error messages from validation helpers. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f8bb6a5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "github-actions" + open-pull-requests-limit: 5 + commit-message: + prefix: "ci" diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..0ba8fd7 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,43 @@ +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 + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + args: 'c("--no-manual", "--as-cran")' diff --git a/.gitignore b/.gitignore index ec2f6fc..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 @@ -37,3 +37,6 @@ data-raw/* docker/* /scripts/strip-libs.sh /Rmd/sn-get-data.Rmd +doc +Meta +*.Rcheck 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 5691fa8..4fd506d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,18 +1,30 @@ -Package: superNetballR -Title: Downloads and tidies 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. -Depends: R (>= 3.4.0) +Package: netballR +Title: Download and Tidy Netball Match Statistics +Version: 0.5.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 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 LazyData: true -Suggests: knitr, +Suggests: ggplot2, + knitr, rmarkdown, - here + shiny, + testthat (>= 3.0.0) VignetteBuilder: knitr -Imports: dplyr, +Imports: dplyr (>= 1.1.0), httr, - tidyr, - purrr -RoxygenNote: 6.0.1 + purrr, + tidyr +URL: https://craigmoyle.github.io/netballR/, + 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/Makefile b/Makefile index e0fdc73..3f0f877 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_local('.', 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/NAMESPACE b/NAMESPACE index b063ed6..b824f5c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,9 +1,18 @@ # Generated by roxygen2: do not edit by hand +export(downloadFixture) export(downloadMatch) export(ladders) +export(ladders_pre_2020) +export(listAllCompetitions) +export(listCompetitions) +export(listCompetitionsEnglandNetball) +export(listCompetitionsNetballAus) +export(listCompetitionsNetballNZ) +export(listCompetitionsWorldCup) export(matchPoints) +export(matchPoints_pre_2020) export(matchResults) +export(shinyNetballR) export(tidyMatch) export(tidyPlayers) -importFrom(dplyr,"%>%") diff --git a/R/competitions.R b/R/competitions.R new file mode 100644 index 0000000..8154a59 --- /dev/null +++ b/R/competitions.R @@ -0,0 +1,308 @@ +## Named mapping from user-facing source identifiers to Champion Data application paths. +## Use this to add new sources without changing the public API. +.application_source_map <- c( + netball_aus = "netball_aus", + netball_nz = "netball_nz", + england_netball = "england_netball", + nwc2015 = "nwc2015", + nwc2019 = "VitalityNetballWorldCup2019", + nwc2023 = "nwc2023" +) + +resolve_application_source <- function(source) { + if (source %in% names(.application_source_map)) { + return(.application_source_map[[source]]) + } + stop( + "'", source, "' is not a recognised application source. ", + "Supported sources: ", paste(names(.application_source_map), collapse = ", "), ".", + call. = FALSE + ) +} + +build_application_settings_url <- function(application_path) { + sprintf( + "https://mc.championdata.com/%s/settings/application_settings.json", + application_path + ) +} + +fetch_application_settings <- function(application_path) { + dat <- httr::RETRY( + "GET", + build_application_settings_url(application_path), + 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_competitions <- function(payload, source) { + competitions <- payload$competitionList$competition + if (is.null(competitions) || length(competitions) == 0L) { + stop( + "'", source, "' application settings did not include competitionList$competition.", + call. = FALSE + ) + } + + ## Normalise: a single competition may parse as a named list rather than a + ## list-of-lists. Wrap it so lapply always iterates over competition entries. + if (!is.list(competitions[[1]])) { + competitions <- list(competitions) + } + + rows <- lapply(competitions, function(comp) { + id <- as.integer(comp$id %||% NA_integer_) + if (is.na(id)) { + return(NULL) + } + dplyr::tibble( + comp_id = id, + competition_name = as.character(comp$competition_name %||% NA_character_), + application_source = source, + 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[!vapply(rows, is.null, logical(1L))]) +} + +#' List competitions from a Champion Data application catalogue +#' +#' \code{listCompetitions()} downloads the public application settings for a +#' named Champion Data iStats catalogue and returns a tidy tibble of +#' discoverable competitions. +#' +#' @param source A string naming the application catalogue. Supported values: +#' \describe{ +#' \item{\code{"netball_aus"}}{Current Australian competitions including +#' Super Netball and Australian Diamonds internationals.} +#' \item{\code{"netball_nz"}}{New Zealand competitions including the NZ +#' National Netball League (ANE) and Silver Ferns internationals.} +#' \item{\code{"england_netball"}}{England Netball competitions.} +#' \item{\code{"nwc2015"}}{Netball World Cup 2015.} +#' \item{\code{"nwc2019"}}{Vitality Netball World Cup 2019.} +#' \item{\code{"nwc2023"}}{Netball World Cup 2023.} +#' } +#' @return A \code{\link[dplyr]{tibble}} with one row per competition and +#' columns: +#' \describe{ +#' \item{comp_id}{Champion Data competition identifier.} +#' \item{competition_name}{Competition name, or \code{NA} for World Cup +#' catalogues which do not include names in their application settings.} +#' \item{application_source}{The \code{source} value supplied, identifying +#' which catalogue the row came from.} +#' \item{season}{Season year when available.} +#' \item{competition_type}{Competition type when available.} +#' \item{squad_id}{Optional squad filter.} +#' \item{application_logo}{Relative logo path when available.} +#' } +#' @details +#' The same \code{comp_id} can appear in more than one catalogue +#' (e.g. international competitions may be listed by both \code{netball_aus} +#' and \code{netball_nz}). Use \code{\link{listAllCompetitions}} to query +#' multiple catalogues at once and deduplicate by \code{comp_id}. +#' +#' All returned \code{comp_id} values are compatible with +#' \code{\link{downloadFixture}} and \code{\link{downloadMatch}}. +#' @examples +#' \dontrun{ +#' listCompetitions("netball_nz") +#' listCompetitions("england_netball") +#' listCompetitions("nwc2023") +#' } +#' @seealso \code{\link{listAllCompetitions}}, \code{\link{listCompetitionsNetballAus}}, +#' \code{\link{listCompetitionsNetballNZ}}, \code{\link{listCompetitionsEnglandNetball}}, +#' \code{\link{listCompetitionsWorldCup}} +#' @export +listCompetitions <- function(source) { + path <- resolve_application_source(source) + extract_competitions(fetch_application_settings(path), source) +} + +#' 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 currently discoverable competitions. +#' +#' @return A \code{\link[dplyr]{tibble}} with one row per competition. See +#' \code{\link{listCompetitions}} for column descriptions. +#' @details +#' The live catalogue includes Super Netball, Australian Diamonds +#' internationals, and other Australian competitions. +#' +#' For historical ANZ Championship and NZ National Netball League IDs, use +#' \code{\link{anzc_comp_ids}} instead. +#' @examples +#' \dontrun{ +#' comps <- listCompetitionsNetballAus() +#' subset(comps, grepl("Diamonds", competition_name, ignore.case = TRUE)) +#' fixture <- downloadFixture(comps$comp_id[[1]]) +#' } +#' @seealso \code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +#' @export +listCompetitionsNetballAus <- function() { + listCompetitions("netball_aus") +} + +#' List competitions from the Champion Data netball_nz application +#' +#' \code{listCompetitionsNetballNZ()} returns competitions from the New Zealand +#' catalogue, including the NZ National Netball League (ANE), Silver Ferns +#' internationals, and domestic NZ competitions. +#' +#' @return A \code{\link[dplyr]{tibble}} with one row per competition. See +#' \code{\link{listCompetitions}} for column descriptions. +#' @details +#' The same competition may appear in both \code{netball_nz} and +#' \code{netball_aus} catalogues (e.g. Constellation Cup). Use +#' \code{\link{listAllCompetitions}} to deduplicate across sources. +#' @examples +#' \dontrun{ +#' listCompetitionsNetballNZ() +#' } +#' @seealso \code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +#' @export +listCompetitionsNetballNZ <- function() { + listCompetitions("netball_nz") +} + +#' List competitions from the Champion Data england_netball application +#' +#' \code{listCompetitionsEnglandNetball()} returns competitions from the England +#' Netball catalogue, including Vitality Roses internationals and domestic +#' England competitions. +#' +#' @return A \code{\link[dplyr]{tibble}} with one row per competition. See +#' \code{\link{listCompetitions}} for column descriptions. +#' @examples +#' \dontrun{ +#' listCompetitionsEnglandNetball() +#' } +#' @seealso \code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +#' @export +listCompetitionsEnglandNetball <- function() { + listCompetitions("england_netball") +} + +#' List competitions from a Netball World Cup application catalogue +#' +#' \code{listCompetitionsWorldCup()} returns competitions from the specified +#' Netball World Cup Champion Data catalogue. +#' +#' @param year Integer. The World Cup year. Must be one of \code{2015}, +#' \code{2019}, or \code{2023}. +#' @return A \code{\link[dplyr]{tibble}} with one row per competition. See +#' \code{\link{listCompetitions}} for column descriptions. +#' @details +#' World Cup catalogues do not include \code{competition_name} in their +#' application settings, so that column will be \code{NA}. The +#' \code{application_source} column (\code{"nwc2015"}, \code{"nwc2019"}, or +#' \code{"nwc2023"}) identifies the catalogue. +#' @examples +#' \dontrun{ +#' listCompetitionsWorldCup(2023) +#' listCompetitionsWorldCup(2019) +#' } +#' @seealso \code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +#' @export +listCompetitionsWorldCup <- function(year) { + valid_years <- c(2015L, 2019L, 2023L) + year <- validate_positive_whole_number(year, "year") + if (!year %in% valid_years) { + stop( + "'year' must be one of: ", paste(valid_years, collapse = ", "), ".", + call. = FALSE + ) + } + source <- c("2015" = "nwc2015", "2019" = "nwc2019", "2023" = "nwc2023")[[as.character(year)]] + listCompetitions(source) +} + +#' List competitions from all known Champion Data application catalogues +#' +#' \code{listAllCompetitions()} queries multiple Champion Data application +#' catalogues and returns a combined tidy tibble. +#' +#' @param sources Character vector of source identifiers to query. Defaults to +#' all known sources. See \code{\link{listCompetitions}} for supported values. +#' @param deduplicate Logical. If \code{TRUE} (default), rows with duplicate +#' \code{comp_id} values are removed, keeping the first occurrence based on +#' the order of \code{sources}. Set to \code{FALSE} to retain all rows and +#' inspect cross-catalogue coverage via \code{application_source}. +#' @param on_error One of \code{"warn"} (default), \code{"error"}, or +#' \code{"ignore"}. Controls behaviour when fetching a single source fails. +#' \code{"warn"} issues a warning and continues; \code{"error"} stops +#' immediately; \code{"ignore"} silently skips the failed source. An error is +#' always raised if every source fails. +#' @return A \code{\link[dplyr]{tibble}} with one row per competition (after +#' optional deduplication). Includes all columns from +#' \code{\link{listCompetitions}}. +#' @details +#' International competitions may appear in more than one catalogue. When +#' \code{deduplicate = TRUE}, the first occurrence wins, so source ordering +#' in \code{sources} determines which \code{application_source} label is +#' retained for shared \code{comp_id} values. +#' +#' Set \code{deduplicate = FALSE} to inspect which catalogues list a given +#' competition: +#' \preformatted{ +#' all <- listAllCompetitions(deduplicate = FALSE) +#' all[all$comp_id == 9315, c("comp_id", "competition_name", "application_source")] +#' } +#' @examples +#' \dontrun{ +#' ## Deduplicated (default) +#' listAllCompetitions() +#' +#' ## Full cross-catalogue view +#' listAllCompetitions(deduplicate = FALSE) +#' +#' ## Subset of sources +#' listAllCompetitions(sources = c("netball_aus", "netball_nz")) +#' } +#' @seealso \code{\link{listCompetitions}} +#' @export +listAllCompetitions <- function( + sources = names(.application_source_map), + deduplicate = TRUE, + on_error = c("warn", "error", "ignore") +) { + on_error <- match.arg(on_error) + + results <- lapply(sources, function(src) { + tryCatch( + listCompetitions(src), + error = function(e) { + msg <- paste0( + "Failed to fetch competitions from '", src, "': ", conditionMessage(e) + ) + if (on_error == "error") stop(msg, call. = FALSE) + if (on_error == "warn") warning(msg, call. = FALSE) + NULL + } + ) + }) + + non_null <- results[!vapply(results, is.null, logical(1L))] + if (length(non_null) == 0L) { + stop("Failed to fetch competitions from all sources.", call. = FALSE) + } + + out <- dplyr::bind_rows(non_null) + + if (deduplicate) { + out <- dplyr::distinct(out, comp_id, .keep_all = TRUE) + } + + out +} diff --git a/R/data.R b/R/data.R index ed85300..e6aeb9a 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}{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} @@ -22,15 +22,17 @@ #' 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} +#' \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{value}{Character representation of the statistic value} #' \item{round}{Round number of the match} #' \item{game}{Game number of the match} #' } @@ -43,3 +45,68 @@ #' #' @format A list. "round5_game3" + +#' Historical ANZ Championship and NZ National Netball League competition IDs. +#' +#' A historical lookup dataset mapping Champion Data \code{comp_id} values to +#' the corresponding netball season and competition, covering every season from +#' 2008 to 2025. +#' +#' @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 +#' \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 +#' Use \code{anzc_comp_ids} as a historical lookup when you already know you +#' need ANZ Championship (2008--2016) or NZ National Netball League +#' (2017--present) competition IDs. For current / active Super Netball seasons, +#' Australian Diamonds internationals, and other broader Australian +#' competitions exposed by the live Champion Data application, use +#' \code{\link{listCompetitionsNetballAus}} instead. +#' +#' 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. +#' +#' @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 +#' 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 +#' competition teams, plus the historical Magpies entry used by the bundled +#' 2017 data. +#' +#' @format A data frame with 9 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/R/downloadMatch.R b/R/downloadMatch.R index 8b86f53..5d3a1fc 100644 --- a/R/downloadMatch.R +++ b/R/downloadMatch.R @@ -1,38 +1,228 @@ +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 + ) +} + +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)) { + stop("Champion Data response did not include matchStats.", call. = FALSE) + } + + 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) +} + +#' @noRd +`%||%` <- 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 +#' @param comp_id A string identifying which season or competition the game is #' in. \code{comp_id} is different depending on regular season or finals. +#' Use \code{\link{anzc_comp_ids}} as a historical lookup for ANZ +#' Championship competition IDs, or +#' \code{\link{listCompetitionsNetballAus}} for the broader live +#' Australian catalogue, including active Super Netball competitions and +#' Australian Diamonds internationals 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 #' 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. +#' +#' Once a \code{comp_id} is known, ANZ Championship matches use the same data +#' format as Super Netball and can be downloaded with the same function by +#' supplying the appropriate identifier. 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 after finding the +#' relevant \code{comp_id} through \code{\link{anzc_comp_ids}} or +#' \code{\link{listCompetitionsNetballAus}}. #' #' @examples #' \dontrun{ +#' ## Super Netball (discover current comp_ids via listCompetitionsNetballAus()) #' downloadMatch("10083", 1, 1) +#' +#' ## ANZ Championship (historical comp_id from anzc_comp_ids) +#' downloadMatch("10088", 1, 1) #' } #' #' @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 <- 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 ) - pg <- paste0( - "https://mc.championdata.com/data/", - comp_id, - "/", - comp_id, - r_id, - paste0("0", as.character(game_id)), - ".json" + httr::stop_for_status(dat) + extract_match_stats(httr::content( + dat, + as = "parsed", + 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. Use +#' \code{\link{anzc_comp_ids}} as a historical lookup for ANZ Championship +#' competition IDs or \code{\link{listCompetitionsNetballAus}} for the +#' broader live Australian catalogue, including active Super Netball +#' competitions and Australian Diamonds internationals 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.} +#' \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}}. Use +#' \code{\link{listCompetitionsNetballAus}} for current / active Australian +#' competitions and \code{\link{anzc_comp_ids}} for historical ANZ +#' Championship or NZ National Netball League IDs. +#' +#' 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 (historical comp_id from anzc_comp_ids) +#' downloadFixture("10088") +#' +#' ## Super Netball (discover current comp_ids via listCompetitionsNetballAus()) +#' 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 ) - dat <- httr::GET(pg) - dat_list <- httr::content(dat, "parsed")$matchStats - dat_list + 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 425937d..1a06257 100644 --- a/R/ladders.R +++ b/R/ladders.R @@ -1,3 +1,25 @@ +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. @@ -5,51 +27,99 @@ #' @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. 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 #' 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. 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 +#' 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) { - 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)), - 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(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)), + .groups = "drop" + ) + 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 <- 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() - df + df |> + group_match_data() |> + tidyr::nest() |> + dplyr::mutate(game_results = purrr::map(data, matchPoints)) |> + dplyr::select(-data) |> + tidyr::unnest(cols = c(game_results)) +} + +matchResults_pre_2020 <- function(df) { + df |> + group_match_data() |> + tidyr::nest() |> + dplyr::mutate(game_results = purrr::map(data, matchPoints_pre_2020)) |> + dplyr::select(-data) |> + tidyr::unnest(cols = c(game_results)) +} + +#' @rdname ladders +#' @export +ladders_pre_2020 <- function(df, round_num = NULL, game_num = NULL, old_system = FALSE) { + 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 46b6f86..209a734 100644 --- a/R/matchPoints.R +++ b/R/matchPoints.R @@ -5,76 +5,136 @@ #' @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. - 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) %>% + home <- df |> + dplyr::filter(stat == "homeTeam") |> + dplyr::select(-period) |> + dplyr::distinct() + 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) |> + 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") |> + dplyr::mutate( + goals = dplyr::coalesce(goals, 0), + goals2 = dplyr::coalesce(goals2, 0), + goals = goals + goals2 + ) |> + dplyr::select(-goals2) + + 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 |> + dplyr::mutate( + score_diff = goals - rev(goals), + points = dplyr::case_when( + score_diff > 0 ~ 4, + score_diff < 0 ~ 0, + .default = 2 + ) + ) |> + dplyr::rename(isHome = value) |> + dplyr::select(-stat) +} + +#' 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. +#' @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) { + 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::select(-period) |> dplyr::distinct() - goals <- dplyr::left_join(goals, home, by = "squadName") %>% + goals <- dplyr::left_join(goals, home, by = "squadName") |> dplyr::arrange(value) - score_diff <- diff(goals[['goals']]) - 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), - 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 %>% - dplyr::filter(stat == "goals") - homeScores <- goals_new %>% - dplyr::filter(squadName == home[['squadName']][home[['value']] == 1]) %>% + ## Quarter-points bonus (new system) + goals_new <- df |> + dplyr::filter(stat == "goals", period <= 4) + 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) %>% - dplyr::summarise(homePoints = sum(homePoints), - awayPoints = sum(awayPoints)) %>% - dplyr::ungroup() - df1 <- points_new %>% - dplyr::select(dplyr::contains("home")) %>% + 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")) |> 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/netballR-package.R b/R/netballR-package.R new file mode 100644 index 0000000..5cff90b --- /dev/null +++ b/R/netballR-package.R @@ -0,0 +1,44 @@ +#' netballR: Download and Tidy Netball Statistics +#' +#' @description +#' Download Champion Data netball match feeds and transform team and player +#' statistics into tidy data frames for analysis. +#' +#' Current / active Australian coverage includes Super Netball plus Australian +#' Diamonds international matches and other competitions discoverable through +#' \code{\link{listCompetitionsNetballAus}}. +#' +#' @details +#' Use \code{\link{listCompetitionsNetballAus}} to discover live competition +#' IDs exposed by the Champion Data \code{netball_aus} application, including +#' active Super Netball seasons and Australian Diamonds internationals when +#' they appear in the live catalogue. +#' +#' Use \code{\link{anzc_comp_ids}} as a historical lookup for ANZ Championship +#' and NZ National Netball League competition IDs. +#' +#' Once you know a \code{comp_id}, use \code{\link{downloadFixture}} and +#' \code{\link{downloadMatch}} to retrieve data from the shared Champion Data +#' \code{/data/...} transport. +#' +#' @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/R/superNetballR.R b/R/superNetballR.R deleted file mode 100644 index d6964f2..0000000 --- a/R/superNetballR.R +++ /dev/null @@ -1,15 +0,0 @@ -#' \code{superNetballR} package -#' -#' Functions getting and manipulating Super Netball data. -#' -#' @docType package -#' @name superNetballR -#' @importFrom dplyr %>% -NULL - -## 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")) -} diff --git a/R/tidiers.R b/R/tidiers.R index a8c3315..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) { @@ -24,13 +26,17 @@ 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) %>% - tidyr::gather(stat, value, -squadId, -squadName, - -squadNickname, -squadCode, -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 + game = match$matchInfo$matchNumber, + matchId = match$matchInfo$matchId ) team_stats } @@ -41,7 +47,12 @@ 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. #' #' @export tidyPlayers <- function(match) { @@ -50,16 +61,35 @@ 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) + 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) %>% + player_stats <- player_stats |> + dplyr::filter(period <= final_period) |> + dplyr::select(-displayName) |> + tidyr::pivot_longer( + cols = -c( + playerId, shortDisplayName, firstname, surname, + period, squadId, squadName + ), + names_to = "stat", + values_to = "value", + values_transform = list(value = as.character) + ) |> dplyr::mutate( round = match$matchInfo$roundNumber, - game = match$matchInfo$matchNumber - ) + game = match$matchInfo$matchNumber, + matchId = match$matchInfo$matchId + ) player_stats } diff --git a/R/zzz.R b/R/zzz.R index 97d0e88..2d8f43e 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 netballR-package.R for globalVariables declarations. diff --git a/README.md b/README.md index 249b886..a143e83 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,135 @@ -# superNetballR +# netballR -[![Build Status](https://travis-ci.org/SteveLane/superNetballR.svg?branch=master)](https://travis-ci.org/SteveLane/superNetballR) +[![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 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. +`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 supports two complementary competition-discovery pathways that feed the same download workflow: + +1. use the Champion Data application catalogue functions to discover current competitions: + - `listCompetitionsNetballAus()` — Super Netball, Australian Diamonds internationals, and other Australian competitions + - `listCompetitionsNetballNZ()` — NZ National Netball League, Silver Ferns internationals, and domestic NZ competitions + - `listCompetitionsEnglandNetball()` — England Netball competitions + - `listCompetitionsWorldCup(year)` — Netball World Cup catalogues for 2015, 2019, and 2023 + - `listCompetitions(source)` — generic function for any named Champion Data catalogue + - `listAllCompetitions()` — query all catalogues at once, with optional deduplication +2. use `anzc_comp_ids` as a historical lookup for ANZ Championship and NZ National Netball League competition IDs + +Current / active Australian coverage includes Super Netball plus Australian Diamonds international matches and other competitions surfaced by the live `netball_aus` catalogue. Historical coverage includes ANZ Championship and NZ National Netball League workflows. + +Once you know a `comp_id`, use `downloadFixture()` and `downloadMatch()` to retrieve data from the Champion Data `/data//...` feed. ## 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 +install.packages("remotes") +remotes::install_github("craigmoyle/netballR") +``` + +To install the current `main` branch explicitly: + +```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 supported by the Champion Data `/data/...` feed. +- `listCompetitions(source)` returns competitions from any named Champion Data application catalogue. Named convenience wrappers exist for each catalogue (`listCompetitionsNetballAus()`, `listCompetitionsNetballNZ()`, `listCompetitionsEnglandNetball()`, `listCompetitionsWorldCup(year)`). +- `listAllCompetitions()` queries all known catalogues at once. The same competition can appear in multiple catalogues (e.g. the Constellation Cup appears in both `netball_aus` and `netball_nz`); `deduplicate = TRUE` (default) keeps one row per `comp_id`. +- `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. +- `tidyMatch()` and `tidyPlayers()` append the Champion Data `matchId` so combined live tidy outputs can keep distinct matches separate. -``` R -devtools::install_github("stevelane/superNetballR") +## Discover competitions across all catalogues + +Use `listAllCompetitions()` to query every known Champion Data catalogue in one call. Duplicate `comp_id` values (international competitions listed by multiple countries) are removed by default, with the first-matched source taking precedence. + +```r +library(netballR) + +## All competitions, deduplicated +all_comps <- listAllCompetitions() + +## Inspect cross-catalogue duplicates +all_comps_raw <- listAllCompetitions(deduplicate = FALSE) +all_comps_raw[all_comps_raw$comp_id == 9315, c("comp_id", "competition_name", "application_source")] ``` -To install the development version: +## Discover competitions by region or tournament + +```r +library(netballR) -``` R -devtools::install_github("stevelane/superNetballR@develop") +## New Zealand competitions +listCompetitionsNetballNZ() + +## England Netball competitions +listCompetitionsEnglandNetball() + +## Netball World Cup catalogues (2015, 2019, or 2023) +listCompetitionsWorldCup(2023) + +## Any named catalogue +listCompetitions("nwc2019") ``` +## Discover current competitions from `netball_aus` + +Use `listCompetitionsNetballAus()` to inspect the live competition catalogue exposed by the Champion Data `netball_aus` application. This is the recommended discovery path for current / active Super Netball seasons, Australian Diamonds international matches, and other broader Australian competitions. + +```r +library(netballR) + +competitions <- listCompetitionsNetballAus() +competitions + +subset(competitions, grepl("Diamonds", competition_name, ignore.case = TRUE)) +``` + +The returned `comp_id` values work directly with `downloadFixture()` and `downloadMatch()`. + +```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) +``` + +## Historical competition lookups with `anzc_comp_ids` + +`anzc_comp_ids` is a historical lookup dataset for ANZ Championship and NZ National Netball League workflows. Super Netball is current coverage and should be discovered through `listCompetitionsNetballAus()` when you need active competition IDs. + +```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 + +Local developer commands are available through the `Makefile`: + +```sh +make test +make build +make check +``` + +## Notes + +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 7a341e1..2e9a1f7 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -1,13 +1,17 @@ +url: https://craigmoyle.github.io/netballR/ + template: params: bootswatch: spacelab 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/netballR/ diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..46b7ec4 --- /dev/null +++ b/changelog.md @@ -0,0 +1,114 @@ +# Changelog + +All notable changes in this project are documented here. + +This changelog covers the changes introduced in `craigmoyle/netballR` since the fork diverged from the original `SteveLane/superNetballR` (historical upstream) project published at . + +## 0.5.0 - 2026-05-21 + +Multi-source competition discovery across all major Champion Data netball catalogues. + +### Added + +- `listCompetitions(source)` — generic function that queries any named Champion Data application catalogue. Supported sources: `"netball_aus"`, `"netball_nz"`, `"england_netball"`, `"nwc2015"`, `"nwc2019"`, `"nwc2023"`. +- `listCompetitionsNetballNZ()` — NZ National Netball League, Silver Ferns internationals, and domestic NZ competitions from the `netball_nz` catalogue. +- `listCompetitionsEnglandNetball()` — England Netball competitions from the `england_netball` catalogue. +- `listCompetitionsWorldCup(year)` — Netball World Cup competitions for 2015, 2019, or 2023. World Cup catalogues do not include `competition_name`; that column is `NA` and `application_source` identifies the catalogue. +- `listAllCompetitions(sources, deduplicate, on_error)` — queries all known catalogues in one call. `deduplicate = TRUE` (default) removes rows with duplicate `comp_id` values, keeping the first occurrence in `sources` order. This handles international competitions (e.g. Constellation Cup, Quad Series) that appear in more than one catalogue. `on_error = "warn"` (default) warns on single-source failures and continues; `"error"` stops immediately; `"ignore"` skips silently. Always errors if all sources fail. + +### Changed + +- `listCompetitionsNetballAus()` is now a thin wrapper around the new generic `listCompetitions("netball_aus")`. Behaviour and output schema are unchanged. +- Internal URL building, HTTP fetching, and payload extraction have been consolidated into generic helpers (`build_application_settings_url`, `fetch_application_settings`, `extract_competitions`) driven by a central `.application_source_map`. Adding a new Champion Data catalogue requires only a map entry and a named wrapper. +- `extract_competitions()` now silently drops competition entries that have no `id` field, and normalises single-entry catalogues that parse as a named list rather than a list-of-lists. + +## 0.4.0 - 2026-05-18 + +- 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. +- Refreshed README, vignette, and reference docs so Super Netball is described as current coverage, ANZ Championship as historical coverage, and Australian Diamonds internationals as discoverable through `listCompetitionsNetballAus()`. + +## 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 into a single package-level 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. + +### 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. + +### 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/data/anzc_comp_ids.rda b/data/anzc_comp_ids.rda new file mode 100644 index 0000000..ffdc633 Binary files /dev/null and b/data/anzc_comp_ids.rda differ diff --git a/data/players_2017.rda b/data/players_2017.rda index bc1a595..e65a4bb 100644 Binary files a/data/players_2017.rda and b/data/players_2017.rda differ diff --git a/data/season_2017.rda b/data/season_2017.rda index 2edd04f..9b7c68d 100644 Binary files a/data/season_2017.rda and b/data/season_2017.rda differ diff --git a/data/team_colours.rda b/data/team_colours.rda new file mode 100644 index 0000000..e1e9990 Binary files /dev/null and b/data/team_colours.rda differ diff --git a/docs/articles/getting-started.html b/docs/articles/getting-started.html index fd4027b..3100ec7 100644 --- a/docs/articles/getting-started.html +++ b/docs/articles/getting-started.html @@ -1,67 +1,64 @@ - + -Getting Started with superNetballR • superNetballR - - - - - - - + + + + + + - + + +
@@ -89,133 +88,228 @@

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 +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.

-
-

-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 Super Netball home-and-away competition ID +(10083) and the 2017 finals competition ID +(10084). The full processed 2017 datasets are supplied with +this package, while current Super Netball seasons, Australian Diamonds +internationals, and other live Australian competitions can be queried +with the appropriate competition IDs discovered from the current +netball_aus 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.

+
+

Competition discovery pathways +

+

Use listCompetitionsNetballAus() for current / active +Australian competitions exposed by the Champion Data +netball_aus application, including Super Netball and +Australian Diamonds internationals when they are present in the live +catalogue.

+

Use anzc_comp_ids as a historical lookup for ANZ +Championship and NZ National Netball League workflows.

+

Both discovery pathways lead into the same +downloadFixture() / downloadMatch() workflow +once you know the relevant comp_id.

+
+competitions <- listCompetitionsNetballAus()
+subset(competitions, grepl("Diamonds", competition_name, ignore.case = TRUE))
+
+anzc_comp_ids
+

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:

+
+library(dplyr)
+library(netballR)
+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:

-
-
-

-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.

+
+

Tidying Match and Player Statistics +

+

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:

+
+tidied_match <- tidyMatch(round5_game3)
+tidied_match
+#> # A tibble: 256 × 10
+#>    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
+#> # ℹ 1 more variable: matchId <int>
+
+# 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:

+
+tidied_players <- tidyPlayers(round5_game3)
+tidied_players
+#> # A tibble: 2,560 × 12
+#>    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
+#> # ℹ 4 more variables: value <chr>, round <int>, game <int>, matchId <int>
+
+
+

Season Data and Ladders +

+

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:

+
+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)
+#> Warning: There were 60 warnings in `dplyr::mutate()`.
+#> The first warning was:
+#>  In argument: `game_results = purrr::map(data, matchPoints)`.
+#>  In group 1: `round = 1`, `game = 1`.
+#> Caused by 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?
+#>  Run `dplyr::last_dplyr_warnings()` to see the 59 remaining warnings.
+ladder
+#> # A tibble: 8 × 6
+#>   squadName                games goals_for goals_against percentage points
+#>   <chr>                    <int>     <dbl>         <dbl>      <dbl>  <int>
+#> 1 Adelaide Thunderbirds       14         0             0        Inf     28
+#> 2 GIANTS Netball              14         0             0        Inf     28
+#> 3 Magpies Netball             14         0             0        Inf     28
+#> 4 Melbourne Vixens            14         0             0        Inf     28
+#> 5 NSW Swifts                  14         0             0        Inf     28
+#> 6 Queensland Firebirds        14         0             0        Inf     28
+#> 7 Sunshine Coast Lightning    14         0             0        Inf     28
+#> 8 West Coast Fever            14         0             0        Inf     28
+

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..2d28d3e 100644 --- a/docs/articles/index.html +++ b/docs/articles/index.html @@ -1,109 +1,53 @@ - - - - - - - -Articles • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Articles • netballR + - - -
-
- - -
-
+ +
+
Getting Started with netballR
+
+
-
- +
+ + + + - - - + diff --git a/docs/authors.html b/docs/authors.html index ce44587..fe26bb3 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -1,140 +1,109 @@ - - - - - - - -Authors • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Authors and Citation • netballR - + - - -
-
-
-
+ +
-
-
- +
+ + + + - - - + 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..e4e53be 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,67 +1,65 @@ - + -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.

    -

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

    +
    + +

    R-CMD-check

    +
    +

    Description +

    +

    netballR provides tools to discover netball competitions, download Champion Data match feeds, and transform team and player statistics into tidy data for analysis.

    +

    This package supports two complementary competition-discovery pathways that feed the same download workflow:

    +
      +
    1. use listCompetitionsNetballAus() to discover current competitions exposed by the Champion Data netball_aus iStats application
    2. +
    3. use anzc_comp_ids as a historical lookup for ANZ Championship and NZ National Netball League competition IDs
    4. +
    +

    Current / active Australian coverage includes Super Netball plus Australian Diamonds international matches and other competitions surfaced by the live netball_aus catalogue. Historical coverage includes ANZ Championship and NZ National Netball League workflows.

    +

    Once you know a comp_id, use downloadFixture() and downloadMatch() to retrieve data from the Champion Data /data/<comp_id>/... feed.

    +
    +
    +

    Installation +

    +

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

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

    To install the current main branch explicitly:

    +
    +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 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.
    • +
    • +tidyMatch() and tidyPlayers() append the Champion Data matchId so combined live tidy outputs can keep distinct matches separate.
    • +
    +
    +
    +

    Discover current competitions from netball_aus + +

    +

    Use listCompetitionsNetballAus() to inspect the live competition catalogue exposed by the Champion Data netball_aus application. This is the recommended discovery path for current / active Super Netball seasons, Australian Diamonds international matches, and other broader Australian competitions.

    +
    +library(netballR)
    +
    +competitions <- listCompetitionsNetballAus()
    +competitions
    +
    +subset(competitions, grepl("Diamonds", competition_name, ignore.case = TRUE))
    +

    The returned comp_id values work directly with downloadFixture() and downloadMatch().

    +
    +library(netballR)
    +
    +competitions <- listCompetitionsNetballAus()
    +comp_id <- competitions$comp_id[[1]]
    +
    +fixture <- downloadFixture(comp_id)
    +match <- downloadMatch(comp_id, 1, 1)
    +match_stats <- tidyMatch(match)
    +
    +
    +

    Historical competition lookups with anzc_comp_ids + +

    +

    anzc_comp_ids is a historical lookup dataset for ANZ Championship and NZ National Netball League workflows. Super Netball is current coverage and should be discovered through listCompetitionsNetballAus() when you need active competition IDs.

    +
    +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 +

    +

    Local developer commands are available through the Makefile:

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

    Notes +

    +

    Champion Data’s public netball_aus site still loads fixture and match JSON from /data/<comp_id>/..., so netballR uses netball_aus for competition discovery and /data/... for actual fixture/match downloads.

    - - + + + + 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..7b34c1b 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -1,6 +1,9 @@ -pandoc: 2.2.1 -pkgdown: 1.1.0 +pandoc: 3.9.0.2 +pkgdown: 2.2.0 pkgdown_sha: ~ articles: getting-started: getting-started.html - +last_built: 2026-05-17T23:33Z +urls: + reference: https://craigmoyle.github.io/netballR/reference + article: https://craigmoyle.github.io/netballR/articles diff --git a/docs/reference/downloadMatch.html b/docs/reference/downloadMatch.html index 6c433e0..1b72d58 100644 --- a/docs/reference/downloadMatch.html +++ b/docs/reference/downloadMatch.html @@ -1,186 +1,147 @@ - - - - - - - -Download data from a single match — downloadMatch • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Download data from a single match — downloadMatch • netballR + - - -
    -
    - - -
    -
    + +
    -

    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 or competition the game is +in. comp_id is different depending on regular season or finals. +Use anzc_comp_ids as a historical lookup for ANZ +Championship competition IDs, or +listCompetitionsNetballAus for the broader live +Australian catalogue, including active Super Netball competitions and +Australian Diamonds internationals exposed by the Champion Data +netball_aus application.

    + + +
    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.

    +

    Once a comp_id is known, ANZ Championship matches use the same data +format as Super Netball and can be downloaded with the same function by +supplying the appropriate identifier. Because ANZ Championship matches do +not use the super-shot scoring zone, use ladders_pre_2020 +(and matchPoints_pre_2020) when calculating standings for ANZ +Championship data. Use downloadFixture to discover the rounds +and game numbers available for a given competition after finding the +relevant comp_id through anzc_comp_ids or +listCompetitionsNetballAus.

    +
    +
    +

    Examples

    +
    if (FALSE) { # \dontrun{
    +## Super Netball (discover current comp_ids via listCompetitionsNetballAus())
    +downloadMatch("10083", 1, 1)
    +
    +## ANZ Championship (historical comp_id from anzc_comp_ids)
    +downloadMatch("10088", 1, 1)
    +} # }
    +
    +
    +
    +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/index.html b/docs/reference/index.html index 96282bc..a33c322 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -1,210 +1,140 @@ - - - - - - - -Function reference • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Package index • netballR - + - -
    -
    - - -
    -
    + +
    - - - - - - - - - - -
    -

    All functions

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

    All functions

    +
    +

    anzc_comp_ids

    +

    Historical ANZ Championship and NZ National Netball League competition IDs.

    +

    downloadFixture()

    +

    Download the fixture for a competition

    downloadMatch()

    Download data from a single match

    -

    ladders() matchResults()

    +
    +

    ladders() matchResults() ladders_pre_2020()

    Calculates ladder positions

    +
    +

    listCompetitionsNetballAus()

    +

    List competitions from the Champion Data netball_aus application

    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

    +
    +

    shinyNetballR()

    +

    Runs the demo shiny app

    +

    team_colours

    superNetballR package

    +

    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..53f530b 100644 --- a/docs/reference/ladders.html +++ b/docs/reference/ladders.html @@ -1,184 +1,140 @@ - - - - - - - -Calculates ladder positions — ladders • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Calculates ladder positions — ladders • netballR - + - -
    -
    - - -
    -
    + +
    -

    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. For ladders(), retained for compatibility +and ignored (2020+ scoring always applies). For +ladders_pre_2020(), if TRUE sorts the ladder by the +legacy 2-point win system (points); if FALSE (default) +sorts by the updated 4-point win system (points_new).

    + +
    +
    +

    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. Legacy ladders break ties on percentage after +ordering by either points_new or points.

    +

    When a tidy input data frame includes matchId, the internal +match-result helpers use it as the grouping key; otherwise they fall back to +the legacy round/game grouping used by bundled datasets.

    +

    ANZ Championship: ANZ Championship matches record scores in the +goals statistic rather than the goal_from_zone1 / +goal_from_zone2 statistics used by the 2020+ Super Netball super-shot +era. Use ladders_pre_2020 (and +matchPoints_pre_2020) for all ANZ Championship seasons.

    +
    +
    -
    - +
    + + + + - - - + diff --git a/docs/reference/matchPoints.html b/docs/reference/matchPoints.html index 72782ce..afa1c0f 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 • netballR - + - -
    -
    - - -
    -
    + +
    -

    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..df12605 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 • netballR - + - -
    -
    - - -
    -
    + +
    -

    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..0e2184a 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 • netballR - + - -
    -
    - - -
    -
    + +
    -

    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..9c509dd 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 • netballR + - - -
    -
    - - -
    -
    + +
    -

    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 deleted file mode 100644 index 74b6722..0000000 --- a/docs/reference/superNetballR.html +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - -<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..3e791ef 100644 --- a/docs/reference/tidyMatch.html +++ b/docs/reference/tidyMatch.html @@ -1,169 +1,106 @@ - - - - - - - -Takes a downloaded match list and tidies the match statistics. — tidyMatch • superNetballR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Takes a downloaded match list and tidies the match statistics. — tidyMatch • netballR + - - -
    -
    - - -
    -
    + +
    -

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

    -
    -
    tidyMatch(match)
    - -

    Arguments

    - - - - - - -
    match

    List of match details.

    - -

    Value

    - -

    A tidy dataframe containing match statistics.

    - +
    +
    tidyMatch(match)
    +
    + +
    +

    Arguments

    -
    - +
    +

    Value

    +

    A tidy dataframe containing match statistics. Live tidy outputs + append the Champion Data matchId, which uniquely identifies the + source match.

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

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

    -
    -
    tidyPlayers(match)
    - -

    Arguments

    - - - - - - -
    match

    List of match details.

    - -

    Value

    - -

    A tidy dataframe containing player statistics.

    - +
    +
    tidyPlayers(match)
    +
    -
    - +
    -
    - +
    + + + + - - - + diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 0000000..ba82e85 --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,27 @@ + +https://craigmoyle.github.io/netballR/404.html +https://craigmoyle.github.io/netballR/LICENSE-text.html +https://craigmoyle.github.io/netballR/articles/getting-started.html +https://craigmoyle.github.io/netballR/articles/index.html +https://craigmoyle.github.io/netballR/authors.html +https://craigmoyle.github.io/netballR/changelog.html +https://craigmoyle.github.io/netballR/index.html +https://craigmoyle.github.io/netballR/reference/anzc_comp_ids.html +https://craigmoyle.github.io/netballR/reference/downloadFixture.html +https://craigmoyle.github.io/netballR/reference/downloadMatch.html +https://craigmoyle.github.io/netballR/reference/index.html +https://craigmoyle.github.io/netballR/reference/ladders.html +https://craigmoyle.github.io/netballR/reference/listCompetitionsNetballAus.html +https://craigmoyle.github.io/netballR/reference/matchPoints.html +https://craigmoyle.github.io/netballR/reference/matchPoints_pre_2020.html +https://craigmoyle.github.io/netballR/reference/netballR-package.html +https://craigmoyle.github.io/netballR/reference/players_2017.html +https://craigmoyle.github.io/netballR/reference/round5_game3.html +https://craigmoyle.github.io/netballR/reference/season_2017.html +https://craigmoyle.github.io/netballR/reference/shinyNetballR.html +https://craigmoyle.github.io/netballR/reference/shinySuperNetballR.html +https://craigmoyle.github.io/netballR/reference/team_colours.html +https://craigmoyle.github.io/netballR/reference/tidyMatch.html +https://craigmoyle.github.io/netballR/reference/tidyPlayers.html + + diff --git a/docs/superpowers/plans/2026-05-17-netballr-cleanup.md b/docs/superpowers/plans/2026-05-17-netballr-cleanup.md new file mode 100644 index 0000000..2b8a10f --- /dev/null +++ b/docs/superpowers/plans/2026-05-17-netballr-cleanup.md @@ -0,0 +1,234 @@ +# netballR Post-Rename Cleanup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Clean up the remaining post-rename metadata and documentation issues so active package/docs metadata consistently reflects `netballR` while preserving only intentional historical references. + +**Architecture:** Fix source-of-truth metadata first (`DESCRIPTION`, `_pkgdown.yml`, `R/zzz.R`, `changelog.md`), then regenerate roxygen and pkgdown artifacts so generated outputs match the cleaned source. Preserve rename/upstream references only where they explain history, not where they imply the current package still uses old file names or branding. + +**Tech Stack:** R package metadata, roxygen2, pkgdown, testthat, Makefile-based verification + +--- + +## File map + +- `DESCRIPTION` — add package website URL expected by pkgdown metadata. +- `_pkgdown.yml` — keep site metadata aligned with the renamed package. +- `R/zzz.R` — remove stale reference to old package-doc filename. +- `changelog.md` — keep historical rename/upstream notes, but normalize misleading old file references. +- `man/netballR-package.Rd` — regenerated package docs after metadata cleanup. +- `docs/` — regenerated pkgdown site output after cleanup. + +### Task 1: Clean source metadata and historical wording + +**Files:** +- Modify: `DESCRIPTION` +- Modify: `R/zzz.R` +- Modify: `changelog.md` +- Test: source grep / metadata review + +- [ ] **Step 1: Write the failing metadata/history checks** + +Run these inspection commands before changing anything: + +```bash +rg -n "superNetballR_updated|superNetballR" changelog.md R/zzz.R +python - <<'PY' +from pathlib import Path +text = Path('DESCRIPTION').read_text() +print('Has pkgdown website URL:', 'https://craigmoyle.github.io/netballR/' in text) +print(text) +PY +``` + +Expected: +- `R/zzz.R` still refers to `superNetballR.R` +- `changelog.md` still contains a misleading internal-file reference like `superNetballR.R` +- `DESCRIPTION` does not yet include the pkgdown website URL `https://craigmoyle.github.io/netballR/` + +- [ ] **Step 2: Update `DESCRIPTION` to include the pkgdown website URL** + +In `DESCRIPTION`, replace: + +```dcf +URL: https://github.com/craigmoyle/netballR +BugReports: https://github.com/craigmoyle/netballR/issues +``` + +with: + +```dcf +URL: https://craigmoyle.github.io/netballR/, + https://github.com/craigmoyle/netballR +BugReports: https://github.com/craigmoyle/netballR/issues +``` + +- [ ] **Step 3: Remove the stale old-file reference from `R/zzz.R`** + +Replace the contents of `R/zzz.R` with: + +```r +## Nothing required here — see netballR-package.R for globalVariables declarations. +``` + +- [ ] **Step 4: Normalize misleading changelog references while preserving history** + +In `changelog.md`, replace: + +```md +- `globalVariables()` declarations consolidated from `zzz.R` + `superNetballR.R` into a single call; missing names (`goals2`, `isHome`, `games`, `qtr_diff`) added. +``` + +with: + +```md +- `globalVariables()` declarations consolidated into a single package-level call; missing names (`goals2`, `isHome`, `games`, `qtr_diff`) added. +``` + +Keep these historical references unchanged because they are intentional lineage notes: + +- upstream project identity `SteveLane/superNetballR` +- rename history `superNetballR` / `superNetballR_updated` to `netballR` + +- [ ] **Step 5: Re-run the source checks to verify they now pass** + +Run: + +```bash +rg -n "superNetballR_updated|superNetballR" changelog.md R/zzz.R +python - <<'PY' +from pathlib import Path +text = Path('DESCRIPTION').read_text() +assert 'https://craigmoyle.github.io/netballR/' in text +print('DESCRIPTION now includes pkgdown website URL') +PY +``` + +Expected: +- `R/zzz.R` no longer contains `superNetballR` +- `changelog.md` retains only intentional historical `superNetballR` mentions +- the DESCRIPTION assertion passes + +- [ ] **Step 6: Commit** + +```bash +git add DESCRIPTION R/zzz.R changelog.md +git commit -m "docs: clean post-rename metadata and history references" +``` + +### Task 2: Regenerate roxygen and pkgdown artifacts from the cleaned source + +**Files:** +- Modify: `man/netballR-package.Rd` +- Modify: `docs/` +- Test: grep on generated output + +- [ ] **Step 1: Regenerate roxygen docs from the cleaned source** + +Run: + +```bash +Rscript -e "roxygen2::roxygenise()" +``` + +Expected: roxygen rewrites package docs such as `man/netballR-package.Rd` using the current cleaned metadata. + +- [ ] **Step 2: Rebuild the pkgdown site** + +Run: + +```bash +Rscript -e "pkgdown::build_site()" +``` + +Expected: +- site rebuild completes +- the pkgdown complaint about missing package URL is gone +- Bootstrap 3 deprecation may still be reported and is acceptable for this cleanup pass + +- [ ] **Step 3: Search generated output for stale active references** + +Run: + +```bash +rg -n "superNetballR_updated|superNetballR" man docs --glob '!docs/superpowers/**' +``` + +Expected: +- no stale active references remain in generated package docs/site output +- any remaining mentions must be clearly historical and intentional; if unexpected generated stale references appear, stop and fix the source before proceeding + +- [ ] **Step 4: Commit** + +```bash +git add man docs +git commit -m "docs: regenerate site after netballR cleanup" +``` + +### Task 3: Verify the cleaned package state end-to-end + +**Files:** +- Test only: full repository verification commands + +- [ ] **Step 1: Run the focused stale-reference scan** + +Run: + +```bash +rg -n "superNetballR_updated|superNetballR" . --glob '!docs/superpowers/**' --glob '!superNetballR.Rcheck/**' --glob '!netballR.Rcheck/**' +``` + +Expected: +- only intentional historical references remain, primarily in `changelog.md` +- no active code/config/package-doc references remain + +- [ ] **Step 2: Run the full test suite** + +Run: + +```bash +make test +``` + +Expected: all tests pass. + +- [ ] **Step 3: Run package check** + +Run: + +```bash +make check +``` + +Expected: +- `R CMD build` and `R CMD check --no-manual --as-cran` succeed +- the previous pkgdown metadata-related issue is gone +- the GitHub URL NOTE may still remain until the external repository rename actually happens on GitHub, which is acceptable for now + +- [ ] **Step 4: Review the exact NOTE content** + +Run: + +```bash +grep -n "Found the following (possibly) invalid URLs\|Status:" netballR.Rcheck/00check.log || true +``` + +Expected: any remaining NOTE is limited to GitHub URLs that will stop 404ing only after the external repository rename is completed. + +- [ ] **Step 5: Commit** + +```bash +git commit --allow-empty -m "test: verify netballR cleanup" +``` + +## Self-review + +- Spec coverage: + - active metadata cleanup: Task 1 + - pkgdown/doc regeneration: Task 2 + - verification with only expected historical references left: Task 3 +- Placeholder scan: no `TBD`, `TODO`, or content-free steps remain. +- Type consistency: + - package website URL is consistently `https://craigmoyle.github.io/netballR/` + - current package name is consistently `netballR` + - historical references are preserved only in changelog lineage notes 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. diff --git a/docs/superpowers/specs/2026-05-17-netballr-cleanup-design.md b/docs/superpowers/specs/2026-05-17-netballr-cleanup-design.md new file mode 100644 index 0000000..10b528f --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-netballr-cleanup-design.md @@ -0,0 +1,101 @@ +# netballR Post-Rename Cleanup Design + +## Summary + +Perform a balanced cleanup pass after the `netballR` rename and `netball_aus` discovery work. + +This pass should: + +- fix active metadata and documentation issues left by the rename +- remove stale `superNetballR` references from active package code/docs/site output +- preserve intentional historical references where they explain lineage or release history +- improve pkgdown metadata consistency without over-refactoring the broader documentation set + +## Goals + +- Add missing pkgdown package URL metadata so site generation is internally consistent. +- Remove stale active references to `superNetballR` from package code, docs, and generated site output. +- Keep historical rename/upstream references only where they are intentionally explanatory. +- Update changelog wording where older entries refer to now-renamed internal files in a misleading way. +- Rebuild generated documentation/site outputs so the repository reflects the cleaned state. + +## Non-goals + +- No further package API redesign. +- No new features beyond cleanup. +- No repository-remote changes; GitHub rename remains an external administrative step. +- No wholesale rewrite of the full changelog/history unless required for accuracy. + +## Design Decisions + +### 1. Fix active metadata first + +The cleanup should prioritize active metadata problems that still affect tooling: + +- `DESCRIPTION` should include the package website URL expected by pkgdown +- pkgdown configuration and generated site output should align with the renamed package +- active package docs/man pages should reflect `netballR` consistently + +This is the highest-value cleanup because it reduces noise in verification output and keeps published artifacts coherent. + +### 2. Preserve historical references selectively + +Not every `superNetballR` string should be removed. + +These references should remain when they describe historical facts: + +- original upstream project identity +- rename history from `superNetballR` / `superNetballR_updated` to `netballR` +- historical release notes that are explicitly framed as legacy context + +However, references that imply the current package still uses old names, old files, or old branding should be updated. + +### 3. Normalize misleading changelog references + +The changelog currently includes some older entries that mention now-renamed internal files such as `superNetballR.R`. Those mentions are historically understandable, but after the rename they read like current file references. + +The cleanup should rewrite those specific mentions into neutral historical wording, for example: + +- refer to package-level docs or global variable declarations generically +- avoid naming obsolete files unless the filename itself is historically important + +This keeps the historical record while reducing confusion. + +### 4. Regenerate derived artifacts after source cleanup + +After source/doc cleanup, regenerate: + +- roxygen docs +- pkgdown site + +This ensures generated files do not preserve stale references that were already removed from source files. + +## File Impact + +### Source metadata and docs + +- `DESCRIPTION` +- `_pkgdown.yml` +- `R/netballR-package.R` if package-level wording still needs cleanup +- `R/zzz.R` if any stale historical file references remain there +- `changelog.md` + +### Generated artifacts + +- `man/*.Rd` +- `docs/` pkgdown output + +## Acceptance Criteria + +- Active package/docs metadata no longer implies the package is `superNetballR`. +- `DESCRIPTION` and pkgdown metadata are aligned for the `netballR` site. +- Historical references remain only where they are intentional and explanatory. +- Misleading old internal file references are rewritten to neutral historical wording. +- Generated `man/` and `docs/` outputs are refreshed after cleanup. +- Verification remains green aside from the known GitHub-rename-dependent URL note until the external repository rename happens. + +## Risks + +- Over-cleaning could erase useful historical context from the changelog. +- Generated docs may still contain stale references if source cleanup is incomplete before regeneration. +- pkgdown may continue to warn about unrelated configuration modernization (for example Bootstrap 3) even after this cleanup pass. 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. diff --git a/docs/superpowers/specs/2026-05-18-dependabot-config-design.md b/docs/superpowers/specs/2026-05-18-dependabot-config-design.md new file mode 100644 index 0000000..2990693 --- /dev/null +++ b/docs/superpowers/specs/2026-05-18-dependabot-config-design.md @@ -0,0 +1,64 @@ +# netballR Dependabot Configuration Design + +## Summary + +Refine the existing Dependabot setup so GitHub Actions dependency updates keep the current weekly schedule while adding basic triage controls. + +This is a configuration-only change. + +## Goals + +- Keep Dependabot enabled for the `github-actions` ecosystem only. +- Keep the existing weekly update schedule. +- Automatically label Dependabot PRs with: + - `dependencies` + - `github-actions` +- Limit the number of open Dependabot PRs for this ecosystem to `5`. + +## Non-goals + +- No package dependency updates for R packages or other ecosystems. +- No reviewers or assignees. +- No grouping rules. +- No ignore rules. +- No workflow or package code changes. + +## Design Decisions + +### 1. Preserve the current scope + +The repo already has a valid Dependabot file at `.github/dependabot.yml` covering GitHub Actions. This change should extend that config rather than broaden it. + +### 2. Keep the weekly schedule + +The user explicitly chose to keep weekly updates, so the `schedule.interval` remains unchanged. + +### 3. Add basic PR hygiene controls + +Add two standard controls: + +- `labels` + - `dependencies` + - `github-actions` +- `open-pull-requests-limit: 5` + +These improve triage without introducing repo-specific policy assumptions. + +## File Impact + +### Source config + +- `.github/dependabot.yml` + +## Acceptance Criteria + +- `.github/dependabot.yml` still contains one `github-actions` update block. +- The schedule remains weekly. +- Dependabot PRs for GitHub Actions receive `dependencies` and `github-actions` labels. +- The config limits open Dependabot PRs to `5`. +- No other repository files are changed. + +## Risks + +- If the repository does not already have the referenced labels, GitHub may not apply them until they exist. +- Over-configuring beyond this scope would add unnecessary maintenance burden. 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/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/anzc_comp_ids.Rd b/man/anzc_comp_ids.Rd new file mode 100644 index 0000000..6b784c5 --- /dev/null +++ b/man/anzc_comp_ids.Rd @@ -0,0 +1,61 @@ +% 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{Historical ANZ Championship and NZ National Netball League competition IDs.} +\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 + \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{ +data(anzc_comp_ids) +} +\description{ +A historical lookup dataset mapping Champion Data \code{comp_id} values to +the corresponding netball season and competition, covering every season from +2008 to 2025. +} +\details{ +Use \code{anzc_comp_ids} as a historical lookup when you already know you +need ANZ Championship (2008--2016) or NZ National Netball League +(2017--present) competition IDs. For current / active Super Netball seasons, +Australian Diamonds internationals, and other broader Australian +competitions exposed by the live Champion Data application, use +\code{\link{listCompetitionsNetballAus}} instead. + +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{ +data(anzc_comp_ids) +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..c3f813b --- /dev/null +++ b/man/downloadFixture.Rd @@ -0,0 +1,62 @@ +% 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. Use +\code{\link{anzc_comp_ids}} as a historical lookup for ANZ Championship +competition IDs or \code{\link{listCompetitionsNetballAus}} for the +broader live Australian catalogue, including active Super Netball +competitions and Australian Diamonds internationals exposed by the +Champion Data \code{netball_aus} application.} +} +\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}}. Use +\code{\link{listCompetitionsNetballAus}} for current / active Australian +competitions and \code{\link{anzc_comp_ids}} for historical ANZ +Championship or NZ National Netball League IDs. + +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 (historical comp_id from anzc_comp_ids) +downloadFixture("10088") + +## Super Netball (discover current comp_ids via listCompetitionsNetballAus()) +downloadFixture("10083") +} + +} diff --git a/man/downloadMatch.Rd b/man/downloadMatch.Rd index 2a02bcf..2fb38fd 100644 --- a/man/downloadMatch.Rd +++ b/man/downloadMatch.Rd @@ -7,8 +7,14 @@ 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.} +\item{comp_id}{A string identifying which season or competition the game is +in. \code{comp_id} is different depending on regular season or finals. +Use \code{\link{anzc_comp_ids}} as a historical lookup for ANZ +Championship competition IDs, or +\code{\link{listCompetitionsNetballAus}} for the broader live +Australian catalogue, including active Super Netball competitions and +Australian Diamonds internationals exposed by the Champion Data +\code{netball_aus} application.} \item{round_id}{An integer identifying which round the game is in. Finals reset round number to 1.} @@ -23,9 +29,28 @@ 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. + +Once a \code{comp_id} is known, ANZ Championship matches use the same data +format as Super Netball and can be downloaded with the same function by +supplying the appropriate identifier. 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 after finding the +relevant \code{comp_id} through \code{\link{anzc_comp_ids}} or +\code{\link{listCompetitionsNetballAus}}. +} \examples{ \dontrun{ +## Super Netball (discover current comp_ids via listCompetitionsNetballAus()) downloadMatch("10083", 1, 1) + +## ANZ Championship (historical comp_id from anzc_comp_ids) +downloadMatch("10088", 1, 1) } } diff --git a/man/ladders.Rd b/man/ladders.Rd index f76e096..28db15b 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.} @@ -16,8 +19,11 @@ matchResults(df) \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. 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 @@ -27,3 +33,20 @@ Data frame containing the ladder position of all teams. If round and \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. 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 +era. Use \code{\link{ladders_pre_2020}} (and +\code{\link{matchPoints_pre_2020}}) for all ANZ Championship seasons. +} diff --git a/man/listAllCompetitions.Rd b/man/listAllCompetitions.Rd new file mode 100644 index 0000000..912397a --- /dev/null +++ b/man/listAllCompetitions.Rd @@ -0,0 +1,64 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/competitions.R +\name{listAllCompetitions} +\alias{listAllCompetitions} +\title{List competitions from all known Champion Data application catalogues} +\usage{ +listAllCompetitions( + sources = names(.application_source_map), + deduplicate = TRUE, + on_error = c("warn", "error", "ignore") +) +} +\arguments{ +\item{sources}{Character vector of source identifiers to query. Defaults to +all known sources. See \code{\link{listCompetitions}} for supported values.} + +\item{deduplicate}{Logical. If \code{TRUE} (default), rows with duplicate +\code{comp_id} values are removed, keeping the first occurrence based on +the order of \code{sources}. Set to \code{FALSE} to retain all rows and +inspect cross-catalogue coverage via \code{application_source}.} + +\item{on_error}{One of \code{"warn"} (default), \code{"error"}, or +\code{"ignore"}. Controls behaviour when fetching a single source fails. +\code{"warn"} issues a warning and continues; \code{"error"} stops +immediately; \code{"ignore"} silently skips the failed source. An error is +always raised if every source fails.} +} +\value{ +A \code{\link[dplyr]{tibble}} with one row per competition (after + optional deduplication). Includes all columns from + \code{\link{listCompetitions}}. +} +\description{ +\code{listAllCompetitions()} queries multiple Champion Data application +catalogues and returns a combined tidy tibble. +} +\details{ +International competitions may appear in more than one catalogue. When +\code{deduplicate = TRUE}, the first occurrence wins, so source ordering +in \code{sources} determines which \code{application_source} label is +retained for shared \code{comp_id} values. + +Set \code{deduplicate = FALSE} to inspect which catalogues list a given +competition: +\preformatted{ +all <- listAllCompetitions(deduplicate = FALSE) +all[all$comp_id == 9315, c("comp_id", "competition_name", "application_source")] +} +} +\examples{ +\dontrun{ +## Deduplicated (default) +listAllCompetitions() + +## Full cross-catalogue view +listAllCompetitions(deduplicate = FALSE) + +## Subset of sources +listAllCompetitions(sources = c("netball_aus", "netball_nz")) +} +} +\seealso{ +\code{\link{listCompetitions}} +} diff --git a/man/listCompetitions.Rd b/man/listCompetitions.Rd new file mode 100644 index 0000000..8b23fff --- /dev/null +++ b/man/listCompetitions.Rd @@ -0,0 +1,62 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/competitions.R +\name{listCompetitions} +\alias{listCompetitions} +\title{List competitions from a Champion Data application catalogue} +\usage{ +listCompetitions(source) +} +\arguments{ +\item{source}{A string naming the application catalogue. Supported values: +\describe{ + \item{\code{"netball_aus"}}{Current Australian competitions including + Super Netball and Australian Diamonds internationals.} + \item{\code{"netball_nz"}}{New Zealand competitions including the NZ + National Netball League (ANE) and Silver Ferns internationals.} + \item{\code{"england_netball"}}{England Netball competitions.} + \item{\code{"nwc2015"}}{Netball World Cup 2015.} + \item{\code{"nwc2019"}}{Vitality Netball World Cup 2019.} + \item{\code{"nwc2023"}}{Netball World Cup 2023.} +}} +} +\value{ +A \code{\link[dplyr]{tibble}} with one row per competition and + columns: + \describe{ + \item{comp_id}{Champion Data competition identifier.} + \item{competition_name}{Competition name, or \code{NA} for World Cup + catalogues which do not include names in their application settings.} + \item{application_source}{The \code{source} value supplied, identifying + which catalogue the row came from.} + \item{season}{Season year when available.} + \item{competition_type}{Competition type when available.} + \item{squad_id}{Optional squad filter.} + \item{application_logo}{Relative logo path when available.} + } +} +\description{ +\code{listCompetitions()} downloads the public application settings for a +named Champion Data iStats catalogue and returns a tidy tibble of +discoverable competitions. +} +\details{ +The same \code{comp_id} can appear in more than one catalogue +(e.g. international competitions may be listed by both \code{netball_aus} +and \code{netball_nz}). Use \code{\link{listAllCompetitions}} to query +multiple catalogues at once and deduplicate by \code{comp_id}. + +All returned \code{comp_id} values are compatible with +\code{\link{downloadFixture}} and \code{\link{downloadMatch}}. +} +\examples{ +\dontrun{ +listCompetitions("netball_nz") +listCompetitions("england_netball") +listCompetitions("nwc2023") +} +} +\seealso{ +\code{\link{listAllCompetitions}}, \code{\link{listCompetitionsNetballAus}}, + \code{\link{listCompetitionsNetballNZ}}, \code{\link{listCompetitionsEnglandNetball}}, + \code{\link{listCompetitionsWorldCup}} +} diff --git a/man/listCompetitionsEnglandNetball.Rd b/man/listCompetitionsEnglandNetball.Rd new file mode 100644 index 0000000..5acf0c0 --- /dev/null +++ b/man/listCompetitionsEnglandNetball.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/competitions.R +\name{listCompetitionsEnglandNetball} +\alias{listCompetitionsEnglandNetball} +\title{List competitions from the Champion Data england_netball application} +\usage{ +listCompetitionsEnglandNetball() +} +\value{ +A \code{\link[dplyr]{tibble}} with one row per competition. See + \code{\link{listCompetitions}} for column descriptions. +} +\description{ +\code{listCompetitionsEnglandNetball()} returns competitions from the England +Netball catalogue, including Vitality Roses internationals and domestic +England competitions. +} +\examples{ +\dontrun{ +listCompetitionsEnglandNetball() +} +} +\seealso{ +\code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +} diff --git a/man/listCompetitionsNetballAus.Rd b/man/listCompetitionsNetballAus.Rd new file mode 100644 index 0000000..27621fe --- /dev/null +++ b/man/listCompetitionsNetballAus.Rd @@ -0,0 +1,34 @@ +% 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. See + \code{\link{listCompetitions}} for column descriptions. +} +\description{ +\code{listCompetitionsNetballAus()} downloads the public application settings +used by the Champion Data \code{netball_aus} iStats app and returns a tidy +tibble of currently discoverable competitions. +} +\details{ +The live catalogue includes Super Netball, Australian Diamonds +internationals, and other Australian competitions. + +For historical ANZ Championship and NZ National Netball League IDs, use +\code{\link{anzc_comp_ids}} instead. +} +\examples{ +\dontrun{ +comps <- listCompetitionsNetballAus() +subset(comps, grepl("Diamonds", competition_name, ignore.case = TRUE)) +fixture <- downloadFixture(comps$comp_id[[1]]) +} +} +\seealso{ +\code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +} diff --git a/man/listCompetitionsNetballNZ.Rd b/man/listCompetitionsNetballNZ.Rd new file mode 100644 index 0000000..bcb027e --- /dev/null +++ b/man/listCompetitionsNetballNZ.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/competitions.R +\name{listCompetitionsNetballNZ} +\alias{listCompetitionsNetballNZ} +\title{List competitions from the Champion Data netball_nz application} +\usage{ +listCompetitionsNetballNZ() +} +\value{ +A \code{\link[dplyr]{tibble}} with one row per competition. See + \code{\link{listCompetitions}} for column descriptions. +} +\description{ +\code{listCompetitionsNetballNZ()} returns competitions from the New Zealand +catalogue, including the NZ National Netball League (ANE), Silver Ferns +internationals, and domestic NZ competitions. +} +\details{ +The same competition may appear in both \code{netball_nz} and +\code{netball_aus} catalogues (e.g. Constellation Cup). Use +\code{\link{listAllCompetitions}} to deduplicate across sources. +} +\examples{ +\dontrun{ +listCompetitionsNetballNZ() +} +} +\seealso{ +\code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +} diff --git a/man/listCompetitionsWorldCup.Rd b/man/listCompetitionsWorldCup.Rd new file mode 100644 index 0000000..7cf65ef --- /dev/null +++ b/man/listCompetitionsWorldCup.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/competitions.R +\name{listCompetitionsWorldCup} +\alias{listCompetitionsWorldCup} +\title{List competitions from a Netball World Cup application catalogue} +\usage{ +listCompetitionsWorldCup(year) +} +\arguments{ +\item{year}{Integer. The World Cup year. Must be one of \code{2015}, +\code{2019}, or \code{2023}.} +} +\value{ +A \code{\link[dplyr]{tibble}} with one row per competition. See + \code{\link{listCompetitions}} for column descriptions. +} +\description{ +\code{listCompetitionsWorldCup()} returns competitions from the specified +Netball World Cup Champion Data catalogue. +} +\details{ +World Cup catalogues do not include \code{competition_name} in their +application settings, so that column will be \code{NA}. The +\code{application_source} column (\code{"nwc2015"}, \code{"nwc2019"}, or +\code{"nwc2023"}) identifies the catalogue. +} +\examples{ +\dontrun{ +listCompetitionsWorldCup(2023) +listCompetitionsWorldCup(2019) +} +} +\seealso{ +\code{\link{listCompetitions}}, \code{\link{listAllCompetitions}} +} diff --git a/man/matchPoints.Rd b/man/matchPoints.Rd index f6be993..e64ac2c 100644 --- a/man/matchPoints.Rd +++ b/man/matchPoints.Rd @@ -15,3 +15,7 @@ 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. +} diff --git a/man/matchPoints_pre_2020.Rd b/man/matchPoints_pre_2020.Rd new file mode 100644 index 0000000..cf0de85 --- /dev/null +++ b/man/matchPoints_pre_2020.Rd @@ -0,0 +1,23 @@ +% 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. +} +\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}. +} diff --git a/man/netballR-package.Rd b/man/netballR-package.Rd new file mode 100644 index 0000000..7179dc1 --- /dev/null +++ b/man/netballR-package.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/netballR-package.R +\docType{package} +\name{netballR-package} +\alias{netballR} +\alias{netballR-package} +\title{netballR: Download and Tidy Netball Statistics} +\description{ +Download Champion Data netball match feeds and transform team and player +statistics into tidy data frames for analysis. + +Current / active Australian coverage includes Super Netball plus Australian +Diamonds international matches and other competitions discoverable through +\code{\link{listCompetitionsNetballAus}}. +} +\details{ +Use \code{\link{listCompetitionsNetballAus}} to discover live competition +IDs exposed by the Champion Data \code{netball_aus} application, including +active Super Netball seasons and Australian Diamonds internationals when +they appear in the live catalogue. + +Use \code{\link{anzc_comp_ids}} as a historical lookup for ANZ Championship +and NZ National Netball League competition IDs. + +Once you know a \code{comp_id}, use \code{\link{downloadFixture}} and +\code{\link{downloadMatch}} to retrieve data from the shared Champion Data +\code{/data/...} transport. +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://craigmoyle.github.io/netballR/} + \item \url{https://github.com/craigmoyle/netballR} + \item Report bugs at \url{https://github.com/craigmoyle/netballR/issues} +} + +} +\author{ +\strong{Maintainer}: Craig Moyle \email{craig.moyle@mantelgroup.com.au} + +Authors: +\itemize{ + \item Craig Moyle \email{craig.moyle@mantelgroup.com.au} + \item Steve Lane \email{lane.s@unimelb.edu.au} +} + +} +\keyword{internal} diff --git a/man/players_2017.Rd b/man/players_2017.Rd index e7e0e18..22bf87c 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 153728 rows and 11 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{value}{Character representation of the statistic value} \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..333b4ab 100644 --- a/man/season_2017.Rd +++ b/man/season_2017.Rd @@ -4,18 +4,20 @@ \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 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}{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} -}} +} +} \usage{ season_2017 } 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/superNetballR.Rd b/man/superNetballR.Rd deleted file mode 100644 index 745d134..0000000 --- a/man/superNetballR.Rd +++ /dev/null @@ -1,10 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/superNetballR.R -\docType{package} -\name{superNetballR} -\alias{superNetballR} -\alias{superNetballR-package} -\title{\code{superNetballR} package} -\description{ -Functions getting and manipulating Super Netball data. -} diff --git a/man/team_colours.Rd b/man/team_colours.Rd new file mode 100644 index 0000000..361bdcc --- /dev/null +++ b/man/team_colours.Rd @@ -0,0 +1,23 @@ +% 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 9 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 the current Super Netball +competition teams, plus the historical Magpies entry used by the bundled +2017 data. +} +\keyword{datasets} 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 fbb361c..dcdfc9e 100644 --- a/man/tidyPlayers.Rd +++ b/man/tidyPlayers.Rd @@ -10,9 +10,15 @@ 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 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.R b/tests/testthat.R new file mode 100644 index 0000000..e25ab4f --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(netballR) + +test_check("netballR") diff --git a/tests/testthat/helper-fixtures.R b/tests/testthat/helper-fixtures.R new file mode 100644 index 0000000..82438ec --- /dev/null +++ b/tests/testthat/helper-fixtures.R @@ -0,0 +1,293 @@ +make_sample_match <- function(period_completed = 2) { + list( + matchInfo = list( + homeSquadId = 10L, + awaySquadId = 20L, + periodCompleted = period_completed, + roundNumber = 5L, + matchNumber = 3L, + matchId = 500503L + ), + 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, + 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" + ) + ) + ) + ) +} + +make_modern_match_stats <- function( + round, + game, + home_team, + away_team, + home_zone1, + home_zone2 = 0, + away_zone1, + away_zone2 = 0 +) { + make_stat_row <- function(team, stat_name, stat_value) { + data.frame( + 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"), + value = c(1L, 0L), + period = c(1L, 1L), + round = c(round, round), + game = c(game, game), + stringsAsFactors = FALSE + ) + ) + + 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]] <- make_stat_row(home_team, "goal_from_zone2", home_zone2) + } + + if (!is.null(away_zone2)) { + rows[[length(rows) + 1L]] <- make_stat_row(away_team, "goal_from_zone2", away_zone2) + } + + 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 + ) + ) + ) +} + +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 +} + +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" + ) + ) + ) + ) +} + +## World Cup catalogues only contain id + full_names — no competition_name. +make_world_cup_settings <- function() { + list( + applicationInfo = list( + defaultCompetitionID = 12115L, + defaultMatchID = 121150101L, + defaultSeason = 2023L, + defaultRound = 1L, + version = "2024.10.28.1" + ), + competitionList = list( + competition = list( + list(id = 12115L, full_names = TRUE), + list(id = 12116L, full_names = TRUE) + ) + ) + ) +} + +## A settings payload with one competition that has no id — for testing that +## extract_competitions drops it. +make_settings_with_missing_id <- function() { + list( + competitionList = list( + competition = list( + list(id = 9315L, competition_name = "2014 Constellation Cup"), + list(competition_name = "No ID competition") + ) + ) + ) +} + +## Minimal settings with a single competition entry (not wrapped in a list-of-lists). +make_settings_single_competition <- function() { + list( + competitionList = list( + competition = list(id = 9315L, competition_name = "2014 Constellation Cup") + ) + ) +} diff --git a/tests/testthat/test-downloadFixture.R b/tests/testthat/test-downloadFixture.R new file mode 100644 index 0000000..15fb2db --- /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( + netballR:::build_fixture_url("10088"), + "https://mc.championdata.com/data/10088/fixture.json" + ) + expect_equal( + netballR:::build_fixture_url(10088), + "https://mc.championdata.com/data/10088/fixture.json" + ) + + expect_error( + netballR:::build_fixture_url("anz-2017"), + "comp_id must contain digits only" + ) + expect_error( + 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( + netballR:::extract_fixture(list()), + "did not include fixture" + ) + expect_error( + 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 <- netballR:::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 <- netballR:::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" + ) +}) diff --git a/tests/testthat/test-downloadMatch.R b/tests/testthat/test-downloadMatch.R new file mode 100644 index 0000000..7ba1b02 --- /dev/null +++ b/tests/testthat/test-downloadMatch.R @@ -0,0 +1,48 @@ +test_that("build_match_url validates and formats request identifiers", { + expect_equal( + netballR:::build_match_url("10083", 5, 3), + "https://mc.championdata.com/data/10083/100830503.json" + ) + expect_equal( + netballR:::build_match_url(10083, "5", "3"), + "https://mc.championdata.com/data/10083/100830503.json" + ) + + expect_error( + netballR:::build_match_url("season-2025", 5, 3), + "comp_id must contain digits only" + ) + expect_error( + netballR:::build_match_url("10083", 0, 3), + "round_id must be greater than or equal to 1" + ) + expect_error( + netballR:::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(netballR:::extract_match_stats(payload), payload$matchStats) + expect_error( + netballR:::extract_match_stats(list()), + "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 new file mode 100644 index 0000000..9dd0f09 --- /dev/null +++ b/tests/testthat/test-ladders.R @@ -0,0 +1,125 @@ +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) + expect_equal(ladders(season, old_system = TRUE), ladder) +}) + +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"], 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]]) +}) + +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 <- netballR:::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)) +}) diff --git a/tests/testthat/test-match-points.R b/tests/testthat/test-match-points.R new file mode 100644 index 0000000..1471399 --- /dev/null +++ b/tests/testthat/test-match-points.R @@ -0,0 +1,92 @@ +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 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, + 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) +}) + +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) +}) diff --git a/tests/testthat/test-netball-aus-competitions.R b/tests/testthat/test-netball-aus-competitions.R new file mode 100644 index 0000000..4e2d661 --- /dev/null +++ b/tests/testthat/test-netball-aus-competitions.R @@ -0,0 +1,255 @@ +## ── URL builder ────────────────────────────────────────────────────────────── + +test_that("build_application_settings_url returns the correct endpoint for known sources", { + expect_equal( + netballR:::build_application_settings_url("netball_aus"), + "https://mc.championdata.com/netball_aus/settings/application_settings.json" + ) + expect_equal( + netballR:::build_application_settings_url("VitalityNetballWorldCup2019"), + "https://mc.championdata.com/VitalityNetballWorldCup2019/settings/application_settings.json" + ) +}) + +## ── Source resolution ───────────────────────────────────────────────────────── + +test_that("resolve_application_source maps friendly names to Champion Data paths", { + expect_equal(netballR:::resolve_application_source("netball_aus"), "netball_aus") + expect_equal(netballR:::resolve_application_source("netball_nz"), "netball_nz") + expect_equal(netballR:::resolve_application_source("england_netball"), "england_netball") + expect_equal(netballR:::resolve_application_source("nwc2015"), "nwc2015") + expect_equal(netballR:::resolve_application_source("nwc2019"), "VitalityNetballWorldCup2019") + expect_equal(netballR:::resolve_application_source("nwc2023"), "nwc2023") +}) + +test_that("resolve_application_source errors on unknown source", { + expect_error( + netballR:::resolve_application_source("unknown_source"), + "not a recognised application source" + ) +}) + +## ── extract_competitions ───────────────────────────────────────────────────── + +test_that("extract_competitions parses a full-format catalogue into a tidy tibble", { + result <- netballR:::extract_competitions(make_netball_aus_settings(), "netball_aus") + + 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_competitions parses World Cup catalogue with NA competition_name", { + result <- netballR:::extract_competitions(make_world_cup_settings(), "nwc2023") + + expect_s3_class(result, "tbl_df") + expect_equal(nrow(result), 2L) + expect_equal(result$comp_id, c(12115L, 12116L)) + expect_equal(result$competition_name, rep(NA_character_, 2)) + expect_equal(result$application_source, rep("nwc2023", 2)) +}) + +test_that("extract_competitions fails loudly when competition list is absent", { + expect_error( + netballR:::extract_competitions(list(), "netball_aus"), + "did not include competitionList\\$competition" + ) +}) + +test_that("extract_competitions drops competition entries that have no id", { + result <- netballR:::extract_competitions(make_settings_with_missing_id(), "netball_aus") + + expect_equal(nrow(result), 1L) + expect_equal(result$comp_id, 9315L) +}) + +test_that("extract_competitions handles a single-entry catalogue", { + result <- netballR:::extract_competitions(make_settings_single_competition(), "netball_aus") + + expect_s3_class(result, "tbl_df") + expect_equal(nrow(result), 1L) + expect_equal(result$comp_id, 9315L) + expect_equal(result$competition_name, "2014 Constellation Cup") +}) + +## ── listCompetitions ────────────────────────────────────────────────────────── + +test_that("listCompetitions errors on an unknown source", { + expect_error(listCompetitions("unknown"), "not a recognised application source") +}) + +## ── Named wrappers ──────────────────────────────────────────────────────────── + +test_that("listCompetitionsNetballAus returns extracted live competitions", { + local_mocked_bindings( + fetch_application_settings = function(path) 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")) +}) + +test_that("listCompetitionsNetballNZ sets application_source to netball_nz", { + local_mocked_bindings( + fetch_application_settings = function(path) make_netball_aus_settings(), + .package = "netballR" + ) + + result <- listCompetitionsNetballNZ() + + expect_true(all(result$application_source == "netball_nz")) +}) + +test_that("listCompetitionsEnglandNetball sets application_source to england_netball", { + local_mocked_bindings( + fetch_application_settings = function(path) make_netball_aus_settings(), + .package = "netballR" + ) + + result <- listCompetitionsEnglandNetball() + + expect_true(all(result$application_source == "england_netball")) +}) + +## ── listCompetitionsWorldCup ────────────────────────────────────────────────── + +test_that("listCompetitionsWorldCup returns World Cup competitions for supported years", { + local_mocked_bindings( + fetch_application_settings = function(path) make_world_cup_settings(), + .package = "netballR" + ) + + for (yr in c(2015L, 2019L, 2023L)) { + result <- listCompetitionsWorldCup(yr) + expect_s3_class(result, "tbl_df") + expect_equal(nrow(result), 2L) + expect_equal(result$competition_name, rep(NA_character_, 2)) + } +}) + +test_that("listCompetitionsWorldCup errors on unsupported year", { + expect_error(listCompetitionsWorldCup(2022), "must be one of: 2015, 2019, 2023") + expect_error(listCompetitionsWorldCup(0), "must be greater than or equal to 1") +}) + +test_that("listCompetitionsWorldCup uses correct application_source for each year", { + local_mocked_bindings( + fetch_application_settings = function(path) make_world_cup_settings(), + .package = "netballR" + ) + + expect_true(all(listCompetitionsWorldCup(2015)$application_source == "nwc2015")) + expect_true(all(listCompetitionsWorldCup(2019)$application_source == "nwc2019")) + expect_true(all(listCompetitionsWorldCup(2023)$application_source == "nwc2023")) +}) + +## ── listAllCompetitions ─────────────────────────────────────────────────────── + +test_that("listAllCompetitions deduplicates shared comp_ids keeping first source", { + ## Two sources, both returning the same comp_id (9315), simulating a + ## cross-catalogue duplicate like the Constellation Cup. + call_count <- 0L + local_mocked_bindings( + fetch_application_settings = function(path) { + call_count <<- call_count + 1L + make_netball_aus_settings() + }, + .package = "netballR" + ) + + result <- listAllCompetitions( + sources = c("netball_aus", "netball_nz"), + deduplicate = TRUE + ) + + ## Only 3 unique comp_ids (from first source); duplicates from netball_nz dropped. + expect_equal(nrow(result), 3L) + ## All rows should come from the first source (netball_aus wins). + expect_true(all(result$application_source == "netball_aus")) + expect_equal(call_count, 2L) +}) + +test_that("listAllCompetitions with deduplicate = FALSE retains all rows", { + local_mocked_bindings( + fetch_application_settings = function(path) make_netball_aus_settings(), + .package = "netballR" + ) + + result <- listAllCompetitions( + sources = c("netball_aus", "netball_nz"), + deduplicate = FALSE + ) + + expect_equal(nrow(result), 6L) + expect_equal(unique(result$application_source), c("netball_aus", "netball_nz")) +}) + +test_that("listAllCompetitions warns and continues when one source fails", { + local_mocked_bindings( + fetch_application_settings = function(path) { + if (path == "netball_nz") stop("simulated network failure") + make_netball_aus_settings() + }, + .package = "netballR" + ) + + expect_warning( + result <- listAllCompetitions( + sources = c("netball_aus", "netball_nz"), + on_error = "warn" + ), + "simulated network failure" + ) + expect_s3_class(result, "tbl_df") + expect_true(all(result$application_source == "netball_aus")) +}) + +test_that("listAllCompetitions errors immediately when on_error = 'error'", { + local_mocked_bindings( + fetch_application_settings = function(path) stop("simulated failure"), + .package = "netballR" + ) + + expect_error( + listAllCompetitions(sources = c("netball_aus"), on_error = "error"), + "simulated failure" + ) +}) + +test_that("listAllCompetitions errors when every source fails", { + local_mocked_bindings( + fetch_application_settings = function(path) stop("simulated failure"), + .package = "netballR" + ) + + expect_error( + suppressWarnings( + listAllCompetitions(sources = c("netball_aus", "netball_nz"), on_error = "warn") + ), + "Failed to fetch competitions from all sources" + ) +}) 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) +}) diff --git a/tests/testthat/test-tidiers.R b/tests/testthat/test-tidiers.R new file mode 100644 index 0000000..0f5d6a6 --- /dev/null +++ b/tests/testthat/test-tidiers.R @@ -0,0 +1,37 @@ +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) + 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", { + result <- tidyPlayers(make_sample_match(period_completed = 2)) + + expect_true(all(result$period <= 2)) + expect_equal(nrow(result), 16) + expect_false("displayName" %in% names(result)) + 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" + ) + expect_true("matchId" %in% names(result)) + expect_equal(tail(names(result), 3), c("round", "game", "matchId")) + expect_equal(unique(result$matchId), 500503L) +}) diff --git a/vignettes/getting-started.Rmd b/vignettes/getting-started.Rmd index 46e9f7f..5097e87 100644 --- a/vignettes/getting-started.Rmd +++ b/vignettes/getting-started.Rmd @@ -1,10 +1,10 @@ --- -title: "Getting Started with superNetballR" -author: "Steve Lane" +title: "Getting Started with netballR" +author: "Steve Lane and Craig Moyle" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > - %\VignetteIndexEntry{Vignette Title} + %\VignetteIndexEntry{Getting Started with netballR} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- @@ -20,17 +20,38 @@ 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 `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. # 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 Super Netball home-and-away competition ID (`10083`) and the 2017 finals competition ID (`10084`). The full processed 2017 datasets are supplied with this package, while current Super Netball seasons, Australian Diamonds internationals, and other live Australian competitions can be queried with the appropriate competition IDs discovered from the current `netball_aus` 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. + +## Competition discovery pathways + +Use `listCompetitionsNetballAus()` for current / active Australian competitions exposed by the Champion Data `netball_aus` application, including Super Netball and Australian Diamonds internationals when they are present in the live catalogue. + +Use `anzc_comp_ids` as a historical lookup for ANZ Championship and NZ National Netball League workflows. + +Both discovery pathways lead into the same `downloadFixture()` / `downloadMatch()` workflow once you know the relevant `comp_id`. + +```{r competition-discovery, eval=FALSE} +competitions <- listCompetitionsNetballAus() +subset(competitions, grepl("Diamonds", competition_name, ignore.case = TRUE)) + +anzc_comp_ids +``` + +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) ``` @@ -39,7 +60,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) ``` @@ -52,7 +73,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: @@ -62,6 +83,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} @@ -72,7 +99,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) @@ -89,3 +116,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 +```