diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a3ac618..21efb73 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -29,7 +29,7 @@ jobs: R_KEEP_PKG_SOURCE: yes steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - uses: r-lib/actions/setup-pandoc@v2 diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index ea362c8..1194d5e 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -1,47 +1,41 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples on: push: branches: [main, master] + workflow_dispatch: name: pkgdown jobs: pkgdown: - runs-on: macOS-latest + runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v2 + permissions: + contents: write - - uses: r-lib/actions/setup-r@v1 + steps: + - uses: actions/checkout@v5 - - uses: r-lib/actions/setup-pandoc@v1 + - uses: r-lib/actions/setup-pandoc@v2 - - name: Query dependencies - run: | - install.packages('remotes') - saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) - writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") - shell: Rscript {0} + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true - - name: Cache R packages - uses: actions/cache@v2 + - uses: r-lib/actions/setup-r-dependencies@v2 with: - path: ${{ env.R_LIBS_USER }} - key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} - restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- - - - name: Install dependencies - run: | - remotes::install_deps(dependencies = TRUE) - remotes::install_github("ThinkR-open/thinkrtemplate") - install.packages("pkgdown", type = "binary") - shell: Rscript {0} + extra-packages: any::pkgdown, ThinkR-open/thinkrtemplate + needs: website - - name: Install package - run: R CMD INSTALL . + - name: Build site + run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + shell: Rscript {0} - - name: Deploy package - run: | - git config --local user.email "actions@github.com" - git config --local user.name "GitHub Actions" - Rscript -e 'pkgdown::deploy_to_branch(new_process = FALSE)' + - name: Deploy to GitHub pages + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4.5.0 + with: + clean: false + branch: gh-pages + folder: docs diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index 0182b4e..731cc6c 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -1,46 +1,30 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples on: push: - branches: - - master + branches: [main, master] pull_request: - branches: - - master + branches: [main, master] name: test-coverage jobs: test-coverage: - runs-on: macOS-latest + runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v2 - - - uses: r-lib/actions/setup-r@v1 - - uses: r-lib/actions/setup-pandoc@v1 - - - name: Query dependencies - run: | - install.packages('remotes') - saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) - writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") - shell: Rscript {0} + steps: + - uses: actions/checkout@v5 - - name: Cache R packages - uses: actions/cache@v2 + - uses: r-lib/actions/setup-r@v2 with: - path: ${{ env.R_LIBS_USER }} - key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} - restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- + use-public-rspm: true - - name: Install dependencies - run: | - install.packages(c("remotes")) - remotes::install_deps(dependencies = TRUE) - remotes::install_cran("covr") - shell: Rscript {0} + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::covr + needs: coverage - name: Test coverage - run: covr::codecov() + run: covr::codecov(quiet = FALSE) shell: Rscript {0} diff --git a/DESCRIPTION b/DESCRIPTION index af6527b..b1faa67 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,4 +36,4 @@ Suggests: Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.1 +RoxygenNote: 7.3.3 diff --git a/R/Image.R b/R/Image.R index a3fbadd..d7a8d96 100644 --- a/R/Image.R +++ b/R/Image.R @@ -2,14 +2,25 @@ #' #' This function returns a random image that can be passed into `renderImage` and `plotOutput`. #' -#' @return an image +#' @param width image width passed through to the rendered `` tag. +#' `NULL` (default) leaves the attribute unset (#9). +#' @param height image height, same semantics as `width` (#9). +#' @param alt `alt` attribute for accessibility. `NULL` (default) leaves +#' the attribute unset (#9). +#' +#' @return a list compatible with `shiny::renderImage()`: `src`, plus the +#' `width` / `height` / `alt` attributes when provided. #' #' @export -random_image <- function(){ +random_image <- function(width = NULL, height = NULL, alt = NULL){ l <- list.files(system.file("img", package = "shinipsum"), full.names = TRUE) img <- normalizePath(sample(l, 1)) tmpimg <- paste(tempfile(), basename(img), sep = "-") file.copy(img, tmpimg) - list(src = tmpimg) + out <- list(src = tmpimg) + if (!is.null(width)) out$width <- width + if (!is.null(height)) out$height <- height + if (!is.null(alt)) out$alt <- alt + out } diff --git a/R/Plot.R b/R/Plot.R index 217f78e..0fdeabe 100644 --- a/R/Plot.R +++ b/R/Plot.R @@ -1,8 +1,21 @@ +#' Build the categorical labels used by random_ggplot('bar', n_bars=). +#' @noRd +make_bar_labels <- function(n) { + if (n <= 26L) { + LETTERS[seq_len(n)] + } else { + sprintf("Cat%02d", seq_len(n)) + } +} + #' A Random ggplot #' #' This function returns a ggplot object, which can be passed to `renderPlot` and `plotOutput` #' #' @param type type of the geom. Can be any of "random", "point", "bar", "boxplot","col", "tile", "line", "bin2d", "contour", "density", "density_2d", "dotplot", "hex", "freqpoly", "histogram", "ribbon", "raster", "tile", "violin" and defines the geom of the ggplot. Default is "random", and chooses a random geom for you. +#' @param n_bars integer, number of bars to draw when `type == "bar"`. When +#' `NULL` (default), one of the built-in datasets is sampled; otherwise +#' a synthetic data frame with `n_bars` categories is used (#5). #' #' @importFrom ggplot2 ggplot aes geom_point geom_bar scale_color_viridis_d theme_minimal geom_boxplot labs coord_flip geom_tile geom_line facet_grid geom_col scale_fill_viridis_c #' @importFrom ggplot2 xlim ylim geom_bin2d geom_contour geom_density geom_density_2d geom_dotplot @@ -18,7 +31,8 @@ random_ggplot <- function(type = c("random", "point", "bar", "density", "density_2d", "dotplot", "hex", "freqpoly", "histogram", "ribbon", "raster", "tile", - "violin")) { + "violin"), + n_bars = NULL) { type_matched <- match.arg(type) if (type_matched == "random") { @@ -28,6 +42,27 @@ random_ggplot <- function(type = c("random", "point", "bar", type_matched <- sample( form, 1 ) } + # User asked for a specific number of bars -> short-circuit the + # builtin-dataset switch with synthetic data so we hit exactly n_bars (#5). + if (type_matched == "bar" && !is.null(n_bars)) { + stopifnot(is.numeric(n_bars), length(n_bars) == 1L, + !is.na(n_bars), n_bars >= 1L) + n_bars <- as.integer(n_bars) + df <- data.frame( + category = factor(make_bar_labels(n_bars), + levels = make_bar_labels(n_bars)), + value = sample.int(100L, n_bars, replace = TRUE) + ) + return( + ggplot2::ggplot(df) + + ggplot2::aes(category, value, fill = category) + + ggplot2::geom_col() + + ggplot2::scale_fill_viridis_d() + + ggplot2::theme_minimal() + + ggplot2::theme(legend.position = "none") + ) + } + r <- switch(as.character(type_matched), "point" = sample(0:5, 1), "bar" = sample(10:11, 1), @@ -144,7 +179,7 @@ random_ggplot <- function(type = c("random", "point", "bar", "50" = list( ggplot(datasets::women) + aes(height, weight) + - geom_line(size = 2) + + geom_line(linewidth = 2) + theme_minimal() ), "51" = list( diff --git a/R/globals.R b/R/globals.R index 9560def..5a79f87 100644 --- a/R/globals.R +++ b/R/globals.R @@ -13,7 +13,9 @@ utils::globalVariables(unique(c( "x", "y", "depth", "cut", "carat", "price", "color", "year", "level", - "z", "w" + "z", "w", + # random_ggplot('bar', n_bars=) + "category", "value" # random dygraphs # @importFrom datasets mdeaths fdeaths ldeaths nhtemp AirPassengers discoveries presidents austres diff --git a/dev/SUIVI_ISSUES.md b/dev/SUIVI_ISSUES.md new file mode 100644 index 0000000..492b58d --- /dev/null +++ b/dev/SUIVI_ISSUES.md @@ -0,0 +1,17 @@ +# Suivi — passe `fix/multiple-issues` + +| # | Type | Résumé | Fix | Test | +|---|---|---|---|---| +| #13 | bug | `random_ggplot("line")` triggered ggplot2's `size→linewidth` deprecation warning | `geom_line(size = 2)` -> `geom_line(linewidth = 2)` | `tests/testthat/test-no_deprecated_args.R` (build no warning + static sweep) | +| #9 | feat | `random_image()` couldn't propagate `width` / `height` / `alt` | new params `width = NULL, height = NULL, alt = NULL` (kept default `list(src = ...)` shape) | `tests/testthat/test-image_args.R` | +| #5 | feat | no way to control the number of bars in `random_ggplot("bar")` | new `n_bars = NULL` parameter — when set, generates a synthetic categorical data frame so the rendered plot has *exactly* that many bars | `tests/testthat/test-bar_n.R` (1 / 3 / 7 / 27 bars + invalid input) | + +## Issues envisagées mais non traitées + +| # | Pourquoi pas | +|---|---| +| #4 | "time series option" — design choice (date axis, density, faceting) à arbitrer avec mainteneur. | +| #3 | "other lorem ipsum options" — choix de corpora alternatifs (cat ipsum, hipster ipsum…), demande d'inclure de nouveaux assets. | +| #2 | "Release 0.1.0" — meta. | +| #1 | `mock_frame()` — feature, demande design. | +| #10 | `_R_CHECK_USE_CODETOOLS_=false` — dépend du contexte de test downstream. | diff --git a/man/random_ggplot.Rd b/man/random_ggplot.Rd index 07a25c6..bf2257f 100644 --- a/man/random_ggplot.Rd +++ b/man/random_ggplot.Rd @@ -7,11 +7,16 @@ random_ggplot( type = c("random", "point", "bar", "boxplot", "col", "tile", "line", "bin2d", "contour", "density", "density_2d", "dotplot", "hex", "freqpoly", "histogram", - "ribbon", "raster", "tile", "violin") + "ribbon", "raster", "tile", "violin"), + n_bars = NULL ) } \arguments{ \item{type}{type of the geom. Can be any of "random", "point", "bar", "boxplot","col", "tile", "line", "bin2d", "contour", "density", "density_2d", "dotplot", "hex", "freqpoly", "histogram", "ribbon", "raster", "tile", "violin" and defines the geom of the ggplot. Default is "random", and chooses a random geom for you.} + +\item{n_bars}{integer, number of bars to draw when \code{type == "bar"}. When +\code{NULL} (default), one of the built-in datasets is sampled; otherwise +a synthetic data frame with \code{n_bars} categories is used (#5).} } \value{ a ggplot diff --git a/man/random_image.Rd b/man/random_image.Rd index c699a5e..d150da0 100644 --- a/man/random_image.Rd +++ b/man/random_image.Rd @@ -4,10 +4,20 @@ \alias{random_image} \title{A Random Image} \usage{ -random_image() +random_image(width = NULL, height = NULL, alt = NULL) +} +\arguments{ +\item{width}{image width passed through to the rendered \verb{} tag. +\code{NULL} (default) leaves the attribute unset (#9).} + +\item{height}{image height, same semantics as \code{width} (#9).} + +\item{alt}{\code{alt} attribute for accessibility. \code{NULL} (default) leaves +the attribute unset (#9).} } \value{ -an image +a list compatible with \code{shiny::renderImage()}: \code{src}, plus the +\code{width} / \code{height} / \code{alt} attributes when provided. } \description{ This function returns a random image that can be passed into \code{renderImage} and \code{plotOutput}. diff --git a/man/shinipsum-package.Rd b/man/shinipsum-package.Rd index b0b0ca3..0fa84ce 100644 --- a/man/shinipsum-package.Rd +++ b/man/shinipsum-package.Rd @@ -6,8 +6,7 @@ \alias{shinipsum-package} \title{shinipsum: Lorem-Ipsum-like Helpers for fast Shiny Prototyping} \description{ -Prototype your shiny apps quickly with these - Lorem-Ipsum-like Helpers. +Prototype your shiny apps quickly with these Lorem-Ipsum-like Helpers. } \seealso{ Useful links: diff --git a/tests/testthat/test-bar_n.R b/tests/testthat/test-bar_n.R new file mode 100644 index 0000000..0682d5c --- /dev/null +++ b/tests/testthat/test-bar_n.R @@ -0,0 +1,33 @@ +test_that("random_ggplot('bar', n_bars=) produces exactly n bars (#5)", { + for (n in c(1L, 3L, 7L, 27L)) { + p <- random_ggplot("bar", n_bars = n) + expect_s3_class(p, "ggplot") + # Build the plot and count distinct bars in the rendered data layer. + built <- ggplot2::ggplot_build(p) + bars <- nrow(built$data[[1]]) + expect_equal(bars, n, + info = paste0("requested ", n, " bars, got ", bars)) + } +}) + +test_that("random_ggplot('bar') without n_bars keeps the legacy datasets behaviour", { + old_seed <- if (exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) { + get(".Random.seed", envir = .GlobalEnv) + } else NULL + on.exit( + if (!is.null(old_seed)) assign(".Random.seed", old_seed, envir = .GlobalEnv) + else if (exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) { + rm(list = ".Random.seed", envir = .GlobalEnv) + }, + add = TRUE + ) + set.seed(1) + p <- random_ggplot("bar") + expect_s3_class(p, "ggplot") +}) + +test_that("random_ggplot rejects nonsense n_bars (#5)", { + expect_error(random_ggplot("bar", n_bars = -1)) + expect_error(random_ggplot("bar", n_bars = "five")) + expect_error(random_ggplot("bar", n_bars = c(3, 4))) +}) diff --git a/tests/testthat/test-image_args.R b/tests/testthat/test-image_args.R new file mode 100644 index 0000000..3dfc011 --- /dev/null +++ b/tests/testthat/test-image_args.R @@ -0,0 +1,20 @@ +test_that("random_image() returns just `src` by default (regression)", { + res <- random_image() + expect_type(res, "list") + expect_named(res, "src") + expect_true(file.exists(res$src)) +}) + +test_that("random_image(width=, height=, alt=) propagates the arguments (#9)", { + res <- random_image(width = "100px", height = "80px", alt = "Yo") + expect_named(res, c("src", "width", "height", "alt")) + expect_equal(res$width, "100px") + expect_equal(res$height, "80px") + expect_equal(res$alt, "Yo") +}) + +test_that("random_image() drops NULL extras (#9)", { + res <- random_image(width = "200px") + expect_named(res, c("src", "width")) + expect_equal(res$width, "200px") +}) diff --git a/tests/testthat/test-no_deprecated_args.R b/tests/testthat/test-no_deprecated_args.R new file mode 100644 index 0000000..18a238e --- /dev/null +++ b/tests/testthat/test-no_deprecated_args.R @@ -0,0 +1,26 @@ +test_that("random_ggplot('line') does not emit ggplot2 'size' deprecation (#13)", { + # ggplot2 >= 3.4 deprecated size= for line geoms in favour of linewidth=. + # Force the line geom by passing type = 'line'; either of the two line + # variants must be deprecation-clean. + old_seed <- if (exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) { + get(".Random.seed", envir = .GlobalEnv) + } else NULL + on.exit( + if (!is.null(old_seed)) assign(".Random.seed", old_seed, envir = .GlobalEnv) + else if (exists(".Random.seed", envir = .GlobalEnv, inherits = FALSE)) { + rm(list = ".Random.seed", envir = .GlobalEnv) + }, + add = TRUE + ) + set.seed(42) + p <- random_ggplot("line") + # Building the plot is what triggers the lifecycle warning, not the + # construction. expect_no_warning() requires testthat 3.1.5+. + if (utils::packageVersion("testthat") >= "3.1.5") { + expect_no_warning(invisible(ggplot2::ggplot_build(p))) + } else { + msgs <- capture_warnings(invisible(ggplot2::ggplot_build(p))) + expect_false(any(grepl("size.*deprecated", msgs))) + } +}) +