diff --git a/DESCRIPTION b/DESCRIPTION index 1569566b..2e9c0fe9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -52,11 +52,12 @@ Collate: 'align-gg.R' 'align-group.R' 'align-kmeans.R' - 'align-order.R' 'alignpatch-.R' 'alignpatch-build.R' 'alignpatch-ggplot2.R' - 'align-ranges.R' + 'align-link.R' + 'align-order.R' + 'align-range.R' 'align-reorder.R' 'alignpatch-align_plots.R' 'alignpatch-alignpatches.R' diff --git a/NAMESPACE b/NAMESPACE index 67b9ab42..a6d624bc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,8 @@ S3method("$<-",AlignProto) S3method("+",alignpatches) -S3method("[",alignRangesGtable) +S3method("[",alignLinkGtable) +S3method("[",alignRangeGtable) S3method(.raster_magick,Layer) S3method(.raster_magick,QuadLayout) S3method(.raster_magick,StackLayout) @@ -25,7 +26,8 @@ S3method(alignpatch,Heatmap) S3method(alignpatch,HeatmapAnnotation) S3method(alignpatch,HeatmapList) S3method(alignpatch,LayoutProto) -S3method(alignpatch,align_ranges_plot) +S3method(alignpatch,align_link_plot) +S3method(alignpatch,align_range_plot) S3method(alignpatch,alignpatches) S3method(alignpatch,default) S3method(alignpatch,formula) @@ -152,7 +154,8 @@ S3method(layout_and_add,QuadLayout) S3method(layout_and_add,StackLayout) S3method(layout_subtract,QuadLayout) S3method(layout_subtract,StackLayout) -S3method(makeContent,alignRangesGtable) +S3method(makeContent,alignLinkGtable) +S3method(makeContent,alignRangeGtable) S3method(makeContext,ggalign_raster_magick) S3method(make_wrap,alignpatches) S3method(make_wrap,ggplot) @@ -293,10 +296,11 @@ export(align_gg) export(align_group) export(align_hclust) export(align_kmeans) +export(align_link) export(align_order) export(align_panel) export(align_plots) -export(align_ranges) +export(align_range) export(align_reorder) export(alignpatch) export(anno_bottom) @@ -400,6 +404,7 @@ importFrom(ggplot2,calc_element) importFrom(ggplot2,el_def) importFrom(ggplot2,element_blank) importFrom(ggplot2,element_grob) +importFrom(ggplot2,element_line) importFrom(ggplot2,element_rect) importFrom(ggplot2,element_render) importFrom(ggplot2,find_panel) diff --git a/R/align-ranges.R b/R/align-link.R similarity index 66% rename from R/align-ranges.R rename to R/align-link.R index 8cf471f9..2c555a26 100644 --- a/R/align-ranges.R +++ b/R/align-link.R @@ -1,14 +1,24 @@ -#' Add a plot to annotate a series of ranges of observations +#' Add a plot to annotate selected observations +#' +#' - `align_link`: Annotate a list of spread observations. Observations will be +#' connected to the panel by a line. +#' - `align_range`: Annotate a list of ranges of observations. Observation +#' ranges will be connected to the panel by a polygon. #' #' @inheritParams align_gg -#' @param ranges A list of ranges to be annotated. Each range will be -#' represented by a facet panal. +#' @param ranges A list of observation ranges. Each range will be represented +#' by a facet panel. +#' @param links A list of observations. Each group of observations will be +#' represented by a facet panel. #' @param position Which side the link should be added to? A string containing #' one or more of `r oxford_and(.tlbr)`. For a horizontal [`stack_layout()`], #' only `l` (left) and `r` (right) can be used. For a vertical -#' [`stack_layout()`], only `b` (bottom) and `t` (top) are available. Link -#' ranges can be customized using the `plot.ggalign_ranges` theme element with -#' [`element_polygon()`]. +#' [`stack_layout()`], only `b` (bottom) and `t` (top) are available. +#' +#' - Link ranges can be customized using the `plot.ggalign_ranges` theme element +#' with [`element_polygon()`]. +#' - Link lines can be customized using the `plot.ggalign_links` theme element +#' with [`element_line()`]. #' #' @section ggplot2 specification: #' `align_ranges` initializes a ggplot. @@ -42,9 +52,9 @@ #' (`.panel`, `.index`, `.names`) are added to the data frame. #' #' @export -align_ranges <- function(data = waiver(), mapping = aes(), - ranges = waiver(), position = waiver(), - size = NULL, active = NULL) { +align_link <- function(data = waiver(), mapping = aes(), + links = waiver(), position = waiver(), + size = NULL, active = NULL) { assert_layout_position(position) if (inherits(data, "uneval")) { cli_abort(c( @@ -54,213 +64,36 @@ align_ranges <- function(data = waiver(), mapping = aes(), } assert_active(active) active <- update_active(active, new_active(use = TRUE)) - align(AlignRanges, + align( + new_align_link( + "AlignLink", + arg = "links", + class = "align_link_plot", + element = "plot.ggalign_links" + ), plot = ggplot(mapping = mapping), size = size, data = data, - params = list(ranges = ranges, position = position), + params = list(links = links, position = position), schemes = new_schemes(), active = active ) } -#' @importFrom ggplot2 ggproto ggplot margin element_rect -AlignRanges <- ggproto("AlignRanges", AlignGG, - extra_params = c("ranges", "position"), - setup_params = function(self, nobs, params) { - if (!is.waive(ranges <- .subset2(params, "ranges"))) { - if (!is.list(ranges)) ranges <- list(ranges) - params$ranges <- lapply( - .subset2(params, "ranges"), - function(range) { - ans <- vec_as_location( - unclass(range), - n = nobs, - names = self$labels, - missing = "error", - arg = "ranges", - call = self$call - ) - if (inherits(range, "AsIs")) ans <- I(ans) - ans - } - ) - } - params$margin <- .subset2(params, "margin") %||% margin() - params - }, - setup_plot = function(self, plot, layout_data, layout_coords, layout_name) { - ggadd_default(plot, theme = theme( - panel.border = element_rect(fill = NA, colour = "grey20"), - panel.background = element_rect(fill = "white", colour = NA) - )) - }, - - #' @importFrom stats reorder - build = function(self, plot, coords, extra_coords, previous_coords = NULL) { - params <- .subset2(self, "params") - direction <- self$direction - position <- self$position - axis <- to_coord_axis(direction) - # parse link - support_link <- switch_direction( - direction, c("left", "right"), c("top", "bottom") - ) - if (is.waive(link_position <- .subset2(params, "position"))) { - if (is.null(position)) { - link_position <- support_link - } else { - link_position <- opposite_pos(position) - } - } else if (!is.null(link_position)) { - link_position <- complete_pos(split_position(link_position)) - warn <- setdiff(link_position, support_link) - if (length(warn)) { - cli_warn(sprintf("Cannot add link ranges in {.field %s}", warn)) - } - link_position <- intersect(link_position, support_link) - if (length(link_position) == 0L) link_position <- NULL - } - - # parse ranges - panel <- .subset2(coords, "panel") - index <- .subset2(coords, "index") - subset <- seq_along(index) # used to match the original data - full_breaks <- split(subset, panel) - if (is.waive(ranges <- .subset2(params, "ranges"))) { - breaks <- full_breaks - } else { - breaks <- lapply(ranges, function(range) { - if (!inherits(range, "AsIs")) { # match the original data index - range <- match(range, index) - } - subset[range] - }) - } - subset <- unlist(breaks, FALSE, FALSE) - - # prepare data for the plot ------------------------------ - plot_panel <- names(breaks) %||% seq_along(breaks) - plot_data <- data_frame0(.panel = factor( - vec_rep_each(plot_panel, list_sizes(breaks)), unique(plot_panel) - )) - plot_data$.index <- index[subset] - if (!is.null(self$labels)) { - plot_data[[".names"]] <- .subset( - self$labels, - .subset2(plot_data, ".index") - ) - } - if (!is.null(data <- .subset2(self, "data"))) { - plot_data <- inner_join(plot_data, data, by = ".index") - } - plot$data <- ggalign_attr_restore(plot_data, data) - - # set up facets - if (length(breaks) > 1L) { - default_facet <- switch_direction( - direction, - ggplot2::facet_wrap( - facets = ggplot2::vars(.data$.panel), - ncol = 1L, as.table = FALSE - ), - ggplot2::facet_wrap( - facets = ggplot2::vars(.data$.panel), - nrow = 1L, as.table = FALSE - ) - ) - } else { - default_facet <- ggplot2::facet_null() - } - plot <- plot + align_melt_facet(default_facet, plot$facet) - if (!is.null(link_position)) { - plot$align_ranges_data <- list( - full_breaks = full_breaks, - breaks = breaks, direction = direction, - link_position = link_position - ) - plot <- add_class(plot, "align_ranges_plot", "patch_ggplot") - } - plot - }, - finish_plot = function(self, plot, schemes, theme) { - plot <- plot_add_schemes(plot, schemes) - if (inherits(plot, "align_ranges_plot")) { - element <- calc_element( - "plot.ggalign_ranges", - complete_theme(plot$theme) - ) - if (inherits(element, "element_blank")) { - class(plot) <- setdiff(class(plot), "align_ranges_plot") - plot$align_ranges_data <- NULL - } else { - # save spacing for usage - plot$align_ranges_data$spacing <- calc_element( - switch_direction( - self$direction, - "panel.spacing.y", - "panel.spacing.x" - ), - complete_theme(theme) - ) %||% unit(0, "mm") - plot$align_ranges_data$element <- element - } - } - plot - } -) - #' @importFrom ggplot2 ggproto #' @export -alignpatch.align_ranges_plot <- function(x) { - ggproto(NULL, PatchAlignRangesPlot, plot = x) +alignpatch.align_link_plot <- function(x) { + ggproto(NULL, PatchAlignLinkPlot, plot = x) } #' @export -`[.alignRangesGtable` <- function(x, i, j) { - # subset will violate the `alignRangesGtable` `shape` +`[.alignLinkGtable` <- function(x, i, j) { + # subset will violate the `alignLinkGtable` `shape` # we always use the next method - class(x) <- setdiff(class(x), "alignRangesGtable") - x$align_ranges_data <- NULL + class(x) <- setdiff(class(x), "alignLinkGtable") + x$links_data <- NULL NextMethod() } -#' @include alignpatch-ggplot2.R -PatchAlignRangesPlot <- ggproto( - "PatchAlignRangesPlot", PatchGgplot, - patch_gtable = function(self, plot = self$plot) { - ans <- ggproto_parent(PatchGgplot, self)$patch_gtable(plot = plot) - # re-define the draw method, we assign new class - ans <- add_class(ans, "alignRangesGtable") - ans$align_ranges_data <- .subset2(plot, "align_ranges_data") - ans - }, - add_plot = function(self, gt, plot, t, l, b, r, name, z = 2L) { - gtable_add_grob( - gt, - grobs = plot, - # t = 8, l = 6, b = 14, r = 12 - # t = t + 7L, l = l + 5L, b = b - 6L, r = r - 5L, - t = t + TOP_BORDER, l = l + LEFT_BORDER, - name = name, z = z - ) - }, - add_background = function(self, gt, bg, t, l, b, r, name, z = 1L) { - gtable_add_grob( - gt, - grobs = bg, - t = t + TOP_BORDER, l = l + LEFT_BORDER, - name = name, z = z - ) - }, - get_sizes = function(self, free = NULL, gt = self$gt) { - PatchGgplot$get_sizes(.tlbr, gt = gt) - }, - align_border = function(self, t = NULL, l = NULL, b = NULL, r = NULL, - gt = self$gt) { - gt # free from alignment - } -) - # preDraw: # - makeContext # - pushvpgp @@ -272,7 +105,7 @@ PatchAlignRangesPlot <- ggproto( # - popgrobvp #' @importFrom grid makeContent unit convertHeight convertWidth viewport #' @export -makeContent.alignRangesGtable <- function(x) { +makeContent.alignLinkGtable <- function(x) { # Grab viewport information width <- convertWidth(unit(1, "npc"), "mm", valueOnly = TRUE) height <- convertHeight(unit(1, "npc"), "mm", valueOnly = TRUE) @@ -286,13 +119,13 @@ makeContent.alignRangesGtable <- function(x) { valueOnly = TRUE ) panel_loc <- find_panel(x) - range_data <- .subset2(x, "align_ranges_data") - breaks <- .subset2(range_data, "breaks") - link_position <- .subset2(range_data, "link_position") - full_breaks <- .subset2(range_data, "full_breaks") - direction <- .subset2(range_data, "direction") + links_data <- .subset2(x, "links_data") + breaks <- .subset2(links_data, "breaks") + link_position <- .subset2(links_data, "link_position") + full_breaks <- .subset2(links_data, "full_breaks") + direction <- .subset2(links_data, "direction") spacing <- convertHeight( - .subset2(range_data, "spacing"), "mm", + .subset2(links_data, "spacing"), "mm", valueOnly = TRUE ) @@ -309,18 +142,18 @@ makeContent.alignRangesGtable <- function(x) { if (is_horizontal(direction)) { # the link should be in left or right sizes[!is.na(obs)] <- (height - spacing * n_spacing) / sum(lengths(full_breaks)) # nobs - cum_sizes <- cumsum(sizes) + points_y <- cumsum(sizes) - (sizes / 2L) # from bottom to the top, following the ordering of the `breaks` panel_index <- seq( from = .subset2(panel_loc, "b"), to = .subset2(panel_loc, "t"), length.out = length(breaks) ) + l_border <- plot_widths[seq_len(.subset2(panel_loc, "l") - 1L)] + r_border <- plot_widths[-seq_len(.subset2(panel_loc, "r"))] # we'll reverse the `plot_cum_heights`, so the ordering index should # also be reversed panel_index <- nrow(x) - panel_index + 1L - l_border <- plot_widths[seq_len(.subset2(panel_loc, "l") - 1L)] - r_border <- plot_widths[-seq_len(.subset2(panel_loc, "r"))] # for a gtable, heights are from top to the bottom, # we reverse the heights plot_cum_heights <- cumsum(rev(plot_heights)) @@ -328,25 +161,28 @@ makeContent.alignRangesGtable <- function(x) { for (i in seq_along(panel_index)) { # we match the observations pos <- match(.subset2(breaks, i), obs) - coord_y <- c( - coord_y, - # for height next to the plot panel - plot_cum_heights[panel_index[i] + (-1:0)], - # for height in the border - c( - cum_sizes[max(pos)], - cum_sizes[min(pos)] - sizes[min(pos)] - ) - ) + # we arrange pos from lower to higher + coord_y <- c(coord_y, vec_interleave( + # seq( + # from = plot_cum_heights[panel_index[i] - 1L], + # to = plot_cum_heights[panel_index[i]], + # length.out = length(pos) + # )[seq_along(pos)[order(pos)]], + vec_rep( # we always link the midpoint + mean(plot_cum_heights[panel_index[i] + (-1:0)]), + length(pos) + ), + points_y[pos] + )) if (position == "left") { coord_x <- c( coord_x, - vec_rep_each(c(sum(l_border), 0), 2L) + vec_rep(c(sum(l_border), 0), length(pos)) ) } else { coord_x <- c( coord_x, - vec_rep_each(c(width - sum(r_border), width), 2L) + vec_rep(c(width - sum(r_border), width), length(pos)) ) } } @@ -354,7 +190,7 @@ makeContent.alignRangesGtable <- function(x) { } else { sizes[!is.na(obs)] <- (width - spacing * n_spacing) / sum(lengths(full_breaks)) # nobs - cum_sizes <- cumsum(sizes) + points_x <- cumsum(sizes) - (sizes / 2) panel_index <- seq( from = .subset2(panel_loc, "l"), to = .subset2(panel_loc, "r"), @@ -367,39 +203,40 @@ makeContent.alignRangesGtable <- function(x) { for (i in seq_along(panel_index)) { # we match the observations pos <- match(.subset2(breaks, i), obs) - coord_x <- c( - coord_x, - # for width next to the plot panel - plot_cum_widths[panel_index[i] + (-1:0)], - # for width in the border - c( - cum_sizes[max(pos)], - cum_sizes[min(pos)] - sizes[min(pos)] - ) - ) + coord_x <- c(coord_x, vec_interleave( + # seq( + # from = plot_cum_widths[panel_index[i] - 1L], + # to = plot_cum_widths[panel_index[i]], + # length.out = length(pos) + # )[seq_along(pos)[order(pos)]], + vec_rep( # we always link the midpoint + mean(plot_cum_widths[panel_index[i] + (-1:0)]), + length(pos) + ), + points_x[pos] + )) if (position == "bottom") { coord_y <- c( coord_y, - vec_rep_each(c(sum(b_border), 0), 2L) + vec_rep(c(sum(b_border), 0), length(pos)) ) } else { coord_y <- c( coord_y, - vec_rep_each(c(height - sum(t_border), height), 2L) + vec_rep(c(height - sum(t_border), height), length(pos)) ) } } } } - layout <- .subset2(x, "layout") panels <- layout[grepl("^panel", .subset2(layout, "name")), , drop = FALSE] x <- gtable_add_grob( x, grobs = ggplot2::element_grob( - .subset2(range_data, "element"), + .subset2(links_data, "element"), x = coord_x, y = coord_y, - id.lengths = vec_rep(4L, length(coord_x) / 4L), + id.lengths = vec_rep(2L, length(coord_x) / 2L), default.units = "mm" ), t = 1L, l = 1L, b = -1L, r = -1L, @@ -408,3 +245,195 @@ makeContent.alignRangesGtable <- function(x) { ) NextMethod() } + +#################################################################### +#' @importFrom ggplot2 ggproto +new_align_link <- function(`_class` = NULL, arg, class, element, ...) { + ggproto(`_class`, AlignLinkProto, + extra_params = c(arg, "position"), + arg = arg, class = class, element = element, ... + ) +} + +#' @importFrom ggplot2 ggproto ggproto_parent +#' @include alignpatch-ggplot2.R +PatchAlignLinkProto <- ggproto( + "PatchAlignLinkProto", PatchGgplot, + add_plot = function(self, gt, plot, t, l, b, r, name, z = 2L) { + gtable_add_grob( + gt, + grobs = plot, + # t = 8, l = 6, b = 14, r = 12 + # t = t + 7L, l = l + 5L, b = b - 6L, r = r - 5L, + t = t + TOP_BORDER, l = l + LEFT_BORDER, + name = name, z = z + ) + }, + add_background = function(self, gt, bg, t, l, b, r, name, z = 1L) { + gtable_add_grob( + gt, + grobs = bg, + t = t + TOP_BORDER, l = l + LEFT_BORDER, + name = name, z = z + ) + }, + get_sizes = function(self, free = NULL, gt = self$gt) { + PatchGgplot$get_sizes(.tlbr, gt = gt) + }, + align_border = function(self, t = NULL, l = NULL, b = NULL, r = NULL, + gt = self$gt) { + gt # free from alignment + } +) + +PatchAlignLinkPlot <- ggproto( + "PatchAlignLinkPlot", PatchAlignLinkProto, + patch_gtable = function(self, plot = self$plot) { + ans <- ggproto_parent(PatchAlignLinkProto, self)$patch_gtable( + plot = plot + ) + # re-define the draw method, we assign new class + ans <- add_class(ans, "alignLinkGtable") + ans$links_data <- .subset2(plot, "links_data") + ans + } +) + +#' @importFrom ggplot2 ggproto ggplot margin element_rect +AlignLinkProto <- ggproto("AlignLinkProto", AlignGG, + class = NULL, element = NULL, + finish_plot = function(self, plot, schemes, theme) { + plot <- plot_add_schemes(plot, schemes) + if (inherits(plot, self$class)) { + element <- calc_element(self$element, complete_theme(plot$theme)) + if (inherits(element, "element_blank")) { + class(plot) <- setdiff(class(plot), self$class) + plot$links_data <- NULL + } else { + # save spacing for usage + plot$links_data$spacing <- calc_element( + switch_direction( + self$direction, + "panel.spacing.y", + "panel.spacing.x" + ), + complete_theme(theme) + ) %||% unit(0, "mm") + plot$links_data$element <- element + } + } + plot + }, + setup_params = function(self, nobs, params) { + if (!is.waive(x <- .subset2(params, self$arg))) { + if (!is.list(x)) x <- list(x) + params[[self$arg]] <- lapply(x, function(link) { + ans <- vec_as_location( + unclass(link), + n = nobs, + names = self$labels, + missing = "error", + arg = self$arg, + call = self$call + ) + if (inherits(link, "AsIs")) ans <- I(ans) + ans + }) + } + params + }, + setup_plot = function(self, plot, layout_data, layout_coords, layout_name) { + ggadd_default(plot, theme = theme( + panel.border = element_rect(fill = NA, colour = "grey20"), + panel.background = element_rect(fill = "white", colour = NA) + )) + }, + + #' @importFrom stats reorder + build = function(self, plot, coords, extra_coords, previous_coords = NULL) { + params <- .subset2(self, "params") + direction <- self$direction + position <- self$position + axis <- to_coord_axis(direction) + # parse link position + support_link <- switch_direction( + direction, c("left", "right"), c("bottom", "top") + ) + if (is.waive(link_position <- .subset2(params, "position"))) { + if (is.null(position)) { + link_position <- support_link + } else { + link_position <- opposite_pos(position) + } + } else if (!is.null(link_position)) { + link_position <- complete_pos(split_position(link_position)) + warn <- setdiff(link_position, support_link) + if (length(warn)) { + cli_warn(sprintf("Cannot add links in {.field %s}", warn)) + } + link_position <- intersect(link_position, support_link) + if (length(link_position) == 0L) link_position <- NULL + } + + # parse ranges + panel <- .subset2(coords, "panel") + index <- .subset2(coords, "index") + subset <- seq_along(index) # used to match the original data + full_breaks <- split(subset, panel) + if (is.waive(data <- .subset2(params, self$arg))) { + breaks <- full_breaks + } else { + breaks <- lapply(data, function(link) { + if (!inherits(link, "AsIs")) { # match the original data index + link <- match(link, index) + } + subset[link] + }) + } + subset <- unlist(breaks, FALSE, FALSE) + + # prepare data for the plot ------------------------------ + plot_panel <- names(breaks) %||% seq_along(breaks) + plot_data <- data_frame0(.panel = factor( + vec_rep_each(plot_panel, list_sizes(breaks)), unique(plot_panel) + )) + plot_data$.index <- index[subset] + if (!is.null(self$labels)) { + plot_data[[".names"]] <- .subset( + self$labels, + .subset2(plot_data, ".index") + ) + } + if (!is.null(data <- .subset2(self, "data"))) { + plot_data <- inner_join(plot_data, data, by = ".index") + } + plot$data <- ggalign_attr_restore(plot_data, data) + + # set up facets + if (length(breaks) > 1L) { + default_facet <- switch_direction( + direction, + ggplot2::facet_wrap( + facets = ggplot2::vars(.data$.panel), + ncol = 1L, as.table = FALSE + ), + ggplot2::facet_wrap( + facets = ggplot2::vars(.data$.panel), + nrow = 1L, as.table = FALSE + ) + ) + } else { + default_facet <- ggplot2::facet_null() + } + plot <- plot + align_melt_facet(default_facet, plot$facet) + if (!is.null(link_position)) { + plot$links_data <- list( + full_breaks = full_breaks, + breaks = breaks, direction = direction, + link_position = link_position + ) + plot <- add_class(plot, self$class, "patch_ggplot") + } + plot + } +) diff --git a/R/align-range.R b/R/align-range.R new file mode 100644 index 00000000..43b3f732 --- /dev/null +++ b/R/align-range.R @@ -0,0 +1,205 @@ +#' @export +#' @rdname align_link +align_range <- function(data = waiver(), mapping = aes(), + ranges = waiver(), position = waiver(), + size = NULL, active = NULL) { + assert_layout_position(position) + if (inherits(data, "uneval")) { + cli_abort(c( + "{.arg data} cannot be {.obj_type_friendly {data}}", + "i" = "Have you misspelled the {.arg data} argument in {.fn ggalign}" + )) + } + assert_active(active) + active <- update_active(active, new_active(use = TRUE)) + align( + new_align_link( + "AlignRange", + arg = "ranges", + class = "align_range_plot", + element = "plot.ggalign_ranges" + ), + plot = ggplot(mapping = mapping), + size = size, data = data, + params = list(ranges = ranges, position = position), + schemes = new_schemes(), + active = active + ) +} + +#' @importFrom ggplot2 ggproto +#' @export +alignpatch.align_range_plot <- function(x) { + ggproto(NULL, PatchAlignRangePlot, plot = x) +} + +#' @export +`[.alignRangeGtable` <- function(x, i, j) { + # subset will violate the `alignRangeGtable` `shape` + # we always use the next method + class(x) <- setdiff(class(x), "alignRangeGtable") + x$links_data <- NULL + NextMethod() +} + +#' @include align-link.R +PatchAlignRangePlot <- ggproto( + "PatchAlignRangePlot", PatchAlignLinkProto, + patch_gtable = function(self, plot = self$plot) { + ans <- ggproto_parent(PatchAlignLinkProto, self)$patch_gtable( + plot = plot + ) + # re-define the draw method, we assign new class + ans <- add_class(ans, "alignRangeGtable") + ans$links_data <- .subset2(plot, "links_data") + ans + } +) + +# preDraw: +# - makeContext +# - pushvpgp +# - preDrawDetails: by default, do noting +# makeContent: +# drawDetails: +# postDraw: +# - postDrawDetails: by default, do noting +# - popgrobvp +#' @importFrom grid makeContent unit convertHeight convertWidth viewport +#' @export +makeContent.alignRangeGtable <- function(x) { + # Grab viewport information + width <- convertWidth(unit(1, "npc"), "mm", valueOnly = TRUE) + height <- convertHeight(unit(1, "npc"), "mm", valueOnly = TRUE) + + # Grab grob metadata + plot_widths <- compute_null_width(.subset2(x, "widths"), + valueOnly = TRUE + ) + # from top to the bottom + plot_heights <- compute_null_height(.subset2(x, "heights"), + valueOnly = TRUE + ) + panel_loc <- find_panel(x) + range_data <- .subset2(x, "links_data") + breaks <- .subset2(range_data, "breaks") + link_position <- .subset2(range_data, "link_position") + full_breaks <- .subset2(range_data, "full_breaks") + direction <- .subset2(range_data, "direction") + spacing <- convertHeight( + .subset2(range_data, "spacing"), "mm", + valueOnly = TRUE + ) + + # each break represent an `observation`, for panel space, we use `NA` + # obs arranged from left to top, and from bottom to top + obs <- unlist(vec_interleave(full_breaks, list(NA)), FALSE, FALSE) + obs <- obs[-length(obs)] # remove the last panel space, shouldn't exist + sizes <- numeric(length(obs)) + sizes[is.na(obs)] <- spacing + n_spacing <- length(full_breaks) - 1L + + # then, we define the link grobs + coord_x <- coord_y <- numeric() + if (is_horizontal(direction)) { # the link should be in left or right + sizes[!is.na(obs)] <- (height - spacing * n_spacing) / + sum(lengths(full_breaks)) # nobs + cum_sizes <- cumsum(sizes) + # from bottom to the top, following the ordering of the `breaks` + panel_index <- seq( + from = .subset2(panel_loc, "b"), + to = .subset2(panel_loc, "t"), + length.out = length(breaks) + ) + # we'll reverse the `plot_cum_heights`, so the ordering index should + # also be reversed + panel_index <- nrow(x) - panel_index + 1L + l_border <- plot_widths[seq_len(.subset2(panel_loc, "l") - 1L)] + r_border <- plot_widths[-seq_len(.subset2(panel_loc, "r"))] + # for a gtable, heights are from top to the bottom, + # we reverse the heights + plot_cum_heights <- cumsum(rev(plot_heights)) + for (position in link_position) { + for (i in seq_along(panel_index)) { + # we match the observations + pos <- match(.subset2(breaks, i), obs) + coord_y <- c( + coord_y, + # for height next to the plot panel + plot_cum_heights[panel_index[i] + (-1:0)], + # for height in the border + c( + cum_sizes[max(pos)], + cum_sizes[min(pos)] - sizes[min(pos)] + ) + ) + if (position == "left") { + coord_x <- c( + coord_x, + vec_rep_each(c(sum(l_border), 0), 2L) + ) + } else { + coord_x <- c( + coord_x, + vec_rep_each(c(width - sum(r_border), width), 2L) + ) + } + } + } + } else { + sizes[!is.na(obs)] <- (width - spacing * n_spacing) / + sum(lengths(full_breaks)) # nobs + cum_sizes <- cumsum(sizes) + panel_index <- seq( + from = .subset2(panel_loc, "l"), + to = .subset2(panel_loc, "r"), + length.out = length(breaks) + ) + t_border <- plot_heights[seq_len(.subset2(panel_loc, "t") - 1L)] + b_border <- plot_heights[-seq_len(.subset2(panel_loc, "b"))] + plot_cum_widths <- cumsum(plot_widths) + for (position in link_position) { + for (i in seq_along(panel_index)) { + # we match the observations + pos <- match(.subset2(breaks, i), obs) + coord_x <- c( + coord_x, + # for width next to the plot panel + plot_cum_widths[panel_index[i] + (-1:0)], + # for width in the border + c( + cum_sizes[max(pos)], + cum_sizes[min(pos)] - sizes[min(pos)] + ) + ) + if (position == "bottom") { + coord_y <- c( + coord_y, + vec_rep_each(c(sum(b_border), 0), 2L) + ) + } else { + coord_y <- c( + coord_y, + vec_rep_each(c(height - sum(t_border), height), 2L) + ) + } + } + } + } + + layout <- .subset2(x, "layout") + panels <- layout[grepl("^panel", .subset2(layout, "name")), , drop = FALSE] + x <- gtable_add_grob( + x, + grobs = ggplot2::element_grob( + .subset2(range_data, "element"), + x = coord_x, y = coord_y, + id.lengths = vec_rep(4L, length(coord_x) / 4L), + default.units = "mm" + ), + t = 1L, l = 1L, b = -1L, r = -1L, + # always draw with panel area + z = min(panels$z) + ) + NextMethod() +} diff --git a/R/ggplot-theme.R b/R/ggplot-theme.R index 9de9fda2..d4e0461b 100644 --- a/R/ggplot-theme.R +++ b/R/ggplot-theme.R @@ -164,7 +164,7 @@ element_grob.element_polygon <- function(element, x, y, ) } -#' @importFrom ggplot2 register_theme_elements el_def +#' @importFrom ggplot2 register_theme_elements el_def element_line theme_elements <- function() { register_theme_elements( plot.ggalign_ranges = element_polygon( @@ -173,6 +173,12 @@ theme_elements <- function() { linewidth = 0.5, linetype = 1 ), + plot.ggalign_links = element_line( + color = "black", + linewidth = 0.5, + linetype = 1, + lineend = "butt" + ), element_tree = list( plot.patch_title = el_def("element_text", "text"), plot.patch_title.top = el_def("element_text", "text"), @@ -184,7 +190,8 @@ theme_elements <- function() { plot.patch_title.position.left = el_def("character"), plot.patch_title.position.bottom = el_def("character"), plot.patch_title.position.right = el_def("character"), - plot.ggalign_ranges = el_def("element_polygon") + plot.ggalign_ranges = el_def("element_polygon"), + plot.ggalign_links = el_def("element_line") ) ) } diff --git a/_pkgdown.yml b/_pkgdown.yml index 68656893..27a214c1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -66,7 +66,7 @@ reference: - ggalign - ggfree - align_dendro - - align_ranges + - align_link - title: Schemes desc: > diff --git a/man/align_ranges.Rd b/man/align_link.Rd similarity index 71% rename from man/align_ranges.Rd rename to man/align_link.Rd index 3d2c4862..6c13691f 100644 --- a/man/align_ranges.Rd +++ b/man/align_link.Rd @@ -1,10 +1,20 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/align-ranges.R -\name{align_ranges} -\alias{align_ranges} -\title{Add a plot to annotate a series of ranges of observations} +% Please edit documentation in R/align-link.R, R/align-range.R +\name{align_link} +\alias{align_link} +\alias{align_range} +\title{Add a plot to annotate selected observations} \usage{ -align_ranges( +align_link( + data = waiver(), + mapping = aes(), + links = waiver(), + position = waiver(), + size = NULL, + active = NULL +) + +align_range( data = waiver(), mapping = aes(), ranges = waiver(), @@ -27,24 +37,36 @@ plot data, please use \code{\link[=scheme_data]{scheme_data()}}. \item{mapping}{Default list of aesthetic mappings to use for plot. If not specified, must be supplied in each layer added to the plot.} -\item{ranges}{A list of ranges to be annotated. Each range will be -represented by a facet panal.} +\item{links}{A list of observations. Each group of observations will be +represented by a facet panel.} \item{position}{Which side the link should be added to? A string containing one or more of \code{"t"}, \code{"l"}, \code{"b"}, and \code{"r"}. For a horizontal \code{\link[=stack_layout]{stack_layout()}}, only \code{l} (left) and \code{r} (right) can be used. For a vertical -\code{\link[=stack_layout]{stack_layout()}}, only \code{b} (bottom) and \code{t} (top) are available. Link -ranges can be customized using the \code{plot.ggalign_ranges} theme element with -\code{\link[=element_polygon]{element_polygon()}}.} +\code{\link[=stack_layout]{stack_layout()}}, only \code{b} (bottom) and \code{t} (top) are available. +\itemize{ +\item Link ranges can be customized using the \code{plot.ggalign_ranges} theme element +with \code{\link[=element_polygon]{element_polygon()}}. +\item Link lines can be customized using the \code{plot.ggalign_links} theme element +with \code{\link[=element_line]{element_line()}}. +}} \item{size}{The relative size of the plot, can be specified as a \code{\link[grid:unit]{unit}}.} \item{active}{A \code{\link[=active]{active()}} object that defines the context settings when added to a layout.} + +\item{ranges}{A list of observation ranges. Each range will be represented +by a facet panel.} } \description{ -Add a plot to annotate a series of ranges of observations +\itemize{ +\item \code{align_link}: Annotate a list of spread observations. Observations will be +connected to the panel by a line. +\item \code{align_range}: Annotate a list of ranges of observations. Observation +ranges will be connected to the panel by a polygon. +} } \section{ggplot2 specification}{