R/coord-radial.R

Defines functions validate_polar_guide theta_grid flip_data_text_angle flip_text_angle rotate_r_axis deg2rad rad2deg in_arc polar_bbox view_scales_polar coord_radial

Documented in coord_radial

#' Polar coordinates
#'
#' The polar coordinate system is most commonly used for pie charts, which
#' are a stacked bar chart in polar coordinates. \cr
#' `r lifecycle::badge("superseded")`: `coord_polar()` has been in favour of
#' `coord_radial()`.
#'
#' @param theta variable to map angle to (`x` or `y`)
#' @param start Offset of starting point from 12 o'clock in radians. Offset
#'   is applied clockwise or anticlockwise depending on value of `direction`.
#' @param direction 1, clockwise; -1, anticlockwise
#' @param clip Should drawing be clipped to the extent of the plot panel? A
#'   setting of `"on"` (the default) means yes, and a setting of `"off"`
#'   means no. For details, please see [`coord_cartesian()`].
#' @param end Position from 12 o'clock in radians where plot ends, to allow
#'   for partial polar coordinates. The default, `NULL`, is set to
#'   `start + 2 * pi`.
#' @param thetalim,rlim Limits for the theta and r axes.
#' @param expand If `TRUE`, the default, adds a small expansion factor to
#'   the limits to prevent overlap between data and axes. If `FALSE`, limits
#'   are taken directly from the scale.
#' @param r.axis.inside One of the following:
#'   * `NULL` (default) places the axis next to the panel if `start` and
#'     `end` arguments form a full circle and inside the panel otherwise.
#'   * `TRUE` to place the radius axis inside the panel.
#'   * `FALSE` to place the radius axis next to the panel.
#'   * A numeric value, setting a theta axis value at which
#'     the axis should be placed inside the panel. Can be given as a length 2
#'     vector to control primary and secondary axis placement separately.
#' @param rotate.angle If `TRUE`, transforms the `angle` aesthetic in data
#'   in accordance with the computed `theta` position. If `FALSE` (default),
#'   no such transformation is performed. Can be useful to rotate text geoms in
#'   alignment with the coordinates.
#' @param inner.radius A `numeric` between 0 and 1 setting the size of a
#'   inner radius hole.
#' @param reverse A string giving which directions to reverse. `"none"`
#'   (default) keep directions as is. `"theta"` reverses the angle and `"r"`
#'   reverses the radius. `"thetar"` reverses both the angle and the radius.
#' @param r_axis_inside,rotate_angle `r lifecycle::badge("deprecated")`
#' @export
#' @note
#' In `coord_radial()`, position guides can be defined by using
#' `guides(r = ..., theta = ..., r.sec = ..., theta.sec = ...)`. Note that
#' these guides require `r` and `theta` as available aesthetics. The classic
#' [guide_axis()] can be used for the `r` positions and [guide_axis_theta()] can
#' be used for the `theta` positions. Using the `theta.sec` position is only
#' sensible when `inner.radius > 0`.
#'
#' @seealso
#' The `r link_book("polar coordinates section", "coord#polar-coordinates-with-coord_polar")`
#' @examples
#' # NOTE: Use these plots with caution - polar coordinates has
#' # major perceptual problems.  The main point of these examples is
#' # to demonstrate how these common plots can be described in the
#' # grammar.  Use with EXTREME caution.
#'
#' # A pie chart = stacked bar chart + polar coordinates
#' pie <- ggplot(mtcars, aes(x = factor(1), fill = factor(cyl))) +
#'  geom_bar(width = 1)
#' pie + coord_radial(theta = "y", expand = FALSE)
#'
#' \donttest{
#'
#' # A coxcomb plot = bar chart + polar coordinates
#' cxc <- ggplot(mtcars, aes(x = factor(cyl))) +
#'   geom_bar(width = 1, colour = "black")
#' cxc + coord_radial(expand = FALSE)
#' # A new type of plot?
#' cxc + coord_radial(theta = "y", expand = FALSE)
#'
#' # The bullseye chart
#' pie + coord_radial(expand = FALSE)
#'
#' # Hadley's favourite pie chart
#' df <- data.frame(
#'   variable = c("does not resemble", "resembles"),
#'   value = c(20, 80)
#' )
#' ggplot(df, aes(x = "", y = value, fill = variable)) +
#'   geom_col(width = 1) +
#'   scale_fill_manual(values = c("red", "yellow")) +
#'   coord_radial("y", start = pi / 3, expand =  FALSE) +
#'   labs(title = "Pac man")
#'
#' # Windrose + doughnut plot
#' if (require("ggplot2movies")) {
#' movies$rrating <- cut_interval(movies$rating, length = 1)
#' movies$budgetq <- cut_number(movies$budget, 4)
#'
#' doh <- ggplot(movies, aes(x = rrating, fill = budgetq))
#'
#' # Wind rose
#' doh + geom_bar(width = 1) + coord_radial(expand = FALSE)
#' # Race track plot
#' doh + geom_bar(width = 0.9, position = "fill") +
#'   coord_radial(theta = "y", expand = FALSE)
#' }
#' }
#' # A partial polar plot
#' ggplot(mtcars, aes(disp, mpg)) +
#'   geom_point() +
#'   coord_radial(start = -0.4 * pi, end = 0.4 * pi, inner.radius = 0.3)
#'
#' # Similar with coord_cartesian(), you can set limits.
#' ggplot(mtcars, aes(disp, mpg)) +
#'   geom_point() +
#'   coord_radial(
#'     start = -0.4 * pi,
#'     end = 0.4 * pi, inner.radius = 0.3,
#'     thetalim = c(200, 300),
#'     rlim = c(15, 30),
#'   )
coord_radial <- function(theta = "x",
                         start = 0, end = NULL,
                         thetalim = NULL, rlim = NULL, expand = TRUE,
                         direction = deprecated(),
                         clip = "off",
                         r.axis.inside = NULL,
                         rotate.angle = FALSE,
                         inner.radius = 0,
                         reverse = "none",
                         r_axis_inside = deprecated(),
                         rotate_angle = deprecated()) {

  if (lifecycle::is_present(r_axis_inside)) {
    deprecate_warn0(
      "3.5.1", "coord_radial(r_axis_inside)", "coord_radial(r.axis.inside)"
    )
    r.axis.inside <- r_axis_inside
  }
  if (lifecycle::is_present(rotate_angle)) {
    deprecate_warn0(
      "3.5.1", "coord_radial(rotate_angle)", "coord_radial(rotate.angle)"
    )
    rotate.angle <- rotate_angle
  }
  if (lifecycle::is_present(direction)) {
    deprecate_warn0(
      "3.5.2", "coord_radial(direction)", "coord_radial(reverse)"
    )
    reverse <- switch(reverse, "r" = "thetar", "theta")
  }

  theta <- arg_match0(theta, c("x", "y"))
  r <- if (theta == "x") "y" else "x"
  if (!is.numeric(r.axis.inside)) {
    check_bool(r.axis.inside, allow_null = TRUE)
  }
  reverse <- arg_match0(reverse, c("theta", "thetar", "r", "none"))

  check_bool(rotate.angle)
  check_number_decimal(start, allow_infinite = FALSE)
  check_number_decimal(end, allow_infinite = FALSE, allow_null = TRUE)
  check_number_decimal(inner.radius, min = 0, max = 1, allow_infinite = FALSE)

  arc <- c(start, end %||% (start + 2 * pi))
  if (arc[1] > arc[2]) {
    n_rotate <- ((arc[1] - arc[2]) %/% (2 * pi)) + 1
    arc[1] <- arc[1] - n_rotate * 2 * pi
  }
  arc <- switch(reverse, thetar = , theta = rev(arc), arc)

  r.axis.inside <- r.axis.inside %||% !(abs(arc[2] - arc[1]) >= 1.999 * pi)
  if (isFALSE(r.axis.inside)) {
    place <- in_arc(c(0, 0.5, 1, 1.5) * pi, arc)
    if (!any(place)) {
      cli::cli_warn(c(
        "No appropriate placement found for outside {.field r.axis}.",
        i = "Will use {.code r.axis.inside = TRUE} instead"
      ))
      r.axis.inside <- TRUE
    }
  }

  inner.radius <- c(inner.radius, 1) * 0.4
  inner.radius <- switch(reverse, thetar = , r = rev, identity)(inner.radius)

  ggproto(NULL, CoordRadial,
    limits = list(theta = thetalim, r = rlim),
    theta = theta,
    r = r,
    arc = arc,
    expand = expand,
    reverse = reverse,
    r_axis_inside = r.axis.inside,
    rotate_angle = rotate.angle,
    inner_radius = inner.radius,
    clip = clip
  )
}

#' @rdname Coord
#' @format NULL
#' @usage NULL
#' @export
CoordRadial <- ggproto("CoordRadial", Coord,

  aspect = function(details) {
    diff(details$bbox$y) / diff(details$bbox$x)
  },

  is_free = function() {
    TRUE
  },

  distance = function(self, x, y, details, boost = 0.75) {
    arc <- details$arc %||% c(0, 2 * pi)
    if (self$theta == "x") {
      r <- rescale(y, from = details$r.range, to = self$inner_radius / 0.4)
      theta <- theta_rescale_no_clip(x, details$theta.range, arc)
    } else {
      r <- rescale(x, from = details$r.range, to = self$inner_radius / 0.4)
      theta <- theta_rescale_no_clip(y, details$theta.range, arc)
    }
    # The ^boost boosts detailed munching when r is small
    dist_polar(r^boost, theta)
  },

  backtransform_range = function(self, panel_params) {
    self$range(panel_params)
  },

  range = function(self, panel_params) {
    # summarise_layout() expects that the x and y ranges here
    # match the setting from self$theta and self$r
    setNames(
      list(panel_params$theta.range, panel_params$r.range),
      c(self$theta, self$r)
    )
  },

  setup_panel_params = function(self, scale_x, scale_y, params = list()) {
    if (self$theta == "x") {
      xlimits <- self$limits$theta
      ylimits <- self$limits$r
    } else {
      xlimits <- self$limits$r
      ylimits <- self$limits$theta
    }
    params <- c(
      view_scales_polar(scale_x, self$theta, xlimits,
        expand = params$expand[c(4, 2)]
      ),
      view_scales_polar(scale_y, self$theta, ylimits,
        expand = params$expand[c(3, 1)]
      ),
      list(bbox = polar_bbox(self$arc, inner_radius = self$inner_radius),
           arc = self$arc, inner_radius = self$inner_radius)
    )

    axis_rotation <- self$r_axis_inside
    if (is.numeric(axis_rotation)) {
      theta_scale <- switch(self$theta, x = scale_x, y = scale_y)
      axis_rotation <- theta_scale$transform(axis_rotation)
      axis_rotation <- oob_squish(axis_rotation, params$theta.range)
      axis_rotation <- theta_rescale(
        axis_rotation, params$theta.range,
        params$arc, 1
      )
      params$axis_rotation <- rep_len(axis_rotation, length.out = 2)
    } else {
      params$axis_rotation <- params$arc
    }

    params
  },

  setup_panel_guides = function(self, panel_params, guides, params = list()) {

    aesthetics <- c("r", "theta", "r.sec", "theta.sec")
    names(aesthetics) <- aesthetics
    is_sec <- grepl("sec$", aesthetics)
    scales <- panel_params[aesthetics]

    # Fill in theta guide default
    panel_params$theta$guide <- panel_params$theta$guide %|W|% guide_axis_theta()

    guides <- guides$setup(
      scales, aesthetics,
      default = params$guide_default %||% guide_axis(),
      missing = params$guide_missing %||% guide_none()
    )

    # Validate appropriateness of guides
    drop_guides <- character(0)
    for (type in aesthetics) {
      drop_guides <- validate_polar_guide(drop_guides, guides, type)
    }

    guide_params <- guides$get_params(aesthetics)
    names(guide_params) <- aesthetics

    # Set guide positions
    guide_params[["theta"]]$position     <- "theta"
    guide_params[["theta.sec"]]$position <- "theta.sec"

    if (self$theta == "x") {
      opposite_r <- isTRUE(scales$r$position %in% c("top", "right"))
    } else {
      opposite_r <- isTRUE(scales$r$position %in% c("bottom", "left"))
    }

    if (!isFALSE(self$r_axis_inside)) {

      r_position <- c("left", "right")
      # If both opposite direction and opposite position, don't flip
      if (xor(self$reverse %in% c("thetar", "theta"), opposite_r)) {
        r_position <- rev(r_position)
      }
      arc <- rad2deg(panel_params$axis_rotation)
      if (opposite_r) {
        arc <- rev(arc)
      }
      # Set guide text angles
      guide_params[["r"]]$angle     <- guide_params[["r"]]$angle     %|W|% arc[1]
      guide_params[["r.sec"]]$angle <- guide_params[["r.sec"]]$angle %|W|% arc[2]
    } else {
      r_position <- c(params$r_axis, opposite_position(params$r_axis))
      if (opposite_r) {
        r_position <- rev(r_position)
      }
    }
    guide_params[["r"]]$position     <- r_position[1]
    guide_params[["r.sec"]]$position <- r_position[2]

    guide_params[drop_guides] <- list(NULL)
    guides$update_params(guide_params)

    panel_params$guides <- guides
    panel_params
  },

  train_panel_guides = function(self, panel_params, layers, params = list()) {

    aesthetics <- c("r", "theta", "r.sec", "theta.sec")
    aesthetics <- intersect(aesthetics, names(panel_params$guides$aesthetics))
    names(aesthetics) <- aesthetics

    guides <- panel_params$guides$get_guide(aesthetics)
    names(guides) <- aesthetics
    empty  <- vapply(guides, inherits, logical(1), "GuideNone")
    gdefs <- panel_params$guides$get_params(aesthetics)
    names(gdefs) <- aesthetics

    # Train theta guide
    t <- intersect(c("theta", "theta.sec"), aesthetics[!empty])
    gdefs[t] <- Map(
      function(guide, guide_param, scale) {
        guide_param$theme_suffix <- "theta"
        guide_param <- guide$train(guide_param, scale)
        guide_param <- guide$transform(guide_param, self, panel_params)
        guide_param <- guide$get_layer_key(guide_param, layers)
      },
      guide = guides[t],
      guide_param = gdefs[t],
      scale = panel_params[t]
    )

    if (!isFALSE(self$r_axis_inside)) {
      # For radial axis, we need to pretend that rotation starts at 0 and
      # the bounding box is for circles, otherwise tick positions will be
      # spaced too closely.
      mod <- list(bbox = list(x = c(0, 1), y = c(0, 1)), arc = c(0, 2 * pi))
    } else {
      # When drawing radial axis outside, we need to pretend that arcs starts
      # at horizontal or vertical position to have the transform work right.
      mod <- list(arc = params$fake_arc)
    }
    temp <- modify_list(panel_params, mod)

    # Train radial guide
    r <- intersect(c("r", "r.sec"), aesthetics[!empty])
    gdefs[r] <- Map(
      function(guide, guide_param, scale) {
        guide_param$theme_suffix <- "r"
        guide_param <- guide$train(guide_param, scale)
        guide_param <- guide$transform(guide_param, self, temp)
        guide_param <- guide$get_layer_key(guide_param, layers)
      },
      guide = guides[r],
      guide_param = gdefs[r],
      scale = panel_params[r]
    )

    panel_params$guides$update_params(gdefs)
    panel_params
  },

  transform = function(self, data, panel_params) {
    if (is_transform_immune(data, snake_class(self))) {
      return(data)
    }

    data <- rename_data(self, data)
    bbox <- panel_params$bbox %||% list(x = c(0, 1), y = c(0, 1))
    arc  <- panel_params$arc  %||% c(0, 2 * pi)

    data$r  <- r_rescale(data$r, panel_params$r.range, panel_params$inner_radius)
    data$theta <- theta_rescale(data$theta, panel_params$theta.range, arc)
    data$x <- rescale(data$r * sin(data$theta) + 0.5, from = bbox$x)
    data$y <- rescale(data$r * cos(data$theta) + 0.5, from = bbox$y)

    if (self$rotate_angle && "angle" %in% names(data)) {
      data <- flip_data_text_angle(data)
    }

    data
  },

  render_axis_v = function(self, panel_params, theme) {
    if (!isFALSE(self$r_axis_inside)) {
      return(list(left = zeroGrob(), right = zeroGrob()))
    }
    CoordCartesian$render_axis_v(panel_params, theme)
  },

  render_axis_h = function(self, panel_params, theme) {
    if (!isFALSE(self$r_axis_inside)) {
      return(list(top = zeroGrob(), bottom = zeroGrob()))
    }
    CoordCartesian$render_axis_h(panel_params, theme)
  },

  render_bg = function(self, panel_params, theme) {
    panel_params <- switch(
      self$theta,
      x = rename(panel_params, c(theta = "x", r = "y")),
      y = rename(panel_params, c(theta = "y", r = "x"))
    )
    guide_grid(theme, panel_params, self, square = FALSE)
  },

  render_fg = function(self, panel_params, theme) {

    border <- element_render(theme, "panel.border", fill = NA)

    if (isFALSE(self$r_axis_inside)) {
      out <- grobTree(
        panel_guides_grob(panel_params$guides, "theta", theme),
        panel_guides_grob(panel_params$guides, "theta.sec", theme),
        border
      )
      return(out)
    }

    bbox <- panel_params$bbox
    dir  <- self$direction
    rot  <- panel_params$axis_rotation
    rot  <- switch(self$reverse, thetar = , theta = rev(rot), rot)
    rot  <- rad2deg(-rot)

    left <- panel_guides_grob(panel_params$guides, position = "left", theme)
    left <- rotate_r_axis(left, rot[1], bbox, "left")

    right <- panel_guides_grob(panel_params$guides, position = "right", theme)
    right <- rotate_r_axis(right, rot[2], bbox, "right")

    grobTree(
      panel_guides_grob(panel_params$guides, "theta", theme),
      panel_guides_grob(panel_params$guides, "theta.sec", theme),
      left, right,
      border
    )
  },


  draw_panel = function(self, panel, params, theme) {
    clip_support <- check_device("clippingPaths", "test", maybe = TRUE)
    if (self$clip == "on" && !isFALSE(clip_support)) {
      clip_path <- data_frame0(
        x = c(Inf, Inf, -Inf, -Inf),
        y = c(Inf, -Inf, -Inf, Inf)
      )
      clip_path <- coord_munch(self, clip_path, params, is_closed = TRUE)
      clip_path <- polygonGrob(clip_path$x, clip_path$y)
      # Note that clipping path is applied to panel without coord
      # foreground/background (added in parent method).
      # These may contain decorations that needn't be clipped
      panel <- list(gTree(
        children = inject(gList(!!!panel)),
        vp = viewport(clip = clip_path)
      ))
    }
    ggproto_parent(Coord, self)$draw_panel(panel, params, theme)
  },

  labels = function(self, labels, panel_params) {
    # `Layout$resolve_label()` doesn't know to look for theta/r/r.sec guides,
    # so we'll handle title propagation here.
    titles <- lapply(
      panel_params$guides$get_params(c("theta", "r", "r.sec")),
      function(x) x$title
    )
    if (self$theta == "y") {
      # Need to use single brackets for labels to avoid deleting an element by
      # assigning NULL
      labels$y['primary']   <- list(titles[[1]] %|W|% labels$y$primary)
      labels$x['primary']   <- list(titles[[2]] %|W|% labels$x$primary)
      labels$x['secondary'] <- list(titles[[3]] %|W|% labels$x$secondary)
      if (any(in_arc(c(0, 1) * pi, panel_params$arc))) {
        labels <- list(x = labels$y, y = labels$x)
      } else {
        labels <- list(x = rev(labels$x), y = rev(labels$y))
      }
    } else {
      labels$x['primary']   <- list(titles[[1]] %|W|% labels$x$primary)
      labels$y['primary']   <- list(titles[[2]] %|W|% labels$y$primary)
      labels$y['secondary'] <- list(titles[[3]] %|W|% labels$y$secondary)

      if (!any(in_arc(c(0, 1) * pi, panel_params$arc))) {
        labels <- list(x = rev(labels$y), y = rev(labels$x))
      }
    }
    labels
  },

  modify_scales = function(self, scales_x, scales_y) {
    if (self$theta != "y")
      return()

    lapply(scales_x, scale_flip_position)
    lapply(scales_y, scale_flip_position)
  },

  setup_params = function(self, data) {
    params <- ggproto_parent(Coord, self)$setup_params(data)
    if (isFALSE(self$r_axis_inside)) {
      place <- in_arc(c(0, 0.5, 1, 1.5) * pi, self$arc)
      params$r_axis   <- if (any(place[c(1, 3)])) "left" else "bottom"
      params$fake_arc <- switch(
        which(place[c(1, 3, 2, 4)])[1],
        c(0, 2), c(1, 3), c(0.5, 2.5), c(1.5, 3.5)
      ) * pi
    }
    params
  }
)

view_scales_polar <- function(scale, theta = "x", coord_limits = NULL,
                              expand = TRUE) {

  aesthetic <- scale$aesthetics[1]
  is_theta  <- theta == aesthetic
  name <- if (is_theta) "theta" else "r"

  expansion <- default_expansion(scale, expand = expand)
  limits <- scale$get_limits()
  continuous_range <- expand_limits_scale(
    scale, expansion, limits,
    coord_limits
  )

  primary <- view_scale_primary(scale, limits, continuous_range)
  view_scales <- list(
    primary,
    sec = view_scale_secondary(scale, limits, continuous_range),
    major = primary$map(primary$get_breaks()),
    minor = primary$map(primary$get_breaks_minor()),
    range = continuous_range
  )

  names(view_scales) <- c(name, paste0(name, ".", names(view_scales)[-1]))
  view_scales
}

#' Bounding box for partial polar coordinates
#'
#' Calculates the appropriate area to display a partial polar plot.
#'
#' @param arc The theta limits of the arc spanned by the partial polar plot.
#' @param margin A `numeric[4]` giving the margin that should be added to the
#'   top, right, bottom and left to the plot at edges that are shortened.
#'
#' @return A `list` with element `x`, containing the 'xmin' and 'xmax', and
#'   element `y` giving 'ymin' and 'ymax' of the bounding box.
#'
#' @noRd
#' @examples
#' polar_bbox(c(0, 1) * pi)
polar_bbox <- function(arc, margin = c(0.05, 0.05, 0.05, 0.05),
                       inner_radius = c(0, 0.4)) {

  # Early exit if we have full circle or more
  if (abs(diff(arc)) >= 2 * pi) {
    return(list(x = c(0, 1), y = c(0, 1)))
  }
  arc <- sort(arc)
  inner_radius <- sort(inner_radius)

  # X and Y position of the sector arc ends
  xmax <- 0.5 * sin(arc) + 0.5
  ymax <- 0.5 * cos(arc) + 0.5
  xmin <- inner_radius[1] * sin(arc) + 0.5
  ymin <- inner_radius[1] * cos(arc) + 0.5

  margin <- c(
    max(ymin) + margin[1],
    max(xmin) + margin[2],
    min(ymin) - margin[3],
    min(xmin) - margin[4]
  )

  # Check for top, right, bottom and left if it falls in sector
  pos_theta <- seq(0, 1.5 * pi, length.out = 4)
  in_sector <- in_arc(pos_theta, arc)

  bounds <- ifelse(
    in_sector,
    c(1, 1, 0, 0),
    c(max(ymax, margin[1]), max(xmax, margin[2]),
      min(ymax, margin[3]), min(xmax, margin[4]))
  )
  list(x = c(bounds[4], bounds[2]),
       y = c(bounds[3], bounds[1]))
}

# For any `theta` in [0, 2 * pi), test if theta is inside the span
# given by `arc`
in_arc <- function(theta, arc) {
  # Full circle case
  if (abs(diff(arc)) > 2 * pi - sqrt(.Machine$double.eps)) {
    return(rep(TRUE, length(theta)))
  }
  # Partial circle case
  arc <- arc %% (2 * pi)
  if (arc[1] < arc[2]) {
    theta >= arc[1] & theta <= arc[2]
  } else {
    !(theta < arc[1] & theta > arc[2])
  }
}

# Helpers to convert degrees to radians and vice versa
rad2deg <- function(rad) rad * 180 / pi
deg2rad <- function(deg) deg * pi / 180

# Function to rotate a radius axis through viewport
rotate_r_axis <- function(axis, angle, bbox, position = "left") {

  if (is_zero(axis)) {
    return(axis)
  }

  gTree(
    children = gList(axis),
    vp = viewport(
      angle = angle,
      x = unit(rescale(0.5, from = bbox$x), "npc"),
      y = unit(rescale(0.5, from = bbox$y), "npc"),
      just = c(as.numeric(position == "left"), 0.5),
      height = unit(1 / diff(bbox$y), "npc")
    )
  )
}

flip_text_angle <- function(angle) {
  angle <- angle %% 360
  flip <- angle > 90 & angle < 270
  angle[flip] <- angle[flip] + 180
  angle
}

flip_data_text_angle <- function(data) {
  if (!all(c("angle", "theta") %in% names(data))) {
    return(data)
  }
  angle <- (data$angle - rad2deg(data$theta)) %% 360
  flip  <- angle > 90 & angle < 270
  angle[flip] <- angle[flip] + 180
  data$angle <- angle
  if ("hjust" %in% names(data)) {
    data$hjust[flip] <- 1 - data$hjust[flip]
  }
  if ("vjust" %in% names(data)) {
    data$vjust[flip] <- 1 - data$vjust[flip]
  }
  data
}


theta_grid <- function(theta, element, inner_radius = c(0, 0.4),
                       bbox = list(x = c(0, 1), y = c(0, 1))) {
  n <- length(theta)
  if (n < 1) {
    return(NULL)
  }

  inner_radius <- rep(inner_radius, n)
  x <- rep(sin(theta), each = 2) * inner_radius + 0.5
  y <- rep(cos(theta), each = 2) * inner_radius + 0.5

  element_grob(
    element,
    x = rescale(x, from = bbox$x),
    y = rescale(y, from = bbox$y),
    id.lengths = rep(2, n),
    default.units = "native"
  )
}

validate_polar_guide <- function(drop_list, guides, type = "theta") {
  guide <- guides$get_guide(type)
  primary <- gsub("\\.sec$", "", type)
  if (inherits(guide, "GuideNone") || primary %in% guide$available_aes) {
    return(drop_list)
  }
  cli::cli_warn(c(
    "{.fn {snake_class(guide)}} cannot be used for {.field {primary}}.",
    i = "Use {?one of} {.or {.field {guide$available_aes}}} instead."
  ))
  c(drop_list, type)
}
hadley/ggplot2 documentation built on June 13, 2025, 12:51 a.m.