- Part 1. Work on functionality
- Step 0. Do it with base ggplot2 can become problem statement later
- Step 0.b Write like-to-have code code chunk option eval = F
- Step 1. Write compute group function and test
- Step 2. Pass to ggproto object
- Step 3. Write user-facing geom_pointmeans() function
- Step 4. Test it out enjoy! (possibly basis of examples and tests)
- Step 5. Write messages/warnings etc in the function
- Part II. Packaging and documentation π§
β
- Phase 1. Minimal working
package
- Bit A. Created package archetecture, running
devtools::create(".")
in interactive session. π§ β - Bit B. Added roxygen skeleton? π§ β
- Bit C. Managed dependencies ? π§ β
- Bit D. Moved functions R folder? π§ β
- Bit E. Run
devtools::check()
and addressed errors. π§ β - Bit F. Build package π§ β
- Bit G. Write traditional README that uses built package (also serves as a test of build. π§ β
- Bit H. Chosen a license? π§ β
- Bit I. Add lifecycle badge (experimental) π§ β
- Bit A. Created package archetecture, running
- Phase 2: Listen & iterate π§ β
- Phase 3: Let things settle
- Phase 4. Promote to wider audienceβ¦
- Phase 5: Harden/commit
- Phase 1. Minimal working
package
- Appendix: Reports, Environment
Letβs build a vanilla ggplot2 Stat and use it in an extension function.
tasks_df <- tibble::tribble(~my_task, ~time_minutes,
"eat breakfast", 15,
"brush teeth", 3,
"run", 30,
"draft email", 5
)
library(tidyverse)
#> ββ Attaching core tidyverse packages βββββββββββββββββββ tidyverse 2.0.0.9000 ββ
#> β dplyr 1.1.0 β readr 2.1.4
#> β forcats 1.0.0 β stringr 1.5.0
#> β ggplot2 3.4.4.9000 β tibble 3.2.1
#> β lubridate 1.9.2 β tidyr 1.3.0
#> β purrr 1.0.1
#> ββ Conflicts ββββββββββββββββββββββββββββββββββββββββββ tidyverse_conflicts() ββ
#> β dplyr::filter() masks stats::filter()
#> β dplyr::lag() masks stats::lag()
#> βΉ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
ggplot(data = cars) +
aes(speed, dist) +
geom_point() +
geom_pointmeans(size = 8)
reference: https://evamaerey.github.io/mytidytuesday/2022-01-03-easy-geom-recipes/easy_geom_recipes.html
readme2pkg::chunk_to_r("compute_panel_timebox")
compute_panel_timebox <- function(data, scales, break_time = 3, start_hour = 9){
start_time_scalar <- start_hour * 60
data |>
dplyr::mutate(full_time = .data$minutes + break_time) |>
dplyr::mutate(end_time_minutes = cumsum(.data$full_time)) |>
dplyr::mutate(start_time_minutes = lag(.data$end_time_minutes) |>
tidyr::replace_na(0)) |>
dplyr::mutate(clock_start = Sys.Date() +
minutes(start_time_minutes) + hours(start_hour)) |>
dplyr::mutate(clock_end = Sys.Date() +
minutes(end_time_minutes) + hours(start_hour)) |>
dplyr::mutate(y = start_time_minutes) |>
dplyr::mutate(x = 0) |>
dplyr::mutate(ymin = start_time_minutes,
ymax = end_time_minutes) |>
dplyr::mutate(xmin = 0,
xmax = 1) |>
dplyr::mutate(task_and_minutes = paste(task, minutes)) |>
dplyr::mutate(clock_hour_minute =
hms::as_hms(clock_start) %>% stringr::str_remove("...$"))
}
StatTimebox <- ggplot2::ggproto(`_class` = "StatTimebox",
`_inherit` = ggplot2::Stat,
compute_panel = compute_panel_timebox,
required_aes = c("task", "minutes"),
default_aes = ggplot2::aes(label =
ggplot2::after_stat(task_and_minutes)))
readme2pkg::chunk_to_r("stamp_workday")
stamp_workday <- function(...){
annotate(geom = "rect",
ymin = 0*60, ymax = -8*60,
xmin = -.45, xmax = .45, ... )
}
readme2pkg::chunk_to_r("stamp_currenttime")
stamp_currenttime <- function(..., color = "magenta", alpha = .5, start_hour = 9){
current_time <- (Sys.time() |> hms::as_hms() |> as.numeric()) / 60 - start_hour*60
ggplot2::geom_hline(yintercept = current_time, color = color, alpha = alpha, ...)
}
tasks_df |>
select(task = my_task, minutes = time_minutes) |>
compute_panel_timebox()
#> # A tibble: 4 Γ 15
#> task minutes full_time end_time_minutes start_time_minutes
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 eat breakfast 15 18 18 0
#> 2 brush teeth 3 6 24 18
#> 3 run 30 33 57 24
#> 4 draft email 5 8 65 57
#> # βΉ 10 more variables: clock_start <dttm>, clock_end <dttm>, y <dbl>, x <dbl>,
#> # ymin <dbl>, ymax <dbl>, xmin <dbl>, xmax <dbl>, task_and_minutes <chr>,
#> # clock_hour_minute <chr>
ggplot(data = tasks_df) +
aes(task = my_task, minutes = time_minutes) +
layer(stat = StatTimebox,
geom = GeomRect,
position = "identity") +
layer(stat = StatTimebox,
geom = GeomText,
position = "identity",
params = list(vjust = 1.1, hjust = -.1)) +
coord_trans(y = "reverse")
readme2pkg::chunk_to_r("stat_timebox")
stat_timebox <- function(mapping = NULL,
data = NULL,
geom = GeomRect,
position = "identity",
na.rm = FALSE,
show.legend = NA,
inherit.aes = TRUE, ...) {
ggplot2::layer(
stat = StatTimebox, # proto object from step 2
geom = geom, # inherit other behavior
data = data,
mapping = mapping,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...)
)
}
readme2pkg::chunk_to_r("coord_transfixed")
CoordTransfixed <- ggplot2::ggproto(`_class` = "CoordTransfixed",
`_inherit` = ggplot2::CoordTrans)
CoordTransfixed$aspect <- function (self, ranges){ # inner function of CoordFixed$aspect
diff(ranges$y.range)/diff(ranges$x.range) * self$ratio
}
# borrowing from coord_trans set up and adding ratio argument
coord_transfixed <- function (x = "identity", y = "identity", xlim = NULL, ylim = NULL,
limx = lifecycle::deprecated(), limy = lifecycle::deprecated(), clip = "on", expand = TRUE, ratio = 1)
{
if (lifecycle::is_present(limx)) {
deprecate_warn0("3.3.0", "coord_trans(limx)", "coord_trans(xlim)")
xlim <- limx
}
if (lifecycle::is_present(limy)) {
deprecate_warn0("3.3.0", "coord_trans(limy)", "coord_trans(ylim)")
ylim <- limy
}
ggplot2:::check_coord_limits(xlim)
ggplot2:::check_coord_limits(ylim)
if (is.character(x))
x <- scales::as.transform(x)
if (is.character(y))
y <- scales::as.transform(y)
ggplot2::ggproto(NULL, CoordTransfixed, trans = list(x = x, y = y),
limits = list(x = xlim, y = ylim), expand = expand,
clip = clip, ratio = ratio)
}
coord_canvas <- function(ratio = 1, ...){
coord_transfixed(y = "reverse", ratio = ratio, ...)
}
coord_timebox <- function(ratio = 1/60, ...){
coord_canvas(ratio = ratio, ...)
}
ggplot(data = tasks_df) +
aes(task = my_task,
minutes = time_minutes) +
# stamp_workday(alpha = .25, fill = "magenta") +
stat_timebox(color = "grey35", alpha = .25, fill = "cadetblue") +
stat_timebox(geom = "text", color = "grey30",
vjust = 1.2, hjust = -0.1, nudge_x = .5,
lineheight = .7) +
# stamp_currenttime() +
stat_timebox(geom = "text", color = "grey30",
vjust = 1.2, hjust = 1.1,
lineheight = .7,
aes(label = after_stat(clock_hour_minute)))
#> Warning in stat_timebox(geom = "text", color = "grey30", vjust = 1.2, hjust =
#> -0.1, : Ignoring unknown parameters: `nudge_x`
last_plot() +
coord_canvas(ratio = 1/30)
last_plot() +
coord_timebox() +
scale_x_continuous(expand = expansion(mult = c(.2, 0)))
#> Coordinate system already present. Adding new coordinate system, which will
#> replace the existing one.
last_plot() +
theme_void()
devtools::create(".")
Use a roxygen skeleton for auto documentation and making sure proposed functions are exported. Generally, early on, I donβt do much (anything) in terms of filling in the skeleton for documentation, because things may change.
Package dependencies managed, i.e.Β depend::function()
in proposed
functions and declared in the DESCRIPTION
usethis::use_package("ggplot2")
usethis::use_package("dplyr")
usethis::use_package("stringr")
usethis::use_package("hms")
Use new {readme2pkg} function to do this from readmeβ¦
library(tidyverse)
devtools::check(pkg = ".")
devtools::install(upgrade ="never")
The goal of the {ggtedious} package is to make it easy to draw posts (and to learn about package building and testing)
Install package with:
remotes::install_github("EvaMaeRey/ggvanilla")
Once functions are exported you can remove go to two colons, and when things are are really finalized, then go without colons (and rearrange your readmeβ¦)
library(ggtimebox)
library(ggplot2)
tasks_df <- tibble::tribble(~my_task, ~time_minutes,
"eat breakfast", 15,
"brush teeth", 3,
"run", 30,
"draft email", 5
)
ggplot(data = tasks_df) +
aes(task = my_task,
minutes = time_minutes) +
ggtimebox:::stat_timebox(color = "grey35", alpha = .25, fill = "cadetblue") +
ggtimebox:::stat_timebox(geom = "text", color = "grey30",
vjust = 1.2, hjust = -0.1, nudge_x = .5,
lineheight = .7) +
ggtimebox:::stat_timebox(geom = "text", color = "grey30",
vjust = 1.2, hjust = 1.1,
lineheight = .7,
aes(label = after_stat(clock_hour_minute))) +
# ggtimebox:::stamp_currenttime() +
ggtimebox:::coord_timebox() +
scale_x_continuous(expand = expansion(mult = c(.2,0)))
#> Warning in ggtimebox:::stat_timebox(geom = "text", color = "grey30", vjust =
#> 1.2, : Ignoring unknown parameters: `nudge_x`
usethis::use_mit_license()
usethis::use_lifecycle_badge("experimental")
Try to get feedback from experts on API, implementation, default decisions. Is there already work that solves this problem?
That would look like thisβ¦
library(testthat)
test_that("calc times 2 works", {
expect_equal(times_two(4), 8)
expect_equal(times_two(5), 10)
})
readme2pkg::chunk_to_tests_testthat("test_calc_times_two_works")
usethis::use_pkgdown()
pkgdown::build_site()
readLines("DESCRIPTION")