├── docs
└── .nojekyll
├── LICENSE
├── man
├── figures
│ ├── image.png
│ ├── Athlytics_Final.png
│ ├── 02b_ef_multi_group.png
│ ├── 01b_acwr_multi_group.png
│ ├── 05b_decoupling_multi_group.png
│ └── advanced_cohort_reference.png
├── parse_fit_file.Rd
├── parse_gpx_file.Rd
├── parse_tcx_file.Rd
├── find_best_effort.Rd
├── athlytics_palette_cell.Rd
├── athlytics_palette_science.Rd
├── athlytics_colors_ef.Rd
├── athlytics_palette_nature.Rd
├── athlytics_palette_academic.Rd
├── athlytics_colors_training_load.Rd
├── athlytics_palette_vibrant.Rd
├── theme_athlytics.Rd
├── athlytics_colors_acwr_zones.Rd
├── athlytics_sample_decoupling.Rd
├── athlytics_sample_ef.Rd
├── scale_athlytics.Rd
├── athlytics_sample_acwr.Rd
├── athlytics_sample_exposure.Rd
├── quality_summary.Rd
├── parse_activity_file.Rd
├── add_reference_bands.Rd
├── athlytics_sample_pbs.Rd
├── plot_acwr_comparison.Rd
├── calculate_ef_from_stream.Rd
├── plot_with_reference.Rd
├── calculate_pbs.Rd
├── plot_acwr_enhanced.Rd
├── calculate_exposure.Rd
├── plot_ef.Rd
├── cohort_reference.Rd
├── plot_decoupling.Rd
├── plot_acwr.Rd
├── flag_quality.Rd
├── plot_exposure.Rd
├── plot_pbs.Rd
├── load_local_activities.Rd
├── calculate_acwr_ewma.Rd
└── calculate_decoupling.Rd
├── tests
├── testthat.R
└── testthat
│ ├── helper-mockapi.R
│ ├── test-calculate-decoupling.R
│ ├── test-parse-activity-file.R
│ ├── test-date-ranges.R
│ ├── test-parameter-boundaries.R
│ ├── test-utils.R
│ ├── test-additional-edge-cases.R
│ ├── test-load-local-activities.R
│ ├── test-calculate-exposure.R
│ ├── test-plot-ef-simple.R
│ ├── test-plot-pbs-simple.R
│ ├── test-plot-pbs-extended.R
│ ├── test-plot-ef-extended.R
│ ├── test-exposure.R
│ ├── test-calculate-exposure-extended.R
│ ├── test-ef.R
│ ├── helper-mockdata.R
│ ├── test-utils-extended.R
│ ├── test-flag-quality.R
│ ├── test-acwr.R
│ ├── test-plot-ef-comprehensive.R
│ ├── test-calculate-ef-extended.R
│ ├── test-absolute-real-data.R
│ ├── test-calculate-ef-simple.R
│ └── test-parse-activity-file-stream.R
├── analysis_output
├── logo.png
├── 01b_acwr_multi_group.png
├── 02b_ef_multi_group.png
├── advanced_cohort_reference.png
└── 05b_decoupling_multi_group.png
├── data
├── athlytics_sample_acwr.rda
├── athlytics_sample_ef.rda
├── athlytics_sample_pbs.rda
├── athlytics_sample_exposure.rda
└── athlytics_sample_decoupling.rda
├── R
├── utils.R
├── zzz.R
└── data.R
├── inst
└── CITATION
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── documentation.md
│ ├── feature_request.md
│ └── bug_report.md
├── workflows
│ ├── pkgcheck.yml
│ ├── pkgdown.yml
│ ├── update-downloads-badge.yml
│ └── R-CMD-check.yml
└── PULL_REQUEST_TEMPLATE.md
├── run_coverage.R
├── DESCRIPTION
├── .gitignore
├── _pkgdown.yml
├── CONTRIBUTING.md
├── data-raw
└── generate_sample_pbs.R
├── paper
├── paper.bib
└── paper.md
├── NAMESPACE
├── .Rbuildignore
├── generate_plot_examples.R
└── CODE_OF_CONDUCT.md
/docs/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2025
2 | COPYRIGHT HOLDER: Zhiang He
3 |
--------------------------------------------------------------------------------
/man/figures/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/man/figures/image.png
--------------------------------------------------------------------------------
/tests/testthat.R:
--------------------------------------------------------------------------------
1 | library(testthat)
2 | library(Athlytics)
3 |
4 | test_check("Athlytics")
5 |
--------------------------------------------------------------------------------
/analysis_output/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/analysis_output/logo.png
--------------------------------------------------------------------------------
/data/athlytics_sample_acwr.rda:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/data/athlytics_sample_acwr.rda
--------------------------------------------------------------------------------
/data/athlytics_sample_ef.rda:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/data/athlytics_sample_ef.rda
--------------------------------------------------------------------------------
/data/athlytics_sample_pbs.rda:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/data/athlytics_sample_pbs.rda
--------------------------------------------------------------------------------
/man/figures/Athlytics_Final.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/man/figures/Athlytics_Final.png
--------------------------------------------------------------------------------
/data/athlytics_sample_exposure.rda:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/data/athlytics_sample_exposure.rda
--------------------------------------------------------------------------------
/man/figures/02b_ef_multi_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/man/figures/02b_ef_multi_group.png
--------------------------------------------------------------------------------
/data/athlytics_sample_decoupling.rda:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/data/athlytics_sample_decoupling.rda
--------------------------------------------------------------------------------
/man/figures/01b_acwr_multi_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/man/figures/01b_acwr_multi_group.png
--------------------------------------------------------------------------------
/analysis_output/01b_acwr_multi_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/analysis_output/01b_acwr_multi_group.png
--------------------------------------------------------------------------------
/analysis_output/02b_ef_multi_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/analysis_output/02b_ef_multi_group.png
--------------------------------------------------------------------------------
/man/figures/05b_decoupling_multi_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/man/figures/05b_decoupling_multi_group.png
--------------------------------------------------------------------------------
/man/figures/advanced_cohort_reference.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/man/figures/advanced_cohort_reference.png
--------------------------------------------------------------------------------
/analysis_output/advanced_cohort_reference.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/analysis_output/advanced_cohort_reference.png
--------------------------------------------------------------------------------
/analysis_output/05b_decoupling_multi_group.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HzaCode/Athlytics/HEAD/analysis_output/05b_decoupling_multi_group.png
--------------------------------------------------------------------------------
/tests/testthat/helper-mockapi.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/helper-mockapi.R
2 |
3 | with_mocked_strava_api <- function(expr, mock_data = mock_activity_list_df) {
4 | mockery::stub(
5 | where = rStrava::get_activity_list,
6 | what = "rStrava::get_activity_list",
7 | how = mock_data
8 | )
9 | eval(expr)
10 | }
11 |
--------------------------------------------------------------------------------
/man/parse_fit_file.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/parse_activity_file.R
3 | \name{parse_fit_file}
4 | \alias{parse_fit_file}
5 | \title{Parse FIT file}
6 | \usage{
7 | parse_fit_file(file_path)
8 | }
9 | \description{
10 | Parse FIT file
11 | }
12 | \keyword{internal}
13 |
--------------------------------------------------------------------------------
/man/parse_gpx_file.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/parse_activity_file.R
3 | \name{parse_gpx_file}
4 | \alias{parse_gpx_file}
5 | \title{Parse GPX file}
6 | \usage{
7 | parse_gpx_file(file_path)
8 | }
9 | \description{
10 | Parse GPX file
11 | }
12 | \keyword{internal}
13 |
--------------------------------------------------------------------------------
/man/parse_tcx_file.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/parse_activity_file.R
3 | \name{parse_tcx_file}
4 | \alias{parse_tcx_file}
5 | \title{Parse TCX file}
6 | \usage{
7 | parse_tcx_file(file_path)
8 | }
9 | \description{
10 | Parse TCX file
11 | }
12 | \keyword{internal}
13 |
--------------------------------------------------------------------------------
/tests/testthat/test-calculate-decoupling.R:
--------------------------------------------------------------------------------
1 | # Load data: sample data from package & mock API returns from helper
2 | data(athlytics_sample_decoupling)
3 | source(test_path("helper-mockdata.R"), local = TRUE) # Provides mock_activity_list_list, mock_activity_streams
4 |
5 | # Mock Strava token - needed for function signature but API calls will be mocked
6 |
--------------------------------------------------------------------------------
/man/find_best_effort.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/calculate_pbs.R
3 | \name{find_best_effort}
4 | \alias{find_best_effort}
5 | \title{Find Best Effort for Target Distance}
6 | \usage{
7 | find_best_effort(stream_data, target_distance)
8 | }
9 | \description{
10 | Find Best Effort for Target Distance
11 | }
12 | \keyword{internal}
13 |
--------------------------------------------------------------------------------
/R/utils.R:
--------------------------------------------------------------------------------
1 | # R/utils.R
2 |
3 | # Internal helper function for English month-year labels
4 | english_month_year <- function(dates) {
5 | months_en <- c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
6 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
7 | paste(months_en[lubridate::month(dates)], lubridate::year(dates))
8 | }
9 |
10 | # Add other internal utility functions here in the future if needed
--------------------------------------------------------------------------------
/tests/testthat/test-parse-activity-file.R:
--------------------------------------------------------------------------------
1 | # Test parse_activity_file functionality
2 |
3 | library(testthat)
4 | library(Athlytics)
5 |
6 | # Skip all tests in this file - parse_activity_file and related functions are internal
7 | # They are tested indirectly through calculate_pbs, calculate_decoupling, etc.
8 |
9 | test_that("parse functions are tested via calculate functions", {
10 | skip("parse_activity_file and related functions are not exported - tested indirectly via calculate functions")
11 | })
12 |
--------------------------------------------------------------------------------
/R/zzz.R:
--------------------------------------------------------------------------------
1 | # R/zzz.R
2 |
3 | .onAttach <- function(libname, pkgname) {
4 |
5 | pkg_desc <- utils::packageDescription(pkgname)
6 | pkg_version <- pkg_desc$Version
7 |
8 | startup_msg <- paste0(
9 | "\nLoading Athlytics version ", pkg_version, ".\n",
10 | "Analyze your Strava data locally with ease!\n",
11 | "Use load_local_activities() to get started.\n",
12 | "For documentation, see: https://hzacode.github.io/Athlytics/"
13 | )
14 |
15 |
16 | packageStartupMessage(startup_msg)
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/inst/CITATION:
--------------------------------------------------------------------------------
1 | bibentry(
2 | bibtype = "Manual",
3 | title = "Athlytics: A Computational Framework for Longitudinal Analysis of Exercise Physiology",
4 | author = person("Zhiang", "He", email = "ang@hezhiang.com"),
5 | year = 2025,
6 | note = "R package version 1.0.0",
7 | url = "https://github.com/HzaCode/Athlytics",
8 | textVersion = paste("Zhiang He (2025). Athlytics: A Computational Framework for Longitudinal Analysis of Exercise Physiology. R package version 1.0.0. https://github.com/HzaCode/Athlytics")
9 | )
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: ❓ Ask a Question
4 | url: https://github.com/HzaCode/Athlytics/discussions
5 | about: Ask questions about using Athlytics or discuss ideas
6 | - name: 📖 Documentation
7 | url: https://hezhiang.com/Athlytics/
8 | about: Read the full documentation and vignettes
9 | - name: 💬 Community Discussion
10 | url: https://github.com/HzaCode/Athlytics/discussions
11 | about: Join community discussions about sports analytics and training science
12 |
13 |
--------------------------------------------------------------------------------
/man/athlytics_palette_cell.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_palette_cell}
4 | \alias{athlytics_palette_cell}
5 | \title{Cell Journal Palette}
6 | \usage{
7 | athlytics_palette_cell()
8 | }
9 | \value{
10 | A character vector of 8 hex color codes
11 | }
12 | \description{
13 | Modern palette based on Cell Press visualization standards.
14 | Balances professional appearance with visual clarity.
15 | }
16 | \examples{
17 | # Get Cell palette colors
18 | colors <- athlytics_palette_cell()
19 | colors[1] # Blue
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/man/athlytics_palette_science.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_palette_science}
4 | \alias{athlytics_palette_science}
5 | \title{Science Magazine Palette}
6 | \usage{
7 | athlytics_palette_science()
8 | }
9 | \value{
10 | A character vector of 8 hex color codes
11 | }
12 | \description{
13 | Classic palette inspired by Science magazine's figure guidelines.
14 | Conservative and widely accepted in scientific community.
15 | }
16 | \examples{
17 | # Get Science palette colors
18 | colors <- athlytics_palette_science()
19 | colors[1] # Dark blue
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/man/athlytics_colors_ef.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_colors_ef}
4 | \alias{athlytics_colors_ef}
5 | \title{Efficiency Factor Colors}
6 | \usage{
7 | athlytics_colors_ef()
8 | }
9 | \value{
10 | A named list with four color codes by sport:
11 | \item{run}{Navy blue for running}
12 | \item{ride}{Coral for cycling}
13 | \item{swim}{Cyan for swimming}
14 | \item{other}{Slate for other activities}
15 | }
16 | \description{
17 | Colors for efficiency factor trends by activity type.
18 | }
19 | \examples{
20 | # Get EF colors by sport
21 | colors <- athlytics_colors_ef()
22 | colors$run # Navy for running
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/man/athlytics_palette_nature.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_palette_nature}
4 | \alias{athlytics_palette_nature}
5 | \title{Nature-Inspired Color Palette}
6 | \usage{
7 | athlytics_palette_nature()
8 | }
9 | \value{
10 | A character vector of 9 hex color codes
11 | }
12 | \description{
13 | Professional, colorblind-friendly palette based on Nature journal's
14 | visualization guidelines. Suitable for multi-series plots.
15 | }
16 | \examples{
17 | \dontrun{
18 | ggplot2::ggplot(data, ggplot2::aes(x, y, color = group)) +
19 | ggplot2::geom_line() +
20 | ggplot2::scale_color_manual(values = athlytics_palette_nature())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/man/athlytics_palette_academic.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_palette_academic}
4 | \alias{athlytics_palette_academic}
5 | \title{Academic Muted Color Palette}
6 | \usage{
7 | athlytics_palette_academic()
8 | }
9 | \value{
10 | A character vector of 8 hex color codes
11 | }
12 | \description{
13 | Low-saturation, elegant palette suitable for formal publications and
14 | technical reports. Emphasizes clarity over visual impact.
15 | }
16 | \examples{
17 | \dontrun{
18 | ggplot2::ggplot(data, ggplot2::aes(x, y, color = group)) +
19 | ggplot2::geom_line() +
20 | ggplot2::scale_color_manual(values = athlytics_palette_academic())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/man/athlytics_colors_training_load.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_colors_training_load}
4 | \alias{athlytics_colors_training_load}
5 | \title{Training Load Colors}
6 | \usage{
7 | athlytics_colors_training_load()
8 | }
9 | \value{
10 | A named list with three color codes:
11 | \item{acute}{Red for short-term load (7-day)}
12 | \item{chronic}{Blue for long-term load (28-day)}
13 | \item{ratio}{Teal for ACWR ratio}
14 | }
15 | \description{
16 | Colors for acute and chronic training load visualization.
17 | }
18 | \examples{
19 | # Get training load colors
20 | colors <- athlytics_colors_training_load()
21 | colors$acute # Red for acute load
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/man/athlytics_palette_vibrant.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_palette_vibrant}
4 | \alias{athlytics_palette_vibrant}
5 | \title{Vibrant High-Contrast Palette}
6 | \usage{
7 | athlytics_palette_vibrant()
8 | }
9 | \value{
10 | A character vector of 8 hex color codes
11 | }
12 | \description{
13 | High-saturation palette optimized for presentations and posters.
14 | Maximum visual impact while maintaining colorblind accessibility.
15 | }
16 | \examples{
17 | \dontrun{
18 | ggplot2::ggplot(data, ggplot2::aes(x, y, fill = category)) +
19 | ggplot2::geom_bar(stat = "identity") +
20 | ggplot2::scale_fill_manual(values = athlytics_palette_vibrant())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/man/theme_athlytics.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{theme_athlytics}
4 | \alias{theme_athlytics}
5 | \title{Get Athlytics Theme}
6 | \usage{
7 | theme_athlytics(base_size = 13, base_family = "")
8 | }
9 | \arguments{
10 | \item{base_size}{Base font size (default: 12)}
11 |
12 | \item{base_family}{Font family (default: "")}
13 | }
14 | \value{
15 | A ggplot2 theme object that can be added to plots
16 | }
17 | \description{
18 | Publication-ready ggplot2 theme with sensible defaults for scientific figures.
19 | }
20 | \examples{
21 | # Apply theme to a plot
22 | ggplot2::ggplot(mtcars, ggplot2::aes(mpg, wt)) +
23 | ggplot2::geom_point() +
24 | theme_athlytics()
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/pkgcheck.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | pull_request:
6 | branches:
7 | - main
8 |
9 | name: pkgcheck
10 |
11 | jobs:
12 | pkgcheck:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read
16 | issues: write
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - uses: r-lib/actions/setup-r@v2
21 | with:
22 | r-version: 'release'
23 | use-public-rspm: true
24 |
25 | - uses: r-lib/actions/setup-r-dependencies@v2
26 | with:
27 | extra-packages: any::rcmdcheck, any::roxygen2, any::pkgbuild
28 |
29 | - uses: ropensci-review-tools/pkgcheck-action@main
30 | env:
31 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/man/athlytics_colors_acwr_zones.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{athlytics_colors_acwr_zones}
4 | \alias{athlytics_colors_acwr_zones}
5 | \title{ACWR Zone Colors}
6 | \usage{
7 | athlytics_colors_acwr_zones()
8 | }
9 | \value{
10 | A named list with four color codes for ACWR zones:
11 | \item{undertraining}{Light blue for low load}
12 | \item{safe}{Green for optimal training zone}
13 | \item{caution}{Orange for moderate risk}
14 | \item{high_risk}{Red for high injury risk}
15 | }
16 | \description{
17 | Standardized colors for ACWR risk zones following sports science conventions.
18 | }
19 | \examples{
20 | # Get ACWR zone colors
21 | colors <- athlytics_colors_acwr_zones()
22 | colors$safe # Returns green color code
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/man/athlytics_sample_decoupling.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/data.R
3 | \docType{data}
4 | \name{athlytics_sample_decoupling}
5 | \alias{athlytics_sample_decoupling}
6 | \title{Sample Aerobic Decoupling Data for Athlytics}
7 | \format{
8 | A tibble with 365 rows and 2 variables:
9 | \describe{
10 | \item{date}{Date of the activity, as a Date object.}
11 | \item{decoupling}{Calculated decoupling percentage, as a numeric value.}
12 | }
13 | }
14 | \source{
15 | Simulated data generated for package examples.
16 | }
17 | \usage{
18 | athlytics_sample_decoupling
19 | }
20 | \description{
21 | A dataset containing pre-calculated aerobic decoupling percentages,
22 | derived from simulated Strava data. Used in examples and tests.
23 | }
24 | \keyword{datasets}
25 |
--------------------------------------------------------------------------------
/man/athlytics_sample_ef.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/data.R
3 | \docType{data}
4 | \name{athlytics_sample_ef}
5 | \alias{athlytics_sample_ef}
6 | \title{Sample Efficiency Factor (EF) Data for Athlytics}
7 | \format{
8 | A data.frame with 50 rows and 3 variables:
9 | \describe{
10 | \item{date}{Date of the activity, as a Date object.}
11 | \item{activity_type}{Type of activity (e.g., "Run", "Ride"), as a character string.}
12 | \item{ef_value}{Calculated Efficiency Factor, as a numeric value.}
13 | }
14 | }
15 | \source{
16 | Simulated data generated for package examples.
17 | }
18 | \usage{
19 | athlytics_sample_ef
20 | }
21 | \description{
22 | A dataset containing pre-calculated Efficiency Factor (EF) values,
23 | derived from simulated Strava data. Used in examples and tests.
24 | }
25 | \keyword{datasets}
26 |
--------------------------------------------------------------------------------
/man/scale_athlytics.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/color_palettes.R
3 | \name{scale_athlytics}
4 | \alias{scale_athlytics}
5 | \title{Apply Color Palette to ggplot}
6 | \usage{
7 | scale_athlytics(palette_name = "nature", type = "color")
8 | }
9 | \arguments{
10 | \item{palette_name}{Name of palette: "nature", "academic", "vibrant", "science", or "cell"}
11 |
12 | \item{type}{Either "color" or "fill"}
13 | }
14 | \value{
15 | A ggplot2 scale object (scale_color_manual or scale_fill_manual)
16 | }
17 | \description{
18 | Helper function to apply Athlytics color palettes to existing plots.
19 | }
20 | \examples{
21 | # Apply nature palette to plot
22 | ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Sepal.Width, color = Species)) +
23 | ggplot2::geom_point() +
24 | scale_athlytics("nature", "color")
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/man/athlytics_sample_acwr.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/data.R
3 | \docType{data}
4 | \name{athlytics_sample_acwr}
5 | \alias{athlytics_sample_acwr}
6 | \title{Sample ACWR Data for Athlytics}
7 | \format{
8 | A tibble with 365 rows and 5 variables:
9 | \describe{
10 | \item{date}{Date of the metrics, as a Date object.}
11 | \item{atl}{Acute Training Load, as a numeric value.}
12 | \item{ctl}{Chronic Training Load, as a numeric value.}
13 | \item{acwr}{Acute:Chronic Workload Ratio, as a numeric value.}
14 | \item{acwr_smooth}{Smoothed ACWR, as a numeric value.}
15 | }
16 | }
17 | \source{
18 | Simulated data generated for package examples.
19 | }
20 | \usage{
21 | athlytics_sample_acwr
22 | }
23 | \description{
24 | A dataset containing pre-calculated Acute:Chronic Workload Ratio (ACWR)
25 | and related metrics, derived from simulated Strava data. Used in examples and tests.
26 | }
27 | \keyword{datasets}
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation improvement
3 | about: Suggest improvements to documentation, examples, or vignettes
4 | title: '[DOCS] '
5 | labels: documentation
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Which documentation needs improvement?**
11 | - [ ] Function documentation (Rd files)
12 | - [ ] README
13 | - [ ] Vignettes
14 | - [ ] Website (pkgdown)
15 | - [ ] Code comments
16 | - [ ] Other: ___________
17 |
18 | **Location**
19 | Please specify where the documentation issue is:
20 | - Function name:
21 | - File path:
22 | - URL:
23 |
24 | **Current documentation (if applicable)**
25 | Paste or describe the current documentation that needs improvement.
26 |
27 | **Suggested improvement**
28 | Describe what should be added, clarified, or corrected.
29 |
30 | **Why is this improvement needed?**
31 | Explain how this would help users understand or use Athlytics better.
32 |
33 | **Additional context**
34 | Add any other context, examples, or references here.
35 |
36 |
--------------------------------------------------------------------------------
/man/athlytics_sample_exposure.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/data.R
3 | \docType{data}
4 | \name{athlytics_sample_exposure}
5 | \alias{athlytics_sample_exposure}
6 | \title{Sample Training Load Exposure Data for Athlytics}
7 | \format{
8 | A tibble with 365 rows and 5 variables:
9 | \describe{
10 | \item{date}{Date of the metrics, as a Date object.}
11 | \item{daily_load}{Calculated daily training load, as a numeric value.}
12 | \item{ctl}{Chronic Training Load, as a numeric value.}
13 | \item{atl}{Acute Training Load, as a numeric value.}
14 | \item{acwr}{Acute:Chronic Workload Ratio, as a numeric value.}
15 | }
16 | }
17 | \source{
18 | Simulated data generated for package examples.
19 | }
20 | \usage{
21 | athlytics_sample_exposure
22 | }
23 | \description{
24 | This dataset contains daily training load, ATL, CTL, and ACWR, derived from
25 | simulated Strava data. Used in examples and tests, particularly for \code{plot_exposure}.
26 | }
27 | \keyword{datasets}
28 |
--------------------------------------------------------------------------------
/tests/testthat/test-date-ranges.R:
--------------------------------------------------------------------------------
1 |
2 | test_that("date range tests for coverage", {
3 | activities <- data.frame(
4 | date = as.Date(c("2023-01-01", "2023-06-15", "2023-12-31")),
5 | type = c("Run", "Run", "Run"),
6 | distance = c(10, 15, 20),
7 | moving_time = c(3600, 5400, 7200),
8 | average_heartrate = c(150, 160, 170),
9 | average_speed = c(10/3600*1000, 15/5400*1000, 20/7200*1000)
10 | )
11 |
12 | # Test start date filtering
13 | result1 <- calculate_ef(activities, start_date = "2023-03-01", end_date = "2024-01-01", quality_control = "off")
14 | expect_s3_class(result1, "data.frame")
15 |
16 | # Test end date filtering
17 | result2 <- calculate_ef(activities, start_date = "2022-01-01", end_date = "2023-09-01", quality_control = "off")
18 | expect_s3_class(result2, "data.frame")
19 |
20 | # Test date range filtering
21 | result3 <- calculate_ef(activities, start_date = "2023-02-01", end_date = "2023-08-01", quality_control = "off")
22 | expect_s3_class(result3, "data.frame")
23 | })
24 |
25 |
--------------------------------------------------------------------------------
/man/quality_summary.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/flag_quality.R
3 | \name{quality_summary}
4 | \alias{quality_summary}
5 | \title{Get Quality Summary Statistics}
6 | \usage{
7 | quality_summary(flagged_streams)
8 | }
9 | \arguments{
10 | \item{flagged_streams}{A data frame returned by \code{flag_quality()}.}
11 | }
12 | \value{
13 | A list with summary statistics:
14 | \describe{
15 | \item{total_points}{Total number of data points}
16 | \item{flagged_points}{Number of flagged points}
17 | \item{flagged_pct}{Percentage of flagged points}
18 | \item{steady_state_points}{Number of steady-state points}
19 | \item{steady_state_pct}{Percentage in steady-state}
20 | \item{quality_score}{Overall quality score (0-1)}
21 | \item{hr_spike_pct}{Percentage with HR spikes}
22 | \item{pw_spike_pct}{Percentage with power spikes}
23 | \item{gps_drift_pct}{Percentage with GPS drift}
24 | }
25 | }
26 | \description{
27 | Provides a summary of quality flags and steady-state segments.
28 | }
29 | \examples{
30 | \dontrun{
31 | flagged_data <- flag_quality(stream_data)
32 | quality_summary(flagged_data)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/man/parse_activity_file.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/parse_activity_file.R
3 | \name{parse_activity_file}
4 | \alias{parse_activity_file}
5 | \title{Parse Activity File (FIT, TCX, or GPX)}
6 | \usage{
7 | parse_activity_file(file_path, export_dir = NULL)
8 | }
9 | \arguments{
10 | \item{file_path}{Path to the activity file (can be .fit, .tcx, .gpx, or .gz compressed)}
11 |
12 | \item{export_dir}{Base directory of the Strava export (for resolving relative paths)}
13 | }
14 | \value{
15 | A data frame with columns: time, latitude, longitude, elevation,
16 | heart_rate, power, cadence, speed (all optional depending on file content).
17 | Returns NULL if file cannot be parsed or does not exist.
18 | }
19 | \description{
20 | Parse activity files from Strava export data.
21 | Supports FIT, TCX, and GPX formats (including .gz compressed files).
22 | }
23 | \examples{
24 | \dontrun{
25 | # Parse a FIT file
26 | streams <- parse_activity_file("activity_12345.fit", export_dir = "strava_export/")
27 |
28 | # Parse a compressed GPX file
29 | streams <- parse_activity_file("activity_12345.gpx.gz")
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/tests/testthat/test-parameter-boundaries.R:
--------------------------------------------------------------------------------
1 |
2 | test_that("parameter boundary tests for coverage", {
3 | activities <- data.frame(
4 | date = as.Date("2023-06-01"),
5 | type = "Run",
6 | distance = 10,
7 | moving_time = 3600,
8 | average_heartrate = 150,
9 | average_speed = 10/3600*1000
10 | )
11 |
12 | # Test minimum duration parameter
13 | result1 <- calculate_ef(activities, start_date = "2023-05-01", end_date = "2023-07-01", min_duration_mins = 0, quality_control = "off")
14 | expect_s3_class(result1, "data.frame")
15 |
16 | # Test maximum duration parameter
17 | result2 <- calculate_ef(activities, start_date = "2023-05-01", end_date = "2023-07-01", min_duration_mins = 1000, quality_control = "off")
18 | expect_s3_class(result2, "data.frame")
19 |
20 | # Test different quality control settings
21 | result3 <- calculate_ef(activities, start_date = "2023-05-01", end_date = "2023-07-01", quality_control = "off")
22 | expect_s3_class(result3, "data.frame")
23 |
24 | result4 <- calculate_ef(activities, start_date = "2023-05-01", end_date = "2023-07-01", quality_control = "flag")
25 | expect_s3_class(result4, "data.frame")
26 | })
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/pkgdown.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [main, master]
4 | pull_request:
5 | branches: [main, master]
6 | release:
7 | types: [published]
8 | workflow_dispatch:
9 |
10 | name: pkgdown
11 |
12 | jobs:
13 | pkgdown:
14 | runs-on: ubuntu-latest
15 |
16 | env:
17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
18 |
19 | permissions:
20 | contents: write
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 |
25 | - uses: r-lib/actions/setup-pandoc@v2
26 |
27 | - uses: r-lib/actions/setup-r@v2
28 | with:
29 | use-public-rspm: true
30 |
31 | - uses: r-lib/actions/setup-r-dependencies@v2
32 | with:
33 | extra-packages: any::pkgdown, local::.
34 | needs: website
35 |
36 | - name: Build site
37 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
38 | shell: Rscript {0}
39 |
40 | - name: Create .nojekyll file
41 | run: touch docs/.nojekyll
42 |
43 | - name: Deploy to GitHub pages 🚀
44 | if: github.event_name != 'pull_request'
45 | uses: JamesIves/github-pages-deploy-action@v4.6.8
46 | with:
47 | clean: false
48 | branch: gh-pages
49 | folder: docs
50 |
51 |
--------------------------------------------------------------------------------
/tests/testthat/test-utils.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/test-utils.R
2 |
3 | library(testthat)
4 | library(Athlytics) # To make internal functions available if using devtools::load_all()
5 | library(lubridate)
6 |
7 | # Utility Functions Tests
8 |
9 | test_that("english_month_year correctly formats dates", {
10 | # Test with a single date
11 | single_date <- ymd("2023-01-15")
12 | expect_equal(Athlytics:::english_month_year(single_date), "Jan 2023")
13 |
14 | # Test with multiple dates
15 | multiple_dates <- c(ymd("2023-03-10"), ymd("2024-11-05"))
16 | expect_equal(Athlytics:::english_month_year(multiple_dates), c("Mar 2023", "Nov 2024"))
17 |
18 | # Test with dates spanning year-end
19 | year_end_dates <- c(ymd("2022-12-25"), ymd("2023-01-01"))
20 | expect_equal(Athlytics:::english_month_year(year_end_dates), c("Dec 2022", "Jan 2023"))
21 |
22 | # Test with a leap year date
23 | leap_date <- ymd("2024-02-29")
24 | expect_equal(Athlytics:::english_month_year(leap_date), "Feb 2024")
25 |
26 | # Test with an empty vector of dates (should return empty character vector)
27 | empty_dates <- ymd(character(0))
28 | expect_equal(Athlytics:::english_month_year(empty_dates), character(0))
29 |
30 | # Test with NA date
31 | # expect_equal(Athlytics:::english_month_year(ymd(NA)), NA_character_)
32 | })
33 |
--------------------------------------------------------------------------------
/run_coverage.R:
--------------------------------------------------------------------------------
1 | # Run coverage test with fresh session
2 | message("Current working directory: ", getwd())
3 |
4 | # Load covr library
5 | library(covr)
6 |
7 | # Use package coverage (more reliable than file coverage)
8 | message("Calculating package coverage...")
9 | cov <- tryCatch({
10 | package_coverage(
11 | type = "all",
12 | quiet = FALSE,
13 | clean = FALSE
14 | )
15 | }, error = function(e) {
16 | message("Error calculating coverage: ", e$message)
17 | NULL
18 | })
19 |
20 | # Print coverage results
21 | if (!is.null(cov) && length(cov) > 0) {
22 | message("\n=== Coverage Summary ===")
23 | print(cov)
24 |
25 | message("\n=== Coverage Percentage ===")
26 | print(percent_coverage(cov))
27 |
28 | # Upload coverage with error handling
29 | message("\nUploading coverage data to Codecov...")
30 | tryCatch({
31 | codecov(coverage = cov, quiet = FALSE)
32 | message("Coverage upload successful!")
33 | }, error = function(e) {
34 | message("Coverage upload failed: ", e$message)
35 | message("Coverage data available but upload failed - this may be due to network issues or Codecov configuration.")
36 | })
37 | } else {
38 | message("Coverage object is NULL or empty.")
39 | message("This may indicate issues with test execution or package structure.")
40 | quit(status = 1)
41 | }
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for Athlytics
4 | title: '[FEATURE] '
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Problem or motivation**
11 | What problem would this feature solve?
12 |
13 | **Proposed solution**
14 | Describe the feature you'd like to see.
15 |
16 | **Alternatives considered**
17 | Other approaches you've evaluated.
18 |
19 | **Use case**
20 | How would this feature be used? What problem does it solve?
21 | - For single-athlete analysis?
22 | - For cohort/team analysis?
23 | - For research purposes?
24 |
25 | **Expected function signature (if applicable)**
26 | ```r
27 | # Example of how you envision using this feature
28 | result <- new_function(
29 | data = activities,
30 | parameter1 = value1,
31 | parameter2 = value2
32 | )
33 | ```
34 |
35 | **Additional context**
36 | Any other relevant information.
37 |
38 | **References**
39 | If this feature is based on published research or methodology, please cite the relevant papers:
40 | - Author et al. (Year). Title. Journal. DOI: ...
41 |
42 | **Implementation considerations**
43 | Are you willing to contribute to implementing this feature? (optional)
44 | - [ ] Yes, I can help with implementation
45 | - [ ] Yes, I can help with testing
46 | - [ ] Yes, I can help with documentation
47 | - [ ] I can provide example data for testing (de-identified)
48 |
49 |
--------------------------------------------------------------------------------
/man/add_reference_bands.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/cohort_reference.R
3 | \name{add_reference_bands}
4 | \alias{add_reference_bands}
5 | \title{Add Cohort Reference Bands to Existing Plot}
6 | \usage{
7 | add_reference_bands(
8 | p,
9 | reference_data,
10 | bands = c("p25_p75", "p05_p95", "p50"),
11 | alpha = 0.15,
12 | colors = list(p25_p75 = "#440154FF", p05_p95 = "#3B528BFF", p50 = "#21908CFF")
13 | )
14 | }
15 | \arguments{
16 | \item{p}{A ggplot object (typically from plot_acwr or similar).}
17 |
18 | \item{reference_data}{A data frame from \code{cohort_reference()}.}
19 |
20 | \item{bands}{Character vector specifying which bands to plot. Options:
21 | "p25_p75" (inner quartiles), "p05_p95" (outer 5-95 range), "p50" (median).
22 | Default c("p25_p75", "p05_p95", "p50").}
23 |
24 | \item{alpha}{Transparency for reference bands (0-1). Default 0.15.}
25 |
26 | \item{colors}{Named list of colors for bands. Default uses viridis colors.}
27 | }
28 | \value{
29 | A ggplot object with added reference bands.
30 | }
31 | \description{
32 | Adds percentile reference bands from a cohort to an individual's metric plot.
33 | }
34 | \examples{
35 | \dontrun{
36 | # Create base plot
37 | p <- plot_acwr(acwr_df = individual_acwr)
38 |
39 | # Add reference bands
40 | p_with_ref <- add_reference_bands(p, reference_data = cohort_ref)
41 | print(p_with_ref)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/testthat/test-additional-edge-cases.R:
--------------------------------------------------------------------------------
1 |
2 | test_that("additional edge cases for coverage", {
3 | # Test with very small numeric values - using reasonable date range
4 | activities <- data.frame(
5 | date = as.Date("2023-06-01"),
6 | type = "Run",
7 | distance = 0.001,
8 | moving_time = 1,
9 | average_heartrate = 50,
10 | average_speed = 0.001/1*1000
11 | )
12 |
13 | result <- calculate_ef(activities, start_date = "2023-05-01", end_date = "2023-07-01", quality_control = "off")
14 | expect_s3_class(result, "data.frame")
15 |
16 | # Test with very large numeric values
17 | activities2 <- data.frame(
18 | date = as.Date("2023-06-01"),
19 | type = "Run",
20 | distance = 100000,
21 | moving_time = 86400,
22 | average_heartrate = 200,
23 | average_speed = 100000/86400*1000
24 | )
25 |
26 | result2 <- calculate_ef(activities2, start_date = "2023-05-01", end_date = "2023-07-01", quality_control = "off")
27 | expect_s3_class(result2, "data.frame")
28 |
29 | # Test with NA values in data
30 | activities3 <- data.frame(
31 | date = as.Date("2023-06-01"),
32 | type = "Run",
33 | distance = 10,
34 | moving_time = 3600,
35 | average_heartrate = NA,
36 | average_speed = 10/3600*1000
37 | )
38 |
39 | result3 <- calculate_ef(activities3, start_date = "2023-05-01", end_date = "2023-07-01", quality_control = "off")
40 | expect_s3_class(result3, "data.frame")
41 | })
42 |
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a bug in Athlytics
4 | title: '[BUG] '
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Load data with '...'
16 | 2. Run function '....'
17 | 3. See error
18 |
19 | **Minimal reproducible example**
20 | ```r
21 | # Please provide a minimal reproducible example
22 | library(Athlytics)
23 |
24 | # Your code here
25 | ```
26 |
27 | **Expected behavior**
28 | A clear and concise description of what you expected to happen.
29 |
30 | **Actual behavior**
31 | What actually happened instead.
32 |
33 | **Error message**
34 | ```
35 | Paste the complete error message here
36 | ```
37 |
38 | **System information:**
39 | - OS: [e.g., Windows 11, macOS 14.0, Ubuntu 22.04]
40 | - R version: [e.g., 4.3.2]
41 | - Athlytics version: [e.g., 1.0.0]
42 | - Installation source: [CRAN or GitHub]
43 |
44 | **Session Info**
45 |
46 | Click to expand
47 |
48 | ```r
49 | # Please run sessionInfo() and paste the output here
50 | sessionInfo()
51 | ```
52 |
53 |
54 | **Additional context**
55 | Add any other context about the problem here (e.g., size of dataset, specific activity types, etc.)
56 |
57 | **Data Privacy Note**
58 | Please do NOT share any personal training data or identifiable information in this issue.
59 |
60 |
--------------------------------------------------------------------------------
/man/athlytics_sample_pbs.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/data.R
3 | \docType{data}
4 | \name{athlytics_sample_pbs}
5 | \alias{athlytics_sample_pbs}
6 | \title{Sample Personal Bests (PBs) Data for Athlytics}
7 | \format{
8 | A tibble with 100 rows and 10 variables:
9 | \describe{
10 | \item{activity_id}{ID of the activity where the effort occurred, as a character string.}
11 | \item{activity_date}{Date and time of the activity, as a POSIXct object.}
12 | \item{distance}{Target distance in meters for the best effort, as a numeric value.}
13 | \item{elapsed_time}{Elapsed time for the effort in seconds, as a numeric value.}
14 | \item{moving_time}{Moving time for the effort in seconds, as a numeric value.}
15 | \item{time_seconds}{Typically the same as elapsed_time for best efforts, in seconds, as a numeric value.}
16 | \item{cumulative_pb_seconds}{The personal best time for that distance up to that date, in seconds, as a numeric value.}
17 | \item{is_pb}{Logical, TRUE if this effort set a new personal best.}
18 | \item{distance_label}{Factor representing the distance (e.g., "1k", "5k").}
19 | \item{time_period}{Formatted time of the effort, as a Period object from lubridate.}
20 | }
21 | }
22 | \source{
23 | Simulated data generated for package examples.
24 | }
25 | \usage{
26 | athlytics_sample_pbs
27 | }
28 | \description{
29 | A dataset containing pre-calculated Personal Best (PB) times for various distances,
30 | derived from simulated Strava data. Used in examples and tests.
31 | }
32 | \keyword{datasets}
33 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: Athlytics
2 | Title: Academic R Package for Sports Physiology Analysis from Local 'Strava' Data
3 | Version: 1.0.1
4 | Author: Zhiang He [aut, cre]
5 | Maintainer: Zhiang He
6 | Authors@R:
7 | person("Zhiang", "He", email = "ang@hezhiang.com", role = c("aut", "cre"))
8 | Description: An open-source computational framework for longitudinal analysis of
9 | exercise physiology metrics using local 'Strava' data exports. Designed
10 | for personal analysis and sports science applications, this package provides
11 | standardized functions to calculate and visualize key physiological indicators
12 | including Acute:Chronic Workload Ratio (ACWR), Efficiency Factor (EF), and
13 | training load metrics.
14 | License: MIT + file LICENSE
15 | URL: https://hzacode.github.io/Athlytics/, https://github.com/HzaCode/Athlytics
16 | BugReports: https://github.com/HzaCode/Athlytics/issues
17 | Encoding: UTF-8
18 | LazyData: true
19 | Depends:
20 | R (>= 3.6.0)
21 | Imports:
22 | dplyr (>= 1.0.0),
23 | ggplot2,
24 | lubridate,
25 | purrr,
26 | readr,
27 | rlang (>= 0.4.0),
28 | scales,
29 | tidyr,
30 | zoo,
31 | R.utils,
32 | tools,
33 | utils
34 | Suggests:
35 | devtools,
36 | knitr,
37 | pkgdown,
38 | rmarkdown,
39 | testthat (>= 3.0.0),
40 | mockery,
41 | rStrava,
42 | xml2,
43 | FITfileR
44 | Remotes:
45 | grimbough/FITfileR
46 | VignetteBuilder: knitr
47 | RoxygenNote: 7.3.3
48 | Roxygen: list(markdown = TRUE)
49 | NeedsCompilation: no
50 |
--------------------------------------------------------------------------------
/man/plot_acwr_comparison.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot_acwr_enhanced.R
3 | \name{plot_acwr_comparison}
4 | \alias{plot_acwr_comparison}
5 | \title{Compare RA and EWMA Methods Side-by-Side}
6 | \usage{
7 | plot_acwr_comparison(
8 | acwr_ra,
9 | acwr_ewma,
10 | title = "ACWR Method Comparison: RA vs EWMA"
11 | )
12 | }
13 | \arguments{
14 | \item{acwr_ra}{A data frame from \code{calculate_acwr_ewma(..., method = "ra")}.}
15 |
16 | \item{acwr_ewma}{A data frame from \code{calculate_acwr_ewma(..., method = "ewma")}.}
17 |
18 | \item{title}{Plot title. Default "ACWR Method Comparison: RA vs EWMA".}
19 | }
20 | \value{
21 | A ggplot object with faceted comparison.
22 | }
23 | \description{
24 | Creates a faceted plot comparing Rolling Average and EWMA ACWR calculations.
25 | }
26 | \examples{
27 | # Example using sample data
28 | data("athlytics_sample_acwr", package = "Athlytics")
29 | if (!is.null(athlytics_sample_acwr) && nrow(athlytics_sample_acwr) > 0) {
30 | # Create two versions for comparison (simulate RA vs EWMA)
31 | acwr_ra <- athlytics_sample_acwr
32 | acwr_ewma <- athlytics_sample_acwr
33 | acwr_ewma$acwr_smooth <- acwr_ewma$acwr_smooth * runif(nrow(acwr_ewma), 0.95, 1.05)
34 |
35 | p <- plot_acwr_comparison(acwr_ra, acwr_ewma)
36 | print(p)
37 | }
38 |
39 | \dontrun{
40 | activities <- load_local_activities("export.zip")
41 |
42 | acwr_ra <- calculate_acwr_ewma(activities, method = "ra")
43 | acwr_ewma <- calculate_acwr_ewma(activities, method = "ewma")
44 |
45 | plot_acwr_comparison(acwr_ra, acwr_ewma)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 | .RData
3 | .aspell/defaults.R
4 | .Rbuildignore
5 | Athlytics.Rcheck/00check.log
6 | cran-comments.md
7 | CRAN-SUBMISSION
8 | /Athlytics.Rcheck/00_pkg_src/Athlytics/.aspell
9 | /Athlytics.Rcheck/00_pkg_src/Athlytics
10 | /Athlytics.Rcheck
11 | *.Rcheck/
12 | *.Rproj.Rcheck/
13 | # Archive files
14 | *.gz
15 | *.tar.gz
16 | *.zip
17 | *.tar
18 | *.rar
19 | *.7z
20 | *.Rcheck.tar.gz
21 | /tests/run_all_tests.R
22 | .httr-oauth
23 | tests/test_direct_api_streams.R
24 | .Rhistory
25 | coverage_report.html
26 | runnable_function_examples.html
27 |
28 | # User data files (not for GitHub)
29 | export_data/
30 | tests/manual_tests/
31 |
32 | /..Rcheck
33 |
34 | # User's private Strava data - DO NOT COMMIT
35 | strava_export_data/
36 |
37 | # Temporary test files
38 | test-*.html
39 | test-*.md
40 | *_test.md
41 | README_test*
42 |
43 | # Node modules (if accidentally created)
44 | node_modules/
45 | /doc/
46 | /Meta/
47 |
48 | # Paper drafts and submissions
49 |
50 | # Batch and PowerShell scripts (installation helpers)
51 | *.bat
52 | *.ps1
53 |
54 | # R temporary and debug files
55 | *.Rout
56 | *.rds
57 | *_coverage*.R
58 | *_debug*.R
59 | *_test*.R
60 | check_*.R
61 | debug_*.R
62 | final_*.R
63 | improve_*.R
64 | install_*.R
65 | parallel_*.R
66 | performance_*.R
67 | quick_*.R
68 | run_coverage_*.R
69 | run_specific_*.R
70 | run_tests*.R
71 | !run_coverage.R
72 | serial_*.R
73 | simple_*.R
74 | stable_*.R
75 | coverage_*.R
76 | test_*.R
77 | rename_*.R
78 | 调试.R
79 | .lang
80 | temp_order.txt
81 | test-results.txt
82 |
83 | # Strava data
84 | strava_data/
85 |
86 | # Auto-generated library files (for HTML reports)
87 | lib/
88 |
89 | # Auto-generated documentation (built by pkgdown CI)
90 | docs/
91 |
92 | # Temporary image files
93 | image.png
94 | image/
95 | preview_*.png
96 | reproduce_*.png
97 | reproduce_*.R
98 | temp_*.png
--------------------------------------------------------------------------------
/tests/testthat/test-load-local-activities.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/test-load_local_activities.R
2 |
3 | # Load Local Activities Tests
4 |
5 | library(Athlytics)
6 | library(testthat)
7 |
8 | test_that("load_local_activities works with sample data", {
9 | # Skip this test as athlytics_sample_data doesn't exist
10 | skip("athlytics_sample_data not available")
11 |
12 | # Check that sample data has the right structure
13 | expect_true(!is.null(athlytics_sample_acwr))
14 | expect_s3_class(athlytics_sample_acwr, "data.frame")
15 | })
16 |
17 | test_that("load_local_activities validates input parameters", {
18 | # Test with non-existent file
19 | expect_error(
20 | load_local_activities("nonexistent_file.csv"),
21 | "File not found"
22 | )
23 |
24 | # Test with invalid activity types
25 | skip_if_not(file.exists("strava_export_data/activities.csv"))
26 | expect_error(
27 | load_local_activities(
28 | "strava_export_data/activities.csv",
29 | activity_types = 123
30 | ),
31 | "character vector"
32 | )
33 | })
34 |
35 | test_that("load_local_activities detects ZIP files", {
36 | skip("Requires actual ZIP file for testing")
37 |
38 | # This test would run if a test ZIP file is available
39 | # activities <- load_local_activities("test_export.zip")
40 | # expect_s3_class(activities, "data.frame")
41 | # expect_true("id" %in% colnames(activities))
42 | })
43 |
44 | test_that("load_local_activities handles minimal CSV structure", {
45 | skip("CSV column parsing is complex and already tested with real data")
46 | })
47 |
48 | test_that("load_local_activities handles full Strava CSV structure", {
49 | skip("CSV column parsing is complex and already tested with real data")
50 | })
51 |
52 | test_that("load_local_activities filters by activity type", {
53 | skip("CSV column parsing is complex and already tested with real data")
54 | })
55 |
56 |
--------------------------------------------------------------------------------
/man/calculate_ef_from_stream.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/calculate_ef.R
3 | \name{calculate_ef_from_stream}
4 | \alias{calculate_ef_from_stream}
5 | \title{Calculate EF from Stream Data with Steady-State Analysis}
6 | \usage{
7 | calculate_ef_from_stream(
8 | stream_data,
9 | activity_date,
10 | act_type,
11 | ef_metric,
12 | min_steady_minutes,
13 | steady_cv_threshold,
14 | min_hr_coverage,
15 | quality_control
16 | )
17 | }
18 | \arguments{
19 | \item{stream_data}{Data frame with stream data (time, heartrate, watts/distance columns)}
20 |
21 | \item{activity_date}{Date of the activity}
22 |
23 | \item{act_type}{Activity type (e.g., "Run", "Ride")}
24 |
25 | \item{ef_metric}{Efficiency metric to calculate ("pace_hr" or "power_hr")}
26 |
27 | \item{min_steady_minutes}{Minimum duration for steady-state analysis (minutes)}
28 |
29 | \item{steady_cv_threshold}{Coefficient of variation threshold for steady state}
30 |
31 | \item{min_hr_coverage}{Minimum heart rate data coverage required}
32 |
33 | \item{quality_control}{Quality control setting ("off", "flag", "filter")}
34 | }
35 | \value{
36 | Data frame with EF calculation results
37 | }
38 | \description{
39 | Calculate efficiency factor (EF) from detailed stream data using steady-state analysis.
40 | This function analyzes heart rate and power/pace data to find periods of steady effort
41 | and calculates the efficiency factor for those periods.
42 | }
43 | \examples{
44 | \dontrun{
45 | # Parse activity file and calculate EF from streams
46 | streams <- parse_activity_file("activity_12345.fit")
47 | ef_result <- calculate_ef_from_stream(
48 | stream_data = streams,
49 | activity_date = as.Date("2025-01-15"),
50 | act_type = "Run",
51 | ef_metric = "pace_hr",
52 | min_steady_minutes = 20,
53 | steady_cv_threshold = 0.08,
54 | min_hr_coverage = 0.9,
55 | quality_control = "filter"
56 | )
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/.github/workflows/update-downloads-badge.yml:
--------------------------------------------------------------------------------
1 | name: Update CRAN Downloads Badge
2 |
3 | on:
4 | workflow_dispatch: # Allows manual triggering
5 | schedule:
6 | - cron: '0 * * * *' # Runs every hour at minute 0
7 |
8 | jobs:
9 | update-badge:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Update CRAN Downloads Badge
16 | run: |
17 | PKG_NAME=$(awk -F': ' '/^Package:/{print $2}' DESCRIPTION | tr -d '\r')
18 | DOWNLOADS_JSON=$(curl -s "https://cranlogs.r-pkg.org/downloads/grand-total/$PKG_NAME")
19 | TOTAL_DOWNLOADS=$(echo "$DOWNLOADS_JSON" | grep -o -E '[0-9]+' | head -1)
20 |
21 | if (( TOTAL_DOWNLOADS >= 1000000 )); then
22 | FORMATTED_DOWNLOADS=$(awk -v val=$TOTAL_DOWNLOADS 'BEGIN { printf "%.1fM", val / 1000000 }')
23 | elif (( TOTAL_DOWNLOADS >= 1000 )); then
24 | FORMATTED_DOWNLOADS=$(awk -v val=$TOTAL_DOWNLOADS 'BEGIN { printf "%.1fK", val / 1000 }')
25 | else
26 | FORMATTED_DOWNLOADS=$TOTAL_DOWNLOADS
27 | fi
28 |
29 | OLD_BADGE_URL="https://img.shields.io/badge/downloads-.*-orange?style=for-the-badge&logo=download"
30 | NEW_BADGE_URL="https://img.shields.io/badge/downloads-$FORMATTED_DOWNLOADS-orange?style=for-the-badge&logo=download"
31 |
32 | sed -i "s|$OLD_BADGE_URL|$NEW_BADGE_URL|g" README.md
33 | echo "Updated badge URL to: $NEW_BADGE_URL"
34 |
35 | - name: Commit and push changes
36 | uses: stefanzweifel/git-auto-commit-action@v5
37 | with:
38 | commit_message: "chore: Update CRAN downloads badge"
39 | commit_user_name: "github-actions[bot]"
40 | commit_user_email: "github-actions[bot]@users.noreply.github.com"
41 | commit_author: "github-actions[bot] "
42 |
--------------------------------------------------------------------------------
/tests/testthat/test-calculate-exposure.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/test-calculate_exposure.R
2 |
3 | # Exposure Calculation Tests
4 |
5 | library(Athlytics)
6 | library(testthat)
7 |
8 | # Load sample data
9 | data(athlytics_sample_exposure)
10 |
11 | # Create mock activities
12 | create_mock_activities <- function(n = 30) {
13 | dates <- seq(Sys.Date() - n, Sys.Date(), by = "day")
14 | data.frame(
15 | id = seq_len(length(dates)),
16 | name = paste("Activity", seq_len(length(dates))),
17 | type = sample(c("Run", "Ride"), length(dates), replace = TRUE),
18 | date = as.Date(dates),
19 | distance = runif(length(dates), 1000, 15000),
20 | moving_time = as.integer(runif(length(dates), 1200, 5400)),
21 | elapsed_time = as.integer(runif(length(dates), 1200, 5400)),
22 | average_heartrate = runif(length(dates), 120, 170),
23 | average_watts = runif(length(dates), 150, 250),
24 | elevation_gain = runif(length(dates), 0, 500),
25 | stringsAsFactors = FALSE
26 | )
27 | }
28 |
29 | test_that("calculate_exposure works with local activities data", {
30 | mock_activities <- create_mock_activities(60)
31 |
32 | exposure_result <- calculate_exposure(
33 | activities_data = mock_activities,
34 | load_metric = "duration_mins",
35 | acute_period = 7,
36 | chronic_period = 28
37 | )
38 |
39 | expect_s3_class(exposure_result, "data.frame")
40 | expect_true(all(c("date", "atl", "ctl") %in% colnames(exposure_result)))
41 | expect_gt(nrow(exposure_result), 0)
42 | })
43 |
44 | test_that("calculate_exposure validates input", {
45 | expect_error(
46 | calculate_exposure(activities_data = "not_a_dataframe"),
47 | "data frame"
48 | )
49 | })
50 |
51 | test_that("calculate_exposure works with sample data", {
52 | skip_if(is.null(athlytics_sample_exposure), "Sample exposure data not available")
53 |
54 | expect_s3_class(athlytics_sample_exposure, "data.frame")
55 | expect_true("atl" %in% colnames(athlytics_sample_exposure))
56 | })
57 |
--------------------------------------------------------------------------------
/man/plot_with_reference.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/cohort_reference.R
3 | \name{plot_with_reference}
4 | \alias{plot_with_reference}
5 | \title{Plot Individual Metric with Cohort Reference}
6 | \usage{
7 | plot_with_reference(
8 | individual,
9 | reference,
10 | metric = "acwr_smooth",
11 | date_col = "date",
12 | title = NULL,
13 | bands = c("p25_p75", "p05_p95", "p50")
14 | )
15 | }
16 | \arguments{
17 | \item{individual}{A data frame with individual athlete data (from calculate_acwr, etc.)}
18 |
19 | \item{reference}{A data frame from \code{cohort_reference()}.}
20 |
21 | \item{metric}{Name of the metric to plot. Default "acwr_smooth".}
22 |
23 | \item{date_col}{Name of the date column. Default "date".}
24 |
25 | \item{title}{Plot title. Default NULL (auto-generated).}
26 |
27 | \item{bands}{Which reference bands to show. Default c("p25_p75", "p05_p95", "p50").}
28 | }
29 | \value{
30 | A ggplot object.
31 | }
32 | \description{
33 | Creates a complete plot showing an individual's metric trend with cohort
34 | reference percentile bands.
35 | }
36 | \examples{
37 | # Simple example with fixed data
38 | individual_data <- data.frame(
39 | date = as.Date(c("2023-01-01", "2023-04-01", "2023-07-01", "2023-10-01")),
40 | acwr_smooth = c(1.0, 1.2, 0.9, 1.1)
41 | )
42 | reference_data <- data.frame(
43 | date = as.Date(c("2023-01-01", "2023-04-01", "2023-07-01", "2023-10-01")),
44 | percentile = rep(c("p05", "p25", "p50", "p75", "p95"), 4),
45 | value = c(0.7, 0.9, 1.1, 1.3, 1.5,
46 | 0.7, 0.9, 1.1, 1.3, 1.5,
47 | 0.7, 0.9, 1.1, 1.3, 1.5,
48 | 0.7, 0.9, 1.1, 1.3, 1.5)
49 | )
50 |
51 | p <- plot_with_reference(
52 | individual = individual_data,
53 | reference = reference_data,
54 | metric = "acwr_smooth"
55 | )
56 | print(p)
57 |
58 | \dontrun{
59 | plot_with_reference(
60 | individual = athlete_acwr,
61 | reference = cohort_ref,
62 | metric = "acwr_smooth"
63 | )
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/_pkgdown.yml:
--------------------------------------------------------------------------------
1 | url: https://hzacode.github.io/Athlytics/
2 |
3 | template:
4 | bootstrap: 5
5 | bootswatch: litera
6 |
7 | navbar:
8 | structure:
9 | left: [intro, reference, articles, news]
10 | right: [search, github]
11 | components:
12 | articles:
13 | text: Articles
14 | menu:
15 | - text: Getting Started
16 | href: articles/athlytics_introduction.html
17 | - text: Advanced Features
18 | href: articles/advanced_features.html
19 |
20 | reference:
21 | - title: Data Loading
22 | desc: Functions for loading and parsing local Strava data
23 | contents:
24 | - load_local_activities
25 | - parse_activity_file
26 |
27 | - title: Core Metrics Calculation
28 | desc: Calculate key physiological metrics
29 | contents:
30 | - calculate_acwr
31 | - calculate_acwr_ewma
32 | - calculate_ef
33 | - calculate_ef_from_stream
34 | - calculate_decoupling
35 | - calculate_exposure
36 | - calculate_pbs
37 |
38 | - title: Visualization
39 | desc: Plot and visualize training metrics
40 | contents:
41 | - plot_acwr
42 | - plot_acwr_enhanced
43 | - plot_acwr_comparison
44 | - plot_ef
45 | - plot_decoupling
46 | - plot_exposure
47 | - plot_pbs
48 | - plot_with_reference
49 |
50 | - title: Advanced Analysis
51 | desc: Cohort analysis and quality control
52 | contents:
53 | - cohort_reference
54 | - add_reference_bands
55 | - flag_quality
56 | - quality_summary
57 |
58 | - title: Themes and Colors
59 | desc: Visualization customization
60 | contents:
61 | - theme_athlytics
62 | - scale_athlytics
63 | - starts_with("athlytics_colors")
64 | - starts_with("athlytics_palette")
65 |
66 | - title: Sample Data
67 | desc: Example datasets for testing and learning
68 | contents:
69 | - starts_with("athlytics_sample")
70 |
71 | articles:
72 | - title: Getting Started
73 | navbar: ~
74 | contents:
75 | - athlytics_introduction
76 | - title: Advanced Usage
77 | navbar: ~
78 | contents:
79 | - advanced_features
80 |
81 |
--------------------------------------------------------------------------------
/tests/testthat/test-plot-ef-simple.R:
--------------------------------------------------------------------------------
1 | # Simple test for plot_ef.R to boost coverage
2 |
3 | test_that("plot_ef basic functionality", {
4 | data("athlytics_sample_ef")
5 |
6 | # Test basic plotting
7 | p1 <- plot_ef(athlytics_sample_ef)
8 | expect_s3_class(p1, "ggplot")
9 |
10 | # Test with add_trend_line = FALSE
11 | p2 <- plot_ef(athlytics_sample_ef, add_trend_line = FALSE)
12 | expect_s3_class(p2, "ggplot")
13 |
14 | # Test with different smoothing methods
15 | p3 <- plot_ef(athlytics_sample_ef, smoothing_method = "lm")
16 | expect_s3_class(p3, "ggplot")
17 | })
18 |
19 | test_that("plot_ef handles edge cases", {
20 | # Test with empty data
21 | empty_ef <- data.frame(
22 | date = lubridate::as_date(character(0)),
23 | activity_type = character(0),
24 | ef_value = numeric(0)
25 | )
26 |
27 | p_empty <- plot_ef(empty_ef)
28 | expect_s3_class(p_empty, "ggplot")
29 |
30 | # Test with single data point
31 | single_ef <- data.frame(
32 | date = Sys.Date(),
33 | activity_type = "Run",
34 | ef_value = 0.02
35 | )
36 |
37 | p_single <- plot_ef(single_ef)
38 | expect_s3_class(p_single, "ggplot")
39 | })
40 |
41 | test_that("plot_ef handles different ef_metric values", {
42 | data("athlytics_sample_ef")
43 |
44 | # Test with pace_hr metric
45 | p_pace <- plot_ef(athlytics_sample_ef, ef_metric = "pace_hr")
46 | expect_s3_class(p_pace, "ggplot")
47 |
48 | # Test with power_hr metric
49 | p_power <- plot_ef(athlytics_sample_ef, ef_metric = "power_hr")
50 | expect_s3_class(p_power, "ggplot")
51 | })
52 |
53 | test_that("plot_ef handles different activity types", {
54 | data("athlytics_sample_ef")
55 |
56 | # Test with different activity types
57 | p_run <- plot_ef(athlytics_sample_ef, activity_type = "Run")
58 | expect_s3_class(p_run, "ggplot")
59 |
60 | # Test with multiple activity types
61 | p_multi <- plot_ef(athlytics_sample_ef, activity_type = c("Run", "Ride"))
62 | expect_s3_class(p_multi, "ggplot")
63 | })
64 |
65 | test_that("plot_ef handles NA values gracefully", {
66 | data("athlytics_sample_ef")
67 |
68 | # Add some NA values
69 | ef_with_na <- athlytics_sample_ef
70 | ef_with_na$ef_value[1:5] <- NA
71 |
72 | p <- plot_ef(ef_with_na)
73 | expect_s3_class(p, "ggplot")
74 | })
75 |
--------------------------------------------------------------------------------
/man/calculate_pbs.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/calculate_pbs.R
3 | \name{calculate_pbs}
4 | \alias{calculate_pbs}
5 | \title{Calculate Personal Bests (PBs) from Local Strava Data}
6 | \usage{
7 | calculate_pbs(
8 | activities_data,
9 | export_dir = "strava_export_data",
10 | activity_type = "Run",
11 | start_date = NULL,
12 | end_date = NULL,
13 | distances_m = c(1000, 5000, 10000, 21097.5, 42195)
14 | )
15 | }
16 | \arguments{
17 | \item{activities_data}{A data frame of activities from \code{load_local_activities()}.
18 | Must contain columns: date, type, filename, distance.}
19 |
20 | \item{export_dir}{Base directory of the Strava export containing the activities folder.
21 | Default is "strava_export_data".}
22 |
23 | \item{activity_type}{Type of activities to analyze (typically "Run"). Default "Run".}
24 |
25 | \item{start_date}{Optional start date for analysis (YYYY-MM-DD). Defaults to NULL (all dates).}
26 |
27 | \item{end_date}{Optional end date for analysis (YYYY-MM-DD). Defaults to NULL (all dates).}
28 |
29 | \item{distances_m}{Target distances in meters to track.
30 | Default: c(1000, 5000, 10000, 21097.5, 42195) for 1k, 5k, 10k, half, full marathon.}
31 | }
32 | \value{
33 | A data frame with columns: activity_id, activity_date, distance,
34 | elapsed_time, moving_time, time_seconds, cumulative_pb_seconds, is_pb,
35 | distance_label, time_period
36 | }
37 | \description{
38 | Tracks personal best times for standard distances (1k, 5k, 10k, half marathon, marathon)
39 | by analyzing detailed activity files from Strava export data.
40 | }
41 | \details{
42 | This function analyzes detailed activity files (FIT/TCX/GPX) to find the fastest
43 | efforts at specified distances. It tracks cumulative personal bests over time,
44 | showing when new PBs are set.
45 |
46 | \strong{Note}: Requires detailed activity files from your Strava export. Activities
47 | must be long enough to contain the target distance segments.
48 | }
49 | \examples{
50 | # Example using simulated data
51 | data(athlytics_sample_pbs)
52 | print(head(athlytics_sample_pbs))
53 |
54 | \dontrun{
55 | # Load local activities
56 | activities <- load_local_activities("strava_export_data/activities.csv")
57 |
58 | # Calculate PBs for standard running distances
59 | pbs_data <- calculate_pbs(
60 | activities_data = activities,
61 | export_dir = "strava_export_data",
62 | activity_type = "Run"
63 | )
64 | print(head(pbs_data))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/testthat/test-plot-pbs-simple.R:
--------------------------------------------------------------------------------
1 | # Simple test for plot_pbs.R to boost coverage
2 |
3 | test_that("plot_pbs basic functionality", {
4 | data("athlytics_sample_pbs")
5 |
6 | # Prepare pbs_df with proper column names
7 | pbs_df <- athlytics_sample_pbs
8 | if ("date" %in% names(pbs_df) && !"activity_date" %in% names(pbs_df)) {
9 | names(pbs_df)[names(pbs_df) == "date"] <- "activity_date"
10 | }
11 |
12 | # Get distance_meters from the data
13 | req_dist_meters <- NULL
14 | if ("distance" %in% names(pbs_df)) {
15 | req_dist_meters <- unique(pbs_df$distance)
16 | } else if ("distance_target_m" %in% names(pbs_df)) {
17 | req_dist_meters <- unique(pbs_df$distance_target_m)
18 | }
19 |
20 | if (is.null(req_dist_meters) || length(req_dist_meters) == 0) {
21 | req_dist_meters <- c(1000, 5000, 10000)
22 | }
23 |
24 | # Test basic plotting
25 | p1 <- plot_pbs(pbs_df = pbs_df, distance_meters = req_dist_meters)
26 | expect_s3_class(p1, "ggplot")
27 |
28 | # Test with add_trend_line = FALSE
29 | p2 <- plot_pbs(pbs_df = pbs_df, distance_meters = req_dist_meters, add_trend_line = FALSE)
30 | expect_s3_class(p2, "ggplot")
31 | })
32 |
33 | test_that("plot_pbs handles edge cases", {
34 | # Test with NULL pbs_df and missing distance_meters
35 | expect_error(plot_pbs(pbs_df = NULL), "Either.*data.*or.*pbs_df.*must be provided")
36 |
37 | # Test with single data point
38 | single_pbs <- data.frame(
39 | activity_date = Sys.Date(),
40 | distance = 1000,
41 | elapsed_time = 300,
42 | is_new_pb = TRUE
43 | )
44 |
45 | p_single <- plot_pbs(pbs_df = single_pbs, distance_meters = 1000)
46 | expect_s3_class(p_single, "ggplot")
47 | })
48 |
49 | test_that("plot_pbs handles different PBS value ranges", {
50 | # Test with low time values
51 | low_pbs <- data.frame(
52 | activity_date = seq(Sys.Date() - 30, Sys.Date(), by = "1 day"),
53 | distance = rep(1000, 31),
54 | elapsed_time = runif(31, 180, 240),
55 | is_new_pb = sample(c(TRUE, FALSE), 31, replace = TRUE)
56 | )
57 |
58 | p_low <- plot_pbs(pbs_df = low_pbs, distance_meters = 1000)
59 | expect_s3_class(p_low, "ggplot")
60 |
61 | # Test with high time values
62 | high_pbs <- data.frame(
63 | activity_date = seq(Sys.Date() - 30, Sys.Date(), by = "1 day"),
64 | distance = rep(5000, 31),
65 | elapsed_time = runif(31, 1200, 1500),
66 | is_new_pb = sample(c(TRUE, FALSE), 31, replace = TRUE)
67 | )
68 |
69 | p_high <- plot_pbs(pbs_df = high_pbs, distance_meters = 5000)
70 | expect_s3_class(p_high, "ggplot")
71 | })
72 |
--------------------------------------------------------------------------------
/tests/testthat/test-plot-pbs-extended.R:
--------------------------------------------------------------------------------
1 | # Extended tests for plot_pbs to improve coverage
2 |
3 | library(testthat)
4 | library(Athlytics)
5 | library(ggplot2)
6 |
7 | create_mock_pbs <- function(n = 20) {
8 | data.frame(
9 | activity_id = 1:n,
10 | activity_date = seq(Sys.Date() - n*5, by = "5 days", length.out = n),
11 | distance = rep(c(1000, 5000, 10000, 21097), length.out = n),
12 | time_seconds = runif(n, 180, 7200),
13 | is_pb = sample(c(TRUE, FALSE), n, replace = TRUE, prob = c(0.3, 0.7)),
14 | pace_min_per_km = runif(n, 4, 6),
15 | speed_km_per_h = runif(n, 10, 15),
16 | activity_type = "Run",
17 | stringsAsFactors = FALSE
18 | )
19 | }
20 |
21 | test_that("plot_pbs works with basic pbs_df", {
22 | skip_if_not_installed("ggplot2")
23 |
24 | pbs_data <- create_mock_pbs(15)
25 |
26 | # Create plot using pbs_df parameter
27 | p <- plot_pbs(pbs_df = pbs_data)
28 |
29 | expect_s3_class(p, "ggplot")
30 | })
31 |
32 | test_that("plot_pbs handles different distance groups", {
33 | skip_if_not_installed("ggplot2")
34 |
35 | pbs_data <- create_mock_pbs(30)
36 | pbs_data$distance <- rep(c(1000, 5000, 10000), 10)
37 |
38 | p <- plot_pbs(pbs_df = pbs_data)
39 |
40 | expect_s3_class(p, "ggplot")
41 | })
42 |
43 | test_that("plot_pbs handles date range filtering", {
44 | skip_if_not_installed("ggplot2")
45 |
46 | pbs_data <- create_mock_pbs(40)
47 |
48 | p <- plot_pbs(
49 | pbs_df = pbs_data,
50 | date_range = c(Sys.Date() - 100, Sys.Date() - 50)
51 | )
52 |
53 | expect_s3_class(p, "ggplot")
54 | })
55 |
56 | test_that("plot_pbs handles data with no PBs", {
57 | skip_if_not_installed("ggplot2")
58 |
59 | pbs_data <- create_mock_pbs(10)
60 | pbs_data$is_pb <- FALSE
61 |
62 | p <- plot_pbs(pbs_df = pbs_data)
63 |
64 | expect_s3_class(p, "ggplot")
65 | })
66 |
67 | test_that("plot_pbs handles data with all PBs", {
68 | skip_if_not_installed("ggplot2")
69 |
70 | pbs_data <- create_mock_pbs(10)
71 | pbs_data$is_pb <- TRUE
72 |
73 | p <- plot_pbs(pbs_df = pbs_data)
74 |
75 | expect_s3_class(p, "ggplot")
76 | })
77 |
78 | test_that("plot_pbs handles single distance", {
79 | skip_if_not_installed("ggplot2")
80 |
81 | pbs_data <- create_mock_pbs(15)
82 | pbs_data$distance <- 5000 # All same distance
83 |
84 | p <- plot_pbs(pbs_df = pbs_data)
85 |
86 | expect_s3_class(p, "ggplot")
87 | })
88 |
89 | test_that("plot_pbs works with activities data", {
90 | skip_if_not_installed("ggplot2")
91 | skip("Requires export_dir - tested elsewhere")
92 | })
93 |
94 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Athlytics
2 |
3 | ## How to Contribute
4 |
5 | We follow a "GitHub Flow" like process where contributions are made through Pull Requests. To make a contribution, please follow these steps:
6 |
7 | 1. **Check for Existing Issues:** Before starting work on a new feature or bug fix, please check the [Issues tab](https://github.com/HzaCode/Athlytics/issues) to see if someone else has already reported the issue or is working on it.
8 |
9 | 2. **Open an Issue (If Necessary):**
10 | * **For Bug Reports:** If you find a bug, please open a new issue. Describe the bug clearly, including steps to reproduce it, what you expected to happen, and what actually happened. Include relevant version information for Athlytics and R.
11 | * **For Feature Requests:** If you have an idea for a new feature, please open an issue to discuss it. This allows us to discuss the potential feature and ensure it aligns with the project's goals before significant development effort is made.
12 | * **For Other Contributions:** If you want to contribute in other ways (e.g., documentation improvements, refactoring), it's still a good idea to open an issue first to discuss your proposed changes.
13 |
14 | 3. **Fork the Repository:** Fork the [Athlytics repository](https://github.com/HzaCode/Athlytics) to your own GitHub account.
15 |
16 | 4. **Create a Branch:** Create a new branch in your forked repository for your contribution. Choose a descriptive branch name (e.g., `fix-plot-bug`, `add-new-metric`).
17 |
18 | 5. **Make Your Changes:** Make your changes in your branch. Ensure that your code follows the existing style and that you add relevant tests for new functionality or bug fixes.
19 | * If you add new functions, please include Roxygen documentation.
20 | * Run `devtools::check()` locally to ensure your changes pass all checks.
21 |
22 | 6. **Commit Your Changes:** Commit your changes with a clear and descriptive commit message.
23 |
24 | 7. **Push to Your Fork:** Push your changes to your forked repository.
25 |
26 | 8. **Open a Pull Request (PR):**
27 | * Go to the original athlytics repository and open a Pull Request from your branch to the `main` branch (or the relevant development branch if specified).
28 | * In your PR description, clearly describe the changes you have made and **link to the issue you are addressing** (e.g., "Closes #123" or "Fixes #456").
29 | * Ensure your PR passes any automated checks (e.g., GitHub Actions).
30 |
31 | 9. **Discuss and Iterate:** Be prepared to discuss your changes and make further modifications based on feedback from the maintainers.
32 |
33 | ## Code of Conduct
34 |
35 | By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md).
36 |
37 | ## Questions?
38 |
39 | Open an issue for questions or clarifications.
40 |
--------------------------------------------------------------------------------
/data-raw/generate_sample_pbs.R:
--------------------------------------------------------------------------------
1 |
2 | # Script to regenerate athlytics_sample_pbs data
3 | library(dplyr)
4 | library(lubridate)
5 | library(Athlytics) # Assumes current version is loaded
6 |
7 | set.seed(42)
8 |
9 | # Define time range
10 | start_date <- as.Date("2023-01-01")
11 | dates <- seq(start_date, by = "1 month", length.out = 12)
12 |
13 | # Function to generate improvement trend
14 | generate_trend <- function(base_time, improvement_rate, noise_sd, n) {
15 | time_seq <- base_time * (1 - seq(0, improvement_rate, length.out = n))
16 | time_seq + rnorm(n, 0, noise_sd)
17 | }
18 |
19 | # 1k Data (Improving from ~5:00 to ~4:15)
20 | times_1k <- generate_trend(300, 0.15, 5, 12)
21 | df_1k <- data.frame(
22 | activity_date = as.POSIXct(dates),
23 | distance = 1000,
24 | time_seconds = times_1k,
25 | distance_label = "1k"
26 | )
27 |
28 | # 5k Data (Improving from ~25:00 to ~21:15) - fewer points
29 | dates_5k <- seq(start_date, by = "2 months", length.out = 6)
30 | times_5k <- generate_trend(1500, 0.15, 15, 6)
31 | df_5k <- data.frame(
32 | activity_date = as.POSIXct(dates_5k),
33 | distance = 5000,
34 | time_seconds = times_5k,
35 | distance_label = "5k"
36 | )
37 |
38 | # 10k Data (Improving from ~52:00 to ~45:00) - fewer points
39 | dates_10k <- seq(start_date, by = "3 months", length.out = 4)
40 | times_10k <- generate_trend(3120, 0.13, 30, 4)
41 | df_10k <- data.frame(
42 | activity_date = as.POSIXct(dates_10k),
43 | distance = 10000,
44 | time_seconds = times_10k,
45 | distance_label = "10k"
46 | )
47 |
48 | # Combine all data
49 | all_data <- rbind(df_1k, df_5k, df_10k) %>%
50 | arrange(activity_date) %>%
51 | mutate(
52 | activity_id = paste0("activity_", row_number()),
53 | elapsed_time = time_seconds,
54 | moving_time = time_seconds,
55 | time_period = seconds_to_period(time_seconds)
56 | )
57 |
58 | # Calculate cumulative PBs and is_pb flag
59 | athlytics_sample_pbs <- all_data %>%
60 | group_by(distance) %>%
61 | arrange(activity_date) %>%
62 | mutate(
63 | cumulative_pb_seconds = cummin(time_seconds),
64 | is_pb = time_seconds == cumulative_pb_seconds,
65 | # Ensure distance_label is a factor with correct levels
66 | distance_label = factor(distance_label, levels = c("1k", "5k", "10k"))
67 | ) %>%
68 | ungroup() %>%
69 | # Select and order columns to match package documentation
70 | select(
71 | activity_id, activity_date, distance, elapsed_time, moving_time,
72 | time_seconds, cumulative_pb_seconds, is_pb, distance_label, time_period
73 | )
74 |
75 | # Convert to tibble for consistency
76 | athlytics_sample_pbs <- as_tibble(athlytics_sample_pbs)
77 |
78 | # Save to data directory
79 | save(athlytics_sample_pbs, file = "data/athlytics_sample_pbs.rda", compress = "xz")
80 |
81 | message("athlytics_sample_pbs.rda regenerated with ", nrow(athlytics_sample_pbs), " rows.")
82 | print(table(athlytics_sample_pbs$distance_label))
83 | print(table(athlytics_sample_pbs$is_pb))
84 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 | Fixes #(issue number)
5 |
6 | ## Type of change
7 |
8 |
9 | - [ ] Bug fix (non-breaking change which fixes an issue)
10 | - [ ] New feature (non-breaking change which adds functionality)
11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12 | - [ ] Documentation update
13 | - [ ] Performance improvement
14 | - [ ] Code refactoring
15 | - [ ] Test coverage improvement
16 |
17 | ## Changes Made
18 |
19 |
20 | -
21 | -
22 | -
23 |
24 | ## Testing
25 |
26 |
27 | - [ ] All existing tests pass (`devtools::test()`)
28 | - [ ] Added new tests for new functionality
29 | - [ ] Tested manually with real Strava data
30 | - [ ] R CMD check passes with no errors, warnings, or notes
31 | - [ ] Code coverage maintained or improved
32 |
33 | ### Test results
34 | ```r
35 | # Paste relevant test output here
36 | ```
37 |
38 | ## Documentation
39 |
40 |
41 | - [ ] Updated function documentation (roxygen2)
42 | - [ ] Updated README (if applicable)
43 | - [ ] Updated NEWS.md
44 | - [ ] Updated vignettes (if applicable)
45 | - [ ] All examples run successfully
46 |
47 | ## Code Quality
48 |
49 |
50 | - [ ] My code follows the style guidelines of this project
51 | - [ ] I have performed a self-review of my own code
52 | - [ ] I have commented my code, particularly in hard-to-understand areas
53 | - [ ] My changes generate no new warnings or errors
54 | - [ ] I have checked for and resolved any merge conflicts
55 |
56 | ## Dependencies
57 |
58 |
59 | - [ ] No new dependencies added
60 | - [ ] New dependencies added (list below with justification)
61 | - Package:
62 | - Reason:
63 |
64 | ## Breaking Changes
65 |
66 |
67 | **Does this PR introduce breaking changes?**
68 | - [ ] Yes
69 | - [ ] No
70 |
71 | If yes, please describe:
72 | - What breaks:
73 | - How to migrate:
74 |
75 | ## Additional Notes
76 |
77 |
78 | ## Checklist for Reviewers
79 |
80 |
81 | - [ ] Code follows project conventions
82 | - [ ] Tests are adequate and pass
83 | - [ ] Documentation is complete and accurate
84 | - [ ] Changes are backwards compatible (or breaking changes are justified and documented)
85 | - [ ] NAMESPACE and DESCRIPTION are updated if needed
86 | - [ ] NEWS.md is updated
87 |
88 | ---
89 |
90 | **Note:** By submitting this pull request, I confirm that my contribution is made under the terms of the MIT License and I have read and agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
91 |
92 |
--------------------------------------------------------------------------------
/man/plot_acwr_enhanced.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot_acwr_enhanced.R
3 | \name{plot_acwr_enhanced}
4 | \alias{plot_acwr_enhanced}
5 | \title{Enhanced ACWR Plot with Confidence Bands and Reference}
6 | \usage{
7 | plot_acwr_enhanced(
8 | acwr_data,
9 | reference_data = NULL,
10 | show_ci = TRUE,
11 | show_reference = TRUE,
12 | reference_bands = c("p25_p75", "p05_p95", "p50"),
13 | highlight_zones = TRUE,
14 | title = NULL,
15 | subtitle = NULL,
16 | method_label = NULL
17 | )
18 | }
19 | \arguments{
20 | \item{acwr_data}{A data frame from \code{calculate_acwr_ewma()} containing ACWR values.}
21 |
22 | \item{reference_data}{Optional. A data frame from \code{cohort_reference()} for
23 | adding cohort reference bands.}
24 |
25 | \item{show_ci}{Logical. Whether to show confidence bands (if available in data).
26 | Default TRUE.}
27 |
28 | \item{show_reference}{Logical. Whether to show cohort reference bands (if provided).
29 | Default TRUE.}
30 |
31 | \item{reference_bands}{Which reference bands to show. Default c("p25_p75", "p05_p95", "p50").}
32 |
33 | \item{highlight_zones}{Logical. Whether to highlight ACWR risk zones. Default TRUE.}
34 |
35 | \item{title}{Plot title. Default NULL (auto-generated).}
36 |
37 | \item{subtitle}{Plot subtitle. Default NULL (auto-generated).}
38 |
39 | \item{method_label}{Optional label for the method used (e.g., "RA", "EWMA"). Default NULL.}
40 | }
41 | \value{
42 | A ggplot object.
43 | }
44 | \description{
45 | Creates a comprehensive ACWR visualization with optional confidence bands
46 | and cohort reference percentiles.
47 | }
48 | \details{
49 | This enhanced plot function combines multiple visualization layers:
50 | \itemize{
51 | \item Risk zone shading (sweet spot: 0.8-1.3, caution: 1.3-1.5, high risk: >1.5)
52 | \item Cohort reference percentile bands (if provided)
53 | \item Bootstrap confidence bands (if available in data)
54 | \item Individual ACWR trend line
55 | }
56 |
57 | The layering order (bottom to top):
58 | \enumerate{
59 | \item Risk zones (background)
60 | \item Cohort reference bands (P5-P95, then P25-P75)
61 | \item Confidence intervals (individual uncertainty)
62 | \item ACWR line (individual trend)
63 | }
64 | }
65 | \examples{
66 | # Example using sample data
67 | data("athlytics_sample_acwr", package = "Athlytics")
68 | if (!is.null(athlytics_sample_acwr) && nrow(athlytics_sample_acwr) > 0) {
69 | p <- plot_acwr_enhanced(athlytics_sample_acwr, show_ci = FALSE)
70 | print(p)
71 | }
72 |
73 | \dontrun{
74 | # Load activities
75 | activities <- load_local_activities("export.zip")
76 |
77 | # Calculate ACWR with EWMA and confidence bands
78 | acwr <- calculate_acwr_ewma(
79 | activities,
80 | method = "ewma",
81 | ci = TRUE,
82 | B = 200
83 | )
84 |
85 | # Basic enhanced plot
86 | plot_acwr_enhanced(acwr)
87 |
88 | # With cohort reference
89 | reference <- cohort_reference(cohort_data, metric = "acwr_smooth")
90 | plot_acwr_enhanced(acwr, reference_data = reference)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/man/calculate_exposure.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/calculate_exposure.R
3 | \name{calculate_exposure}
4 | \alias{calculate_exposure}
5 | \title{Calculate Training Load Exposure (ATL, CTL, ACWR)}
6 | \usage{
7 | calculate_exposure(
8 | activities_data,
9 | activity_type = c("Run", "Ride", "VirtualRide", "VirtualRun"),
10 | load_metric = "duration_mins",
11 | acute_period = 7,
12 | chronic_period = 42,
13 | user_ftp = NULL,
14 | user_max_hr = NULL,
15 | user_resting_hr = NULL,
16 | end_date = NULL
17 | )
18 | }
19 | \arguments{
20 | \item{activities_data}{A data frame of activities from \code{load_local_activities()}.
21 | Must contain columns: date, distance, moving_time, elapsed_time,
22 | average_heartrate, average_watts, type, elevation_gain.}
23 |
24 | \item{activity_type}{Type(s) of activities to include (e.g., "Run", "Ride").
25 | Default includes common run/ride types.}
26 |
27 | \item{load_metric}{Method for calculating daily load (e.g., "duration_mins",
28 | "distance_km", "tss", "hrss"). Default "duration_mins".}
29 |
30 | \item{acute_period}{Days for the acute load window (e.g., 7).}
31 |
32 | \item{chronic_period}{Days for the chronic load window (e.g., 42). Must be greater than \code{acute_period}.}
33 |
34 | \item{user_ftp}{Required if \code{load_metric = "tss"}. Your Functional Threshold Power.}
35 |
36 | \item{user_max_hr}{Required if \code{load_metric = "hrss"}. Your maximum heart rate.}
37 |
38 | \item{user_resting_hr}{Required if \code{load_metric = "hrss"}. Your resting heart rate.}
39 |
40 | \item{end_date}{Optional. Analysis end date (YYYY-MM-DD string or Date). Defaults to today.
41 | The analysis period covers the \code{chronic_period} days ending on this date.}
42 | }
43 | \value{
44 | A data frame with columns: \code{date}, \code{daily_load}, \code{atl} (Acute Load),
45 | \code{ctl} (Chronic Load), and \code{acwr} (Acute:Chronic Ratio) for the analysis period.
46 | }
47 | \description{
48 | Calculates training load metrics like ATL, CTL, and ACWR from local Strava data.
49 | }
50 | \details{
51 | Calculates daily load, ATL, CTL, and ACWR from Strava activities based on the chosen metric and periods.
52 |
53 | Provides data for \code{plot_exposure}. Requires extra prior data for
54 | accurate initial CTL. Requires FTP/HR parameters for TSS/HRSS metrics.
55 | }
56 | \examples{
57 | # Example using simulated data
58 | data(athlytics_sample_exposure)
59 | print(head(athlytics_sample_exposure))
60 |
61 | \dontrun{
62 | # Example using local Strava export data
63 | activities <- load_local_activities("strava_export_data/activities.csv")
64 |
65 | # Calculate training load for Rides using TSS
66 | ride_exposure_tss <- calculate_exposure(
67 | activities_data = activities,
68 | activity_type = "Ride",
69 | load_metric = "tss",
70 | user_ftp = 280,
71 | acute_period = 7,
72 | chronic_period = 28
73 | )
74 | print(head(ride_exposure_tss))
75 |
76 | # Calculate training load for Runs using HRSS
77 | run_exposure_hrss <- calculate_exposure(
78 | activities_data = activities,
79 | activity_type = "Run",
80 | load_metric = "hrss",
81 | user_max_hr = 190,
82 | user_resting_hr = 50
83 | )
84 | print(tail(run_exposure_hrss))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/man/plot_ef.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot_ef.R
3 | \name{plot_ef}
4 | \alias{plot_ef}
5 | \title{Plot Efficiency Factor (EF) Trend}
6 | \usage{
7 | plot_ef(
8 | data,
9 | activity_type = c("Run", "Ride"),
10 | ef_metric = c("pace_hr", "power_hr"),
11 | start_date = NULL,
12 | end_date = NULL,
13 | min_duration_mins = 20,
14 | add_trend_line = TRUE,
15 | smoothing_method = "loess",
16 | ef_df = NULL,
17 | group_var = NULL,
18 | group_colors = NULL
19 | )
20 | }
21 | \arguments{
22 | \item{data}{\strong{Recommended: Pass pre-calculated data via \code{ef_df} (local export preferred).}
23 | A data frame from \code{calculate_ef()} or activities data from \code{load_local_activities()}.}
24 |
25 | \item{activity_type}{Type(s) of activities to analyze (e.g., "Run", "Ride").}
26 |
27 | \item{ef_metric}{Metric to calculate: "pace_hr" (Speed/HR) or "power_hr" (Power/HR).}
28 |
29 | \item{start_date}{Optional. Analysis start date (YYYY-MM-DD string or Date). Defaults to ~1 year ago.}
30 |
31 | \item{end_date}{Optional. Analysis end date (YYYY-MM-DD string or Date). Defaults to today.}
32 |
33 | \item{min_duration_mins}{Minimum activity duration (minutes) to include. Default 20.}
34 |
35 | \item{add_trend_line}{Add a smoothed trend line (\code{geom_smooth})? Default \code{TRUE}.}
36 |
37 | \item{smoothing_method}{Smoothing method for trend line (e.g., "loess", "lm"). Default "loess".}
38 |
39 | \item{ef_df}{\strong{Recommended.} A pre-calculated data frame from \code{calculate_ef()}.
40 | When provided, analysis uses local data only (no API calls).}
41 |
42 | \item{group_var}{Optional. Column name for grouping/faceting (e.g., "athlete_id").}
43 |
44 | \item{group_colors}{Optional. Named vector of colors for groups.}
45 | }
46 | \value{
47 | A ggplot object showing the EF trend.
48 | }
49 | \description{
50 | Visualizes the trend of Efficiency Factor (EF) over time.
51 | }
52 | \details{
53 | Plots the Efficiency Factor (EF) trend over time. \strong{Recommended workflow: Use local data via \code{ef_df}.}
54 |
55 | Plots EF (output/HR based on activity averages). An upward trend
56 | often indicates improved aerobic fitness. Points colored by activity type.
57 | \strong{Best practice: Use \code{load_local_activities()} + \code{calculate_ef()} + this function.}
58 | }
59 | \examples{
60 | # Example using pre-calculated sample data
61 | data("athlytics_sample_ef", package = "Athlytics")
62 | p <- plot_ef(athlytics_sample_ef)
63 | print(p)
64 |
65 | \dontrun{
66 | # Example using local Strava export data
67 | activities <- load_local_activities("strava_export_data/activities.csv")
68 |
69 | # Plot Pace/HR EF trend for Runs (last 6 months)
70 | plot_ef(data = activities,
71 | activity_type = "Run",
72 | ef_metric = "pace_hr",
73 | start_date = Sys.Date() - months(6))
74 |
75 | # Plot Power/HR EF trend for Rides
76 | plot_ef(data = activities,
77 | activity_type = "Ride",
78 | ef_metric = "power_hr")
79 |
80 | # Plot Pace/HR EF trend for multiple Run types (no trend line)
81 | plot_ef(data = activities,
82 | activity_type = c("Run", "VirtualRun"),
83 | ef_metric = "pace_hr",
84 | add_trend_line = FALSE)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/man/cohort_reference.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/cohort_reference.R
3 | \name{cohort_reference}
4 | \alias{cohort_reference}
5 | \title{Calculate Cohort Reference Percentiles}
6 | \usage{
7 | cohort_reference(
8 | data,
9 | metric = "acwr_smooth",
10 | by = c("sport"),
11 | probs = c(0.05, 0.25, 0.5, 0.75, 0.95),
12 | min_athletes = 5,
13 | date_col = "date"
14 | )
15 | }
16 | \arguments{
17 | \item{data}{A data frame containing metric values for multiple athletes.
18 | Must include columns: \code{date}, \code{athlete_id}, and the metric column.}
19 |
20 | \item{metric}{Name of the metric column to calculate percentiles for
21 | (e.g., "acwr", "acwr_smooth", "ef", "decoupling"). Default "acwr_smooth".}
22 |
23 | \item{by}{Character vector of grouping variables. Options: "sport", "sex",
24 | "age_band", "athlete_id". Default c("sport").}
25 |
26 | \item{probs}{Numeric vector of probabilities for percentiles (0-1).
27 | Default c(0.05, 0.25, 0.50, 0.75, 0.95) for 5th, 25th, 50th, 75th, 95th percentiles.}
28 |
29 | \item{min_athletes}{Minimum number of athletes required per group to calculate
30 | valid percentiles. Default 5.}
31 |
32 | \item{date_col}{Name of the date column. Default "date".}
33 | }
34 | \value{
35 | A long-format data frame with columns:
36 | \describe{
37 | \item{date}{Date}
38 | \item{...}{Grouping variables (as specified in \code{by})}
39 | \item{percentile}{Percentile label (e.g., "p05", "p25", "p50", "p75", "p95")}
40 | \item{value}{Metric value at that percentile}
41 | \item{n_athletes}{Number of athletes contributing to this percentile}
42 | }
43 | }
44 | \description{
45 | Calculates reference percentiles for a metric across a cohort of athletes,
46 | stratified by specified grouping variables (e.g., sport, sex, age band).
47 | }
48 | \details{
49 | This function creates cohort-level reference bands for comparing individual
50 | athlete metrics to their peers. Common use cases:
51 | \itemize{
52 | \item Compare an athlete's ACWR trend to team averages
53 | \item Identify outliers (athletes outside P5-P95 range)
54 | \item Track team-wide trends over time
55 | }
56 |
57 | \strong{Important}: Percentile bands represent \strong{population variability}, not
58 | statistical confidence intervals for individual values.
59 | }
60 | \examples{
61 | \dontrun{
62 | # Load activities for multiple athletes
63 | athlete1 <- load_local_activities("athlete1_export.zip") \%>\%
64 | mutate(athlete_id = "athlete1")
65 | athlete2 <- load_local_activities("athlete2_export.zip") \%>\%
66 | mutate(athlete_id = "athlete2")
67 | athlete3 <- load_local_activities("athlete3_export.zip") \%>\%
68 | mutate(athlete_id = "athlete3")
69 |
70 | # Combine data
71 | cohort_data <- bind_rows(athlete1, athlete2, athlete3)
72 |
73 | # Calculate ACWR for each athlete
74 | cohort_acwr <- cohort_data \%>\%
75 | group_by(athlete_id) \%>\%
76 | group_modify(~calculate_acwr_ewma(.x))
77 |
78 | # Calculate reference percentiles
79 | reference <- cohort_reference(
80 | cohort_acwr,
81 | metric = "acwr_smooth",
82 | by = c("sport"),
83 | probs = c(0.05, 0.25, 0.5, 0.75, 0.95)
84 | )
85 |
86 | # Plot individual against cohort
87 | plot_with_reference(
88 | individual = cohort_acwr \%>\% filter(athlete_id == "athlete1"),
89 | reference = reference
90 | )
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/paper/paper.bib:
--------------------------------------------------------------------------------
1 | @article{gabbett2016,
2 | author = {Gabbett, Tim J.},
3 | title = {The training–injury prevention paradox: should athletes be training smarter and harder?},
4 | journal = {British Journal of Sports Medicine},
5 | year = {2016},
6 | volume = {50},
7 | number = {5},
8 | pages = {273--280},
9 | doi = {10.1136/bjsports-2015-095788}
10 | }
11 |
12 | @article{impellizzeri2020acwr,
13 | author = {Impellizzeri, Franco M. and Tenan, Matthew S. and Kempton, Tom and Novak, Andrew and Coutts, Aaron J.},
14 | title = {Acute:Chronic Workload Ratio: Conceptual Issues and Fundamental Pitfalls},
15 | journal = {International Journal of Sports Physiology and Performance},
16 | year = {2020},
17 | volume = {15},
18 | number = {6},
19 | pages = {907--913},
20 | doi = {10.1123/ijspp.2019-0864}
21 | }
22 |
23 | @article{kunsch1989,
24 | author = {K{\"u}nsch, Hans R.},
25 | title = {The Jackknife and the Bootstrap for General Stationary Observations},
26 | journal = {The Annals of Statistics},
27 | year = {1989},
28 | volume = {17},
29 | number = {3},
30 | pages = {1217--1241},
31 | doi = {10.1214/aos/1176347265}
32 | }
33 |
34 | @article{politis1994,
35 | author = {Politis, Dimitris N. and Romano, Joseph P.},
36 | title = {The Stationary Bootstrap},
37 | journal = {Journal of the American Statistical Association},
38 | year = {1994},
39 | volume = {89},
40 | number = {428},
41 | pages = {1303--1313},
42 | doi = {10.1080/01621459.1994.10476870}
43 | }
44 |
45 | @Manual{rStrava,
46 | title = {rStrava: Access the Strava API},
47 | author = {Marcus W. Beck and Pedro Villarroel and Daniel Padfield and Lorenzo Gaborini and Niklas von Maltzahn},
48 | year = {2024},
49 | note = {R package version 1.3.2},
50 | doi = {10.32614/CRAN.package.rStrava},
51 | url = {https://CRAN.R-project.org/package=rStrava}
52 | }
53 |
54 | @article{trackeR_jss,
55 | author = {Frick, Hannah and Kosmidis, Ioannis},
56 | title = {trackeR: Infrastructure for Running and Cycling Data from GPS-Enabled Tracking Devices in R},
57 | journal = {Journal of Statistical Software},
58 | year = {2017},
59 | volume = {82},
60 | number = {7},
61 | pages = {1--29},
62 | doi = {10.18637/jss.v082.i07}
63 | }
64 |
65 | @Manual{activatr,
66 | title = {activatr: Utilities for Parsing and Plotting Activities},
67 | author = {Daniel Schafer},
68 | year = {2024},
69 | note = {R package version 0.2.1},
70 | url = {https://CRAN.R-project.org/package=activatr}
71 | }
72 |
73 | @Misc{FITfileR,
74 | title = {FITfileR: Read FIT Files in R},
75 | author = {Mike Smith},
76 | year = {2024},
77 | howpublished = {GitHub repository},
78 | url = {https://github.com/grimbough/FITfileR},
79 | note = {Accessed 2025-10}
80 | }
81 |
82 | @Manual{injurytools,
83 | title = {injurytools: A Toolkit for Sports Injury Data Analysis},
84 | author = {Lore Zumeta Olaskoaga and Dae-Jin Lee},
85 | year = {2023},
86 | note = {R package version 1.0.3},
87 | doi = {10.32614/CRAN.package.injurytools},
88 | url = {https://CRAN.R-project.org/package=injurytools}
89 | }
90 |
91 | @Manual{ACWR,
92 | title = {ACWR: Acute Chronic Workload Ratio Calculation},
93 | author = {Ben Torvaney},
94 | year = {2024},
95 | note = {R package version 0.2.0},
96 | url = {https://CRAN.R-project.org/package=ACWR}
97 | }
--------------------------------------------------------------------------------
/tests/testthat/test-plot-ef-extended.R:
--------------------------------------------------------------------------------
1 | # Extended tests for plot_ef to improve coverage
2 |
3 | library(testthat)
4 | library(Athlytics)
5 | library(ggplot2)
6 |
7 | create_mock_ef_data <- function(n = 30) {
8 | data.frame(
9 | date = seq(Sys.Date() - n, Sys.Date() - 1, by = "day"),
10 | ef_value = runif(n, 2.5, 4.5),
11 | activity_type = sample(c("Run", "Ride"), n, replace = TRUE),
12 | ef_metric = "pace_hr",
13 | stringsAsFactors = FALSE
14 | )
15 | }
16 |
17 | test_that("plot_ef recognizes ef_value column", {
18 | skip_if_not_installed("ggplot2")
19 |
20 | ef_data <- create_mock_ef_data(40)
21 |
22 | p <- plot_ef(ef_data)
23 |
24 | expect_s3_class(p, "ggplot")
25 | })
26 |
27 | test_that("plot_ef handles single activity type", {
28 | skip_if_not_installed("ggplot2")
29 |
30 | ef_data <- create_mock_ef_data(30)
31 | ef_data$activity_type <- "Run"
32 |
33 | p <- plot_ef(ef_data)
34 |
35 | expect_s3_class(p, "ggplot")
36 | })
37 |
38 | test_that("plot_ef handles multiple activity types", {
39 | skip_if_not_installed("ggplot2")
40 |
41 | ef_data <- create_mock_ef_data(40)
42 | ef_data$activity_type <- rep(c("Run", "Ride", "VirtualRun"), length.out = 40)
43 |
44 | p <- plot_ef(ef_data)
45 |
46 | expect_s3_class(p, "ggplot")
47 | })
48 |
49 | test_that("plot_ef works without trend line", {
50 | skip_if_not_installed("ggplot2")
51 |
52 | ef_data <- create_mock_ef_data(25)
53 |
54 | p <- plot_ef(ef_data, add_trend_line = FALSE)
55 |
56 | expect_s3_class(p, "ggplot")
57 | })
58 |
59 | test_that("plot_ef works with different smoothing methods", {
60 | skip_if_not_installed("ggplot2")
61 |
62 | ef_data <- create_mock_ef_data(50)
63 |
64 | # Test loess
65 | p1 <- plot_ef(ef_data, smoothing_method = "loess")
66 | expect_s3_class(p1, "ggplot")
67 |
68 | # Test lm
69 | p2 <- plot_ef(ef_data, smoothing_method = "lm")
70 | expect_s3_class(p2, "ggplot")
71 | })
72 |
73 | test_that("plot_ef handles date filtering", {
74 | skip_if_not_installed("ggplot2")
75 |
76 | ef_data <- create_mock_ef_data(60)
77 |
78 | p <- plot_ef(
79 | ef_data,
80 | start_date = Sys.Date() - 40,
81 | end_date = Sys.Date() - 10
82 | )
83 |
84 | expect_s3_class(p, "ggplot")
85 | })
86 |
87 | test_that("plot_ef handles NA values in ef_value", {
88 | skip_if_not_installed("ggplot2")
89 |
90 | ef_data <- create_mock_ef_data(30)
91 | ef_data$ef_value[c(5, 10, 15)] <- NA
92 |
93 | p <- plot_ef(ef_data)
94 |
95 | expect_s3_class(p, "ggplot")
96 | })
97 |
98 | test_that("plot_ef handles small datasets", {
99 | skip_if_not_installed("ggplot2")
100 |
101 | ef_data <- create_mock_ef_data(5)
102 |
103 | p <- plot_ef(ef_data)
104 |
105 | expect_s3_class(p, "ggplot")
106 | })
107 |
108 | test_that("plot_ef works with pace_hr metric", {
109 | skip_if_not_installed("ggplot2")
110 |
111 | ef_data <- create_mock_ef_data(30)
112 | ef_data$ef_metric <- "pace_hr"
113 |
114 | p <- plot_ef(ef_data)
115 |
116 | expect_s3_class(p, "ggplot")
117 | })
118 |
119 | test_that("plot_ef works with power_hr metric", {
120 | skip_if_not_installed("ggplot2")
121 |
122 | ef_data <- create_mock_ef_data(30)
123 | ef_data$ef_metric <- "power_hr"
124 | ef_data$activity_type <- "Ride"
125 |
126 | p <- plot_ef(ef_data)
127 |
128 | expect_s3_class(p, "ggplot")
129 | })
130 |
131 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | export(add_reference_bands)
4 | export(athlytics_colors_acwr_zones)
5 | export(athlytics_colors_ef)
6 | export(athlytics_colors_training_load)
7 | export(athlytics_palette_academic)
8 | export(athlytics_palette_cell)
9 | export(athlytics_palette_nature)
10 | export(athlytics_palette_science)
11 | export(athlytics_palette_vibrant)
12 | export(calculate_acwr)
13 | export(calculate_acwr_ewma)
14 | export(calculate_decoupling)
15 | export(calculate_ef)
16 | export(calculate_ef_from_stream)
17 | export(calculate_exposure)
18 | export(calculate_pbs)
19 | export(cohort_reference)
20 | export(flag_quality)
21 | export(load_local_activities)
22 | export(parse_activity_file)
23 | export(plot_acwr)
24 | export(plot_acwr_comparison)
25 | export(plot_acwr_enhanced)
26 | export(plot_decoupling)
27 | export(plot_ef)
28 | export(plot_exposure)
29 | export(plot_pbs)
30 | export(plot_with_reference)
31 | export(quality_summary)
32 | export(scale_athlytics)
33 | export(theme_athlytics)
34 | import(ggplot2)
35 | importFrom(dplyr,"%>%")
36 | importFrom(dplyr,arrange)
37 | importFrom(dplyr,bind_rows)
38 | importFrom(dplyr,case_when)
39 | importFrom(dplyr,coalesce)
40 | importFrom(dplyr,distinct)
41 | importFrom(dplyr,filter)
42 | importFrom(dplyr,first)
43 | importFrom(dplyr,full_join)
44 | importFrom(dplyr,group_by)
45 | importFrom(dplyr,if_else)
46 | importFrom(dplyr,lag)
47 | importFrom(dplyr,last)
48 | importFrom(dplyr,lead)
49 | importFrom(dplyr,left_join)
50 | importFrom(dplyr,mutate)
51 | importFrom(dplyr,pull)
52 | importFrom(dplyr,recode)
53 | importFrom(dplyr,rename)
54 | importFrom(dplyr,select)
55 | importFrom(dplyr,slice)
56 | importFrom(dplyr,slice_head)
57 | importFrom(dplyr,summarise)
58 | importFrom(dplyr,tibble)
59 | importFrom(dplyr,ungroup)
60 | importFrom(ggplot2,aes)
61 | importFrom(ggplot2,element_text)
62 | importFrom(ggplot2,facet_wrap)
63 | importFrom(ggplot2,geom_hline)
64 | importFrom(ggplot2,geom_line)
65 | importFrom(ggplot2,geom_point)
66 | importFrom(ggplot2,geom_ribbon)
67 | importFrom(ggplot2,geom_smooth)
68 | importFrom(ggplot2,ggplot)
69 | importFrom(ggplot2,labs)
70 | importFrom(ggplot2,scale_color_viridis_d)
71 | importFrom(ggplot2,scale_x_date)
72 | importFrom(ggplot2,theme)
73 | importFrom(ggplot2,theme_minimal)
74 | importFrom(lubridate,as_date)
75 | importFrom(lubridate,as_datetime)
76 | importFrom(lubridate,ceiling_date)
77 | importFrom(lubridate,date)
78 | importFrom(lubridate,days)
79 | importFrom(lubridate,duration)
80 | importFrom(lubridate,floor_date)
81 | importFrom(lubridate,interval)
82 | importFrom(lubridate,parse_date_time)
83 | importFrom(lubridate,period)
84 | importFrom(lubridate,seconds_to_period)
85 | importFrom(lubridate,time_length)
86 | importFrom(lubridate,ymd)
87 | importFrom(lubridate,ymd_hms)
88 | importFrom(purrr,map_chr)
89 | importFrom(purrr,map_dfr)
90 | importFrom(purrr,possibly)
91 | importFrom(purrr,quietly)
92 | importFrom(readr,cols)
93 | importFrom(readr,read_csv)
94 | importFrom(rlang,"%||%")
95 | importFrom(rlang,.data)
96 | importFrom(scales,pretty_breaks)
97 | importFrom(stats,median)
98 | importFrom(stats,na.omit)
99 | importFrom(stats,quantile)
100 | importFrom(stats,sd)
101 | importFrom(stats,setNames)
102 | importFrom(tidyr,drop_na)
103 | importFrom(tidyr,pivot_longer)
104 | importFrom(tidyr,pivot_wider)
105 | importFrom(tidyr,unnest)
106 | importFrom(tools,toTitleCase)
107 | importFrom(utils,read.csv)
108 | importFrom(zoo,rollapply)
109 | importFrom(zoo,rollmean)
110 |
--------------------------------------------------------------------------------
/man/plot_decoupling.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot_decoupling.R
3 | \name{plot_decoupling}
4 | \alias{plot_decoupling}
5 | \title{Plot Aerobic Decoupling Trend}
6 | \usage{
7 | plot_decoupling(
8 | data,
9 | activity_type = c("Run", "Ride"),
10 | decouple_metric = c("pace_hr", "power_hr"),
11 | start_date = NULL,
12 | end_date = NULL,
13 | min_duration_mins = 45,
14 | add_trend_line = TRUE,
15 | smoothing_method = "loess",
16 | decoupling_df = NULL
17 | )
18 | }
19 | \arguments{
20 | \item{data}{\strong{Recommended: Pass pre-calculated data via \code{decoupling_df} (local export preferred).}
21 | A data frame from \code{calculate_decoupling()} or activities data from \code{load_local_activities()}.}
22 |
23 | \item{activity_type}{Type(s) of activities to analyze (e.g., "Run", "Ride").}
24 |
25 | \item{decouple_metric}{Metric basis: "pace_hr" or "power_hr".}
26 |
27 | \item{start_date}{Optional. Analysis start date (YYYY-MM-DD string or Date). Defaults to ~1 year ago.}
28 |
29 | \item{end_date}{Optional. Analysis end date (YYYY-MM-DD string or Date). Defaults to today.}
30 |
31 | \item{min_duration_mins}{Minimum activity duration (minutes) to include. Default 45.}
32 |
33 | \item{add_trend_line}{Add a smoothed trend line (\code{geom_smooth})? Default \code{TRUE}.}
34 |
35 | \item{smoothing_method}{Smoothing method for trend line (e.g., "loess", "lm"). Default "loess".}
36 |
37 | \item{decoupling_df}{\strong{Recommended.} A pre-calculated data frame from \code{calculate_decoupling()}.
38 | When provided, analysis uses local data only (no API calls).
39 | Must contain 'date' and 'decoupling' columns.}
40 | }
41 | \value{
42 | A ggplot object showing the decoupling trend.
43 | }
44 | \description{
45 | Visualizes the trend of aerobic decoupling over time.
46 | }
47 | \details{
48 | Plots the aerobic decoupling trend over time. \strong{Recommended workflow: Use local data via \code{decoupling_df}.}
49 |
50 | Plots decoupling percentage ((EF_1st_half - EF_2nd_half) / EF_1st_half * 100).
51 | Positive values mean HR drifted relative to output. A 5\\\% threshold line is often
52 | used as reference. \strong{Best practice: Use \code{load_local_activities()} + \code{calculate_decoupling()} + this function.}
53 | }
54 | \examples{
55 | # Example using pre-calculated sample data
56 | data("athlytics_sample_decoupling", package = "Athlytics")
57 | p <- plot_decoupling(decoupling_df = athlytics_sample_decoupling)
58 | print(p)
59 |
60 | \dontrun{
61 | # Example using local Strava export data
62 | activities <- load_local_activities("strava_export_data/activities.csv")
63 |
64 | # Example 1: Plot Decoupling trend for Runs (last 6 months)
65 | decoupling_runs_6mo <- calculate_decoupling(
66 | activities_data = activities,
67 | export_dir = "strava_export_data",
68 | activity_type = "Run",
69 | decouple_metric = "pace_hr",
70 | start_date = Sys.Date() - months(6)
71 | )
72 | plot_decoupling(decoupling_runs_6mo)
73 |
74 | # Example 2: Plot Decoupling trend for Rides
75 | decoupling_rides <- calculate_decoupling(
76 | activities_data = activities,
77 | export_dir = "strava_export_data",
78 | activity_type = "Ride",
79 | decouple_metric = "power_hr"
80 | )
81 | plot_decoupling(decoupling_rides)
82 |
83 | # Example 3: Plot Decoupling trend for multiple Run types (no trend line)
84 | decoupling_multi_run <- calculate_decoupling(
85 | activities_data = activities,
86 | export_dir = "strava_export_data",
87 | activity_type = c("Run", "VirtualRun"),
88 | decouple_metric = "pace_hr"
89 | )
90 | plot_decoupling(decoupling_multi_run, add_trend_line = FALSE)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/man/plot_acwr.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot_acwr.R
3 | \name{plot_acwr}
4 | \alias{plot_acwr}
5 | \title{Plot ACWR Trend}
6 | \usage{
7 | plot_acwr(
8 | data,
9 | activity_type = NULL,
10 | load_metric = "duration_mins",
11 | acute_period = 7,
12 | chronic_period = 28,
13 | start_date = NULL,
14 | end_date = NULL,
15 | user_ftp = NULL,
16 | user_max_hr = NULL,
17 | user_resting_hr = NULL,
18 | smoothing_period = 7,
19 | highlight_zones = TRUE,
20 | acwr_df = NULL,
21 | group_var = NULL,
22 | group_colors = NULL
23 | )
24 | }
25 | \arguments{
26 | \item{data}{\strong{Recommended: Pass pre-calculated data via \code{acwr_df} (local export preferred).}
27 | A data frame from \code{calculate_acwr()} or activities data from \code{load_local_activities()}.}
28 |
29 | \item{activity_type}{Type(s) of activities to analyze (e.g., "Run", "Ride").}
30 |
31 | \item{load_metric}{Method for calculating daily load (e.g., "duration_mins", "distance_km", "tss", "hrss").}
32 |
33 | \item{acute_period}{Days for the acute load window (e.g., 7).}
34 |
35 | \item{chronic_period}{Days for the chronic load window (e.g., 28). Must be greater than \code{acute_period}.}
36 |
37 | \item{start_date}{Optional. Analysis start date (YYYY-MM-DD string or Date). Defaults to ~1 year ago.}
38 |
39 | \item{end_date}{Optional. Analysis end date (YYYY-MM-DD string or Date). Defaults to today.}
40 |
41 | \item{user_ftp}{Required if \code{load_metric = "tss"} and \code{acwr_df} is not provided. Your Functional Threshold Power.}
42 |
43 | \item{user_max_hr}{Required if \code{load_metric = "hrss"} and \code{acwr_df} is not provided. Your maximum heart rate.}
44 |
45 | \item{user_resting_hr}{Required if \code{load_metric = "hrss"} and \code{acwr_df} is not provided. Your resting heart rate.}
46 |
47 | \item{smoothing_period}{Days for smoothing the ACWR using a rolling mean (e.g., 7). Default 7.}
48 |
49 | \item{highlight_zones}{Logical, whether to highlight different ACWR zones (e.g., sweet spot, high risk) on the plot. Default \code{TRUE}.}
50 |
51 | \item{acwr_df}{\strong{Recommended.} A pre-calculated data frame from \code{calculate_acwr()} or \code{calculate_acwr_ewma()}.
52 | When provided, analysis uses local data only (no API calls).}
53 |
54 | \item{group_var}{Optional. Column name for grouping/faceting (e.g., "athlete_id").}
55 |
56 | \item{group_colors}{Optional. Named vector of colors for groups.}
57 | }
58 | \value{
59 | A ggplot object showing the ACWR trend.
60 | }
61 | \description{
62 | Visualizes the Acute:Chronic Workload Ratio (ACWR) trend over time.
63 | }
64 | \details{
65 | Plots the ACWR trend over time. \strong{Best practice: Use \code{load_local_activities()} + \code{calculate_acwr()} + this function.}
66 | ACWR is calculated as acute load / chronic load. A ratio of 0.8-1.3 is often considered the "sweet spot".
67 | }
68 | \examples{
69 | # Example using pre-calculated sample data
70 | data("athlytics_sample_acwr", package = "Athlytics")
71 | p <- plot_acwr(athlytics_sample_acwr)
72 | print(p)
73 |
74 | \dontrun{
75 | # Example using local Strava export data
76 | activities <- load_local_activities("strava_export_data/activities.csv")
77 |
78 | # Plot ACWR trend for Runs (using duration as load metric)
79 | plot_acwr(data = activities,
80 | activity_type = "Run",
81 | load_metric = "duration_mins",
82 | acute_period = 7,
83 | chronic_period = 28)
84 |
85 | # Plot ACWR trend for Rides (using TSS as load metric)
86 | plot_acwr(data = activities,
87 | activity_type = "Ride",
88 | load_metric = "tss",
89 | user_ftp = 280) # FTP value is required
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/man/flag_quality.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/flag_quality.R
3 | \name{flag_quality}
4 | \alias{flag_quality}
5 | \title{Flag Data Quality Issues in Activity Streams}
6 | \usage{
7 | flag_quality(
8 | streams,
9 | sport = "Run",
10 | hr_range = c(30, 220),
11 | pw_range = c(0, 1500),
12 | max_run_speed = 7,
13 | max_ride_speed = 25,
14 | max_accel = 3,
15 | max_hr_jump = 10,
16 | max_pw_jump = 300,
17 | min_steady_minutes = 20,
18 | steady_cv_threshold = 8
19 | )
20 | }
21 | \arguments{
22 | \item{streams}{A data frame containing activity stream data with time-series
23 | measurements. Expected columns: \code{time} (seconds), \code{heartrate} (bpm),
24 | \code{watts} (W), \code{velocity_smooth} or \code{speed} (m/s), \code{distance} (m).}
25 |
26 | \item{sport}{Type of activity (e.g., "Run", "Ride"). Default "Run".}
27 |
28 | \item{hr_range}{Valid heart rate range as c(min, max). Default c(30, 220).}
29 |
30 | \item{pw_range}{Valid power range as c(min, max). Default c(0, 1500).}
31 |
32 | \item{max_run_speed}{Maximum plausible running speed in m/s. Default 7.0 (approx. 2:23/km).}
33 |
34 | \item{max_ride_speed}{Maximum plausible riding speed in m/s. Default 25.0 (approx. 90 km/h).}
35 |
36 | \item{max_accel}{Maximum plausible acceleration in m/s². Default 3.0.}
37 |
38 | \item{max_hr_jump}{Maximum plausible HR change per second (bpm/s). Default 10.}
39 |
40 | \item{max_pw_jump}{Maximum plausible power change per second (W/s). Default 300.}
41 |
42 | \item{min_steady_minutes}{Minimum duration (minutes) for steady-state segment. Default 20.}
43 |
44 | \item{steady_cv_threshold}{Coefficient of variation threshold for steady-state (\\\%). Default 8.}
45 | }
46 | \value{
47 | A data frame identical to \code{streams} with additional flag columns:
48 | \describe{
49 | \item{flag_hr_spike}{Logical. TRUE if HR is out of range or has excessive jump.}
50 | \item{flag_pw_spike}{Logical. TRUE if power is out of range or has excessive jump.}
51 | \item{flag_gps_drift}{Logical. TRUE if speed or acceleration is implausible.}
52 | \item{flag_any}{Logical. TRUE if any quality flag is raised.}
53 | \item{is_steady_state}{Logical. TRUE if segment meets steady-state criteria.}
54 | \item{quality_score}{Numeric 0-1. Proportion of clean data (1 = perfect).}
55 | }
56 | }
57 | \description{
58 | Detects and flags potential data quality issues in activity stream data,
59 | including HR/power spikes, GPS drift, and identifies steady-state segments
60 | suitable for physiological metrics calculation.
61 | }
62 | \details{
63 | This function performs several quality checks:
64 | \itemize{
65 | \item \strong{HR/Power Spikes}: Flags values outside physiological ranges or with
66 | sudden jumps (Delta HR > 10 bpm/s, Delta P > 300 W/s).
67 | \item \strong{GPS Drift}: Flags implausible speeds or accelerations based on sport type.
68 | \item \strong{Steady-State Detection}: Identifies segments with low variability
69 | (CV < 8\%) lasting >= 20 minutes, suitable for EF/decoupling calculations.
70 | }
71 |
72 | The function is sport-aware and adjusts thresholds accordingly. All thresholds
73 | are configurable to accommodate different athlete profiles and data quality.
74 | }
75 | \examples{
76 | \dontrun{
77 | # Create sample activity stream data
78 | stream_data <- data.frame(
79 | time = seq(0, 3600, by = 1),
80 | heartrate = rnorm(3601, mean = 150, sd = 10),
81 | watts = rnorm(3601, mean = 200, sd = 20),
82 | velocity_smooth = rnorm(3601, mean = 3.5, sd = 0.3)
83 | )
84 |
85 | # Flag quality issues
86 | flagged_data <- flag_quality(stream_data, sport = "Run")
87 |
88 | # Check summary
89 | summary(flagged_data$quality_score)
90 | table(flagged_data$flag_any)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/man/plot_exposure.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot_exposure.R
3 | \name{plot_exposure}
4 | \alias{plot_exposure}
5 | \title{Plot Training Load Exposure (ATL vs CTL)}
6 | \usage{
7 | plot_exposure(
8 | data,
9 | activity_type = c("Run", "Ride", "VirtualRide", "VirtualRun"),
10 | load_metric = "duration_mins",
11 | acute_period = 7,
12 | chronic_period = 42,
13 | user_ftp = NULL,
14 | user_max_hr = NULL,
15 | user_resting_hr = NULL,
16 | end_date = NULL,
17 | risk_zones = TRUE,
18 | exposure_df = NULL
19 | )
20 | }
21 | \arguments{
22 | \item{data}{A data frame from \code{load_local_activities()}. Required unless \code{exposure_df} is provided.}
23 |
24 | \item{activity_type}{Type(s) of activities to include (e.g., "Run", "Ride"). Default uses common types.}
25 |
26 | \item{load_metric}{Method for calculating daily load (e.g., "duration_mins", "tss", "hrss"). Default "duration_mins".
27 | See \code{calculate_exposure} for details on approximate TSS/HRSS calculations.}
28 |
29 | \item{acute_period}{Days for acute load window (e.g., 7).}
30 |
31 | \item{chronic_period}{Days for chronic load window (e.g., 42). Must be > \code{acute_period}.}
32 |
33 | \item{user_ftp}{Required if \code{load_metric = "tss"}. Your FTP.}
34 |
35 | \item{user_max_hr}{Required if \code{load_metric = "hrss"}. Your max HR.}
36 |
37 | \item{user_resting_hr}{Required if \code{load_metric = "hrss"}. Your resting HR.}
38 |
39 | \item{end_date}{Optional. Analysis end date (YYYY-MM-DD string or Date). Defaults to today.}
40 |
41 | \item{risk_zones}{Add background shading for typical ACWR risk zones? Default \code{TRUE}.}
42 |
43 | \item{exposure_df}{Optional. A pre-calculated data frame from \code{calculate_exposure}.
44 | If provided, \code{data} and other calculation parameters are ignored. Must contain
45 | \code{date}, \code{atl}, \code{ctl} (and \code{acwr} if \code{risk_zones = TRUE}).}
46 | }
47 | \value{
48 | A ggplot object showing ATL vs CTL.
49 | }
50 | \description{
51 | Visualizes the relationship between Acute and Chronic Training Load.
52 | }
53 | \details{
54 | Plots ATL vs CTL, optionally showing risk zones based on ACWR. Uses
55 | pre-calculated data or calls \code{calculate_exposure}.
56 |
57 | Visualizes training state by plotting ATL vs CTL (related to PMC charts).
58 | Points are colored by date, latest point is highlighted (red triangle).
59 | Optional risk zones (based on ACWR thresholds ~0.8, 1.3, 1.5) can be shaded.
60 | If \code{exposure_df} is not provided, it calls \code{calculate_exposure} first.
61 | }
62 | \examples{
63 | # Example using simulated data
64 | data(Athlytics_sample_data)
65 | # Ensure exposure_df is named and other necessary parameters like activity_type are provided
66 | p <- plot_exposure(exposure_df = athlytics_sample_exposure, activity_type = "Run")
67 | print(p)
68 |
69 | \dontrun{
70 | # Example using local Strava export data
71 | activities <- load_local_activities("strava_export_data/activities.csv")
72 |
73 | # Plot Exposure trend for Runs (last 6 months)
74 | plot_exposure(data = activities,
75 | activity_type = "Run",
76 | end_date = Sys.Date(),
77 | user_ftp = 280) # Example, if load_metric = "tss"
78 |
79 | # Plot Exposure trend for Rides
80 | plot_exposure(data = activities,
81 | activity_type = "Ride",
82 | user_ftp = 280) # Example, provide if load_metric = "tss"
83 |
84 | # Plot Exposure trend for multiple Run types (risk_zones = FALSE for this example)
85 | plot_exposure(data = activities,
86 | activity_type = c("Run", "VirtualRun"),
87 | risk_zones = FALSE,
88 | user_ftp = 280) # Example, provide if load_metric = "tss"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tests/testthat/test-exposure.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/test-exposure.R
2 |
3 | library(testthat)
4 | library(Athlytics)
5 |
6 | # Load sample data from the package
7 | data(Athlytics_sample_data)
8 |
9 | # Load mock data (if helper-mockdata.R contains mocks for direct use)
10 | source(test_path("helper-mockdata.R"), local = TRUE)
11 |
12 | # Mock Strava token (if needed for functions that might call API, though most tests here use sample_df)
13 |
14 | # --- Test plot_exposure (using pre-calculated ACWR data from Athlytics_sample_data) ---
15 |
16 | test_that("plot_exposure returns a ggplot object with athlytics_sample_acwr data", {
17 | # Check if the sample ACWR data subset exists
18 | expect_true(exists("athlytics_sample_acwr"), "athlytics_sample_acwr not found in Athlytics_sample_data.")
19 | expect_s3_class(athlytics_sample_acwr, "data.frame")
20 |
21 | # Ensure athlytics_sample_acwr has the expected columns for plotting
22 | # plot_exposure typically uses date, acwr, atl, ctl.
23 | expected_cols <- c("date", "acwr", "atl", "ctl")
24 | if (nrow(athlytics_sample_acwr) > 0) { # Only check columns if data exists
25 | expect_true(all(expected_cols %in% names(athlytics_sample_acwr)),
26 | paste("athlytics_sample_acwr is missing one or more expected columns:", paste(expected_cols, collapse=", ")))
27 | }
28 |
29 | # Test with actual data if it's not empty
30 | if (nrow(athlytics_sample_acwr) > 0) {
31 | expect_s3_class(plot_exposure(exposure_df = athlytics_sample_acwr), "ggplot")
32 | } else {
33 | skip("athlytics_sample_acwr is empty, skipping main plot test.")
34 | }
35 | })
36 |
37 | test_that("plot_exposure handles risk_zones argument with athlytics_sample_acwr", {
38 | if (!exists("athlytics_sample_acwr") || nrow(athlytics_sample_acwr) == 0) {
39 | skip("athlytics_sample_acwr is empty or not found, skipping risk_zones test.")
40 | }
41 | expect_s3_class(plot_exposure(exposure_df = athlytics_sample_acwr, risk_zones = TRUE), "ggplot")
42 | expect_s3_class(plot_exposure(exposure_df = athlytics_sample_acwr, risk_zones = FALSE), "ggplot")
43 |
44 | # Check for geom_rect layers (risk zones are often drawn with geom_rect for background bands)
45 | # or geom_hline/geom_vline if specific lines are used.
46 | # The original test checked for GeomAbline, let's adapt if needed or keep if appropriate.
47 | # plot_exposure uses geom_abline for risk zones.
48 | p_zones <- plot_exposure(exposure_df = athlytics_sample_acwr, risk_zones = TRUE)
49 | p_no_zones <- plot_exposure(exposure_df = athlytics_sample_acwr, risk_zones = FALSE)
50 |
51 | get_abline_layers <- function(p) sum(sapply(p$layers, function(l) inherits(l$geom, "GeomAbline")))
52 |
53 | expect_equal(get_abline_layers(p_zones), 3)
54 | expect_equal(get_abline_layers(p_no_zones), 0)
55 | })
56 |
57 | test_that("plot_exposure handles empty data frame input", {
58 | # Create an empty data frame with the same structure as athlytics_sample_acwr
59 | empty_df_structure <- data.frame(
60 | date = as.Date(character()),
61 | daily_load = numeric(),
62 | atl = numeric(),
63 | ctl = numeric(),
64 | acwr = numeric(),
65 | stringsAsFactors = FALSE
66 | )
67 |
68 | if (exists("athlytics_sample_acwr") && nrow(athlytics_sample_acwr) > 0) {
69 | empty_df <- athlytics_sample_acwr[0, ]
70 | } else {
71 | empty_df <- empty_df_structure
72 | }
73 |
74 | expect_warning(
75 | p_empty <- plot_exposure(exposure_df = empty_df),
76 | regexp = "No valid exposure data available to plot \\(or missing required columns\\)."
77 | )
78 | expect_s3_class(p_empty, "ggplot")
79 | expect_true(grepl("No exposure data available", p_empty$labels$title, ignore.case = TRUE) || length(p_empty$layers) == 0)
80 | })
--------------------------------------------------------------------------------
/man/plot_pbs.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot_pbs.R
3 | \name{plot_pbs}
4 | \alias{plot_pbs}
5 | \title{Plot Personal Best (PB) Trends}
6 | \usage{
7 | plot_pbs(
8 | data,
9 | activity_type = "Run",
10 | distance_meters,
11 | max_activities = 500,
12 | date_range = NULL,
13 | add_trend_line = TRUE,
14 | pbs_df = NULL
15 | )
16 | }
17 | \arguments{
18 | \item{data}{\strong{Recommended: Pass pre-calculated data via \code{pbs_df} (local export preferred).}
19 | A data frame from \code{calculate_pbs()} or activities data from \code{load_local_activities()}.}
20 |
21 | \item{activity_type}{Type(s) of activities to search (e.g., "Run"). Default "Run".}
22 |
23 | \item{distance_meters}{Numeric vector of distances (meters) to plot PBs for (e.g., \code{c(1000, 5000)}).
24 | Relies on Strava's \code{best_efforts} data.}
25 |
26 | \item{max_activities}{Max number of recent activities to check. Default 500. Reduce for speed.}
27 |
28 | \item{date_range}{Optional. Filter activities by date \code{c("YYYY-MM-DD", "YYYY-MM-DD")}.}
29 |
30 | \item{add_trend_line}{Logical. Whether to add a trend line to the plot. Default TRUE.}
31 |
32 | \item{pbs_df}{\strong{Recommended.} A pre-calculated data frame from \code{calculate_pbs()}.
33 | When provided, analysis uses local data only (no API calls).}
34 | }
35 | \value{
36 | A ggplot object showing PB trends, faceted by distance if multiple are plotted.
37 | }
38 | \description{
39 | Visualizes the trend of personal best times for specific running distances.
40 | }
41 | \details{
42 | Plots the trend of best efforts for specified distances, highlighting new PBs.
43 | \strong{Recommended workflow: Use local data via \code{pbs_df}.}
44 |
45 | Visualizes data from \code{calculate_pbs}. Points show best efforts;
46 | solid points mark new PBs. Y-axis is MM:SS.
47 | \strong{Best practice: Use \code{load_local_activities()} + \code{calculate_pbs()} + this function.}
48 | Legacy API mode is maintained for backward compatibility only.
49 | }
50 | \examples{
51 | # Example using the built-in sample data
52 | # This data now contains a simulated history of performance improvements
53 | data("athlytics_sample_pbs", package = "Athlytics")
54 |
55 | if (!is.null(athlytics_sample_pbs) && nrow(athlytics_sample_pbs) > 0) {
56 | # Plot PBs using the package sample data directly
57 | p <- plot_pbs(pbs_df = athlytics_sample_pbs, activity_type = "Run")
58 | print(p)
59 | }
60 |
61 | if (FALSE) {
62 | # Example using local Strava export data
63 | activities <- load_local_activities("strava_export_data/activities.csv")
64 |
65 | # Plot PBS trend for Runs (last 6 months)
66 | pb_data_run <- calculate_pbs(activities_data = activities,
67 | activity_type = "Run",
68 | distance_meters = c(1000,5000,10000),
69 | date_range = c(format(Sys.Date() - months(6)),
70 | format(Sys.Date())))
71 | if(nrow(pb_data_run) > 0) {
72 | plot_pbs(pbs_df = pb_data_run, distance_meters = c(1000,5000,10000))
73 | }
74 |
75 | # Plot PBS trend for Rides (if applicable, though PBs are mainly for Runs)
76 | pb_data_ride <- calculate_pbs(activities_data = activities,
77 | activity_type = "Ride",
78 | distance_meters = c(10000, 20000))
79 | if(nrow(pb_data_ride) > 0) {
80 | plot_pbs(pbs_df = pb_data_ride, distance_meters = c(10000, 20000))
81 | }
82 |
83 | # Plot PBS trend for multiple Run types (no trend line)
84 | pb_data_multi <- calculate_pbs(activities_data = activities,
85 | activity_type = c("Run", "VirtualRun"),
86 | distance_meters = c(1000,5000))
87 | if(nrow(pb_data_multi) > 0) {
88 | plot_pbs(pbs_df = pb_data_multi, distance_meters = c(1000,5000),
89 | add_trend_line = FALSE)
90 | }
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/man/load_local_activities.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/load_local_activities.R
3 | \name{load_local_activities}
4 | \alias{load_local_activities}
5 | \title{Load Activities from Local Strava Export}
6 | \usage{
7 | load_local_activities(
8 | path = "strava_export_data/activities.csv",
9 | start_date = NULL,
10 | end_date = NULL,
11 | activity_types = NULL
12 | )
13 | }
14 | \arguments{
15 | \item{path}{Path to activities.csv file OR a .zip archive from Strava export.
16 | Supports both CSV and ZIP formats. If a .zip file is provided, the function
17 | will automatically extract and read the activities.csv file from within the archive.
18 | Default is "strava_export_data/activities.csv".}
19 |
20 | \item{start_date}{Optional. Start date (YYYY-MM-DD or Date/POSIXct) for filtering activities.
21 | Defaults to NULL (no filtering).}
22 |
23 | \item{end_date}{Optional. End date (YYYY-MM-DD or Date/POSIXct) for filtering activities.
24 | Defaults to NULL (no filtering).}
25 |
26 | \item{activity_types}{Optional. Character vector of activity types to include
27 | (e.g., c("Run", "Ride")). Defaults to NULL (include all types).}
28 | }
29 | \value{
30 | A tibble of activity data with standardized column names compatible
31 | with Athlytics functions. Key columns include:
32 | \itemize{
33 | \item \code{id}: Activity ID (numeric)
34 | \item \code{name}: Activity name
35 | \item \code{type}: Activity type (Run, Ride, etc.)
36 | \item \code{start_date_local}: Activity start datetime (POSIXct)
37 | \item \code{date}: Activity date (Date)
38 | \item \code{distance}: Distance in meters (numeric)
39 | \item \code{moving_time}: Moving time in seconds (integer)
40 | \item \code{elapsed_time}: Elapsed time in seconds (integer)
41 | \item \code{average_heartrate}: Average heart rate (numeric)
42 | \item \code{average_watts}: Average power in watts (numeric)
43 | \item \code{elevation_gain}: Elevation gain in meters (numeric)
44 | }
45 | }
46 | \description{
47 | Reads and processes activity data from a local Strava export, supporting both
48 | direct CSV files and compressed ZIP archives. This function converts Strava export
49 | data to a format compatible with all Athlytics analysis functions.
50 | Designed to work with Strava's official bulk data export
51 | (Settings > My Account > Download or Delete Your Account > Get Started).
52 | }
53 | \details{
54 | This function reads the activities.csv file from a Strava data export
55 | and transforms the data to match the structure expected by Athlytics
56 | analysis functions. The transformation includes:
57 | \itemize{
58 | \item Converting column names to match API format
59 | \item Parsing dates into POSIXct format
60 | \item Converting distances to meters
61 | \item Converting times to seconds
62 | \item Filtering by date range and activity type if specified
63 | }
64 |
65 | \strong{Privacy Note}: This function processes local export data only and does not
66 | connect to the internet. Ensure you have permission to analyze the data and
67 | follow applicable privacy regulations when using this data for research purposes.
68 | }
69 | \examples{
70 | \dontrun{
71 | # Load all activities from local CSV
72 | activities <- load_local_activities("strava_export_data/activities.csv")
73 |
74 | # Load directly from ZIP archive (no need to extract manually!)
75 | activities <- load_local_activities("export_12345678.zip")
76 |
77 | # Load only running activities from 2023
78 | activities <- load_local_activities(
79 | path = "export_12345678.zip",
80 | start_date = "2023-01-01",
81 | end_date = "2023-12-31",
82 | activity_types = "Run"
83 | )
84 |
85 | # Use with Athlytics functions
86 | acwr_data <- calculate_acwr(activities, load_metric = "distance_km")
87 | plot_acwr(acwr_data, highlight_zones = TRUE)
88 |
89 | # Multi-metric analysis
90 | ef_data <- calculate_ef(activities, ef_metric = "pace_hr")
91 | plot_ef(ef_data, add_trend_line = TRUE)
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | # Regex patterns to ignore files/dirs during R CMD build
2 | ^\.Rproj\.user$
3 | ^\.RData$
4 | ^\.Rbuildignore$
5 | ^\.gitignore$
6 | ^\.git$
7 | ^\.\.Rcheck$
8 | ^cran-comments\.md$
9 | ^.*\.Rproj$
10 | ^Athlytics_.*\\.tar\\.gz$ # Ignore built packages
11 | ^install_deps\.Rout$
12 | ^install_pkgs\.R$ # Added just in case
13 | ^run_tests\.R$ # Added just in case
14 | # Other non-standard files found
15 | ^ijspp-article-p151\.pdf$
16 | ^ijspp-article-p151\.txt$
17 | ^image\.png$
18 | ^install_deps\.R$
19 | ^paper\.md$
20 | ^pdf_to_text\.py$
21 | ^rStrava.*\.txt$ # Ignore any rStrava text files
22 | # Ignore helper files if only used for local testing setup
23 | # ^helper-.*
24 | # Ignore R history if present
25 | ^\.Rhistory$
26 | # Ignore OS specific hidden files
27 | ^\.DS_Store$
28 | ^Thumbs\.db$
29 | # Ignore testthat snapshots if any
30 | ^tests/testthat/_snaps/$
31 | # Ignore temporary check directory if created inside project
32 | ^Athlytics\.Rcheck/$
33 | ^\.\.Rcheck/$
34 | # pkgdown files
35 | ^_pkgdown\.yml$
36 | ^docs$
37 | # Files/dirs noted in R CMD check
38 | ^\\.aspell$ # Escaped dot
39 | ^Athlytics-manual\\.pdf$
40 | ^Athlytics_0\\.1\\.0/$
41 | ^arxiv/$
42 | ^create_sample_data\\.R$
43 | ^figure/$
44 | ^paper\\..*$
45 | ^references\\.bib$
46 | ^\.httr-oauth$
47 | ^runnable_function_examples\\.Rmd$
48 | ^runnable_function_examples\\.html$
49 | ^coverage_report\\.html$
50 | ^test_get_streams\\.R$ # If this root file needs to be ignored
51 | ^test\\.r$ # If this root file needs to be ignored
52 | ^lib$ # Add lib directory to ignore
53 |
54 | # Ignore the manual test script
55 | ^tests/manual_test_script\\.R$
56 |
57 | # Ignore GitHub Actions directory
58 | ^\.github$
59 | ^tests/run_all_tests\.R$ # Ignore the manual test script runner
60 |
61 | # Ignore aspell configuration directory
62 | ^\.aspell$
63 | ^\.aspell/.*
64 |
65 | # Added paper directory to the ignore list
66 | ^paper$
67 |
68 | # Ignore specific test scripts
69 | ^tests/test_direct_api_streams\.R$
70 | ^doc$
71 | ^Meta$
72 | ^LICENSE\.md$
73 | ^tests/manual_tests$
74 | ^CRAN-SUBMISSION$
75 | ^Untitled\.R$
76 |
77 | # User's private data - exclude from R package build
78 | ^strava_export_data$
79 | ^export_.*\.zip$
80 | ^.*\.zip$
81 | ^batch_update_calculate_functions\.py$
82 | ^generate_plot_example\.R$
83 | ^test_local_data_loading\.R$
84 | ^test_package\.R$
85 | ^update_calculate_functions_local_only\.R$
86 | ^take_screenshots\.bat$
87 | ^screenshots$
88 | ^README_NEW\.md$
89 | ^README_old\.md$
90 | ^MIGRATION_SUMMARY\.md$
91 | ^Rplots\.pdf$
92 | ^google_search_demo\.js$
93 | ^node_modules$
94 | ^package\.json$
95 | ^package-lock\.json$
96 | ^strava_api.*\.md$
97 | ^提交指南.*\.md$
98 | ^export_data$
99 | ^athlytics_downloads_raw_data\.csv$
100 | ^Athlytics_.*\.tar\.gz$
101 | ^tatus$
102 | ^check_output\.txt$
103 | ^final_check\.txt$
104 | ^pkgcheck.*\.txt$
105 | ^analysis_output$
106 | ^Athlytics$
107 | ^\.lang$
108 | ^.*调试.*$
109 | ^create_multi_athlete_sample_data\.R$
110 | ^regenerate_figures\.R$
111 | ^.*coverage.*\.R$
112 | ^.*coverage.*\.Rout$
113 | ^.*coverage.*\.rds$
114 | ^.*coverage.*\.html$
115 | ^check_.*\.R$
116 | ^check_.*\.Rout$
117 | ^debug_.*\.R$
118 | ^debug_.*\.Rout$
119 | ^improve_coverage\.R$
120 | ^install_athlytics_.*\.R$
121 | ^install_athlytics_.*\.bat$
122 | ^install_athlytics_.*\.ps1$
123 | ^parallel_.*\.R$
124 | ^performance_comparison\.R$
125 | ^quick_.*\.R$
126 | ^run_.*\.R$
127 | ^serial_test\.R$
128 | ^simple_.*\.R$
129 | ^simple_.*\.Rout$
130 | ^stable_.*\.R$
131 | ^stable_.*\.Rout$
132 | ^start_.*\.bat$
133 | ^test_.*\.bat$
134 | ^test_.*\.R$
135 | ^test_.*\.Rout$
136 | ^test-results\.txt$
137 | ^temp_order\.txt$
138 | ^rename_tests\.ps1$
139 | ^final_.*\.R$
140 | ^final_.*\.Rout$
141 | ^athlytics\.Rproj\.Rcheck$
142 | ^CODE_OF_CONDUCT\.md$
143 | ^CONTRIBUTING\.md$
144 | ^codemeta\.json$
145 | ^image$
146 | ^lib$
147 | ^strava_data$
148 |
149 | ^data-raw$
150 | ^generate_plot_examples\.R$
151 |
--------------------------------------------------------------------------------
/R/data.R:
--------------------------------------------------------------------------------
1 | # R/data.R
2 |
3 | #' Sample ACWR Data for Athlytics
4 | #'
5 | #' A dataset containing pre-calculated Acute:Chronic Workload Ratio (ACWR)
6 | #' and related metrics, derived from simulated Strava data. Used in examples and tests.
7 | #'
8 | #' @format A tibble with 365 rows and 5 variables:
9 | #' \describe{
10 | #' \item{date}{Date of the metrics, as a Date object.}
11 | #' \item{atl}{Acute Training Load, as a numeric value.}
12 | #' \item{ctl}{Chronic Training Load, as a numeric value.}
13 | #' \item{acwr}{Acute:Chronic Workload Ratio, as a numeric value.}
14 | #' \item{acwr_smooth}{Smoothed ACWR, as a numeric value.}
15 | #' }
16 | #' @source Simulated data generated for package examples.
17 | "athlytics_sample_acwr"
18 |
19 | #' Sample Aerobic Decoupling Data for Athlytics
20 | #'
21 | #' A dataset containing pre-calculated aerobic decoupling percentages,
22 | #' derived from simulated Strava data. Used in examples and tests.
23 | #'
24 | #' @format A tibble with 365 rows and 2 variables:
25 | #' \describe{
26 | #' \item{date}{Date of the activity, as a Date object.}
27 | #' \item{decoupling}{Calculated decoupling percentage, as a numeric value.}
28 | #' }
29 | #' @source Simulated data generated for package examples.
30 | "athlytics_sample_decoupling"
31 |
32 | #' Sample Efficiency Factor (EF) Data for Athlytics
33 | #'
34 | #' A dataset containing pre-calculated Efficiency Factor (EF) values,
35 | #' derived from simulated Strava data. Used in examples and tests.
36 | #'
37 | #' @format A data.frame with 50 rows and 3 variables:
38 | #' \describe{
39 | #' \item{date}{Date of the activity, as a Date object.}
40 | #' \item{activity_type}{Type of activity (e.g., "Run", "Ride"), as a character string.}
41 | #' \item{ef_value}{Calculated Efficiency Factor, as a numeric value.}
42 | #' }
43 | #' @source Simulated data generated for package examples.
44 | "athlytics_sample_ef"
45 |
46 | #' Sample Training Load Exposure Data for Athlytics
47 | #'
48 | #' This dataset contains daily training load, ATL, CTL, and ACWR, derived from
49 | #' simulated Strava data. Used in examples and tests, particularly for `plot_exposure`.
50 | #'
51 | #' @format A tibble with 365 rows and 5 variables:
52 | #' \describe{
53 | #' \item{date}{Date of the metrics, as a Date object.}
54 | #' \item{daily_load}{Calculated daily training load, as a numeric value.}
55 | #' \item{ctl}{Chronic Training Load, as a numeric value.}
56 | #' \item{atl}{Acute Training Load, as a numeric value.}
57 | #' \item{acwr}{Acute:Chronic Workload Ratio, as a numeric value.}
58 | #' }
59 | #' @source Simulated data generated for package examples.
60 | "athlytics_sample_exposure"
61 |
62 | #' Sample Personal Bests (PBs) Data for Athlytics
63 | #'
64 | #' A dataset containing pre-calculated Personal Best (PB) times for various distances,
65 | #' derived from simulated Strava data. Used in examples and tests.
66 | #'
67 | #' @format A tibble with 100 rows and 10 variables:
68 | #' \describe{
69 | #' \item{activity_id}{ID of the activity where the effort occurred, as a character string.}
70 | #' \item{activity_date}{Date and time of the activity, as a POSIXct object.}
71 | #' \item{distance}{Target distance in meters for the best effort, as a numeric value.}
72 | #' \item{elapsed_time}{Elapsed time for the effort in seconds, as a numeric value.}
73 | #' \item{moving_time}{Moving time for the effort in seconds, as a numeric value.}
74 | #' \item{time_seconds}{Typically the same as elapsed_time for best efforts, in seconds, as a numeric value.}
75 | #' \item{cumulative_pb_seconds}{The personal best time for that distance up to that date, in seconds, as a numeric value.}
76 | #' \item{is_pb}{Logical, TRUE if this effort set a new personal best.}
77 | #' \item{distance_label}{Factor representing the distance (e.g., "1k", "5k").}
78 | #' \item{time_period}{Formatted time of the effort, as a Period object from lubridate.}
79 | #' }
80 | #' @source Simulated data generated for package examples.
81 | "athlytics_sample_pbs"
--------------------------------------------------------------------------------
/tests/testthat/test-calculate-exposure-extended.R:
--------------------------------------------------------------------------------
1 | # Extended tests for calculate_exposure
2 |
3 | library(testthat)
4 | library(Athlytics)
5 |
6 | create_test_activities_exposure <- function(n_days = 90) {
7 | dates <- seq(Sys.Date() - n_days, Sys.Date() - 1, by = "day")
8 | n_activities <- round(n_days * 0.6) # ~60% of days have activities
9 | activity_dates <- sort(sample(dates, n_activities, replace = FALSE))
10 |
11 | data.frame(
12 | id = seq_along(activity_dates),
13 | name = paste("Activity", seq_along(activity_dates)),
14 | type = sample(c("Run", "Ride"), n_activities, replace = TRUE),
15 | date = activity_dates,
16 | distance = abs(rnorm(n_activities, 8000, 2000)),
17 | moving_time = abs(rnorm(n_activities, 2400, 600)),
18 | average_watts = ifelse(sample(c(TRUE, FALSE), n_activities, replace = TRUE),
19 | rnorm(n_activities, 200, 50), NA_real_),
20 | average_heartrate = rnorm(n_activities, 145, 15),
21 | stringsAsFactors = FALSE
22 | )
23 | }
24 |
25 | test_that("calculate_exposure handles different load metrics", {
26 | activities <- create_test_activities_exposure(60)
27 |
28 | # Test duration_mins
29 | exp_duration <- calculate_exposure(activities, load_metric = "duration_mins")
30 | expect_s3_class(exp_duration, "data.frame")
31 | expect_true("atl" %in% names(exp_duration))
32 | expect_true("ctl" %in% names(exp_duration))
33 |
34 | # Test distance_km
35 | exp_distance <- calculate_exposure(activities, load_metric = "distance_km")
36 | expect_s3_class(exp_distance, "data.frame")
37 | })
38 |
39 | test_that("calculate_exposure handles different time windows", {
40 | activities <- create_test_activities_exposure(100)
41 |
42 | # Test with different acute/chronic windows
43 | exp1 <- calculate_exposure(activities, acute_period = 7, chronic_period = 28)
44 | expect_s3_class(exp1, "data.frame")
45 |
46 | exp2 <- calculate_exposure(activities, acute_period = 10, chronic_period = 42)
47 | expect_s3_class(exp2, "data.frame")
48 | })
49 |
50 | test_that("calculate_exposure filters by activity type", {
51 | activities <- create_test_activities_exposure(60)
52 | activities$type <- rep(c("Run", "Ride"), length.out = nrow(activities))
53 |
54 | exp_runs <- calculate_exposure(activities, activity_type = "Run")
55 | expect_s3_class(exp_runs, "data.frame")
56 |
57 | exp_rides <- calculate_exposure(activities, activity_type = "Ride")
58 | expect_s3_class(exp_rides, "data.frame")
59 | })
60 |
61 | test_that("calculate_exposure handles date ranges", {
62 | activities <- create_test_activities_exposure(120)
63 |
64 | exp <- calculate_exposure(
65 | activities,
66 | end_date = Sys.Date() - 30
67 | )
68 |
69 | expect_s3_class(exp, "data.frame")
70 | if (nrow(exp) > 0) {
71 | expect_true(all(exp$date <= Sys.Date() - 30))
72 | }
73 | })
74 |
75 | test_that("calculate_exposure calculates ACWR correctly", {
76 | activities <- create_test_activities_exposure(60)
77 |
78 | exp <- calculate_exposure(activities)
79 |
80 | expect_s3_class(exp, "data.frame")
81 | expect_true("acwr" %in% names(exp))
82 |
83 | # ACWR should be ATL / CTL (where CTL > 0)
84 | if (nrow(exp) > 0) {
85 | valid_rows <- exp$ctl > 0
86 | if (any(valid_rows)) {
87 | calculated_acwr <- exp$atl[valid_rows] / exp$ctl[valid_rows]
88 | expect_equal(exp$acwr[valid_rows], calculated_acwr, tolerance = 0.01)
89 | }
90 | }
91 | })
92 |
93 | test_that("calculate_exposure handles sparse activity data", {
94 | # Create very sparse data (only a few activities)
95 | activities <- create_test_activities_exposure(90)
96 | activities <- activities[sample(1:nrow(activities), 5), ] # Keep only 5 activities
97 |
98 | exp <- calculate_exposure(activities)
99 |
100 | expect_s3_class(exp, "data.frame")
101 | })
102 |
103 | test_that("calculate_exposure handles missing data gracefully", {
104 | skip("Missing data handling needs to be tested at activity load level")
105 | })
106 |
107 |
--------------------------------------------------------------------------------
/tests/testthat/test-ef.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/test-ef.R
2 |
3 | # Efficiency Factor Calculation Tests
4 |
5 | library(Athlytics)
6 | library(testthat)
7 |
8 | data(athlytics_sample_ef)
9 |
10 | create_mock_activities <- function(n = 30) {
11 | dates <- seq(Sys.Date() - n, Sys.Date(), by = "day")
12 | data.frame(
13 | id = seq_len(length(dates)),
14 | name = paste("Activity", seq_len(length(dates))),
15 | type = sample(c("Run", "Ride"), length(dates), replace = TRUE),
16 | date = as.Date(dates),
17 | distance = runif(length(dates), 5000, 15000),
18 | moving_time = as.integer(runif(length(dates), 1800, 5400)),
19 | average_heartrate = runif(length(dates), 130, 170),
20 | average_watts = runif(length(dates), 180, 250),
21 | average_speed = runif(length(dates), 2.5, 4.5),
22 | stringsAsFactors = FALSE
23 | )
24 | }
25 |
26 | test_that("calculate_ef works with pace_hr metric", {
27 | mock_activities <- create_mock_activities()
28 |
29 | ef_result <- calculate_ef(
30 | activities_data = mock_activities,
31 | ef_metric = "pace_hr"
32 | )
33 |
34 | expect_s3_class(ef_result, "data.frame")
35 | expect_true("ef_value" %in% colnames(ef_result))
36 | expect_gt(nrow(ef_result), 0)
37 | })
38 |
39 | test_that("calculate_ef works with power_hr metric", {
40 | mock_activities <- create_mock_activities()
41 |
42 | ef_result <- calculate_ef(
43 | activities_data = mock_activities,
44 | ef_metric = "power_hr"
45 | )
46 |
47 | expect_s3_class(ef_result, "data.frame")
48 | expect_true("ef_value" %in% colnames(ef_result))
49 | })
50 |
51 | test_that("calculate_ef validates input", {
52 | expect_error(
53 | calculate_ef(activities_data = "not_a_dataframe"),
54 | "data frame"
55 | )
56 | })
57 |
58 | test_that("calculate_ef works with sample data", {
59 | skip_if(is.null(athlytics_sample_ef), "Sample EF data not available")
60 |
61 | expect_s3_class(athlytics_sample_ef, "data.frame")
62 | # Sample data may have either ef_value or efficiency_factor
63 | expect_true(any(c("ef_value", "efficiency_factor") %in% colnames(athlytics_sample_ef)))
64 | })
65 |
66 | test_that("plot_ef works with pre-calculated data", {
67 | skip_if(is.null(athlytics_sample_ef), "Sample EF data not available")
68 |
69 | p <- plot_ef(athlytics_sample_ef)
70 | expect_s3_class(p, "ggplot")
71 | })
72 |
73 | test_that("plot_ef works with activities data", {
74 | mock_activities <- create_mock_activities(50)
75 |
76 | # Test with pace_hr
77 | p1 <- plot_ef(
78 | data = mock_activities,
79 | activity_type = "Run",
80 | ef_metric = "pace_hr"
81 | )
82 | expect_s3_class(p1, "ggplot")
83 |
84 | # Test with power_hr
85 | p2 <- plot_ef(
86 | data = mock_activities,
87 | activity_type = "Ride",
88 | ef_metric = "power_hr"
89 | )
90 | expect_s3_class(p2, "ggplot")
91 | })
92 |
93 | test_that("plot_ef handles various options", {
94 | mock_activities <- create_mock_activities(50)
95 |
96 | # Test without trend line
97 | p1 <- plot_ef(
98 | data = mock_activities,
99 | activity_type = "Run",
100 | ef_metric = "pace_hr",
101 | add_trend_line = FALSE
102 | )
103 | expect_s3_class(p1, "ggplot")
104 |
105 | # Test with different smoothing method
106 | p2 <- plot_ef(
107 | data = mock_activities,
108 | activity_type = "Run",
109 | ef_metric = "pace_hr",
110 | smoothing_method = "lm"
111 | )
112 | expect_s3_class(p2, "ggplot")
113 |
114 | # Test with date range
115 | p3 <- plot_ef(
116 | data = mock_activities,
117 | activity_type = "Run",
118 | ef_metric = "pace_hr",
119 | start_date = Sys.Date() - 30,
120 | end_date = Sys.Date()
121 | )
122 | expect_s3_class(p3, "ggplot")
123 | })
124 |
125 | test_that("plot_ef works with ef_df parameter", {
126 | skip_if(is.null(athlytics_sample_ef), "Sample EF data not available")
127 |
128 | # Plot already calculated EF data
129 | p <- plot_ef(athlytics_sample_ef)
130 | expect_s3_class(p, "ggplot")
131 | })
132 |
--------------------------------------------------------------------------------
/man/calculate_acwr_ewma.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/calculate_acwr_ewma.R
3 | \name{calculate_acwr_ewma}
4 | \alias{calculate_acwr_ewma}
5 | \title{Calculate ACWR using EWMA Method with Confidence Bands}
6 | \usage{
7 | calculate_acwr_ewma(
8 | activities_data,
9 | activity_type = NULL,
10 | load_metric = "duration_mins",
11 | method = c("ra", "ewma"),
12 | acute_period = 7,
13 | chronic_period = 28,
14 | half_life_acute = 3.5,
15 | half_life_chronic = 14,
16 | start_date = NULL,
17 | end_date = NULL,
18 | user_ftp = NULL,
19 | user_max_hr = NULL,
20 | user_resting_hr = NULL,
21 | smoothing_period = 7,
22 | ci = FALSE,
23 | B = 200,
24 | block_len = 7,
25 | conf_level = 0.95
26 | )
27 | }
28 | \arguments{
29 | \item{activities_data}{A data frame of activities from \code{load_local_activities()}.}
30 |
31 | \item{activity_type}{Optional. Filter activities by type. Default NULL includes all.}
32 |
33 | \item{load_metric}{Method for calculating daily load. Default "duration_mins".}
34 |
35 | \item{method}{ACWR calculation method: "ra" (rolling average) or "ewma". Default "ra".}
36 |
37 | \item{acute_period}{Days for acute window (for RA method). Default 7.}
38 |
39 | \item{chronic_period}{Days for chronic window (for RA method). Default 28.}
40 |
41 | \item{half_life_acute}{Half-life for acute EWMA in days. Default 3.5.}
42 |
43 | \item{half_life_chronic}{Half-life for chronic EWMA in days. Default 14.}
44 |
45 | \item{start_date}{Optional. Analysis start date. Defaults to one year ago.}
46 |
47 | \item{end_date}{Optional. Analysis end date. Defaults to today.}
48 |
49 | \item{user_ftp}{Required if \code{load_metric = "tss"}.}
50 |
51 | \item{user_max_hr}{Required if \code{load_metric = "hrss"}.}
52 |
53 | \item{user_resting_hr}{Required if \code{load_metric = "hrss"}.}
54 |
55 | \item{smoothing_period}{Days for smoothing ACWR. Default 7.}
56 |
57 | \item{ci}{Logical. Whether to calculate confidence bands (EWMA only). Default FALSE.}
58 |
59 | \item{B}{Number of bootstrap iterations (if ci = TRUE). Default 200.}
60 |
61 | \item{block_len}{Block length for moving-block bootstrap (days). Default 7.}
62 |
63 | \item{conf_level}{Confidence level (0-1). Default 0.95 (95\\\% CI).}
64 | }
65 | \value{
66 | A data frame with columns: \code{date}, \code{atl}, \code{ctl}, \code{acwr}, \code{acwr_smooth},
67 | and if \code{ci = TRUE} and \code{method = "ewma"}: \code{acwr_lower}, \code{acwr_upper}.
68 | }
69 | \description{
70 | Calculates the Acute:Chronic Workload Ratio (ACWR) using Exponentially
71 | Weighted Moving Average (EWMA) with optional bootstrap confidence bands.
72 | }
73 | \details{
74 | This function extends the basic ACWR calculation with two methods:
75 | \itemize{
76 | \item \strong{RA (Rolling Average)}: Traditional rolling mean approach (default).
77 | \item \strong{EWMA (Exponentially Weighted Moving Average)}: Uses exponential decay
78 | with configurable half-lives. More responsive to recent changes.
79 | }
80 |
81 | \strong{EWMA Formula}: The smoothing parameter alpha is calculated from half-life:
82 | \code{alpha = ln(2) / half_life}. The EWMA update is: \verb{E_t = alpha * L_t + (1-alpha) * E_\{t-1\}}
83 | where L_t is daily load and E_t is the exponentially weighted average.
84 |
85 | \strong{Confidence Bands}: When \code{ci = TRUE} and \code{method = "ewma"}, uses \strong{moving-block
86 | bootstrap} to estimate uncertainty. The daily load sequence is resampled in
87 | weekly blocks (preserving within-week correlation), ACWR is recalculated,
88 | and percentiles form the confidence bands. This accounts for temporal correlation
89 | in training load patterns.
90 | }
91 | \examples{
92 | \dontrun{
93 | # Load local activities
94 | activities <- load_local_activities("export_12345678.zip")
95 |
96 | # Calculate ACWR using Rolling Average (RA)
97 | acwr_ra <- calculate_acwr_ewma(activities, method = "ra")
98 |
99 | # Calculate ACWR using EWMA with confidence bands
100 | acwr_ewma <- calculate_acwr_ewma(
101 | activities,
102 | method = "ewma",
103 | half_life_acute = 3.5,
104 | half_life_chronic = 14,
105 | ci = TRUE,
106 | B = 200
107 | )
108 |
109 | # Compare both methods
110 | head(acwr_ewma)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/testthat/helper-mockdata.R:
--------------------------------------------------------------------------------
1 | library(dplyr)
2 | # tests/testthat/helper-mockdata.R
3 |
4 | mock_pbs_df <- data.frame(
5 | activity_id = c("1", "2", "3", "4"),
6 | activity_date = lubridate::ymd_hms(c("2023-01-01 10:00:00", "2023-01-15 10:00:00", "2023-02-01 10:00:00", "2023-02-15 10:00:00")),
7 | distance = rep(c(5000, 10000), each = 2),
8 | time_seconds = c(1500, 1450, 3200, 3100),
9 | cumulative_pb_seconds = c(1500, 1450, 3200, 3100),
10 | is_pb = c(TRUE, TRUE, TRUE, TRUE),
11 | distance_label = factor(rep(c("5k", "10k"), each = 2), levels = c("5k", "10k")),
12 | stringsAsFactors = FALSE
13 | )
14 |
15 | mock_acwr_df <- data.frame(
16 | date = seq(lubridate::ymd("2023-01-01"), lubridate::ymd("2023-02-10"), by="day"),
17 | atl = round(runif(41, 30, 70) + sin(seq(0, 4*pi, length.out=41))*10, 1),
18 | ctl = round(runif(41, 40, 60) + sin(seq(0, 2*pi, length.out=41))*5, 1)
19 | ) %>%
20 | dplyr::mutate(
21 | ctl_safe = ifelse(ctl <= 0, 1, ctl),
22 | acwr = round(atl / ctl_safe, 2),
23 | acwr_smooth = acwr
24 | ) %>%
25 | dplyr::select(date, atl, ctl, acwr, acwr_smooth)
26 |
27 | latlng_list <- lapply(1:3601, function(i) c(runif(1, 40, 41), runif(1, -75, -74)))
28 |
29 | mock_activity_streams <- data.frame(
30 | time = seq(0, 3600, by = 1),
31 | latlng = I(latlng_list),
32 | distance = seq(0, 10000, length.out = 3601),
33 | altitude = rnorm(3601, 100, 10),
34 | velocity_smooth = rnorm(3601, 3, 0.1), # Reduced variation for steady state
35 | heartrate = round(rnorm(3601, 150, 5)), # More stable HR
36 | cadence = round(runif(3601, 85, 95)),
37 | watts = round(rnorm(3601, 200, 10)), # More stable power
38 | grade_smooth = rnorm(3601, 0, 1),
39 | moving = sample(c(TRUE, FALSE), 3601, replace = TRUE, prob = c(0.95, 0.05)),
40 | temp = rnorm(3601, 20, 3)
41 | )
42 |
43 | mock_ef_df <- data.frame(
44 | activity_id = c("1", "2", "3", "4", "5"),
45 | date = lubridate::ymd(c("2023-01-01", "2023-01-15", "2023-02-01", "2023-02-15", "2023-03-01")),
46 | activity_type = rep("Run", 5),
47 | ef_value = round(rnorm(5, 1.5, 0.1), 2),
48 | ef_metric = rep("pace_hr", 5),
49 | stringsAsFactors = FALSE
50 | )
51 |
52 | mock_exposure_df <- data.frame(
53 | date = seq(lubridate::ymd("2023-01-01"), lubridate::ymd("2023-02-10"), by="day"),
54 | atl = round(runif(41, 30, 70) + sin(seq(0, 4*pi, length.out=41))*10, 1),
55 | ctl = round(runif(41, 40, 60) + sin(seq(0, 2*pi, length.out=41))*5, 1)
56 | ) %>%
57 | dplyr::mutate(
58 | ctl_safe = ifelse(ctl <= 0, 1, ctl),
59 | acwr = round(atl / ctl_safe, 2)
60 | )
61 |
62 | mock_activity_list_df <- data.frame(
63 | id = c(1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010),
64 | name = paste("Activity", 1:10),
65 | start_date_local = seq.POSIXt(as.POSIXct("2023-10-01 08:00:00", tz = "UTC"),
66 | by = "-2 days", length.out = 10),
67 | type = rep(c("Run", "Ride"), length.out = 10),
68 | distance = c(5050, 20100, 10200, 30500, 8030, 15200, 12100, 40300, 6050, 25400),
69 | moving_time = c(1800, 3600, 3000, 5400, 2400, 2700, 3300, 7200, 1900, 4500),
70 | elapsed_time = c(1850, 3700, 3060, 5500, 2450, 2780, 3380, 7350, 1950, 4600),
71 | total_elevation_gain = c(50, 150, 100, 250, 80, 120, 110, 300, 60, 200),
72 | average_heartrate = c(150, 140, 155, 145, 152, 142, 158, 148, 151, 141),
73 | max_heartrate = c(170, 160, 175, 165, 172, 162, 178, 168, 171, 161),
74 | average_watts = c(NA, 200, NA, 220, NA, 190, NA, 230, NA, 210),
75 | stringsAsFactors = FALSE
76 | )
77 |
78 | mock_activity_list_df$start_date_local <- as.POSIXct(mock_activity_list_df$start_date_local, tz = "UTC")
79 |
80 | # Adjust the date of the second "Run" activity (original index 3) to be within the test range
81 | # The test expects two runs between 2023-10-01 and 2023-10-03
82 | # Original dates: 2023-10-01 (Run), 2023-09-29 (Ride), 2023-09-27 (Run)
83 | # Change 2023-09-27 to 2023-10-02 for the second Run
84 | mock_activity_list_df$start_date_local[3] <- as.POSIXct("2023-10-02 09:00:00", tz = "UTC")
85 |
86 | mock_activity_list_df$duration_mins <- mock_activity_list_df$elapsed_time / 60
87 | mock_activity_list_df$distance_km <- mock_activity_list_df$distance / 1000
88 |
89 | mock_activity_list_df_for_list <- mock_activity_list_df %>%
90 | dplyr::mutate(start_date_local = format(start_date_local, "%Y-%m-%d %H:%M:%S"))
91 |
92 | mock_activity_list_list <- purrr::transpose(mock_activity_list_df_for_list)
93 |
--------------------------------------------------------------------------------
/tests/testthat/test-utils-extended.R:
--------------------------------------------------------------------------------
1 | # Extended tests for utils functions
2 |
3 | library(testthat)
4 | library(Athlytics)
5 |
6 | test_that("utils functions exist and work", {
7 | # Test any utility functions that might exist
8 | expect_true(TRUE) # Placeholder if no specific utils to test
9 | })
10 |
11 | test_that("package loads without errors", {
12 | expect_true("Athlytics" %in% loadedNamespaces())
13 | })
14 |
15 | test_that("sample data exists", {
16 | data("athlytics_sample_acwr", package = "Athlytics")
17 | data("athlytics_sample_ef", package = "Athlytics")
18 | data("athlytics_sample_exposure", package = "Athlytics")
19 | data("athlytics_sample_decoupling", package = "Athlytics")
20 | data("athlytics_sample_pbs", package = "Athlytics")
21 |
22 | expect_true(!is.null(athlytics_sample_acwr))
23 | expect_true(!is.null(athlytics_sample_ef))
24 | expect_true(!is.null(athlytics_sample_exposure))
25 | expect_true(!is.null(athlytics_sample_decoupling))
26 | expect_true(!is.null(athlytics_sample_pbs))
27 | })
28 |
29 | test_that("color palettes are accessible", {
30 | nature <- athlytics_palette_nature()
31 | academic <- athlytics_palette_academic()
32 | vibrant <- athlytics_palette_vibrant()
33 | science <- athlytics_palette_science()
34 | cell <- athlytics_palette_cell()
35 |
36 | expect_true(is.character(nature))
37 | expect_true(is.character(academic))
38 | expect_true(is.character(vibrant))
39 | expect_true(is.character(science))
40 | expect_true(is.character(cell))
41 |
42 | expect_gt(length(nature), 0)
43 | expect_gt(length(academic), 0)
44 | expect_gt(length(vibrant), 0)
45 | expect_gt(length(science), 0)
46 | expect_gt(length(cell), 0)
47 | })
48 |
49 | test_that("color zone functions work", {
50 | acwr_zones <- athlytics_colors_acwr_zones()
51 | training_load <- athlytics_colors_training_load()
52 | ef_colors <- athlytics_colors_ef()
53 |
54 | expect_true(is.list(acwr_zones))
55 | expect_true(is.list(training_load))
56 | expect_true(is.list(ef_colors))
57 |
58 | expect_true(length(acwr_zones) > 0)
59 | expect_true(length(training_load) > 0)
60 | expect_true(length(ef_colors) > 0)
61 | })
62 |
63 | test_that("theme_athlytics works", {
64 | skip_if_not_installed("ggplot2")
65 |
66 | theme <- theme_athlytics()
67 | expect_s3_class(theme, "theme")
68 |
69 | # Test with different base sizes
70 | theme_small <- theme_athlytics(base_size = 10)
71 | expect_s3_class(theme_small, "theme")
72 |
73 | theme_large <- theme_athlytics(base_size = 14)
74 | expect_s3_class(theme_large, "theme")
75 | })
76 |
77 | test_that("scale_athlytics works for different palettes", {
78 | skip_if_not_installed("ggplot2")
79 |
80 | # Test different palettes
81 | scale_nature <- scale_athlytics("nature", type = "color")
82 | expect_s3_class(scale_nature, "ScaleDiscrete")
83 |
84 | scale_academic <- scale_athlytics("academic", type = "fill")
85 | expect_s3_class(scale_academic, "ScaleDiscrete")
86 |
87 | scale_vibrant <- scale_athlytics("vibrant", type = "color")
88 | expect_s3_class(scale_vibrant, "ScaleDiscrete")
89 |
90 | scale_science <- scale_athlytics("science", type = "fill")
91 | expect_s3_class(scale_science, "ScaleDiscrete")
92 |
93 | scale_cell <- scale_athlytics("cell", type = "color")
94 | expect_s3_class(scale_cell, "ScaleDiscrete")
95 | })
96 |
97 | test_that("scale_athlytics handles invalid palette names", {
98 | skip_if_not_installed("ggplot2")
99 |
100 | # Should default to nature palette
101 | scale_default <- scale_athlytics("invalid_name", type = "color")
102 | expect_s3_class(scale_default, "ScaleDiscrete")
103 | })
104 |
105 | test_that("package datasets are properly formatted", {
106 | data("athlytics_sample_acwr", package = "Athlytics")
107 |
108 | expect_s3_class(athlytics_sample_acwr, "data.frame")
109 | expect_true("date" %in% names(athlytics_sample_acwr))
110 | expect_true("acwr" %in% names(athlytics_sample_acwr) || "acwr_smooth" %in% names(athlytics_sample_acwr))
111 | })
112 |
113 | test_that("plot functions handle sample data", {
114 | skip_if_not_installed("ggplot2")
115 |
116 | data("athlytics_sample_acwr", package = "Athlytics")
117 |
118 | # Test that plot can be created
119 | p <- plot_acwr(athlytics_sample_acwr)
120 | expect_s3_class(p, "ggplot")
121 | })
122 |
123 |
--------------------------------------------------------------------------------
/man/calculate_decoupling.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/calculate_decoupling.R
3 | \name{calculate_decoupling}
4 | \alias{calculate_decoupling}
5 | \title{Calculate Aerobic Decoupling}
6 | \usage{
7 | calculate_decoupling(
8 | activities_data = NULL,
9 | export_dir = "strava_export_data",
10 | activity_type = c("Run", "Ride"),
11 | decouple_metric = c("pace_hr", "power_hr"),
12 | start_date = NULL,
13 | end_date = NULL,
14 | min_duration_mins = 40,
15 | min_steady_minutes = 40,
16 | steady_cv_threshold = 0.08,
17 | min_hr_coverage = 0.9,
18 | quality_control = c("off", "flag", "filter"),
19 | stream_df = NULL
20 | )
21 | }
22 | \arguments{
23 | \item{activities_data}{A data frame from \code{load_local_activities()}. Required unless \code{stream_df} is provided.}
24 |
25 | \item{export_dir}{Base directory of Strava export containing the activities folder.
26 | Default is "strava_export_data".}
27 |
28 | \item{activity_type}{Type(s) of activities to analyze (e.g., "Run", "Ride").}
29 |
30 | \item{decouple_metric}{Basis for calculation: "pace_hr" or "power_hr"
31 | (legacy "pace_hr"/"power_hr" also supported).}
32 |
33 | \item{start_date}{Optional. Analysis start date (YYYY-MM-DD string or Date). Defaults to one year ago.}
34 |
35 | \item{end_date}{Optional. Analysis end date (YYYY-MM-DD string or Date). Defaults to today.}
36 |
37 | \item{min_duration_mins}{Minimum activity duration (minutes) to include. Default 40.}
38 |
39 | \item{min_steady_minutes}{Minimum duration (minutes) for steady-state segment (default: 40).
40 | Activities shorter than this are automatically rejected for decoupling calculation.}
41 |
42 | \item{steady_cv_threshold}{Coefficient of variation threshold for steady-state (default: 0.08 = 8\%).
43 | Activities with higher variability are rejected as non-steady-state.}
44 |
45 | \item{min_hr_coverage}{Minimum HR data coverage threshold (default: 0.9 = 90\%).
46 | Activities with lower HR coverage are rejected as insufficient data quality.}
47 |
48 | \item{quality_control}{Quality control mode: "off" (no filtering), "flag" (mark issues),
49 | or "filter" (exclude flagged data). Default "filter" for scientific rigor.}
50 |
51 | \item{stream_df}{Optional. A pre-fetched data frame for a \emph{single} activity's stream.
52 | If provided, calculates decoupling for this data directly, ignoring other parameters.
53 | Must include columns: \code{time}, \code{heartrate}, and either \code{velocity_smooth}/\code{distance}
54 | (for pace_hr) or \code{watts} (for power_hr).}
55 | }
56 | \value{
57 | Returns a data frame with columns:
58 | \describe{
59 | \item{date}{Activity date (Date class)}
60 | \item{decoupling}{Decoupling percentage (\\\%). Positive = HR drift, negative = improved efficiency}
61 | \item{status}{Character. "ok" for successful calculation, "non_steady" if steady-state
62 | criteria not met, "insufficient_data" if data quality issues}
63 | }
64 | OR a single numeric decoupling value if \code{stream_df} is provided.
65 | }
66 | \description{
67 | Calculates aerobic decoupling for Strava activities from local export data.
68 | }
69 | \details{
70 | Calculates aerobic decoupling (HR drift relative to pace/power) using detailed
71 | activity stream data from local FIT/TCX/GPX files.
72 |
73 | Provides data for \code{plot_decoupling}. Compares output/HR efficiency
74 | between first and second halves of activities. Positive values indicate
75 | HR drift (cardiovascular drift).
76 |
77 | \strong{Best practice}: Use \code{load_local_activities()} to load data, then pass to this function.
78 |
79 | The function parses FIT/TCX/GPX files from your Strava export to extract detailed
80 | stream data (time, heartrate, distance/power). Activities are split into two halves,
81 | and the efficiency factor (output/HR) is compared between halves.
82 | }
83 | \examples{
84 | # Example using simulated data
85 | data(Athlytics_sample_data)
86 | print(head(athlytics_sample_decoupling))
87 |
88 | \dontrun{
89 | # Load local activities
90 | activities <- load_local_activities("strava_export_data/activities.csv")
91 |
92 | # Calculate Pace/HR decoupling for recent runs
93 | run_decoupling <- calculate_decoupling(
94 | activities_data = activities,
95 | export_dir = "strava_export_data",
96 | activity_type = "Run",
97 | decouple_metric = "pace_hr",
98 | start_date = "2024-01-01"
99 | )
100 | print(tail(run_decoupling))
101 |
102 | # Calculate for a single activity stream
103 | # stream_data <- parse_activity_file("strava_export_data/activities/12345.fit")
104 | # single_decoupling <- calculate_decoupling(stream_df = stream_data, decouple_metric = "pace_hr")
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/testthat/test-flag-quality.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/test-flag_quality.R
2 |
3 | test_that("flag_quality detects HR spikes", {
4 | # Create synthetic data with HR spike
5 | stream_data <- data.frame(
6 | time = 1:100,
7 | heartrate = c(rep(150, 50), 250, rep(150, 49)) # One spike at position 51
8 | )
9 |
10 | result <- flag_quality(stream_data, sport = "Run")
11 |
12 | # Check that spike is flagged
13 | expect_true(result$flag_hr_spike[51])
14 | expect_true(result$flag_any[51])
15 |
16 | # Check that non-spike values before are not flagged
17 | expect_false(result$flag_hr_spike[50])
18 | # Note: position 52 might also be flagged due to jump back down from spike
19 | # This is correct behavior, so we check that at least the spike itself is detected
20 | })
21 |
22 | test_that("flag_quality detects power spikes", {
23 | # Create synthetic data with power spike
24 | stream_data <- data.frame(
25 | time = 1:100,
26 | watts = c(rep(200, 50), 1600, rep(200, 49)) # One spike at position 51
27 | )
28 |
29 | result <- flag_quality(stream_data, sport = "Ride")
30 |
31 | # Check that spike is flagged
32 | expect_true(result$flag_pw_spike[51])
33 | expect_true(result$flag_any[51])
34 | })
35 |
36 | test_that("flag_quality detects excessive HR jumps", {
37 | # Create data with excessive HR jump
38 | stream_data <- data.frame(
39 | time = 1:100,
40 | heartrate = c(rep(140, 50), 160, rep(140, 49)) # Jump of 20 bpm in 1 sec
41 | )
42 |
43 | result <- flag_quality(stream_data, sport = "Run", max_hr_jump = 10)
44 |
45 | # Check that jump is flagged
46 | expect_true(result$flag_hr_spike[51])
47 | })
48 |
49 | test_that("flag_quality detects GPS drift", {
50 | # Create data with implausible speed
51 | stream_data <- data.frame(
52 | time = 1:100,
53 | velocity_smooth = c(rep(3.5, 50), 10, rep(3.5, 49)) # 10 m/s = ~36 km/h running
54 | )
55 |
56 | result <- flag_quality(stream_data, sport = "Run", max_run_speed = 7.0)
57 |
58 | # Check that drift is flagged
59 | expect_true(result$flag_gps_drift[51])
60 | expect_true(result$flag_any[51])
61 | })
62 |
63 | test_that("flag_quality calculates quality score", {
64 | # Create clean data
65 | clean_data <- data.frame(
66 | time = 1:100,
67 | heartrate = rep(150, 100),
68 | watts = rep(200, 100),
69 | velocity_smooth = rep(3.5, 100)
70 | )
71 |
72 | result <- flag_quality(clean_data, sport = "Run")
73 |
74 | # Quality score should be 1.0 (perfect)
75 | expect_equal(result$quality_score[1], 1.0)
76 |
77 | # Create data with 10% flagged points
78 | dirty_data <- data.frame(
79 | time = 1:100,
80 | heartrate = c(rep(250, 10), rep(150, 90)) # 10% out of range
81 | )
82 |
83 | result2 <- flag_quality(dirty_data, sport = "Run")
84 |
85 | # Quality score should be ~0.9
86 | expect_lt(result2$quality_score[1], 1.0)
87 | expect_gt(result2$quality_score[1], 0.85)
88 | })
89 |
90 | test_that("flag_quality handles empty data gracefully", {
91 | empty_data <- data.frame(time = numeric(0))
92 |
93 | expect_warning(result <- flag_quality(empty_data))
94 | expect_equal(nrow(result), 0)
95 | expect_true("flag_any" %in% colnames(result))
96 | })
97 |
98 | test_that("flag_quality detects steady state", {
99 | # Create long steady-state data (25 minutes at constant pace)
100 | steady_data <- data.frame(
101 | time = 0:(25*60-1), # 25 minutes
102 | velocity_smooth = rnorm(25*60, mean = 3.5, sd = 0.1) # Low variability
103 | )
104 |
105 | result <- flag_quality(steady_data, sport = "Run", min_steady_minutes = 20)
106 |
107 | # Should have some steady-state points
108 | expect_gt(sum(result$is_steady_state, na.rm = TRUE), 0)
109 | })
110 |
111 | test_that("quality_summary provides correct statistics", {
112 | stream_data <- data.frame(
113 | time = 1:100,
114 | heartrate = c(rep(250, 10), rep(150, 90)) # 10% out of range + 1 transition
115 | )
116 |
117 | result <- flag_quality(stream_data, sport = "Run")
118 | summary_stats <- quality_summary(result)
119 |
120 | expect_equal(summary_stats$total_points, 100)
121 | # Expect 10 out-of-range + 1 jump (transition from 250 to 150)
122 | expect_equal(summary_stats$flagged_points, 11)
123 | expect_equal(summary_stats$flagged_pct, 11)
124 | expect_true(summary_stats$quality_score > 0 && summary_stats$quality_score <= 1)
125 | })
126 |
127 | test_that("flag_quality is sport-aware", {
128 | # Create data with speed that's OK for cycling but not running
129 | stream_data <- data.frame(
130 | time = 1:100,
131 | velocity_smooth = rep(12, 100) # 12 m/s = ~43 km/h
132 | )
133 |
134 | # Should be flagged for running
135 | result_run <- flag_quality(stream_data, sport = "Run", max_run_speed = 7.0)
136 | expect_gt(sum(result_run$flag_gps_drift), 0)
137 |
138 | # Should NOT be flagged for cycling
139 | result_ride <- flag_quality(stream_data, sport = "Ride", max_ride_speed = 25.0)
140 | expect_equal(sum(result_ride$flag_gps_drift), 0)
141 | })
142 |
143 |
--------------------------------------------------------------------------------
/tests/testthat/test-acwr.R:
--------------------------------------------------------------------------------
1 | # tests/testthat/test-acwr.R
2 |
3 | # ACWR Calculation and Plotting Tests
4 |
5 | library(Athlytics)
6 | library(testthat)
7 |
8 | # Load sample data from the package
9 | data(athlytics_sample_acwr)
10 | data(athlytics_sample_exposure)
11 |
12 | # Create mock activities data for testing
13 | create_mock_activities <- function(n = 30) {
14 | dates <- seq(Sys.Date() - n, Sys.Date(), by = "day")
15 | data.frame(
16 | id = seq_len(length(dates)),
17 | name = paste("Activity", seq_len(length(dates))),
18 | type = sample(c("Run", "Ride"), length(dates), replace = TRUE),
19 | sport_type = sample(c("Run", "Ride"), length(dates), replace = TRUE),
20 | start_date_local = as.POSIXct(dates),
21 | date = as.Date(dates),
22 | distance = runif(length(dates), 1000, 15000), # meters
23 | moving_time = as.integer(runif(length(dates), 1200, 5400)), # seconds
24 | elapsed_time = as.integer(runif(length(dates), 1200, 5400)),
25 | average_heartrate = runif(length(dates), 120, 170),
26 | max_heartrate = runif(length(dates), 160, 190),
27 | average_watts = runif(length(dates), 150, 250),
28 | max_watts = runif(length(dates), 300, 500),
29 | elevation_gain = runif(length(dates), 0, 500),
30 | average_speed = runif(length(dates), 2, 5),
31 | stringsAsFactors = FALSE
32 | )
33 | }
34 |
35 | # --- Test calculate_acwr with local data ---
36 |
37 | test_that("calculate_acwr works with activities_data parameter", {
38 | mock_activities <- create_mock_activities(60)
39 |
40 | acwr_result <- calculate_acwr(
41 | activities_data = mock_activities,
42 | load_metric = "duration_mins",
43 | activity_type = "Run",
44 | acute_period = 7,
45 | chronic_period = 28
46 | )
47 |
48 | # Structure checks
49 | expect_s3_class(acwr_result, "data.frame")
50 | expect_true(all(c("date", "atl", "ctl", "acwr", "acwr_smooth") %in% colnames(acwr_result)))
51 | expect_s3_class(acwr_result$date, "Date")
52 |
53 | # Check that we have results
54 | expect_gt(nrow(acwr_result), 0)
55 |
56 | # Numerical checks
57 | expect_true(is.numeric(acwr_result$atl))
58 | expect_true(is.numeric(acwr_result$ctl))
59 | expect_true(is.numeric(acwr_result$acwr))
60 | })
61 |
62 | test_that("calculate_acwr validates activities_data parameter", {
63 | # Test with non-data.frame
64 | expect_error(
65 | calculate_acwr(activities_data = "not_a_dataframe"),
66 | "must be a data frame"
67 | )
68 |
69 | # Test with empty or incomplete data frame
70 | empty_df <- data.frame()
71 | expect_error(
72 | calculate_acwr(activities_data = empty_df),
73 | "activity_type.*must be explicitly specified" # Now checks for activity_type first
74 | )
75 | })
76 |
77 | test_that("calculate_acwr validates period parameters", {
78 | mock_activities <- create_mock_activities()
79 |
80 | # acute_period must be less than chronic_period
81 | expect_error(
82 | calculate_acwr(
83 | activities_data = mock_activities,
84 | acute_period = 28,
85 | chronic_period = 7
86 | ),
87 | "acute_period.*must be less than.*chronic_period"
88 | )
89 | })
90 |
91 | test_that("calculate_acwr works with different load metrics", {
92 | mock_activities <- create_mock_activities(60)
93 |
94 | # Test duration_mins
95 | acwr_duration <- calculate_acwr(
96 | activities_data = mock_activities,
97 | activity_type = "Run",
98 | load_metric = "duration_mins"
99 | )
100 | expect_s3_class(acwr_duration, "data.frame")
101 |
102 | # Test distance_km
103 | acwr_distance <- calculate_acwr(
104 | activities_data = mock_activities,
105 | activity_type = "Run",
106 | load_metric = "distance_km"
107 | )
108 | expect_s3_class(acwr_distance, "data.frame")
109 |
110 | # Test elevation
111 | acwr_elevation <- calculate_acwr(
112 | activities_data = mock_activities,
113 | activity_type = "Run",
114 | load_metric = "elevation_gain_m"
115 | )
116 | expect_s3_class(acwr_elevation, "data.frame")
117 | })
118 |
119 | test_that("calculate_acwr filters by activity type correctly", {
120 | mock_activities <- create_mock_activities(60)
121 |
122 | acwr_run <- calculate_acwr(
123 | activities_data = mock_activities,
124 | activity_type = "Run",
125 | load_metric = "duration_mins"
126 | )
127 |
128 | expect_s3_class(acwr_run, "data.frame")
129 | expect_gt(nrow(acwr_run), 0)
130 | })
131 |
132 | test_that("calculate_acwr works with sample data", {
133 | skip_if(is.null(athlytics_sample_acwr), "Sample ACWR data not available")
134 |
135 | # Just check that sample data has the right structure
136 | expect_s3_class(athlytics_sample_acwr, "data.frame")
137 | expect_true(all(c("date", "atl", "ctl", "acwr") %in% colnames(athlytics_sample_acwr)))
138 | })
139 |
140 | # --- Test plot_acwr ---
141 |
142 | test_that("plot_acwr works with pre-calculated data", {
143 | skip_if(is.null(athlytics_sample_acwr), "Sample ACWR data not available")
144 |
145 | p <- plot_acwr(athlytics_sample_acwr, highlight_zones = FALSE)
146 |
147 | expect_s3_class(p, "ggplot")
148 | })
149 |
150 | test_that("plot_acwr validates input", {
151 | # Test with non-data.frame - should error
152 | expect_error(
153 | plot_acwr("not_a_dataframe"),
154 | "activities_data.*must be a data frame"
155 | )
156 |
157 | # Test with missing required columns - should error or warn
158 | bad_df <- data.frame(x = 1:10, y = 1:10)
159 | expect_error(
160 | plot_acwr(bad_df),
161 | "activity_type.*must be explicitly specified" # Now checks for activity_type first
162 | )
163 | })
164 |
--------------------------------------------------------------------------------
/.github/workflows/R-CMD-check.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [main, master]
4 | pull_request:
5 | branches: [main, master]
6 |
7 | name: R-CMD-check
8 |
9 | jobs:
10 | R-CMD-check:
11 | runs-on: ${{ matrix.config.os }}
12 |
13 | name: ${{ matrix.config.os }} (${{ matrix.config.r }})
14 |
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | config:
19 | - {os: windows-latest, r: 'release'}
20 | - {os: ubuntu-latest, r: 'release'}
21 | - {os: macOS-latest, r: 'release'}
22 |
23 | env:
24 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
25 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
26 | _R_CHECK_CRAN_INCOMING_REMOTE_: false
27 | _R_CHECK_FORCE_SUGGESTS_: false
28 |
29 | steps:
30 | - uses: actions/checkout@v4
31 |
32 | - uses: r-lib/actions/setup-pandoc@v2
33 |
34 | - uses: r-lib/actions/setup-r@v2
35 | with:
36 | r-version: ${{ matrix.config.r }}
37 | http-user-agent: ${{ matrix.config.http-user-agent }}
38 | use-public-rspm: true
39 |
40 | - uses: r-lib/actions/setup-r-dependencies@v2
41 | with:
42 | extra-packages: any::rcmdcheck, any::covr, any::devtools, any::mockery, any::testthat, any::remotes
43 | needs: check
44 |
45 | - name: Install GitHub dependencies
46 | run: |
47 | if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes")
48 | # Install FITfileR from GitHub (in Suggests, so won't fail if install fails)
49 | tryCatch({
50 | remotes::install_github("grimbough/FITfileR", quiet = TRUE)
51 | }, error = function(e) {
52 | message("Note: FITfileR installation skipped (optional dependency)")
53 | })
54 | shell: Rscript {0}
55 |
56 | - name: Get package name and version
57 | id: pkg_info
58 | run: |
59 | PKG=$(awk -F': ' '/^Package:/{print $2}' DESCRIPTION | tr -d '\r')
60 | VERSION=$(awk -F': ' '/^Version:/{print $2}' DESCRIPTION | tr -d '\r')
61 | echo "Package: $PKG"
62 | echo "Version: $VERSION"
63 | echo "PKG=$PKG" >> "$GITHUB_OUTPUT"
64 | echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
65 | shell: bash
66 |
67 | - name: Clean workspace and build fresh package
68 | run: |
69 | # Remove any existing tarballs to ensure we test the fresh build
70 | rm -f *.tar.gz || true
71 | # Build the package from current source (skip vignettes to avoid pandoc issues)
72 | R CMD build . --no-build-vignettes
73 | # Verify the expected tarball was created
74 | EXPECTED_TARBALL="${{ steps.pkg_info.outputs.PKG }}_${{ steps.pkg_info.outputs.VERSION }}.tar.gz"
75 | if [ ! -f "$EXPECTED_TARBALL" ]; then
76 | echo "Error: Expected tarball $EXPECTED_TARBALL was not created"
77 | ls -la *.tar.gz || echo "No .tar.gz files found"
78 | exit 1
79 | fi
80 | echo "Successfully built: $EXPECTED_TARBALL"
81 | echo "TARBALL_NAME=$EXPECTED_TARBALL" >> "$GITHUB_OUTPUT"
82 | id: build_package
83 | shell: bash
84 |
85 | - name: Run R CMD check on tarball (CRAN-like check)
86 | run: |
87 | R CMD check ${{ steps.build_package.outputs.TARBALL_NAME }} --no-manual --no-build-vignettes --no-examples
88 | shell: bash
89 | env:
90 | _R_CHECK_CRAN_INCOMING_: false
91 | _R_CHECK_CRAN_INCOMING_REMOTE_: false
92 | _R_CHECK_FORCE_SUGGESTS_: false
93 | _R_CHECK_TESTS_NLINES_: 0
94 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
95 |
96 |
97 | - name: Generate and Upload Coverage Report to Codecov
98 | if: success() && matrix.config.os == 'ubuntu-latest' # Only run coverage on Ubuntu
99 | shell: Rscript {0}
100 | run: |
101 | pkg <- "${{ steps.pkg_info.outputs.PKG }}"
102 | tarball <- "${{ steps.build_package.outputs.TARBALL_NAME }}"
103 |
104 | # Install required packages
105 | if (!requireNamespace("devtools", quietly = TRUE)) install.packages("devtools")
106 | if (!requireNamespace("covr", quietly = TRUE)) install.packages("covr")
107 | if (!requireNamespace("testthat", quietly = TRUE)) install.packages("testthat")
108 | if (!requireNamespace("mockery", quietly = TRUE)) install.packages("mockery")
109 |
110 | message(sprintf("Installing package from tarball for coverage testing: %s", tarball))
111 | devtools::install_local(tarball, force = TRUE, quiet = TRUE, dependencies = TRUE)
112 |
113 | message(sprintf("Loading package: %s", pkg))
114 | library(pkg, character.only = TRUE)
115 |
116 | # Run coverage with error handling
117 | tryCatch({
118 | message("Executing run_coverage.R to generate and upload coverage...")
119 | source("run_coverage.R")
120 | }, error = function(e) {
121 | message(paste("Coverage failed:", e$message))
122 | message("Attempting alternative coverage method...")
123 | cov <- covr::package_coverage()
124 | if (!is.null(cov)) {
125 | covr::codecov(coverage = cov)
126 | }
127 | })
128 |
129 | - name: Upload check results
130 | if: failure()
131 | uses: actions/upload-artifact@v4
132 | with:
133 | name: ${{ matrix.config.os }}-r${{ matrix.config.r }}-results
134 | path: ${{ steps.pkg_info.outputs.PKG }}.Rcheck
--------------------------------------------------------------------------------
/paper/paper.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Athlytics: Reproducible Scientific Workflows for Cohort Analysis of Endurance Training using Local Strava Data'
3 | tags:
4 | - R
5 | - sports science
6 | - endurance
7 | - Strava
8 | - cohort analysis
9 | - reproducibility
10 | - ACWR
11 | - cardiac drift
12 | authors:
13 | - name: Zhiang He
14 | orcid: 0009-0009-0171-4578
15 | affiliation: 1
16 | affiliations:
17 | - name: Independent Researcher
18 | index: 1
19 | date: "2025-10-11"
20 | bibliography: paper.bib
21 | version: 1.0.0
22 | license: MIT
23 | ---
24 |
25 | # Summary
26 |
27 | **Athlytics** is an R package that delivers a **complete, API-free workflow for the reproducible analysis of endurance-training data**. Working entirely from **local Strava archives** (ZIP files or CSVs), it provides an integrated pipeline from import and quality control to key physiological metrics, including **acute-to-chronic workload ratios (ACWR)** [@gabbett2016], aerobic efficiency, and cardiac decoupling. Unlike API-dependent approaches, Athlytics is **offline and cohort-first**, integrating quality control, physiological guardrails, and uncertainty reporting into a single, auditable workflow.
28 |
29 | # Statement of Need
30 |
31 | Analyzing endurance training data in R often requires stitching together API clients, file parsers, and custom scripts. This creates workflows that are fragile and difficult to reproduce, especially for **cohort-scale** research. **Athlytics** addresses this gap by providing a **single, research-oriented pipeline** that works offline with local archives, offers an integrated suite of physiological models, and is built from the ground up for multi-athlete analysis. This design **reduces "glue code"** and makes cohort-scale analyses auditable and easy to reproduce.
32 |
33 | # Related Work
34 |
35 | We provide a direct feature comparison to highlight the capabilities essential for reproducible, cohort-scale research.
36 |
37 | | Feature (research-relevant) | **Athlytics** | rStrava [@rStrava] | trackeR [@trackeR_jss] | activatr [@activatr] | ACWR [@ACWR] | injurytools [@injurytools] |
38 | | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
39 | | **Offline archives; No OAuth/tokens/quotas** | ✓ | ✕ (API) | ✓ | ✓ | ✓ (tabular) | ✓ (tabular) |
40 | | **API-limited (OAuth, scope, rate-limits)** | ✕ | ✓ | ✕ | ✕ | ✕ | ✕ |
41 | | **End-to-end pipeline (Import→QC→Models→Plot)** | ✓ | ✕ | **Partial** (parsing/viz) | **Partial** (parsing/pace) | ✕ | ✕ |
42 | | **Built-in metrics (ACWR/EF/Decoupling)** | ✓ | ✕ | ✕ | ✕ | **Partial** (ACWR only) | ✕ |
43 | | **Steady-state guards & HR-coverage checks** | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ |
44 | | **Uncertainty (ACWR-EWMA confidence bands)** | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ |
45 | | **Cohort benchmarking (percentile bands)** | ✓ | ✕ | **Partial** (summaries only) | ✕ | ✕ | **Partial** (for injury/exposure) |
46 | | **Diagnostic outputs (status/reason)** | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ |
47 |
48 | Athlytics is unique in providing an API-free, end-to-end workflow that integrates a full suite of physiological models, uncertainty quantification, and built-in cohort benchmarking features essential for reproducible research.
49 |
50 | # Software Description
51 |
52 | - **Inputs & Data Model:** Reads Strava ZIP archives or `activities.csv`. Activity streams (FIT/TCX/GPX) are loaded **on demand** from the archive, optionally using `FITfileR` for FIT files [@FITfileR]. The core function `load_local_activities()` produces a standardized tibble.
53 | - **Core Metrics:** `calculate_acwr()`, `calculate_ef()`, and `calculate_decoupling()` compute key summaries with sensible, research-oriented defaults. The implementation of ACWR acknowledges its conceptual issues and is presented as a monitoring tool [@impellizzeri2020acwr].
54 | - **Uncertainty Quantification:** Provides confidence intervals for EWMA-based ACWR using a moving-block bootstrap [@kunsch1989; @politis1994], a key feature for research applications.
55 | - **Cohort Benchmarking:** `cohort_reference()` computes percentile bands, which can be layered onto individual plots using `plot_with_reference()`.
56 | - **Plotting & Diagnostics:** Visualization functions follow a **data-first API**. Functions return **diagnostic fields** (e.g., `status`, `reason`) when inputs are insufficient, making the workflow transparent.
57 |
58 | # Example
59 |
60 | The following example demonstrates a common cohort analysis workflow: loading data from multiple athletes, calculating ACWR for each, and plotting an individual against cohort-wide reference bands.
61 |
62 | ```r
63 | library(Athlytics)
64 | library(dplyr)
65 |
66 | # 1. Load and combine data for a cohort of athletes, adding identifiers
67 | athlete1 <- load_local_activities("athlete1_export.zip") %>% mutate(athlete_id = "A1")
68 | athlete2 <- load_local_activities("athlete2_export.zip") %>% mutate(athlete_id = "A2")
69 | cohort_data <- bind_rows(athlete1, athlete2)
70 |
71 | # 2. Calculate ACWR across the entire cohort using a modern dplyr workflow
72 | cohort_acwr <- cohort_data %>%
73 | group_by(athlete_id) %>%
74 | group_modify(~ calculate_acwr(.x, load_metric = "duration_mins")) %>%
75 | ungroup()
76 |
77 | # 3. Generate cohort-wide percentile reference bands
78 | reference_bands <- cohort_reference(cohort_acwr, metric = "acwr_smooth")
79 |
80 | # 4. Plot an individual's data against the cohort reference
81 | individual_acwr <- cohort_acwr %>% filter(athlete_id == "A1")
82 | plot_with_reference(individual = individual_acwr, reference = reference_bands)
83 | ```
84 |
85 | # Acknowledgements
86 |
87 | The author acknowledges the helpful feedback from the pyOpenSci community, and the constructive suggestions provided by Professors Benjamin S. Baumer and Iztok Fister Jr., as well as the developers of the referenced R packages.
88 |
89 | # References
90 |
--------------------------------------------------------------------------------
/tests/testthat/test-plot-ef-comprehensive.R:
--------------------------------------------------------------------------------
1 | # Comprehensive test for plot_ef.R to boost coverage
2 |
3 | test_that("plot_ef handles data calculation from activities", {
4 | # Create mock activities data
5 | mock_activities <- data.frame(
6 | date = seq(Sys.Date() - 100, Sys.Date(), by = "7 days"),
7 | type = rep("Run", 15),
8 | moving_time = rep(2400, 15),
9 | distance = rep(8000, 15),
10 | average_heartrate = seq(140, 160, length.out = 15),
11 | average_watts = rep(200, 15),
12 | filename = rep(NA, 15),
13 | stringsAsFactors = FALSE
14 | )
15 |
16 | # Test plot_ef calculating EF from activities data
17 | p <- plot_ef(data = mock_activities,
18 | activity_type = "Run",
19 | ef_metric = "pace_hr")
20 | expect_s3_class(p, "ggplot")
21 |
22 | # Test with different parameters
23 | p2 <- plot_ef(data = mock_activities,
24 | activity_type = "Run",
25 | ef_metric = "pace_hr",
26 | start_date = Sys.Date() - 50,
27 | end_date = Sys.Date(),
28 | min_duration_mins = 15)
29 | expect_s3_class(p2, "ggplot")
30 | })
31 |
32 | test_that("plot_ef handles different smoothing methods", {
33 | data("athlytics_sample_ef")
34 |
35 | # Test different smoothing methods
36 | p_loess <- plot_ef(athlytics_sample_ef, smoothing_method = "loess")
37 | expect_s3_class(p_loess, "ggplot")
38 |
39 | p_lm <- plot_ef(athlytics_sample_ef, smoothing_method = "lm")
40 | expect_s3_class(p_lm, "ggplot")
41 |
42 | p_gam <- plot_ef(athlytics_sample_ef, smoothing_method = "gam")
43 | expect_s3_class(p_gam, "ggplot")
44 | })
45 |
46 | test_that("plot_ef handles different activity type combinations", {
47 | data("athlytics_sample_ef")
48 |
49 | # Test with single activity type
50 | p_run <- plot_ef(athlytics_sample_ef, activity_type = "Run")
51 | expect_s3_class(p_run, "ggplot")
52 |
53 | # Test with multiple activity types
54 | p_multi <- plot_ef(athlytics_sample_ef, activity_type = c("Run", "Ride"))
55 | expect_s3_class(p_multi, "ggplot")
56 |
57 | # Test with activity type not in data
58 | p_none <- plot_ef(athlytics_sample_ef, activity_type = "Swim")
59 | expect_s3_class(p_none, "ggplot")
60 | })
61 |
62 | test_that("plot_ef handles date filtering", {
63 | data("athlytics_sample_ef")
64 |
65 | # Test with date range
66 | p_date <- plot_ef(athlytics_sample_ef,
67 | start_date = Sys.Date() - 30,
68 | end_date = Sys.Date())
69 | expect_s3_class(p_date, "ggplot")
70 |
71 | # Test with only start_date
72 | p_start <- plot_ef(athlytics_sample_ef,
73 | start_date = Sys.Date() - 30)
74 | expect_s3_class(p_start, "ggplot")
75 |
76 | # Test with only end_date
77 | p_end <- plot_ef(athlytics_sample_ef,
78 | end_date = Sys.Date())
79 | expect_s3_class(p_end, "ggplot")
80 | })
81 |
82 | test_that("plot_ef handles different ef_metric values", {
83 | data("athlytics_sample_ef")
84 |
85 | # Test with pace_hr metric
86 | p_pace <- plot_ef(athlytics_sample_ef, ef_metric = "pace_hr")
87 | expect_s3_class(p_pace, "ggplot")
88 |
89 | # Test with power_hr metric
90 | p_power <- plot_ef(athlytics_sample_ef, ef_metric = "power_hr")
91 | expect_s3_class(p_power, "ggplot")
92 | })
93 |
94 | test_that("plot_ef handles trend line options", {
95 | data("athlytics_sample_ef")
96 |
97 | # Test with trend line
98 | p_trend <- plot_ef(athlytics_sample_ef, add_trend_line = TRUE)
99 | expect_s3_class(p_trend, "ggplot")
100 |
101 | # Test without trend line
102 | p_no_trend <- plot_ef(athlytics_sample_ef, add_trend_line = FALSE)
103 | expect_s3_class(p_no_trend, "ggplot")
104 | })
105 |
106 | test_that("plot_ef handles data with different structures", {
107 | # Test with minimal required columns
108 | minimal_data <- data.frame(
109 | date = c(Sys.Date(), Sys.Date() - 1),
110 | activity_type = c("Run", "Run"),
111 | ef_value = c(0.02, 0.021)
112 | )
113 |
114 | p_minimal <- plot_ef(minimal_data)
115 | expect_s3_class(p_minimal, "ggplot")
116 |
117 | # Test with extra columns
118 | extra_data <- data.frame(
119 | date = c(Sys.Date(), Sys.Date() - 1),
120 | activity_type = c("Run", "Run"),
121 | ef_value = c(0.02, 0.021),
122 | extra_col = c("A", "B")
123 | )
124 |
125 | p_extra <- plot_ef(extra_data)
126 | expect_s3_class(p_extra, "ggplot")
127 | })
128 |
129 | test_that("plot_ef handles edge cases with data", {
130 | # Test with single row
131 | single_row <- data.frame(
132 | date = Sys.Date(),
133 | activity_type = "Run",
134 | ef_value = 0.02
135 | )
136 |
137 | p_single <- plot_ef(single_row)
138 | expect_s3_class(p_single, "ggplot")
139 |
140 | # Test with all NA ef_values
141 | na_data <- data.frame(
142 | date = c(Sys.Date(), Sys.Date() - 1),
143 | activity_type = c("Run", "Run"),
144 | ef_value = c(NA, NA)
145 | )
146 |
147 | p_na <- plot_ef(na_data)
148 | expect_s3_class(p_na, "ggplot")
149 | })
150 |
151 | test_that("plot_ef handles parameter combinations", {
152 | data("athlytics_sample_ef")
153 |
154 | # Test multiple parameters together
155 | p_combo <- plot_ef(athlytics_sample_ef,
156 | activity_type = "Run",
157 | ef_metric = "pace_hr",
158 | add_trend_line = TRUE,
159 | smoothing_method = "loess",
160 | start_date = Sys.Date() - 30,
161 | end_date = Sys.Date(),
162 | min_duration_mins = 15)
163 | expect_s3_class(p_combo, "ggplot")
164 | })
165 |
--------------------------------------------------------------------------------
/tests/testthat/test-calculate-ef-extended.R:
--------------------------------------------------------------------------------
1 | # Extended tests for calculate_ef to improve coverage
2 |
3 | library(testthat)
4 | library(Athlytics)
5 |
6 | create_test_activities <- function(n = 30, with_hr = TRUE, with_power = FALSE, with_speed = TRUE) {
7 | data.frame(
8 | id = 1:n,
9 | name = paste("Activity", 1:n),
10 | type = sample(c("Run", "Ride"), n, replace = TRUE),
11 | date = seq(Sys.Date() - n, Sys.Date() - 1, by = "day"),
12 | distance = runif(n, 5000, 15000),
13 | moving_time = runif(n, 1800, 5400),
14 | average_heartrate = if(with_hr) runif(n, 130, 170) else rep(NA_real_, n),
15 | average_watts = if(with_power) runif(n, 180, 250) else rep(NA_real_, n),
16 | average_speed = if(with_speed) runif(n, 2.5, 4.5) else rep(NA_real_, n),
17 | stringsAsFactors = FALSE
18 | )
19 | }
20 |
21 | test_that("calculate_ef filters by activity type correctly", {
22 | activities <- create_test_activities(50)
23 | activities$type <- rep(c("Run", "Ride", "Run", "Swim", "Run"), 10)
24 |
25 | ef_runs <- calculate_ef(activities, activity_type = "Run", ef_metric = "pace_hr")
26 | expect_true(all(ef_runs$activity_type == "Run"))
27 |
28 | ef_rides <- calculate_ef(activities, activity_type = "Ride", ef_metric = "power_hr")
29 | expect_true(all(ef_rides$activity_type == "Ride"))
30 | })
31 |
32 | test_that("calculate_ef handles missing heart rate data", {
33 | activities <- create_test_activities(20, with_hr = FALSE)
34 |
35 | ef_result <- calculate_ef(activities, ef_metric = "pace_hr")
36 |
37 | # Should return data frame even if all HR is missing
38 | expect_s3_class(ef_result, "data.frame")
39 | })
40 |
41 | test_that("calculate_ef handles missing power data", {
42 | activities <- create_test_activities(20, with_power = FALSE)
43 |
44 | ef_result <- calculate_ef(activities, ef_metric = "power_hr", activity_type = "Ride")
45 |
46 | expect_s3_class(ef_result, "data.frame")
47 | })
48 |
49 | test_that("calculate_ef handles missing speed data", {
50 | activities <- create_test_activities(20, with_speed = FALSE)
51 |
52 | ef_result <- calculate_ef(activities, ef_metric = "pace_hr")
53 |
54 | expect_s3_class(ef_result, "data.frame")
55 | })
56 |
57 | test_that("calculate_ef calculates pace/hr correctly", {
58 | activities <- data.frame(
59 | id = 1:5,
60 | name = paste("Run", 1:5),
61 | type = "Run",
62 | date = seq(Sys.Date() - 5, Sys.Date() - 1, by = "day"),
63 | distance = c(10000, 10000, 10000, 10000, 10000),
64 | moving_time = c(3000, 3000, 3000, 3000, 3000),
65 | average_heartrate = c(150, 150, 150, 150, 150),
66 | average_speed = c(3.33, 3.33, 3.33, 3.33, 3.33),
67 | stringsAsFactors = FALSE
68 | )
69 |
70 | ef_result <- calculate_ef(activities, ef_metric = "pace_hr")
71 |
72 | expect_s3_class(ef_result, "data.frame")
73 | expect_true("ef_value" %in% names(ef_result))
74 | expect_equal(nrow(ef_result), 5)
75 | })
76 |
77 | test_that("calculate_ef calculates power/hr correctly", {
78 | activities <- data.frame(
79 | id = 1:5,
80 | name = paste("Ride", 1:5),
81 | type = "Ride",
82 | date = seq(Sys.Date() - 5, Sys.Date() - 1, by = "day"),
83 | distance = c(20000, 20000, 20000, 20000, 20000),
84 | moving_time = c(3000, 3000, 3000, 3000, 3000),
85 | average_heartrate = c(140, 140, 140, 140, 140),
86 | average_watts = c(200, 200, 200, 200, 200),
87 | stringsAsFactors = FALSE
88 | )
89 |
90 | ef_result <- calculate_ef(activities, ef_metric = "power_hr", activity_type = "Ride")
91 |
92 | expect_s3_class(ef_result, "data.frame")
93 | expect_true("ef_value" %in% names(ef_result))
94 | expect_equal(nrow(ef_result), 5)
95 | })
96 |
97 | test_that("calculate_ef handles date range filtering", {
98 | activities <- create_test_activities(60)
99 |
100 | start_date <- Sys.Date() - 30
101 | end_date <- Sys.Date() - 10
102 |
103 | ef_result <- calculate_ef(
104 | activities,
105 | ef_metric = "pace_hr",
106 | start_date = start_date,
107 | end_date = end_date
108 | )
109 |
110 | expect_s3_class(ef_result, "data.frame")
111 | if (nrow(ef_result) > 0) {
112 | expect_true(all(ef_result$date >= start_date))
113 | expect_true(all(ef_result$date <= end_date))
114 | }
115 | })
116 |
117 | test_that("calculate_ef respects min_duration_mins", {
118 | activities <- create_test_activities(20)
119 | activities$moving_time <- c(rep(600, 10), rep(3600, 10)) # 10 min vs 60 min
120 |
121 | ef_result <- calculate_ef(
122 | activities,
123 | ef_metric = "pace_hr",
124 | min_duration_mins = 30
125 | )
126 |
127 | expect_s3_class(ef_result, "data.frame")
128 | # Should only include activities >= 30 minutes
129 | expect_true(nrow(ef_result) <= 10)
130 | })
131 |
132 | test_that("calculate_ef handles empty result set", {
133 | activities <- create_test_activities(10)
134 |
135 | # Filter to impossible date range should throw error
136 | expect_error(
137 | calculate_ef(
138 | activities,
139 | ef_metric = "pace_hr",
140 | start_date = Sys.Date() + 100,
141 | end_date = Sys.Date() + 200
142 | ),
143 | "No activities found"
144 | )
145 | })
146 |
147 | test_that("calculate_ef handles multiple activity types", {
148 | activities <- create_test_activities(40)
149 | activities$type <- rep(c("Run", "Ride"), 20)
150 |
151 | ef_result <- calculate_ef(
152 | activities,
153 | activity_type = c("Run", "Ride"),
154 | ef_metric = "pace_hr"
155 | )
156 |
157 | expect_s3_class(ef_result, "data.frame")
158 | if (nrow(ef_result) > 0) {
159 | expect_true(all(ef_result$activity_type %in% c("Run", "Ride")))
160 | }
161 | })
162 |
163 |
--------------------------------------------------------------------------------
/tests/testthat/test-absolute-real-data.R:
--------------------------------------------------------------------------------
1 | # Tests with absolute paths to ensure coverage measurement works
2 |
3 | library(testthat)
4 | library(Athlytics)
5 |
6 | # Use absolute path to ensure data is found during coverage runs
7 | base_dir <- "C:/Users/Ang/Documents/GitHub/Athlytics"
8 | csv_path <- file.path(base_dir, "export_data", "activities.csv")
9 | zip_path <- file.path(base_dir, "export_97354582.zip")
10 |
11 | test_that("load with absolute path comprehensive", {
12 | skip_if(!file.exists(csv_path), "CSV file not found")
13 |
14 | # This should definitely work and be counted in coverage
15 | activities <- load_local_activities(csv_path)
16 |
17 | expect_true(is.data.frame(activities) || inherits(activities, "tbl"))
18 | expect_gt(nrow(activities), 0)
19 | expect_true("id" %in% names(activities))
20 | expect_true("date" %in% names(activities))
21 | expect_true("type" %in% names(activities))
22 | })
23 |
24 | test_that("comprehensive activity type filtering", {
25 | skip_if(!file.exists(csv_path), "CSV file not found")
26 |
27 | all_act <- load_local_activities(csv_path)
28 |
29 | # Test each unique activity type
30 | types <- unique(all_act$type)[1:min(5, length(unique(all_act$type)))]
31 | for (atype in types) {
32 | filtered <- load_local_activities(csv_path, activity_types = atype)
33 | expect_true(is.data.frame(filtered) || inherits(filtered, "tbl"))
34 | }
35 | })
36 |
37 | test_that("comprehensive calculate with real data", {
38 | skip_if(!file.exists(csv_path), "CSV file not found")
39 |
40 | act <- load_local_activities(csv_path)
41 |
42 | # Find activity type with most data
43 | type_table <- table(act$type)
44 | main_type <- names(which.max(type_table))
45 |
46 | type_act <- act[act$type == main_type, ]
47 |
48 | if (nrow(type_act) >= 60) {
49 | # Test ACWR functions
50 | acwr1 <- calculate_acwr(type_act, activity_type = main_type, load_metric = "duration_mins")
51 | acwr2 <- calculate_acwr(type_act, activity_type = main_type, load_metric = "distance_km")
52 | acwr3 <- calculate_acwr(type_act, activity_type = main_type, acute_period = 7, chronic_period = 28)
53 |
54 | ewma <- calculate_acwr_ewma(type_act, activity_type = main_type, method = "ewma")
55 | ra <- calculate_acwr_ewma(type_act, activity_type = main_type, method = "ra")
56 |
57 | # Test exposure only if we have recent data
58 | date_range <- range(type_act$date, na.rm = TRUE)
59 | days_ago <- as.numeric(Sys.Date() - date_range[2])
60 |
61 | if (days_ago <= 100) { # Only test if data is recent enough
62 | exp1 <- tryCatch({
63 | calculate_exposure(type_act, activity_type = main_type, load_metric = "duration_mins")
64 | }, error = function(e) NULL)
65 |
66 | exp2 <- tryCatch({
67 | calculate_exposure(type_act, activity_type = main_type, load_metric = "distance_km")
68 | }, error = function(e) NULL)
69 | }
70 |
71 | expect_true(TRUE) # If we get here, all calculations worked
72 | } else {
73 | skip("Not enough activities for comprehensive testing")
74 | }
75 | })
76 |
77 | test_that("comprehensive ef with real data", {
78 | skip_if(!file.exists(csv_path), "CSV file not found")
79 |
80 | act <- load_local_activities(csv_path)
81 |
82 | # Run activities with HR
83 | runs <- act[act$type == "Run" & !is.na(act$average_heartrate), ]
84 |
85 | if (nrow(runs) >= 30) {
86 | ef1 <- calculate_ef(runs, activity_type = "Run", ef_metric = "pace_hr")
87 | ef2 <- calculate_ef(runs, activity_type = "Run", ef_metric = "pace_hr", min_duration_mins = 20)
88 | ef3 <- calculate_ef(runs, activity_type = "Run", ef_metric = "pace_hr", min_duration_mins = 40)
89 |
90 | if (nrow(runs) >= 100) {
91 | dates <- range(runs$date, na.rm = TRUE)
92 | mid <- dates[1] + as.numeric(diff(dates)) / 2
93 | ef4 <- calculate_ef(runs, activity_type = "Run", ef_metric = "pace_hr",
94 | start_date = dates[1], end_date = mid)
95 | }
96 |
97 | expect_true(TRUE)
98 | }
99 | })
100 |
101 | test_that("comprehensive plots with real calculated data", {
102 | skip_if_not_installed("ggplot2")
103 | skip_if(!file.exists(csv_path), "CSV file not found")
104 |
105 | act <- load_local_activities(csv_path)
106 |
107 | type_table <- table(act$type)
108 | main_type <- names(which.max(type_table))
109 | type_act <- act[act$type == main_type, ]
110 |
111 | if (nrow(type_act) >= 60) {
112 | # Calculate
113 | acwr <- calculate_acwr(type_act, activity_type = main_type)
114 |
115 | # Plot all variations
116 | p1 <- plot_acwr(acwr)
117 | p2 <- plot_acwr_enhanced(acwr)
118 | p3 <- plot_acwr_enhanced(acwr, highlight_zones = FALSE)
119 |
120 | expect_s3_class(p1, "gg")
121 | expect_s3_class(p2, "gg")
122 | expect_s3_class(p3, "gg")
123 | }
124 |
125 | # EF plots
126 | runs <- act[act$type == "Run" & !is.na(act$average_heartrate), ]
127 | if (nrow(runs) >= 30) {
128 | p4 <- plot_ef(runs, activity_type = "Run", ef_metric = "pace_hr")
129 | p5 <- plot_ef(runs, activity_type = "Run", ef_metric = "pace_hr", add_trend_line = FALSE)
130 | p6 <- plot_ef(runs, activity_type = "Run", ef_metric = "pace_hr", smoothing_method = "lm")
131 |
132 | expect_s3_class(p4, "gg")
133 | expect_s3_class(p5, "gg")
134 | expect_s3_class(p6, "gg")
135 | }
136 | })
137 |
138 | test_that("load ZIP with absolute path", {
139 | skip_if(!file.exists(zip_path), "ZIP file not found")
140 |
141 | activities <- load_local_activities(zip_path)
142 |
143 | expect_true(is.data.frame(activities) || inherits(activities, "tbl"))
144 | expect_gt(nrow(activities), 0)
145 | })
146 |
147 |
--------------------------------------------------------------------------------
/tests/testthat/test-calculate-ef-simple.R:
--------------------------------------------------------------------------------
1 | # Simple test for calculate_ef.R to boost coverage
2 |
3 | test_that("calculate_ef basic functionality", {
4 | # Create simple mock data
5 | mock_data <- data.frame(
6 | date = c(Sys.Date(), Sys.Date() - 1, Sys.Date() - 2),
7 | type = c("Run", "Run", "Ride"),
8 | moving_time = c(2400, 1800, 3600),
9 | distance = c(8000, 6000, 20000),
10 | average_heartrate = c(150, 160, 140),
11 | average_watts = c(0, 0, 200),
12 | weighted_average_watts = c(0, 0, 220),
13 | filename = c(NA, NA, NA),
14 | stringsAsFactors = FALSE
15 | )
16 |
17 | # Test basic calculation
18 | result <- calculate_ef(mock_data, quality_control = "off")
19 | expect_true(is.data.frame(result))
20 | expect_true("ef_value" %in% colnames(result))
21 | })
22 |
23 | test_that("calculate_ef parameter validation", {
24 | mock_data <- data.frame(
25 | date = Sys.Date(),
26 | type = "Run",
27 | moving_time = 2400,
28 | distance = 8000,
29 | average_heartrate = 150,
30 | average_watts = 0,
31 | filename = NA
32 | )
33 |
34 | # Test missing activities_data
35 | expect_error(calculate_ef(), "activities_data.*must be provided")
36 | expect_error(calculate_ef(NULL), "activities_data.*must be provided")
37 |
38 | # Test invalid parameters
39 | expect_error(calculate_ef(mock_data, min_duration_mins = -1), "non-negative")
40 | expect_error(calculate_ef(mock_data, min_steady_minutes = -1), "non-negative")
41 | expect_error(calculate_ef(mock_data, steady_cv_threshold = 0), "between 0 and 1")
42 | expect_error(calculate_ef(mock_data, min_hr_coverage = 0), "between 0 and 1")
43 | })
44 |
45 | test_that("calculate_ef handles different metrics", {
46 | mock_data <- data.frame(
47 | date = c(Sys.Date(), Sys.Date() - 1),
48 | type = c("Run", "Ride"),
49 | moving_time = c(2400, 3600),
50 | distance = c(8000, 20000),
51 | average_heartrate = c(150, 140),
52 | average_watts = c(0, 200),
53 | weighted_average_watts = c(0, 220),
54 | filename = c(NA, NA),
55 | stringsAsFactors = FALSE
56 | )
57 |
58 | # Test pace_hr metric
59 | result_pace <- calculate_ef(mock_data, activity_type = "Run", ef_metric = "pace_hr", quality_control = "off")
60 | expect_true(is.data.frame(result_pace))
61 |
62 | # Test power_hr metric
63 | result_power <- calculate_ef(mock_data, activity_type = "Ride", ef_metric = "power_hr", quality_control = "off")
64 | expect_true(is.data.frame(result_power))
65 | })
66 |
67 | test_that("calculate_ef handles data quality issues", {
68 | # Test with missing HR
69 | mock_no_hr <- data.frame(
70 | date = Sys.Date(),
71 | type = "Run",
72 | moving_time = 2400,
73 | distance = 8000,
74 | average_heartrate = NA,
75 | average_watts = 0,
76 | filename = NA
77 | )
78 |
79 | result <- calculate_ef(mock_no_hr, quality_control = "off")
80 | expect_true(is.data.frame(result))
81 |
82 | # Test with zero HR
83 | mock_zero_hr <- data.frame(
84 | date = Sys.Date(),
85 | type = "Run",
86 | moving_time = 2400,
87 | distance = 8000,
88 | average_heartrate = 0,
89 | average_watts = 0,
90 | filename = NA
91 | )
92 |
93 | result2 <- calculate_ef(mock_zero_hr, quality_control = "off")
94 | expect_true(is.data.frame(result2))
95 |
96 | # Test with too short duration
97 | mock_short <- data.frame(
98 | date = Sys.Date(),
99 | type = "Run",
100 | moving_time = 300, # 5 minutes
101 | distance = 1000,
102 | average_heartrate = 150,
103 | average_watts = 0,
104 | filename = NA
105 | )
106 |
107 | result3 <- calculate_ef(mock_short, min_duration_mins = 20, quality_control = "off")
108 | expect_true(is.data.frame(result3))
109 | })
110 |
111 | test_that("calculate_ef handles quality control modes", {
112 | mock_data <- data.frame(
113 | date = Sys.Date(),
114 | type = "Run",
115 | moving_time = 2400,
116 | distance = 8000,
117 | average_heartrate = 150,
118 | average_watts = 0,
119 | filename = NA
120 | )
121 |
122 | # Test different quality control modes
123 | result_off <- calculate_ef(mock_data, quality_control = "off")
124 | expect_true(is.data.frame(result_off))
125 |
126 | result_flag <- calculate_ef(mock_data, quality_control = "flag")
127 | expect_true(is.data.frame(result_flag))
128 |
129 | result_filter <- calculate_ef(mock_data, quality_control = "filter")
130 | expect_true(is.data.frame(result_filter))
131 | })
132 |
133 | test_that("calculate_ef handles date filtering", {
134 | mock_data <- data.frame(
135 | date = c(Sys.Date() - 200, Sys.Date() - 100, Sys.Date() - 50, Sys.Date() - 10),
136 | type = rep("Run", 4),
137 | moving_time = rep(2400, 4),
138 | distance = rep(8000, 4),
139 | average_heartrate = rep(150, 4),
140 | average_watts = rep(0, 4),
141 | filename = rep(NA, 4),
142 | stringsAsFactors = FALSE
143 | )
144 |
145 | # Test date filtering
146 | result <- calculate_ef(mock_data,
147 | start_date = Sys.Date() - 60,
148 | end_date = Sys.Date(),
149 | quality_control = "off")
150 | expect_true(is.data.frame(result))
151 | })
152 |
153 | test_that("calculate_ef handles no activities found", {
154 | mock_old_data <- data.frame(
155 | date = Sys.Date() - 500,
156 | type = "Run",
157 | moving_time = 2400,
158 | distance = 8000,
159 | average_heartrate = 150,
160 | average_watts = 0,
161 | filename = NA
162 | )
163 |
164 | # Request activities from future dates
165 | expect_error(calculate_ef(mock_old_data,
166 | start_date = Sys.Date() + 1,
167 | end_date = Sys.Date() + 100),
168 | "No activities found")
169 | })
170 |
--------------------------------------------------------------------------------
/generate_plot_examples.R:
--------------------------------------------------------------------------------
1 | # Generate example plots for documentation
2 | # This script creates example images for all plotting functions
3 |
4 | library(Athlytics)
5 | library(ggplot2)
6 |
7 | # Create output directory
8 | dir.create("man/figures", showWarnings = FALSE, recursive = TRUE)
9 |
10 | # Set consistent plot dimensions
11 | plot_width <- 8
12 | plot_height <- 5
13 | dpi <- 150
14 |
15 | # 1. plot_acwr() example
16 | message("Generating plot_acwr example...")
17 | data("athlytics_sample_acwr")
18 | if (!is.null(athlytics_sample_acwr) && nrow(athlytics_sample_acwr) > 0) {
19 | p1 <- plot_acwr(acwr_df = athlytics_sample_acwr)
20 | ggsave("man/figures/example_plot_acwr.png", p1,
21 | width = plot_width, height = plot_height, dpi = dpi)
22 | message("✓ Saved: man/figures/example_plot_acwr.png")
23 | }
24 |
25 | # 2. plot_acwr_enhanced() example
26 | message("Generating plot_acwr_enhanced example...")
27 | if (!is.null(athlytics_sample_acwr) && nrow(athlytics_sample_acwr) > 0) {
28 | p2 <- plot_acwr_enhanced(acwr_df = athlytics_sample_acwr)
29 | ggsave("man/figures/example_plot_acwr_enhanced.png", p2,
30 | width = plot_width, height = plot_height, dpi = dpi)
31 | message("✓ Saved: man/figures/example_plot_acwr_enhanced.png")
32 | }
33 |
34 | # 3. plot_acwr_comparison() example
35 | message("Generating plot_acwr_comparison example...")
36 | if (!is.null(athlytics_sample_acwr) && nrow(athlytics_sample_acwr) > 0) {
37 | # Create EWMA version for comparison
38 | acwr_ewma <- athlytics_sample_acwr
39 | acwr_ewma$acwr_smooth <- acwr_ewma$acwr_smooth * runif(nrow(acwr_ewma), 0.95, 1.05)
40 |
41 | p3 <- plot_acwr_comparison(
42 | acwr_ra = athlytics_sample_acwr,
43 | acwr_ewma = acwr_ewma
44 | )
45 | ggsave("man/figures/example_plot_acwr_comparison.png", p3,
46 | width = plot_width, height = plot_height, dpi = dpi)
47 | message("✓ Saved: man/figures/example_plot_acwr_comparison.png")
48 | }
49 |
50 | # 4. plot_ef() example
51 | message("Generating plot_ef example...")
52 | data("athlytics_sample_ef")
53 | if (!is.null(athlytics_sample_ef) && nrow(athlytics_sample_ef) > 0) {
54 | p4 <- plot_ef(ef_df = athlytics_sample_ef)
55 | ggsave("man/figures/example_plot_ef.png", p4,
56 | width = plot_width, height = plot_height, dpi = dpi)
57 | message("✓ Saved: man/figures/example_plot_ef.png")
58 | }
59 |
60 | # 5. plot_decoupling() example
61 | message("Generating plot_decoupling example...")
62 | data("athlytics_sample_decoupling")
63 | if (!is.null(athlytics_sample_decoupling) && nrow(athlytics_sample_decoupling) > 0) {
64 | p5 <- plot_decoupling(decoupling_df = athlytics_sample_decoupling)
65 | ggsave("man/figures/example_plot_decoupling.png", p5,
66 | width = plot_width, height = plot_height, dpi = dpi)
67 | message("✓ Saved: man/figures/example_plot_decoupling.png")
68 | }
69 |
70 | # 6. plot_exposure() example
71 | message("Generating plot_exposure example...")
72 | data("athlytics_sample_exposure")
73 | if (!is.null(athlytics_sample_exposure) && nrow(athlytics_sample_exposure) > 0) {
74 | p6 <- plot_exposure(exposure_df = athlytics_sample_exposure, risk_zones = TRUE)
75 | ggsave("man/figures/example_plot_exposure.png", p6,
76 | width = plot_width, height = plot_height, dpi = dpi)
77 | message("✓ Saved: man/figures/example_plot_exposure.png")
78 | }
79 |
80 | # 7. plot_pbs() example
81 | message("Generating plot_pbs example...")
82 | data("athlytics_sample_pbs")
83 | if (!is.null(athlytics_sample_pbs) && nrow(athlytics_sample_pbs) > 0) {
84 | # Prepare data
85 | sample_pbs_for_plot <- athlytics_sample_pbs
86 | if ("date" %in% names(sample_pbs_for_plot) && !"activity_date" %in% names(sample_pbs_for_plot)) {
87 | names(sample_pbs_for_plot)[names(sample_pbs_for_plot) == "date"] <- "activity_date"
88 | }
89 | if ("activity_date" %in% names(sample_pbs_for_plot)) {
90 | sample_pbs_for_plot$activity_date <- as.Date(sample_pbs_for_plot$activity_date)
91 | }
92 |
93 | req_dist_meters <- NULL
94 | if ("distance" %in% names(sample_pbs_for_plot)) {
95 | req_dist_meters <- unique(sample_pbs_for_plot$distance)
96 | } else if ("distance_target_m" %in% names(sample_pbs_for_plot)) {
97 | req_dist_meters <- unique(sample_pbs_for_plot$distance_target_m)
98 | }
99 |
100 | if (!is.null(req_dist_meters) && length(req_dist_meters) > 0) {
101 | p7 <- plot_pbs(pbs_df = sample_pbs_for_plot,
102 | activity_type = "Run",
103 | distance_meters = req_dist_meters)
104 | ggsave("man/figures/example_plot_pbs.png", p7,
105 | width = plot_width, height = plot_height * 1.5, dpi = dpi)
106 | message("✓ Saved: man/figures/example_plot_pbs.png")
107 | }
108 | }
109 |
110 | # 8. plot_with_reference() example
111 | message("Generating plot_with_reference example...")
112 | if (!is.null(athlytics_sample_acwr) && nrow(athlytics_sample_acwr) > 0) {
113 | # Create a simple reference band
114 | reference_data <- data.frame(
115 | lower = 0.8,
116 | upper = 1.3,
117 | label = "Sweet Spot"
118 | )
119 |
120 | p8 <- ggplot(athlytics_sample_acwr, aes(x = date, y = acwr_smooth)) +
121 | geom_line(color = athlytics_palette_nature()[1], linewidth = 1.2) +
122 | geom_hline(yintercept = c(0.8, 1.3), linetype = "dashed", alpha = 0.5) +
123 | labs(
124 | title = "Individual Metric with Cohort Reference",
125 | subtitle = "Example showing reference bands",
126 | x = "Date",
127 | y = "ACWR"
128 | ) +
129 | theme_athlytics()
130 |
131 | ggsave("man/figures/example_plot_with_reference.png", p8,
132 | width = plot_width, height = plot_height, dpi = dpi)
133 | message("✓ Saved: man/figures/example_plot_with_reference.png")
134 | }
135 |
136 | message("\n✅ All example plots generated successfully!")
137 | message("📁 Location: man/figures/")
138 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official email address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | ang@hezhiang.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.1, available at
119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder][mozilla coc].
123 |
124 | For answers to common questions about this code of conduct, see the FAQ at
125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at
126 | [https://www.contributor-covenant.org/translations][translations].
127 |
128 | [homepage]: https://www.contributor-covenant.org
129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130 | [mozilla coc]: https://github.com/mozilla/diversity
131 | [faq]: https://www.contributor-covenant.org/faq
132 | [translations]: https://www.contributor-covenant.org/translations
133 |
--------------------------------------------------------------------------------
/tests/testthat/test-parse-activity-file-stream.R:
--------------------------------------------------------------------------------
1 | # Test for parse_activity_file.R to boost coverage
2 |
3 | test_that("parse_activity_file handles different file types", {
4 | # Test with FIT files
5 | result1 <- Athlytics:::parse_activity_file("test.fit")
6 | expect_null(result1)
7 |
8 | # Test with TCX files
9 | result2 <- Athlytics:::parse_activity_file("test.tcx")
10 | expect_null(result2)
11 |
12 | # Test with GPX files
13 | result3 <- Athlytics:::parse_activity_file("test.gpx")
14 | expect_null(result3)
15 |
16 | # Test with uppercase extensions
17 | result4 <- Athlytics:::parse_activity_file("test.FIT")
18 | expect_null(result4)
19 |
20 | result5 <- Athlytics:::parse_activity_file("test.TCX")
21 | expect_null(result5)
22 |
23 | result6 <- Athlytics:::parse_activity_file("test.GPX")
24 | expect_null(result6)
25 | })
26 |
27 | test_that("parse_activity_file handles compressed files", {
28 | # Test with .gz extension
29 | result1 <- Athlytics:::parse_activity_file("test.fit.gz")
30 | expect_null(result1)
31 |
32 | # Test with .GZ extension (case insensitive)
33 | result2 <- Athlytics:::parse_activity_file("test.fit.GZ")
34 | expect_null(result2)
35 |
36 | # Test with .gz in middle of filename
37 | result3 <- Athlytics:::parse_activity_file("test.gz.fit")
38 | expect_null(result3)
39 | })
40 |
41 | test_that("parse_activity_file handles unsupported file types", {
42 | # Test with unsupported extension
43 | result1 <- Athlytics:::parse_activity_file("test.txt")
44 | expect_null(result1)
45 |
46 | # Test with no extension
47 | result2 <- Athlytics:::parse_activity_file("test")
48 | expect_null(result2)
49 |
50 | # Test with multiple extensions
51 | result3 <- Athlytics:::parse_activity_file("test.fit.backup")
52 | expect_null(result3)
53 | })
54 |
55 | test_that("parse_activity_file handles error conditions", {
56 | # Test with NULL file_path (should error)
57 | expect_error(Athlytics:::parse_activity_file(NULL))
58 |
59 | # Test with empty string
60 | result2 <- Athlytics:::parse_activity_file("")
61 | expect_null(result2)
62 |
63 | # Test with NA (should error)
64 | expect_error(Athlytics:::parse_activity_file(NA))
65 | })
66 |
67 | test_that("parse_activity_file handles export_dir parameter", {
68 | # Test with NULL export_dir
69 | result1 <- Athlytics:::parse_activity_file("test.fit", export_dir = NULL)
70 | expect_null(result1)
71 |
72 | # Test with empty export_dir
73 | result2 <- Athlytics:::parse_activity_file("test.fit", export_dir = "")
74 | expect_null(result2)
75 |
76 | # Test with nonexistent export_dir
77 | result3 <- Athlytics:::parse_activity_file("test.fit", export_dir = "/nonexistent/dir")
78 | expect_null(result3)
79 | })
80 |
81 | test_that("parse_activity_file handles file path edge cases", {
82 | # Test with path containing spaces
83 | result1 <- Athlytics:::parse_activity_file("test file.fit")
84 | expect_null(result1)
85 |
86 | # Test with path containing special characters
87 | result2 <- Athlytics:::parse_activity_file("test-file.fit")
88 | expect_null(result2)
89 |
90 | # Test with very long path
91 | long_path <- paste(rep("a", 200), collapse = "")
92 | result3 <- Athlytics:::parse_activity_file(paste0(long_path, ".fit"))
93 | expect_null(result3)
94 | })
95 |
96 | test_that("parse_activity_file handles compression errors", {
97 | # Test with corrupted .gz file (simulated by nonexistent file)
98 | result1 <- Athlytics:::parse_activity_file("corrupted.fit.gz")
99 | expect_null(result1)
100 |
101 | # Test with .gz file that's not actually compressed
102 | result2 <- Athlytics:::parse_activity_file("notcompressed.fit.gz")
103 | expect_null(result2)
104 | })
105 |
106 | test_that("parse_activity_file handles file parsing errors", {
107 | # Test with empty file
108 | empty_file <- tempfile(fileext = ".fit")
109 | file.create(empty_file)
110 | result1 <- Athlytics:::parse_activity_file(empty_file)
111 | expect_null(result1)
112 | unlink(empty_file)
113 |
114 | # Test with corrupted file
115 | corrupted_file <- tempfile(fileext = ".fit")
116 | writeLines("corrupted data", corrupted_file)
117 | result2 <- Athlytics:::parse_activity_file(corrupted_file)
118 | expect_null(result2)
119 | unlink(corrupted_file)
120 | })
121 |
122 | test_that("parse_activity_file handles temporary file cleanup", {
123 | # Test that temporary files are cleaned up properly
124 | # This is more of an integration test
125 | result <- Athlytics:::parse_activity_file("test.fit.gz")
126 | expect_null(result)
127 |
128 | # Check that no temp files are left behind
129 | temp_files <- list.files(tempdir(), pattern = ".*\\.fit$", full.names = TRUE)
130 | # Should be empty or contain only files from other tests
131 | })
132 |
133 | test_that("parse_activity_file handles different file formats", {
134 | # Test with mixed case file extensions
135 | result1 <- Athlytics:::parse_activity_file("test.FiT")
136 | expect_null(result1)
137 |
138 | result2 <- Athlytics:::parse_activity_file("test.TcX")
139 | expect_null(result2)
140 |
141 | result3 <- Athlytics:::parse_activity_file("test.GpX")
142 | expect_null(result3)
143 |
144 | # Test with file extensions in different positions
145 | result4 <- Athlytics:::parse_activity_file("test.fit.gz")
146 | expect_null(result4)
147 |
148 | result5 <- Athlytics:::parse_activity_file("test.tcx.gz")
149 | expect_null(result5)
150 |
151 | result6 <- Athlytics:::parse_activity_file("test.gpx.gz")
152 | expect_null(result6)
153 | })
154 |
155 | test_that("parse_activity_file handles path resolution", {
156 | # Test with absolute path
157 | result1 <- Athlytics:::parse_activity_file("nonexistent.fit")
158 | expect_null(result1)
159 |
160 | # Test with export_dir parameter
161 | result2 <- Athlytics:::parse_activity_file("nonexistent.fit", export_dir = ".")
162 | expect_null(result2)
163 |
164 | # Test with relative path resolution
165 | result3 <- Athlytics:::parse_activity_file("nonexistent.fit", export_dir = "/tmp")
166 | expect_null(result3)
167 | })
168 |
--------------------------------------------------------------------------------