diff --git a/DESCRIPTION b/DESCRIPTION index 50be892..b08bafe 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: plasmapR Title: Create Plasmid Maps in R Type: Package -Version: 0.2.1 +Version: 0.3.1 Authors@R: person(given = "Brady", family = "Johnston", @@ -22,11 +22,10 @@ Imports: stringr, ggrepel, cli, - stringi, readr, - shades, rlang, - methods + methods, + ggfittext (>= 0.10.0) Suggests: knitr, rmarkdown, diff --git a/NAMESPACE b/NAMESPACE index 977208a..a7cc87e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,11 +1,8 @@ -# Generated by roxygen2: do not edit by hand - -S3method(as.data.frame,plasmid) -S3method(makeContent,fittexttree) -S3method(makeContent,fittexttreepolar) -export(StatArrow) -export(StatArrowLabel) -export(plot_plasmid) -export(read_gb) -importFrom(grid,makeContent) -importFrom(rlang,.data) +# Generated by roxygen2: do not edit by hand + +S3method(as.data.frame,plasmid) +export(StatArrow) +export(StatArrowLabel) +export(plot_plasmid) +export(read_gb) +importFrom(rlang,.data) diff --git a/R/geom_fit_text.R b/R/geom_fit_text.R deleted file mode 100644 index dc962f7..0000000 --- a/R/geom_fit_text.R +++ /dev/null @@ -1,633 +0,0 @@ -############# - -# Code taken from ggfitttext. (https://github.com/wilkox/ggfittext) -# Ideally the changes required to make this package function properly will -# make it to the main CRAN branch, but until then, the code will be embedded -# directly into this package to help it function properly. -# Credit goes to David Wilkins for the original code, with some modifications -# by me (Brady Johnston) to get the flipping of circular text working. - - -############# - -#' @noRd -geom_fit_text <- function( - mapping = NULL, - data = NULL, - stat = "identity", - position = "identity", - na.rm = FALSE, - show.legend = NA, - inherit.aes = TRUE, - padding.x = grid::unit(1, "mm"), - padding.y = grid::unit(1, "mm"), - min.size = 4, - place = "centre", - outside = FALSE, - grow = FALSE, - reflow = FALSE, - hjust = NULL, - vjust = NULL, - fullheight = NULL, - width = NULL, - height = NULL, - formatter = NULL, - contrast = FALSE, - flip = FALSE, - ... -) { - ggplot2::layer( - geom = GeomFitText, - mapping = mapping, - data = data, - stat = stat, - position = position, - show.legend = show.legend, - inherit.aes = inherit.aes, - params = list( - na.rm = na.rm, - padding.x = padding.x, - padding.y = padding.y, - place = place, - outside = outside, - min.size = min.size, - grow = grow, - reflow = reflow, - hjust = hjust, - vjust = vjust, - fullheight = fullheight, - width = width, - height = height, - formatter = formatter, - contrast = contrast, - flip = flip, - ... - ) - ) -} - -#' GeomFitText -#' @noRd -GeomFitText <- ggplot2::ggproto( - "GeomFitText", - ggplot2::Geom, - required_aes = c("label"), - default_aes = ggplot2::aes( - alpha = 1, - angle = 0, - colour = "black", - family = "", - fontface = 1, - lineheight = 0.9, - size = 12, - x = NULL, - y = NULL, - xmin = NULL, - xmax = NULL, - ymin = NULL, - ymax = NULL, - fill = NULL - ), - - setup_params = function(data, params) { - - # Standardise the place argument - params$place <- ifelse(params$place %in% c("middle", "center"), "centre", params$place) - - params - }, - - setup_data = function( - data, - params - ) { - - # Check that valid aesthetics have been supplied for each dimension - if (! (! is.null(data$xmin) & ! is.null(data$xmax) | ! is.null(data$x))) { - stop( - "geom_fit_text needs either 'xmin' and 'xmax', or 'x'", - .call = FALSE - ) - } - if (! (! is.null(data$ymin) & ! is.null(data$ymax) | ! is.null(data$y))) { - stop( - "geom_fit_text needs either 'ymin' and 'ymax', or 'y'", - .call = FALSE - ) - } - - # If 'width' is provided, but not as unit, interpret it as a numeric on the - # x scale - if ((! is.null(params$width)) & (! inherits(params$width, "unit"))) { - data$xmin <- data$x - params$width / 2 - data$xmax <- data$x + params$width / 2 - } - - # If 'height' is provided, but not a unit, interpret it as a numeric on the - # y scale - if ((! is.null(params$height)) & (! inherits(params$height, "unit"))) { - data$ymin <- data$y - params$height / 2 - data$ymax <- data$y + params$height / 2 - } - - # If neither a 'width' parameter nor xmin/xmax aesthetics have been - # provided, infer the width using the method of geom_boxplot - if (is.null(params$width) & is.null(data$xmin)) { - data$width <- ggplot2::resolution(data$x, FALSE) * 0.9 - data$xmin <- data$x - data$width / 2 - data$xmax <- data$x + data$width / 2 - data$width <- NULL - } - - # If neither a 'height' parameter nor ymin/ymax aesthetics have been - # provided, infer the height using the method of geom_boxplot - if (is.null(params$height) & is.null(data$ymin)) { - data$height <- ggplot2::resolution(data$y, FALSE) * 0.9 - data$ymin <- data$y - data$height / 2 - data$ymax <- data$y + data$height / 2 - data$height <- NULL - } - - # Apply a formatter function, if one was given - if (! is.null(params$formatter)) { - - # Check that 'formatter' is a function - if (! is.function(params$formatter)) { - stop("`formatter` must be a function") - } - - # Apply formatter to the labels, checking that the output is a character - # vector of the correct length - formatted_labels <- sapply(data$label, params$formatter, USE.NAMES = FALSE) - if ((! length(formatted_labels) == length(data$label)) | - (! is.character(formatted_labels))) { - stop("`formatter` must produce a character vector of same length as input") - } - data$label <- formatted_labels - } - - data$flip <- params$flip - - data - }, - - draw_key = ggplot2::draw_key_label, - - draw_panel = function( - data, - panel_scales, - coord, - padding.x = grid::unit(1, "mm"), - padding.y = grid::unit(1, "mm"), - min.size = 4, - grow = FALSE, - reflow = FALSE, - hjust = NULL, - vjust = NULL, - fullheight = NULL, - width = NULL, - height = NULL, - formatter = NULL, - contrast = FALSE, - place = "centre", - flip = flip, - outside = FALSE - ) { - - # Transform data to plot scales; if in polar coordinates, we need to ensure - # that x and y values are given - if (inherits(coord, "CoordPolar")) { - if (is.null(data$x)) data$x <- 1 - if (is.null(data$y)) data$y <- 1 - } - data <- coord$transform(data, panel_scales) - - # For polar coordinates, we need to transform xmin/xmax & ymin/ymax into - # theta and r values respectively; these can be used later to accurately - # set the width and height of the bounding box in polar space - if (inherits(coord, "CoordPolar")) { - if (! is.null(data$xmin)) { - data$xmin <- ggplot2:::theta_rescale(coord, data$xmin, panel_scales) - } - if (! is.null(data$xmax)) { - data$xmax <- ggplot2:::theta_rescale(coord, data$xmax, panel_scales) - } - if (! is.null(data$ymin)) { - data$ymin <- ggplot2:::r_rescale(coord, data$ymin, panel_scales$r.range) - } - if (! is.null(data$ymax)) { - data$ymax <- ggplot2:::r_rescale(coord, data$ymax, panel_scales$r.range) - } - - } - - gt <- grid::gTree( - data = data, - padding.x = padding.x, - padding.y = padding.y, - place = place, - outside = outside, - min.size = min.size, - grow = grow, - reflow = reflow, - hjust = hjust, - vjust = vjust, - fullheight = fullheight, - width = width, - height = height, - contrast = contrast, - flip = flip, - cl = ifelse(inherits(coord, "CoordPolar"), "fittexttreepolar", "fittexttree") - ) - gt$name <- grid::grobName(gt, "geom_fit_text") - gt - } -) - -#' Used Internally -#' @importFrom grid makeContent -#' @rdname makeContent.fittexttree -#' @param x Fittexttree, used internally. -#' @export -makeContent.fittexttree <- function(x) { - - # Extract data - data <- ftt$data - - # Set and check default parameters - ftt$outside <- ftt$outside %||% FALSE - ftt$contrast <- ftt$contrast %||% FALSE - ftt$vjust <- ftt$vjust %||% 0.5 - if (is.null(ftt$hjust)) { - if (ftt$place %in% c("left", "bottomleft", "topleft")) { - ftt$hjust <- 0 - } else if (ftt$place %in% c("right", "bottomright", "topright")) { - ftt$hjust <- 1 - } else { - ftt$hjust <- 0.5 - } - } - ftt$fullheight <- ftt$fullheight %||% ftt$grow - - # Convert padding.x and padding.y to npc units - ftt$padding.x <- wunit2npc(ftt$padding.x) - ftt$padding.y <- hunit2npc(ftt$padding.y) - - # Default values for missing aesthetics - data$fill <- data$fill %||% "grey35" - data$colour <- data$colour %||% "black" - - # If xmin/xmax are not provided, generate boundary box from width - if (is.null(data$xmin)) { - data$xmin <- data$x - (wmm2npc(ftt$width) / 2) - data$xmax <- data$x + (wmm2npc(ftt$width) / 2) - } - - # If ymin/ymax are not provided, generate boundary box from height - if (is.null(data$ymin)) { - data$ymin <- data$y - (hmm2npc(ftt$height) / 2) - data$ymax <- data$y + (hmm2npc(ftt$height) / 2) - } - - # Remove any rows with NA boundaries - na_rows <- which(is.na(data$xmin) | is.na(data$xmax) | is.na(data$ymin) | - is.na(data$ymax)) - if (length(na_rows) > 0) { - data <- data[-na_rows, ] - warning( - "Removed ", - length(na_rows), - " rows where box limits were outside plot limits (geom_fit_text).", - call. = FALSE - ) - } - - # Remove any rows with blank labels - data <- data[which(! is.na(data$label) | data$label == ""), ] - - # Clean up angles - data$angle <- data$angle %% 360 - - # If contrast is set, and the shade of a text colour is too close to the - # shade of the fill colour, change the colour to its complement - if (ftt$contrast) { - - # If any fill value is NA, emit a warning and set to the default - # ggplot2 background grey - if (! is.null(data$fill)) { - if (any(is.na(data$fill))) { - warning("NA values in fill", call. = FALSE) - data$fill <- data$fill %NA% "grey35" - } - } - - # If any colour value is NA, set to black - data$colour <- data$colour %NA% "black" - - # Change the text colour to its complement if the background fill is too - # dark, then change (perhaps again) if the shades of the colour and fill - # are too similar - data$colour <- ifelse( - shades::lightness(data$fill) < 50, - as.character(shades::complement(shades::shade(data$colour))), - data$colour - ) - data$colour <- ifelse( - abs(shades::lightness(data$fill) - shades::lightness(data$colour)) < 50, - as.character(shades::complement(shades::shade(data$colour))), - data$colour - ) - } - - # Prepare grob for each text label - grobs <- lapply(seq_len(nrow(data)), function(i) { - - # Convenience - text <- data[i, ] - - # Get dimensions of bounding box, in mm - xdim <- wnpc2mm(abs(text$xmin - text$xmax) - (2 * ftt$padding.x)) - ydim <- hnpc2mm(abs(text$ymin - text$ymax) - (2 * ftt$padding.y)) - - # Reflow and/or resize the text into a textGrob - tg <- reflow_and_resize(text, ftt$reflow, ftt$grow, ftt$fullheight, xdim, ydim) - - # If the font size is too small and 'outside' has been set, try reflowing - # and resizing again in the 'outside' position - if (tg$gp$fontsize < ftt$min.size & ftt$outside) { - if (ftt$place == "top") { - text$ymin <- text$ymax - text$ymax <- 1 - ftt$place <- "bottom" - } else if (ftt$place == "bottom") { - text$ymax <- text$ymin - text$ymin <- 0 - ftt$place <- "top" - } else if (ftt$place == "right") { - text$xmin <- text$xmax - text$xmax <- 1 - ftt$place <- "left" - } else if (ftt$place == "left") { - text$xmax <- text$xmin - text$xmin <- 0 - ftt$place <- "right" - } - xdim <- wnpc2mm(abs(text$xmin - text$xmax) - (2 * ftt$padding.x)) - ydim <- hnpc2mm(abs(text$ymin - text$ymax) - (2 * ftt$padding.y)) - tg$gp$fontsize <- text$size - ftt$outside <- FALSE - # If we're moving the text outside and contrast is true, set the text - # in contrast to the default theme_grey panel colour - if (ftt$contrast) { - bg_colour <- "grey92" - text_colour <- text$colour %||% "black" - if ( - abs(shades::lightness(bg_colour) - shades::lightness(text_colour)) < 50 - ) { - complement <- shades::complement(shades::shade(text_colour)) - text$colour <- as.character(complement) - } - } - tg <- reflow_and_resize(text, ftt$reflow, ftt$grow, ftt$fullheight, xdim, ydim) - } - - # If the font size is still too small, don't draw this label - if (tg$gp$fontsize < ftt$min.size) return() - - # Set hjust and vjust - tg$hjust <- ftt$hjust - tg$vjust <- ftt$vjust - - # Update the textGrob dimensions - tgdim <- tgDimensions(tg, ftt$fullheight, text$angle) - - # To calculate the vector from the geometric centre of the text to the - # anchor point, we first need the dimensions of the unrotated text in mm. - # For the common use case of an orthogonal rotation, we can reuse the - # pre-calculated values to save time - if (tg$rot == 0 | tg$rot == 180) { - tg_width_unrot <- tgdim$width - tg_height_unrot <- tgdim$height - if (ftt$fullheight) tg_descent_unrot <- hunit2mm(tgdim$descent) - } else if (tg$rot == 90 | tg$rot == 270) { - tg_width_unrot <- tgdim$height - tg_height_unrot <- tgdim$width - if (ftt$fullheight) { - tg_descent_unrot <- wunit2mm(tgdim$descent) - } - } else { - # For some reason, we don't get accurate values if we do this with the - # original textGrob so we create a copy - unrot <- tg - unrot$rot <- 0 - tg_width_unrot <- wunit2mm(grid::grobWidth(unrot)) - tg_height_unrot <- hunit2mm(grid::grobHeight(unrot)) - if (ftt$fullheight) tg_descent_unrot <- hunit2mm(grid::grobDescent(unrot)) - } - - # We can use these values to calculate the magnitude of the vector from the - # centre point to the anchor point, using the Pythagorean identity - if (ftt$fullheight) { - rise <- ((tg_height_unrot * tg$vjust) + tg_descent_unrot) - - ((tg_height_unrot + tg_descent_unrot) * 0.5) - } else { - rise <- (tg_height_unrot * tg$vjust) - (tg_height_unrot * 0.5) - } - run <- (tg_width_unrot * tg$hjust) - (tg_width_unrot * 0.5) - magnitude <- sqrt((rise ^ 2) + (run ^ 2)) - - # The angle between the baseline and the vector can be calculated from the - # known rise and run - baseline_angle <- asin(abs(rise) / abs(magnitude)) * (180 / pi) - - # To find the 'direction angle' of the vector (expressed in degrees - # anti-clockwise from east), we correct for the quadrant then add the - # rotation of the entire textGrob modulo 360. There's almost certainly a - # clever trigonometry way to do this but I can't figure it out - if (sign(rise) == 1 & sign(run) == 1) { - direction_angle <- baseline_angle - } else if (sign(rise) == 1 & sign(run) == 0) { - direction_angle <- 90 - } else if (sign(rise) == 1 & sign(run) == -1) { - direction_angle <- 180 - baseline_angle - } else if (sign(rise) == 0 & sign(run) == -1) { - direction_angle <- 180 - } else if (sign(rise) == -1 & sign(run) == -1) { - direction_angle <- 180 + baseline_angle - } else if (sign(rise) == -1 & sign(run) == 0) { - direction_angle <- 270 - } else if (sign(rise) == -1 & sign(run) == 1) { - direction_angle <- 360 - baseline_angle - } else if (sign(rise) == 0 & sign(run) == 1) { - direction_angle <- 0 - } else if (sign(rise) == 0 & sign(run) == 0) { - direction_angle <- 0 - } - direction_angle <- (direction_angle + tg$rot) %% 360 - - # We can now use these to calculate the x and y offsets of the anchor point - # from the centre point. For convenience, we will convert these to npc - x_offset <- wmm2npc(magnitude * cos(deg2rad(direction_angle))) - y_offset <- hmm2npc(magnitude * sin(deg2rad(direction_angle))) - - # Specify the bounding box limits in npc coordinates - xmin <- text$xmin + ftt$padding.x - xmax <- text$xmax - ftt$padding.x - ymin <- text$ymin + ftt$padding.y - ymax <- text$ymax - ftt$padding.y - - # Convert the textGrob dimensions into npc - tgdim$width <- wmm2npc(tgdim$width) - tgdim$height <- hmm2npc(tgdim$height) - - # Place the text - if (ftt$place %in% c("topleft", "left", "bottomleft")) { - tg$x <- xmin + (tgdim$width / 2) + x_offset - } else if (ftt$place %in% c("top", "centre", "bottom")) { - tg$x <- ((xmin + xmax) / 2) + x_offset - } else if (ftt$place %in% c("topright", "right", "bottomright")) { - tg$x <- xmax - (tgdim$width / 2) + x_offset - } - if (ftt$place %in% c("topleft", "top", "topright")) { - tg$y <- ymax - (tgdim$height / 2) + y_offset - } else if (ftt$place %in% c("left", "centre", "right")) { - tg$y <- ((ymin + ymax) / 2) + y_offset - } else if (ftt$place %in% c("bottomleft", "bottom", "bottomright")) { - tg$y <- ymin + (tgdim$height / 2) + y_offset - } - - # Convert x and y coordinates to unit objects - tg$x <- grid::unit(tg$x, "npc") - tg$y <- grid::unit(tg$y, "npc") - - # Return the textGrob - tg - }) - - class(grobs) <- "gList" - grid::setChildren(ftt, grobs) -} - -#' geom_fit_text -#' @noRd -geom_grow_text <- function(...) { - .Deprecated("geom_fit_text(grow = T, ...)") -} - -#' geom_fit_text -#' @noRd -geom_shrink_text <- function(...) { - .Deprecated("geom_fit_text(grow = F, ...)") -} - - -#' Return a textGrob with the text reflowed and/or resized to fit given -#' dimensions -#' -#' @noRd -reflow_and_resize <- function(text, reflow, grow, fullheight, xdim, ydim) { - - # Create textGrob - tg <- grid::textGrob( - label = text$label, - x = 0.5, - y = 0.5, - default.units = "npc", - hjust = 0.5, - vjust = 0.5, - rot = text$angle, - gp = grid::gpar( - col = ggplot2::alpha(text$colour, text$alpha), - fontsize = text$size, - fontfamily = text$family, - fontface = text$fontface, - lineheight = text$lineheight - ) - ) - - # Get starting textGrob dimensions, in mm - tgdim <- tgDimensions(tg, fullheight, text$angle) - - # Reflow the text, if reflow = TRUE and either the text doesn't currently - # fit or grow = TRUE and if text contains spaces - if (reflow & (grow | tgdim$width > xdim | tgdim$height > ydim) & - stringi::stri_detect_regex(tg$label, "\\s")) { - - # Try reducing the text width, one character at a time, and see if it - # fits the bounding box - best_aspect_ratio <- Inf - best_width <- stringi::stri_length(tg$label) - label <- unlist(stringi::stri_split(tg$label, regex = "\n")) - stringwidth <- sum(unlist(lapply(label, stringi::stri_length))) - previous_reflow <- "" - for (w in (stringwidth):1) { - - # Reflow text to this width - # By splitting the text on whitespace and passing normalize = F, - # line breaks in the original text are respected - tg$label <- paste( - stringi::stri_wrap(label, w, normalize = FALSE), - collapse = "\n" - ) - - # Skip if the text is unchanged - if (previous_reflow == tg$label) { - previous_reflow <- tg$label - next - } - previous_reflow <- tg$label - - # Recalculate aspect ratio of textGrob using and update if this is the - # new best ratio - tgdim <- tgDimensions(tg, fullheight, text$angle) - aspect_ratio <- tgdim$width / tgdim$height - diff_from_box_ratio <- abs(aspect_ratio - (xdim / ydim)) - best_diff_from_box_ratio <- abs(best_aspect_ratio - (xdim / ydim)) - if (diff_from_box_ratio < best_diff_from_box_ratio) { - best_aspect_ratio <- aspect_ratio - best_width <- w - } - - # If the text now fits the bounding box (and we are not trying to grow - # the text), good to stop here - if (tgdim$width < xdim & tgdim$height < ydim & !grow) break - } - - # If all reflow widths have been tried and none is smaller than the box - # (i.e. some shrinking is still required), or if we are trying to grow - # the text, pick the reflow width that produces the aspect ratio closest - # to that of the bounding box - if (tgdim$width > xdim | tgdim$height > ydim | grow) { - tg$label <- paste( - stringi::stri_wrap(label, best_width, normalize = FALSE), - collapse = "\n" - ) - # Update the textGrob dimensions - tgdim <- tgDimensions(tg, fullheight, text$angle) - } - } - - # Resize text to fit bounding box if it doesn't fit - if ( - # Standard condition - is text too big for box? - (tgdim$width > xdim | tgdim$height > ydim) | - # grow = TRUE condition - is text too small for box? - (grow & tgdim$width < xdim & tgdim$height < ydim) - ) { - - # Get the slopes of the relationships between font size and label - # dimensions - slopew <- tg$gp$fontsize / tgdim$width - slopeh <- tg$gp$fontsize / tgdim$height - - # Calculate the target font size required to fit text to box along each - # dimension - targetfsw <- xdim * slopew - targetfsh <- ydim * slopeh - - # Set to smaller of target font sizes - tg$gp$fontsize <- ifelse(targetfsw < targetfsh, targetfsw, targetfsh) - } - - return(tg) -} diff --git a/R/plot.R b/R/plot.R index 9754d0c..b34a14a 100644 --- a/R/plot.R +++ b/R/plot.R @@ -4,7 +4,15 @@ .plot_plasmid <- function(dat, bp, name = "Plasmid Name", label_wrap = 20) { dat <- dat[dat$type != "source", ] - dat |> + name_supplied <- !is.null(name) & name != "" + + if (name_supplied) { + yintercept = 4 + } else { + yintercept = 0 + } + + plt <- dat |> ggplot2::ggplot(ggplot2::aes( start = .data$start, end = .data$end, @@ -12,8 +20,10 @@ fill = .data$type, group = .data$index )) + - ggplot2::geom_hline(yintercept = 4) + - ggplot2::coord_polar(start = pi / 4) + + ggplot2::geom_hline(yintercept = yintercept) + + ggplot2::coord_polar( + start = pi / 4 + ) + ggrepel::geom_label_repel( @@ -31,10 +41,10 @@ bp = bp, arrowhead_size = 1 ) + - geom_fit_text( + ggfittext::geom_fit_text( ggplot2::aes( label = .data$name, - y = 4 + y = yintercept ), stat = "arrowLabel", grow = FALSE, @@ -45,19 +55,26 @@ flip = FALSE ) + - ggplot2::ylim(c(0, NA)) + + ggplot2::ylim(c(yintercept - 4, NA)) + ggplot2::xlim(c(0, bp)) + ggplot2::theme_void() + - ggplot2::annotate( - geom = "text", - x = 0, - y = 0, - label = stringr::str_glue("{name}\n{bp} bp") - ) + ggplot2::scale_fill_brewer(type = 'qual', palette = 5) + ggplot2::theme( legend.position = "" ) + + if (name_supplied) { + plt <- plt + + ggplot2::annotate( + geom = "text", + x = 0, + y = 0, + label = stringr::str_glue("{name}\n{bp} bp") + ) + } + + plt + } #' Plot a Plasmid diff --git a/R/polar.R b/R/polar.R deleted file mode 100644 index a0c9259..0000000 --- a/R/polar.R +++ /dev/null @@ -1,299 +0,0 @@ -############# - -# Code taken from ggfitttext. (https://github.com/wilkox/ggfittext) -# Ideally the changes required to make this package function properly will -# make it to the main CRAN branch, but until then, the code will be embedded -# directly into this package to help it function properly. -# Credit goes to David Wilkins for the original code, with some modifications -# by me (Brady Johnston) to get the flipping of circular text working. - - -############# - - - - -#' @importFrom grid makeContent -#' @export -makeContent.fittexttreepolar <- function(x) { - flip <- TRUE - - data <- x$data - - # If ymin/ymax are not provided, generate boundary box from height. A similar - # transformation will be performed for xmin/xmax for each grob individually - # later, as it needs to be done in the context of the y position of the grob. - if (!("ymin" %in% names(data))) { - data$ymin <- data$r - - (grid::convertHeight(x$height, "npc", valueOnly = TRUE) / 2) - data$ymax <- data$r + - (grid::convertHeight(x$height, "npc", valueOnly = TRUE) / 2) - } - - # Handle parameters - if (is.null(x$contrast)) x$contrast <- FALSE - if (is.null(x$outside)) x$outside <- FALSE - if (is.null(x$fullheight)) x$fullheight <- x$grow - if (x$outside) warning("Outside is not supported in polar coordinates") - if (x$reflow) warning("Reflowing is not supported in polar coordinates") - if (! is.null(x$hjust)) warning("hjust is not supported in polar coordinates") - if (! is.null(x$vjust)) warning("vjust is not supported in polar coordinates") - - # Convert padding.x and padding.y to mm - padding.x <- grid::convertWidth(x$padding.x, "mm", valueOnly = TRUE) - padding.y <- grid::convertHeight(x$padding.y, "mm", valueOnly = TRUE) - - # Prepare grob for each text label - grobs <- lapply(seq_len(nrow(data)), function(i) { - - # Convenience - text <- data[i, ] - - # Handle angled text - if (! text$angle == 0) warning("Angled text is not supported in polar coordinates") - - # Set hjust and vjust - # A vjust of 0.2 strikes a good visual balance in the kerning of characters - # in polar coordinates - x$hjust <- 0.5 - x$vjust <- 0.2 - - # Create starting textGrob - tg <- grid::textGrob(label = text$label, x = 0.5, y = 0.5, default.units = "mm", - hjust = x$hjust, vjust = x$vjust, rot = text$angle, - gp = grid::gpar(col = ggplot2::alpha(text$colour, text$alpha), - fontsize = text$size, fontfamily = text$family, - fontface = text$fontface, - lineheight = text$lineheight)) - - # Get starting textGrob dimensions - tgdim <- tgDimensions(tg, x$fullheight, text$angle) - - # Convert box y coordinates to mm - ymin <- grid::convertHeight(grid::unit(text$ymin, "npc"), "mm", TRUE) - ymax <- grid::convertHeight(grid::unit(text$ymax, "npc"), "mm", TRUE) - - # Get dimensions of bounding box. The y dimension will be given in mm, while - # the x dimension is given as arc length (radians). For convenience of - # comparing the textGrob to the bounding box on the x dimension, we will - # also calculate it in mm based on the text placement. If xmin/xmax are - # not provided, the boundary box will be generated from width. - ydim <- abs(ymin - ymax) - (2 * padding.y) - - if (!("xmin" %in% names(data))) { - if (x$place %in% c("bottomleft", "bottom", "bottomright")) { - r <- ymin + (x$vjust * tgdim$height) + padding.y - } else if (x$place %in% c("left", "centre", "right")) { - r <- ((ymin + ymax) / 2) - ((0.5 - x$vjust) * tgdim$height) - } else if (x$place %in% c("topleft", "top", "topright")) { - r <- ymax - padding.y - ((1 - x$vjust) * tgdim$height) - } - c <- 2 * pi * r - text$xmin <- text$theta - - (((grid::convertWidth(x$width, "mm", valueOnly = TRUE) / 2) / c) * 2 * pi) - text$xmax <- text$theta + - (((grid::convertWidth(x$width, "mm", valueOnly = TRUE) / 2) / c) * 2 * pi) - } - - xdim <- ifelse( - text$xmax > text$xmin, - text$xmax - text$xmin, - (text$xmax + pi + pi - text$xmin) %% (2 * pi) - ) - - if (x$place %in% c("bottomleft", "bottom", "bottomright")) { - r <- ymin + (x$vjust * tgdim$height) + padding.y - xdim_mm <- r * xdim - - } else if (x$place %in% c("left", "centre", "right")) { - r <- ((ymin + ymax) / 2) - ((0.5 - x$vjust) * tgdim$height) - xdim_mm <- r * xdim - - } else if (x$place %in% c("topleft", "top", "topright")) { - r <- ymax - padding.y - ((1 - x$vjust) * tgdim$height) - xdim_mm <- r * xdim - } - - # Resize text to fit bounding box if it doesn't fit - if ( - # Standard condition - is text too big for box? - (tgdim$width > xdim_mm | tgdim$height > ydim) | - # grow = TRUE condition - is text too small for box? - (x$grow & tgdim$width < xdim_mm & tgdim$height < ydim) - ) { - - # Get the relationships between font size and label dimensions - slopew <- tg$gp$fontsize / tgdim$width - slopeh <- tg$gp$fontsize / tgdim$height - - # Calculate the target font size required to make the text fit - # height-wise - targetfsh <- ydim * slopeh - - # Calculate the target font size required to make the text fit width-wise - # See https://imgur.com/a/z5TvFST for explanation of geometry - if (x$place %in% c("bottomleft", "bottom", "bottomright")) { - w <- xdim * (ymin + padding.y) - targetfsw <- w * slopew - - } else if (x$place %in% c("left", "centre", "right")) { - k <- (tgdim$height * x$vjust) / tgdim$width - R <- (ymin + ymax) / 2 - w <- ((xdim * R) / ((xdim * k) + 1)) - (2 * padding.x) - targetfsw <- w * slopew - - } else if (x$place %in% c("topleft", "top", "topright")) { - k <- tgdim$height / tgdim$width - R <- ymax - padding.y - w <- ((xdim * R) / ((xdim * k) + 1)) - (2 * padding.x) - targetfsw <- w * slopew - } - - # Set to smaller of target font sizes - tg$gp$fontsize <- ifelse(targetfsw < targetfsh, targetfsw, targetfsh) - } - - # Hide if below minimum font size - if (tg$gp$fontsize < x$min.size) return() - - # Update the textGrob dimensions - tgdim <- tgDimensions(tg, x$fullheight, text$angle) - - # r = the radius from the centre to the text anchor (which is not the - # typographic baseline but is defined by vjust). Note that the position of - # the text anchor does not take descenders into account, so these must be - # adjusted for if fullheight is true - if (x$place %in% c("bottomleft", "bottom", "bottomright")) { - r <- ymin + padding.y + (x$vjust * tgdim$height) - } else if (x$place %in% c("left", "centre", "right")) { - r <- ((ymin + ymax) / 2) - ((0.5 - x$vjust) * tgdim$height) - } else if (x$place %in% c("topleft", "top", "topright")) { - r <- ymax - padding.y - ((1 - x$vjust) * tgdim$height) - } - if (x$fullheight) r <- r + (grid::convertHeight(tgdim$descent, "mm", TRUE) * (1 - x$vjust)) - - # c = the circumference of the baseline - c <- 2 * pi * r - - # char_widths = widths of each character in the string - chars <- strsplit(as.character(text$label), "")[[1]] - char_widths <- (grid::calcStringMetric(chars)$width / - sum(grid::calcStringMetric(chars)$width)) * tgdim$width - - # char_arcs = arcwidth of each character, in degrees - char_arcs <- 360 * char_widths / c - - # padding.x.arcrad = the arcwidth of padding.x, expressed in radians, at - # the anchor radius - padding.x.arcrad <- (padding.x / c) * 2 * pi - - # theta = the theta of the text anchor for the entire label in the - # coordinate system, initial calculated in radians - if (x$place %in% c("bottomleft", "left", "topleft")) { - theta <- text$xmin + (deg2rad(sum(char_arcs)) / 2) + padding.x.arcrad - } else if (x$place %in% c("bottom", "centre", "top")) { - theta <- ifelse( - text$xmax > text$xmin, - (text$xmin + text$xmax) / 2, - (text$xmin + text$xmax + pi + pi) / 2 - ) - } else if (x$place %in% c("bottomright", "right", "topright")) { - theta <- text$xmax - (deg2rad(sum(char_arcs)) / 2) - padding.x.arcrad - } - - # check if need to flip (angle == 180) - flip <- x$flip - - if (flip) { - flip <- rad2deg(theta) > 90 && rad2deg(theta) < 270 - } - - # If flipping, over-write necessary strings - if (flip) { - # Reverse the string - text$label <- strrev(as.character(text$label)) - - # re-calc string positions - chars <- strsplit(as.character(text$label), "")[[1]] - char_widths <- (grid::calcStringMetric(chars)$width / - sum(grid::calcStringMetric(chars)$width)) * tgdim$width - - # char_arcs = arcwidth of each character, in degrees - char_arcs <- 360 * char_widths / c - - # padding.x.arcrad = the arcwidth of padding.x, expressed in radians, at - # the anchor radius - padding.x.arcrad <- (padding.x / c) * 2 * pi - - # theta = the theta of the text anchor for the entire label in the - # coordinate system, initial calculated in radians - if (x$place %in% c("bottomleft", "left", "topleft")) { - theta <- text$xmin + (deg2rad(sum(char_arcs)) / 2) + padding.x.arcrad - } else if (x$place %in% c("bottom", "centre", "top")) { - theta <- ifelse( - text$xmax > text$xmin, - (text$xmin + text$xmax) / 2, - (text$xmin + text$xmax + pi + pi) / 2 - ) - } else if (x$place %in% c("bottomright", "right", "topright")) { - theta <- text$xmax - (deg2rad(sum(char_arcs)) / 2) - padding.x.arcrad - } - } - - - # angle = ?? I can't even remember what this is supposed to do but it - # works. Converting from radians to degrees with some sort of correction? - angle <- 450 - rad2deg(theta) - - # char_thetas = theta position of the anchors for each character (assuming - # hjust = 0.5 for the textGrob representing this character), in degrees - lag_vector <- function(x) c(0, x[1:length(x) - 1]) - char_thetas <- angle - lag_vector(cumsum(char_arcs)) - - (char_arcs / 2) + (sum(char_arcs) / 2) - - # Generate a textGrob for each character - tgs <- lapply(1:length(char_thetas), function(i) { - - char <- chars[i] - theta <- char_thetas[i] - theta_rad <- deg2rad(theta) - - x_pos <- r * cos(theta_rad) - x_pos <- 0.5 + grid::convertWidth(grid::unit(x_pos, "mm"), "npc", TRUE) - y_pos <- r * sin(theta_rad) - y_pos <- 0.5 + grid::convertHeight(grid::unit(y_pos, "mm"), "npc", TRUE) - - if (flip) { - # adjust for 0.8 to take into account 0.2 vjust for nice kerning - x$vjust <- 0 - } - - tg <- grid::textGrob( - label = char, - x = x_pos, - y = y_pos, - hjust = x$hjust, - vjust = x$vjust, - rot = theta - 90 + 180 * flip, - default.units = "npc", - gp = grid::gpar( - fontsize = tg$gp$fontsize, - col = ggplot2::alpha(text$colour, text$alpha), - fontfamily = text$family, - fontface = text$fontface, - lineheight = text$lineheight - ) - ) - return(tg) - }) - - # Convert to a gTree - gt <- grid::gTree(children = do.call(grid::gList, tgs)) - - # Return the gTree - gt - }) - - class(grobs) <- "gList" - grid::setChildren(x, grobs) -} diff --git a/R/utilities.R b/R/utilities.R deleted file mode 100644 index ebdf731..0000000 --- a/R/utilities.R +++ /dev/null @@ -1,61 +0,0 @@ -############# - -# Code taken from ggfitttext. (https://github.com/wilkox/ggfittext) -# Ideally the changes required to make this package function properly will -# make it to the main CRAN branch, but until then, the code will be embedded -# directly into this package to help it function properly. -# Credit goes to David Wilkins for the original code, with some modifications -# by me (Brady Johnston) to get the flipping of circular text working. - - -############# - -#' Default values -#' -#' @noRd -"%||%" <- function(a, b) if (is.null(a)) b else a -"%NA%" <- function(a, b) ifelse(is.na(a), b, a) - -#' Conversions between degrees and radians -#' -#' @noRd -deg2rad <- function(deg) { deg * (pi / 180) } -rad2deg <- function(rad) { rad * (180 / pi) } - -#' Return a reversed string -#' -#' @param string String to be reversed. -#' -#' @noRd -strrev <- function(string) { - sapply(lapply(strsplit(string, NULL), rev), paste0, collapse = "") -} - -#' Textgrob dimensions, in mm -#' -#' @noRd -tgDimensions <- function(tg, fullheight, angle) { - width <- wunit2mm(grid::grobWidth(tg)) - height <- grid::convertHeight(grid::grobHeight(tg), "mm", TRUE) - if (fullheight) { - descent <- grid::grobDescent(tg) - width <- width + abs(wunit2mm(descent) * sin(deg2rad(angle))) - height <- height + abs(grid::convertHeight(descent, "mm", TRUE) * - cos(deg2rad(angle))) - } else { - descent <- NULL - } - list(width = width, height = height, descent = descent) -} - -#' Width and height unit conversions -#' -#' @noRd -wunit2npc <- function(w) grid::convertWidth(w, "npc", valueOnly = TRUE) -wmm2npc <- function(w) wunit2npc(grid::unit(w, "mm")) -wunit2mm <- function(w) grid::convertWidth(w, "mm", valueOnly = TRUE) -wnpc2mm <- function(w) wunit2mm(grid::unit(w, "npc")) -hunit2npc <- function(h) grid::convertHeight(h, "npc", valueOnly = TRUE) -hmm2npc <- function(h) hunit2npc(grid::unit(h, "mm")) -hunit2mm <- function(h) grid::convertHeight(h, "mm", valueOnly = TRUE) -hnpc2mm <- function(h) hunit2mm(grid::unit(h, "npc")) diff --git a/README.Rmd b/README.Rmd index c2befec..e66a382 100644 --- a/README.Rmd +++ b/README.Rmd @@ -69,6 +69,19 @@ dat[dat$type == "CDS", ] |> plot_plasmid(name = "pETM-20") ``` +It's not currently intended for linear display, but it can be used as such. I recommend checking out the [`gggenese`](https://wilkox.org/gggenes/) package. + +```{r} +#| fig-height: 3 +#| message: false +#| warning: false +dat[dat$type == "CDS", ] |> + plot_plasmid(name = NULL) + + ggplot2::coord_cartesian() + + ggplot2::scale_y_continuous(limits = NULL) +``` + + ## A {ggplot2} Object The result of the call is just a {ggplot2} plot, which you can further customise diff --git a/README.md b/README.md index 04b6a98..5d79c9d 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,19 @@ dat[dat$type == "CDS", ] |> ![](man/figures/unnamed-chunk-2-1.png) +It’s not currently intended for linear display, but it can be used as +such. I recommend checking out the +[`gggenese`](https://wilkox.org/gggenes/) package. + +``` r +dat[dat$type == "CDS", ] |> + plot_plasmid(name = NULL) + + ggplot2::coord_cartesian() + + ggplot2::scale_y_continuous(limits = NULL) +``` + +![](man/figures/unnamed-chunk-3-1.png) + ## A {ggplot2} Object The result of the call is just a {ggplot2} plot, which you can further diff --git a/man/figures/example-plasmid-1.png b/man/figures/example-plasmid-1.png index 5dda5ab..43e8d05 100644 Binary files a/man/figures/example-plasmid-1.png and b/man/figures/example-plasmid-1.png differ diff --git a/man/figures/example-theme-1.png b/man/figures/example-theme-1.png index 05f566f..73bf3c6 100644 Binary files a/man/figures/example-theme-1.png and b/man/figures/example-theme-1.png differ diff --git a/man/figures/unnamed-chunk-2-1.png b/man/figures/unnamed-chunk-2-1.png index e8b28b0..4335ca9 100644 Binary files a/man/figures/unnamed-chunk-2-1.png and b/man/figures/unnamed-chunk-2-1.png differ diff --git a/man/figures/unnamed-chunk-3-1.png b/man/figures/unnamed-chunk-3-1.png index aaf0bc5..9d68abd 100644 Binary files a/man/figures/unnamed-chunk-3-1.png and b/man/figures/unnamed-chunk-3-1.png differ diff --git a/man/makeContent.fittexttree.Rd b/man/makeContent.fittexttree.Rd deleted file mode 100644 index f578ec6..0000000 --- a/man/makeContent.fittexttree.Rd +++ /dev/null @@ -1,14 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/geom_fit_text.R -\name{makeContent.fittexttree} -\alias{makeContent.fittexttree} -\title{Used Internally} -\usage{ -\method{makeContent}{fittexttree}(x) -} -\arguments{ -\item{x}{Fittexttree, used internally.} -} -\description{ -Used Internally -} diff --git a/renv.lock b/renv.lock index aca72a9..2883f67 100644 --- a/renv.lock +++ b/renv.lock @@ -69,6 +69,24 @@ "Hash": "543776ae6848fde2f48ff3816d0628bc", "Requirements": [] }, + "bit": { + "Package": "bit", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "d242abec29412ce988848d0294b208fd", + "Requirements": [] + }, + "bit64": { + "Package": "bit64", + "Version": "4.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "9fe98599ca456d6552421db0d6772d8f", + "Requirements": [ + "bit" + ] + }, "brio": { "Package": "brio", "Version": "1.1.3", @@ -125,6 +143,14 @@ "Hash": "3177a5a16c243adc199ba33117bd9657", "Requirements": [] }, + "clipr": { + "Package": "clipr", + "Version": "0.8.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "3f038e5ac7f41d4ac41ce658c85e3042", + "Requirements": [] + }, "colorspace": { "Package": "colorspace", "Version": "2.0-3", @@ -133,6 +159,14 @@ "Hash": "bb4341986bc8b914f0f0acf2e4a3f2f7", "Requirements": [] }, + "commonmark": { + "Package": "commonmark", + "Version": "1.9.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "d691c61bff84bd63c383874d2d0c3307", + "Requirements": [] + }, "covr": { "Package": "covr", "Version": "3.6.1", @@ -272,6 +306,19 @@ "Hash": "7c89603d81793f0d5486d91ab1fc6f1d", "Requirements": [] }, + "ggfittext": { + "Package": "ggfittext", + "Version": "0.10.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "bac4a2bf2c6943646aef028c26081e25", + "Requirements": [ + "ggplot2", + "gridtext", + "shades", + "stringi" + ] + }, "ggplot2": { "Package": "ggplot2", "Version": "3.4.0", @@ -314,6 +361,23 @@ "Hash": "4f2596dfb05dac67b9dc558e5c6fba2e", "Requirements": [] }, + "gridtext": { + "Package": "gridtext", + "Version": "0.1.5", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "05e4f5fffb1eecfeaac9ea0b7f255fef", + "Requirements": [ + "Rcpp", + "curl", + "jpeg", + "markdown", + "png", + "rlang", + "stringr", + "xml2" + ] + }, "gtable": { "Package": "gtable", "Version": "0.3.1", @@ -332,6 +396,19 @@ "xfun" ] }, + "hms": { + "Package": "hms", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "b59377caa7ed00fa41808342002138f9", + "Requirements": [ + "lifecycle", + "pkgconfig", + "rlang", + "vctrs" + ] + }, "htmltools": { "Package": "htmltools", "Version": "0.5.4", @@ -368,6 +445,14 @@ "Hash": "0080607b4a1a7b28979aecef976d8bc2", "Requirements": [] }, + "jpeg": { + "Package": "jpeg", + "Version": "0.1-10", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "031a0b683d001a7519202f0628fc0358", + "Requirements": [] + }, "jquerylib": { "Package": "jquerylib", "Version": "0.1.4", @@ -444,6 +529,17 @@ "Hash": "7ce2733a9826b3aeb1775d56fd305472", "Requirements": [] }, + "markdown": { + "Package": "markdown", + "Version": "1.7", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "0ffaea87c070a56d140ce00b0727b278", + "Requirements": [ + "commonmark", + "xfun" + ] + }, "memoise": { "Package": "memoise", "Version": "2.0.1", @@ -574,6 +670,14 @@ "withr" ] }, + "png": { + "Package": "png", + "Version": "0.1-8", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "bd54ba8a0a5faded999a7aab6e46b374", + "Requirements": [] + }, "praise": { "Package": "praise", "Version": "1.0.0", @@ -582,6 +686,14 @@ "Hash": "a555924add98c99d2f411e37e7d25e9f", "Requirements": [] }, + "prettyunits": { + "Package": "prettyunits", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "95ef9167b75dde9d2ccc3c7528393e7e", + "Requirements": [] + }, "processx": { "Package": "processx", "Version": "3.8.0", @@ -593,6 +705,19 @@ "ps" ] }, + "progress": { + "Package": "progress", + "Version": "1.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "14dc9f7a3c91ebb14ec5bb9208a07061", + "Requirements": [ + "R6", + "crayon", + "hms", + "prettyunits" + ] + }, "ps": { "Package": "ps", "Version": "1.7.2", @@ -634,6 +759,26 @@ "Hash": "5e3c5dc0b071b21fa128676560dbe94d", "Requirements": [] }, + "readr": { + "Package": "readr", + "Version": "2.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "b5047343b3825f37ad9d3b5d89aa1078", + "Requirements": [ + "R6", + "cli", + "clipr", + "cpp11", + "crayon", + "hms", + "lifecycle", + "rlang", + "tibble", + "tzdb", + "vroom" + ] + }, "rematch2": { "Package": "rematch2", "Version": "2.1.2", @@ -832,6 +977,21 @@ "vctrs" ] }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "79540e5fcd9e0435af547d885f184fd5", + "Requirements": [ + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ] + }, "tinytex": { "Package": "tinytex", "Version": "0.43", @@ -842,6 +1002,16 @@ "xfun" ] }, + "tzdb": { + "Package": "tzdb", + "Version": "0.4.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "f561504ec2897f4d46f0c7657e488ae1", + "Requirements": [ + "cpp11" + ] + }, "utf8": { "Package": "utf8", "Version": "1.2.2", @@ -871,6 +1041,29 @@ "Hash": "62f4b5da3e08d8e5bcba6cac15603f70", "Requirements": [] }, + "vroom": { + "Package": "vroom", + "Version": "1.6.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "8318e64ffb3a70e652494017ec455561", + "Requirements": [ + "bit64", + "cli", + "cpp11", + "crayon", + "glue", + "hms", + "lifecycle", + "progress", + "rlang", + "tibble", + "tidyselect", + "tzdb", + "vctrs", + "withr" + ] + }, "waldo": { "Package": "waldo", "Version": "0.4.0", @@ -905,10 +1098,10 @@ }, "xfun": { "Package": "xfun", - "Version": "0.36", + "Version": "0.39", "Source": "Repository", "Repository": "CRAN", - "Hash": "f5baec54606751aa53ac9c0e05848ed6", + "Hash": "8f56e9acb54fb525e66464d57ab58bcb", "Requirements": [] }, "xml2": { diff --git a/tests/testthat/test-plot_non-polar.R b/tests/testthat/test-plot_non-polar.R new file mode 100644 index 0000000..2780305 --- /dev/null +++ b/tests/testthat/test-plot_non-polar.R @@ -0,0 +1,17 @@ +test_that("Transform to Non-Polar", { + expect_equal( + { + fl <- system.file('extdata', 'petm20.gb', package = "plasmapR") + + plasmid <- fl |> read_gb() + + dat <- plasmid |> as.data.frame() + + dat[dat$type == "CDS", ] |> + plot_plasmid(name = "pETM-20") + + ggplot2::coord_cartesian() + TRUE + }, + TRUE + ) +})