diff --git a/ggjust.R b/ggjust.R new file mode 100755 index 0000000000000000000000000000000000000000..906bd4e2bd59d80438fec818d5f5d6756e86b83b --- /dev/null +++ b/ggjust.R @@ -0,0 +1,211 @@ +#' @title ggjust +#' @description +#' provide correct justification for text labeling, depending on the chosen angle +#' @param angle integer value of the text angle, using the same rules as in ggplot2. Positive values for counterclockwise rotation: 0 for horizontal, 90 for vertical, 180 for upside down etc. Negative values for clockwise rotation: 0 for horizontal, -90 for vertical, -180 for upside down etc. +#' @param pos where text is? Either "top", "right", "bottom" or "left" of the elements to justify from. +#' @param kind: kind of text? Either "axis" or "text". In the first case, the pos argument refers to the axis position, and in the second to annotated text (using ggplot2::annotate() or ggplot2::geom_text()). +#' @returns a list containing: $angle: the submitted angle (value potentially reduced to fit the [-360 ; 360] interval, e.g., 460 -> 100, without impact on the final angle displayed); $pos: the selected position (argument pos); $kind: the selected kind of text (argument kind); $hjust: the horizontal justification; $vjust: the vertical justification. +#' @examples +#' ggjust(angle = 45, pos = "bottom") +#' ggjust(angle = (360*2 + 45), pos = "left") +#' output <- ggjust(angle = 45, pos = "bottom") ; obs1 <- data.frame(time = 1:20, group = rep(c("CLASS_1", "CLASS_2"), times = 10), stringsAsFactors = TRUE) ; ggplot2::ggplot() + ggplot2::geom_bar(data = obs1, mapping = ggplot2::aes(x = group, y = time), stat = "identity") + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = output$angle, hjust = output$hjust, vjust = output$vjust)) +#' output <- ggjust(angle = -45, pos = "left") ; obs1 <- data.frame(time = 1:20, group = rep(c("CLASS_1", "CLASS_2"), times = 10), stringsAsFactors = TRUE) ; ggplot2::ggplot() + ggplot2::geom_bar(data = obs1, mapping = ggplot2::aes(x = group, y = time), stat = "identity") + ggplot2::theme(axis.text.y = ggplot2::element_text(angle = output$angle, hjust = output$hjust, vjust = output$vjust)) + ggplot2::coord_flip() +#' output1 <- ggjust(angle = 90, pos = "bottom") ; output2 <- ggjust(angle = -45, pos = "left") ; obs1 <- data.frame(time = 1:20, group = rep(c("CLASS_1", "CLASS_2"), times = 10), stringsAsFactors = TRUE) ; ggplot2::ggplot() + ggplot2::geom_bar(data = obs1, mapping = ggplot2::aes(x = group, y = time), stat = "identity") + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = output1$angle, hjust = output1$hjust, vjust = output1$vjust), axis.text.y = ggplot2::element_text(angle = output2$angle, hjust = output2$hjust, vjust = output2$vjust)) +#' output <- ggjust(angle = -45, pos = "left") ; obs1 <- data.frame(time = 1, km = 1, bird = "pigeon", stringsAsFactors = FALSE) ; ggplot2::ggplot(data = obs1, mapping = ggplot2::aes(x = time, y = km)) + ggplot2::geom_point() + ggplot2::geom_text(mapping = ggplot2::aes(label = bird), angle = output$angle, hjust = output$hjust, vjust = output$vjust) +#' obs1 <- data.frame(time = 1:10, km = 1:10, bird = c(NA, NA, NA, "pigeon", NA, "cat", NA, NA, NA, NA), stringsAsFactors = FALSE) ; fun_open(width = 4, height = 4) ; for(i0 in c("text", "axis")){for(i1 in c("top", "right", "bottom", "left")){for(i2 in c(0, 45, 90, 135, 180, 225, 270, 315, 360)){output <- ggjust(angle = i2, pos = i1, kind = i0) ; title <- paste0("kind: ", i0, " | pos: ", i1, " | angle = ", i2, " | hjust: ", output$hjust, " | vjust: ", output$vjust) ; if(i0 == "text"){print(ggplot2::ggplot(data = obs1, mapping = ggplot2::aes(x = time, y = km)) + ggplot2::geom_point(color = fun_gg_palette(1), alpha = 0.5) + ggplot2::ggtitle(title) + ggplot2::geom_text(mapping = ggplot2::aes(label = bird), angle = output$angle, hjust = output$hjust, vjust = output$vjust) + ggplot2::theme(title = ggplot2::element_text(size = 5)))}else{print(ggplot2::ggplot(data = obs1, mapping = ggplot2::aes(x = time, y = km)) + ggplot2::geom_point(color = fun_gg_palette(1), alpha = 0.5) + ggplot2::ggtitle(title) + ggplot2::geom_text(mapping = ggplot2::aes(label = bird)) + ggplot2::scale_x_continuous(position = ifelse(i1 == "top", "top", "bottom")) + ggplot2::scale_y_continuous(position = ifelse(i1 == "right", "right", "left")) + ggplot2::theme(title = ggplot2::element_text(size = 5), axis.text.x = if(i1 %in% c("top", "bottom")){ggplot2::element_text(angle = output$angle, hjust = output$hjust, vjust = output$vjust)}, axis.text.y = if(i1 %in% c("right", "left")){ggplot2::element_text(angle = output$angle, hjust = output$hjust, vjust = output$vjust)}))}}}} ; dev.off() +#' @importFrom utils find +#' @importFrom saferDev arg_check +#' @details +#' - justification behave differently on plot, depending whether it is used for annotayed text or for axis labelling. Indeed the latter has labelling constrained. +#' - Of note, a bug in ggplot2: vjust sometimes does not work, i.e., the same justification result is obtained whatever the value used. This is the case with angle = 90, pos = "top", kind = "axis". While everything is fine with angle = 90, pos = "bottom", kind = "axis". At least, everything seems fine for kind = "axis" and pos = c("left", "bottom"). +#' @export +ggjust <- function( + angle, + pos, + kind = "axis"){ + # DEBUGGING + # angle = 45 ; pos = "left" ; kind = "axis" + # package name + package.name <- "ggcute" + # end package name + # function name + function.name <- base::paste0(base::as.list(base::match.call(expand.dots = FALSE))[[1]], "()") # function name with "()" paste, which split into a vector of three: c("::()", "package()", "function()") if "package::function()" is used. + if(function.name[1] == "::()"){ + function.name <- function.name[3] + } + arg.names <- base::names(base::formals(fun = base::sys.function(base::sys.parent(n = 2)))) # names of all the arguments + arg.user.setting <- base::as.list(base::match.call(expand.dots = FALSE))[-1] # list of the argument settings (excluding default values not provided by the user) + # end function name + # critical operator checking + .base_op_check(external.function.name = function.name) + # end critical operator checking + # package checking + # check of lib.path + # end check of lib.path + # check of the required function from the required packages + .pack_and_function_check( + fun = base::c( + "saferDev::arg_check", + "utils::find" + ), + lib.path = NULL, + external.function.name = function.name + ) + # end check of the required function from the required packages + # end package checking + + # argument primary checking + # arg with no default values + mandat.args <- base::c( + "angle", + "pos" + ) + tempo <- base::eval(base::parse(text = base::paste0("missing(", base::paste0(mandat.args, collapse = ") | missing("), ")"))) + if(base::any(tempo)){ # normally no NA for missing() output + tempo.cat <- base::paste0("ERROR IN ", function.name, " OF THE ", package.name, " PACKAGE\nFOLLOWING ARGUMENT", base::ifelse(base::sum(tempo, na.rm = TRUE) > 1, "S HAVE", "HAS"), " NO DEFAULT VALUE AND REQUIRE ONE:\n", base::paste0(mandat.args, collapse = "\n")) + base::stop(base::paste0("\n\n================\n\n", tempo.cat, "\n\n================\n\n"), call. = FALSE) # == in stop() to be able to add several messages between == + } + # end arg with no default values + # argument checking with arg_check() + # argument checking + argum.check <- NULL # + text.check <- NULL # + checked.arg.names <- NULL # for function debbuging: used by r_debugging_tools + ee <- base::expression(argum.check <- c(argum.check, tempo$problem) , text.check <- c(text.check, tempo$text) , checked.arg.names <- base::c(checked.arg.names, tempo$object.name)) + tempo <- saferDev::arg_check(data = angle , class = "integer", length = 1, double.as.integer.allowed = TRUE, neg.values = FALSE, fun.name = function.name) ; base::eval(ee) + tempo <- saferDev::arg_check(data = pos, options = c("top", "right", "bottom", "left"), length = 1, fun.name = function.name) ; base::eval(ee) + tempo <- saferDev::arg_check(data = kind, options = c("axis", "text"), length = 1, fun.name = function.name) ; base::eval(ee) + + if( ! base::is.null(argum.check)){ + if(base::any(argum.check, na.rm = TRUE) == TRUE){ + base::stop(base::paste0("\n\n================\n\n", base::paste(text.check[argum.check], collapse = "\n"), "\n\n================\n\n"), call. = FALSE) # + } + } + # end argument checking with arg_check() + # check with r_debugging_tools + # source("C:/Users/yhan/Documents/Git_projects/debugging_tools_for_r_dev/r_debugging_tools.R") ; eval(parse(text = str_basic_arg_check_dev)) ; eval(parse(text = str_arg_check_with_fun_check_dev)) # activate this line and use the function (with no arguments left as NULL) to check arguments status and if they have been checked using arg_check() + # end check with r_debugging_tools + # end argument primary checking + + # second round of checking and data preparation + # management of NA arguments + if( ! (base::all(base::class(arg.user.setting) == "list", na.rm = TRUE) & base::length(arg.user.setting) == 0)){ + tempo.arg <- base::names(arg.user.setting) # values provided by the user + tempo.log <- base::suppressWarnings(base::sapply(base::lapply(base::lapply(tempo.arg, FUN = base::get, envir = base::sys.nframe(), inherits = FALSE), FUN = base::is.na), FUN = base::any)) & base::lapply(base::lapply(tempo.arg, FUN = base::get, envir = base::sys.nframe(), inherits = FALSE), FUN = base::length) == 1L # no argument provided by the user can be just NA + if(base::any(tempo.log) == TRUE){ # normally no NA because is.na() used here + tempo.cat <- base::paste0("ERROR IN ", function.name, " OF THE ", package.name, " PACKAGE\n", base::ifelse(base::sum(tempo.log, na.rm = TRUE) > 1, "THESE ARGUMENTS", "THIS ARGUMENT"), " CANNOT JUST BE NA:", base::paste0(tempo.arg[tempo.log], collapse = "\n")) + base::stop(base::paste0("\n\n================\n\n", tempo.cat, "\n\n================\n\n"), call. = FALSE) # == in stop() to be able to add several messages between == + } + } + # end management of NA arguments + # management of NULL arguments + tempo.arg <-base::c( + "angle", + "pos", + "kind" + ) + tempo.log <- base::sapply(base::lapply(tempo.arg, FUN = base::get, envir = base::sys.nframe(), inherits = FALSE), FUN = base::is.null) + if(base::any(tempo.log) == TRUE){# normally no NA with is.null() + tempo.cat <- base::paste0("ERROR IN ", function.name, " OF THE ", package.name, " PACKAGE:\n", base::ifelse(base::sum(tempo.log, na.rm = TRUE) > 1, "THESE ARGUMENTS\n", "THIS ARGUMENT\n"), base::paste0(tempo.arg[tempo.log], collapse = "\n"),"\nCANNOT BE NULL") + base::stop(base::paste0("\n\n================\n\n", tempo.cat, "\n\n================\n\n"), call. = FALSE) # == in stop() to be able to add several messages between == + } + # end management of NULL arguments + # code that protects set.seed() in the global environment + # end code that protects set.seed() in the global environment + # warning initiation + # end warning initiation + # end second round of checking and data preparation + # main code + # to get angle between -360 and 360 + while(angle > 360){ + angle <- angle - 360 + } + while(angle < -360){ + angle <- angle + 360 + } + # end to get angle between -360 and 360 + # justifications + if(pos %in% c("bottom", "top")){ + # code below is for if(pos == "bottom"){ + if(any(sapply(FUN = all.equal, c(-360, -180, 0, 180, 360), angle) == TRUE)){ # equivalent of angle == -360 | angle == -180 | angle == 0 | angle == 180 | angle == 360 but deals with floats + hjust <- 0.5 + if(kind == "text"){ + if(any(sapply(FUN = all.equal, c(-360, 0, 360), angle) == TRUE)){ + vjust <- 1 + }else if(any(sapply(FUN = all.equal, c(-180, 180), angle) == TRUE)){ + vjust <- 0 + } + }else{ + vjust <- 0.5 + } + }else if(any(sapply(FUN = all.equal, c(-270, 90), angle) == TRUE)){ + hjust <- 1 + vjust <- 0.5 + }else if(any(sapply(FUN = all.equal, c(-90, 270), angle) == TRUE)){ + hjust <- 0 + vjust <- 0.5 + }else if((angle > -360 & angle < -270) | (angle > 0 & angle < 90)){ + hjust <- 1 + vjust <- 1 + }else if((angle > -270 & angle < -180) | (angle > 90 & angle < 180)){ + hjust <- 1 + vjust <- 0 + }else if((angle > -180 & angle < -90) | (angle > 180 & angle < 270)){ + hjust <- 0 + vjust <- 0 + if(kind == "text" & pos == "top"){ + hjust <- 1 + } + }else if((angle > -90 & angle < 0) | (angle > 270 & angle < 360)){ + hjust <- 0 + vjust <- 1 + } + if(pos == "top"){ + if( ! ((angle > -180 & angle < -90) | (angle > 180 & angle < 270))){ + hjust <- 1 - hjust + } + vjust <- 1 - vjust + } + }else if(pos %in% c("left", "right")){ + # code below is for if(pos == "left"){ + if(any(sapply(FUN = all.equal, c(-270, -90, 90, 270), angle) == TRUE)){ # equivalent of angle == -270 | angle == -90 | angle == 90 | angle == 270 but deals with floats + hjust <- 0.5 + if(kind == "text"){ + if(any(sapply(FUN = all.equal, c(-90, 90), angle) == TRUE)){ + vjust <- 0 + }else if(any(sapply(FUN = all.equal, c(-270, 270), angle) == TRUE)){ + vjust <- 1 + } + }else{ + vjust <- 0.5 + } + }else if(any(sapply(FUN = all.equal, c(-360, 0, 360), angle) == TRUE)){ + hjust <- 1 + vjust <- 0.5 + }else if(any(sapply(FUN = all.equal, c(-180, 180), angle) == TRUE)){ + hjust <- 0 + vjust <- 0.5 + }else if((angle > -360 & angle < -270) | (angle > 0 & angle < 90)){ + hjust <- 1 + vjust <- 0 + }else if((angle > -270 & angle < -180) | (angle > 90 & angle < 180)){ + hjust <- 0 + vjust <- 0 + }else if((angle > -180 & angle < -90) | (angle > 180 & angle < 270)){ + hjust <- 0 + vjust <- 1 + }else if((angle > -90 & angle < 0) | (angle > 270 & angle < 360)){ + hjust <- 1 + vjust <- 1 + } + if(pos == "right"){ + hjust <- 1 - hjust + if( ! (((angle > -270 & angle < -180) | (angle > 90 & angle < 180)) | ((angle > -180 & angle < -90) | (angle > 180 & angle < 270)))){ + vjust <- 1 - vjust + } + } + } + # end justifications + output <- list(angle = angle, pos = pos, kind = kind, hjust = hjust, vjust = vjust) + return(output) +}