R/plot.shapes.R

Defines functions .igraph.shape.raster.plot .igraph.shape.sphere.plot .igraph.shape.pie.plot .igraph.shape.pie.clip mypie .igraph.shape.none.plot .igraph.shape.vrectangle.clip .igraph.shape.crectangle.clip .igraph.shape.rectangle.plot .igraph.shape.rectangle.clip .igraph.shape.csquare.clip .igraph.shape.square.plot .igraph.shape.square.clip .igraph.shape.circle.plot .igraph.shape.circle.clip add_shape shape_noplot shape_noclip shapes add.vertex.shape vertex.shapes igraph.shape.noclip igraph.shape.noplot

Documented in add_shape add.vertex.shape igraph.shape.noclip igraph.shape.noplot shape_noclip shape_noplot shapes vertex.shapes

#' Various vertex shapes when plotting igraph graphs
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' `igraph.shape.noplot()` was renamed to [shape_noplot()] to create a more
#' consistent API.
#' @inheritParams shape_noplot
#' @keywords internal
#' @export
igraph.shape.noplot <- function(coords, v = NULL, params) {
  # nocov start
  lifecycle::deprecate_soft("2.0.0", "igraph.shape.noplot()", "shape_noplot()")
  shape_noplot(coords = coords, v = v, params = params)
} # nocov end

#' Various vertex shapes when plotting igraph graphs
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' `igraph.shape.noclip()` was renamed to [shape_noclip()] to create a more
#' consistent API.
#' @inheritParams shape_noclip
#' @keywords internal
#' @export
igraph.shape.noclip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  # nocov start
  lifecycle::deprecate_soft("2.0.0", "igraph.shape.noclip()", "shape_noclip()")
  shape_noclip(coords = coords, el = el, params = params, end = end)
} # nocov end

#' Various vertex shapes when plotting igraph graphs
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' `vertex.shapes()` was renamed to [shapes()] to create a more
#' consistent API.
#' @inheritParams shapes
#' @keywords internal
#' @export
vertex.shapes <- function(shape = NULL) {
  # nocov start
  lifecycle::deprecate_soft("2.0.0", "vertex.shapes()", "shapes()")
  shapes(shape = shape)
} # nocov end

#' Various vertex shapes when plotting igraph graphs
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' `add.vertex.shape()` was renamed to [add_shape()] to create a more
#' consistent API.
#' @inheritParams add_shape
#' @keywords internal
#' @export
add.vertex.shape <- function(
  shape,
  clip = shape_noclip,
  plot = shape_noplot,
  parameters = list()
) {
  # nocov start
  lifecycle::deprecate_soft("2.0.0", "add.vertex.shape()", "add_shape()")
  add_shape(shape = shape, clip = clip, plot = plot, parameters = parameters)
} # nocov end

#   IGraph R package
#   Copyright (C) 2003-2012  Gabor Csardi <csardi.gabor@gmail.com>
#   334 Harvard street, Cambridge, MA 02139 USA
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc.,  51 Franklin Street, Fifth Floor, Boston, MA
#   02110-1301 USA
#
###################################################################

###################################################################
## API design
##
## A vertex shape is defined by two functions: the clipping function and
## the plotting function.
##
## The clipping function is called to determine where to put the
## arrowhead of a potential (incoming) incident edge. Its signature is
##    function(coords, el, params, end=c("both", "from", "to"))
## where the arguments are:
##    coords    A matrix with one row for each edge, and four columns.
##              It contains the coordinates of the end points of all
##              edges. The first two columns are the coordinates of the
##              first end points (sources, if the graph is directed),
##              the last two columns are for the other end points
##              (targets if the graph is directed).
##    el        The edge list itself, with vertex ids.
##    params    A function object to query plotting parameters.
##    end       Which end points to calculate. "both" means both,
##              "from" means the first end point, "to" the second.
## The clipping function must return the new version of "coords",
## modified according to the vertex sizes/shapes, with proper positions
## for the potential arrow heads. The positions are for the tips of the
## arrows.
##
## The plotting function plots the vertex. Its signature is
##    function(coords, v=NULL, params)
## where the arguments are
##    coords    Two column matrix, the coordinates for the vertices to draw.
##    v         The vertex ids of the vertices to draw. If NULL, then all
##              vertices are drawn.
##    params    A function object to query plotting parameters.
##
## shapes()         - lists all vertex shapes
## shapes(shape)    - returns the clipping and plotting functions
##                           for a given vertex shape
## add_shape()             - adds a new vertex shape, the clipping and
##                           plotting functions must be given, and
##                           optionally the newly introduced plotting
##                           parameters. This function can also be used
##                           to overwrite a given vertex shape.
##
## Examples:
## add_shape("image", clip=image.clip, plot=image.plot,
##                   parameters=list(filename=NA))
##
## add_shape("triangle", clip=shapes("circle")$clip,
##                   plot=triangle.plot)
##
## add_shape("polygon", clip=shapes("circle")$clip,
##                   plot=polygon.plot)
##
###################################################################

#' Various vertex shapes when plotting igraph graphs
#'
#' Starting from version 0.5.1 igraph supports different
#' vertex shapes when plotting graphs.
#'
#' @details
#' In igraph a vertex shape is defined by two functions: 1) provides
#' information about the size of the shape for clipping the edges and 2)
#' plots the shape if requested. These functions are called \dQuote{shape
#'   functions} in the rest of this manual page. The first one is the
#' clipping function and the second is the plotting function.
#'
#' The clipping function has the following arguments:
#' \describe{
#'     \item{coords}{
#'       A matrix with four columns, it contains the coordinates of the vertices
#'       for the edge list supplied in the `el` argument.
#'     }
#'     \item{el}{
#'       A matrix with two columns, the edges of which some end points will be clipped.
#'       It should have the same number of rows as `coords`.
#'     }
#'     \item{params}{
#'       This is a function object that can be called to query vertex/edge/plot graphical parameters.
#'       The first argument of the function is \dQuote{`vertex`}, \dQuote{`edge`} or \dQuote{`plot`}
#'       to decide the type of the parameter,
#'       the second is a character string giving the name of the parameter.
#'       E.g. `params("vertex", "size")`.
#'     }
#'     \item{end}{
#'       Character string, it gives which end points will be used.
#'       Possible values are \dQuote{`both`}, \dQuote{`from`} and \dQuote{`to`}.
#'       If \dQuote{`from`} the function is expected to clip the first column in the `el` edge list,
#'       \dQuote{`to`} selects the second column, \dQuote{`both`} selects both.
#'     }
#'   }
#'
#' The clipping function should return a matrix
#' with the same number of rows as the `el` arguments.
#' If `end` is `both` then the matrix must have four
#' columns, otherwise two. The matrix contains the modified coordinates,
#' with the clipping applied.
#'
#' The plotting function has the following arguments:
#' \describe{
#'     \item{coords}{
#'       The coordinates of the vertices, a matrix with two columns.
#'     }
#'     \item{v}{
#'       The ids of the vertices to plot. It should match the number of rows in the `coords` argument.
#'     }
#'     \item{params}{
#'       The same as for the clipping function, see above.
#'     }
#'   }
#'
#' The return value of the plotting function is not used.
#'
#' `shapes()` can be used to list the names of all installed
#' vertex shapes, by calling it without arguments, or setting the
#' `shape` argument to `NULL`. If a shape name is given, then
#' the clipping and plotting functions of that shape are returned in a
#' named list.
#'
#' `add_shape()` can be used to add new vertex shapes to
#' igraph. For this one must give the clipping and plotting functions of
#' the new shape. It is also possible to list the plot/vertex/edge
#' parameters, in the `parameters` argument, that the clipping
#' and/or plotting functions can make use of. An example would be a
#' generic regular polygon shape, which can have a parameter for the
#' number of sides.
#'
#' `shape_noclip()` is a very simple clipping function that the
#' user can use in their own shape definitions. It does no clipping, the
#' edges will be drawn exactly until the listed vertex position
#' coordinates.
#'
#' `shape_noplot()` is a very simple (and probably not very
#' useful) plotting function, that does not plot anything.
#'
#' @aliases   igraph.vertex.shapes
#'
#' @param shape Character scalar, name of a vertex shape. If it is
#'    `NULL` for `shapes()`, then the names of all defined
#'    vertex shapes are returned.
#' @param clip An R function object, the clipping function.
#' @param plot An R function object, the plotting function.
#' @param parameters Named list, additional plot/vertex/edge
#'    parameters. The element named define the new parameters, and the
#'    elements themselves define their default values.
#'    Vertex parameters should have a prefix
#'    \sQuote{`vertex.`}, edge parameters a prefix
#'    \sQuote{`edge.`}. Other general plotting parameters should have
#'    a prefix \sQuote{`plot.`}. See Details below.
#' @param coords,el,params,end,v See parameters of the clipping/plotting
#'    functions below.
#' @return `shapes()` returns a character vector if the
#'    `shape` argument is `NULL`. It returns a named list with
#'    entries named \sQuote{clip} and \sQuote{plot}, both of them R
#'    functions.
#'
#'    `add_shape()` returns `TRUE`, invisibly.
#'
#'    `shape_noclip()` returns the appropriate columns of its
#'    `coords` argument.
#' @family plot.shapes
#' @export
#'
#' @examples
#' # all vertex shapes, minus "raster", that might not be available
#' shapes <- setdiff(shapes(), "")
#' g <- make_ring(length(shapes))
#' set.seed(42)
#' plot(g,
#'   vertex.shape = shapes, vertex.label = shapes, vertex.label.dist = 1,
#'   vertex.size = 15, vertex.size2 = 15,
#'   vertex.pie = lapply(shapes, function(x) if (x == "pie") 2:6 else 0),
#'   vertex.pie.color = list(heat.colors(5))
#' )
#'
#' # add new vertex shape, plot nothing with no clipping
#' add_shape("nil")
#' plot(g, vertex.shape = "nil")
#'
#' #################################################################
#' # triangle vertex shape
#' mytriangle <- function(coords, v = NULL, params) {
#'   vertex.color <- params("vertex", "color")
#'   if (length(vertex.color) != 1 && !is.null(v)) {
#'     vertex.color <- vertex.color[v]
#'   }
#'   vertex.size <- params("vertex", "size")
#'   if (length(vertex.size) != 1 && !is.null(v)) {
#'     vertex.size <- vertex.size[v]
#'   }
#'
#'   symbols(
#'     x = coords[, 1], y = coords[, 2], bg = vertex.color,
#'     stars = cbind(vertex.size, vertex.size, vertex.size),
#'     add = TRUE, inches = FALSE
#'   )
#' }
#' # clips as a circle
#' add_shape("triangle",
#'   clip = shapes("circle")$clip,
#'   plot = mytriangle
#' )
#' plot(g,
#'   vertex.shape = "triangle", vertex.color = rainbow(vcount(g)),
#'   vertex.size = seq(10, 20, length.out = vcount(g))
#' )
#'
#' #################################################################
#' # generic star vertex shape, with a parameter for number of rays
#' mystar <- function(coords, v = NULL, params) {
#'   vertex.color <- params("vertex", "color")
#'   if (length(vertex.color) != 1 && !is.null(v)) {
#'     vertex.color <- vertex.color[v]
#'   }
#'   vertex.size <- params("vertex", "size")
#'   if (length(vertex.size) != 1 && !is.null(v)) {
#'     vertex.size <- vertex.size[v]
#'   }
#'   norays <- params("vertex", "norays")
#'   if (length(norays) != 1 && !is.null(v)) {
#'     norays <- norays[v]
#'   }
#'
#'   mapply(coords[, 1], coords[, 2], vertex.color, vertex.size, norays,
#'     FUN = function(x, y, bg, size, nor) {
#'       symbols(
#'         x = x, y = y, bg = bg,
#'         stars = matrix(c(size, size / 2), nrow = 1, ncol = nor * 2),
#'         add = TRUE, inches = FALSE
#'       )
#'     }
#'   )
#' }
#' # no clipping, edges will be below the vertices anyway
#' add_shape("star",
#'   clip = shape_noclip,
#'   plot = mystar, parameters = list(vertex.norays = 5)
#' )
#' plot(g,
#'   vertex.shape = "star", vertex.color = rainbow(vcount(g)),
#'   vertex.size = seq(10, 20, length.out = vcount(g))
#' )
#' plot(g,
#'   vertex.shape = "star", vertex.color = rainbow(vcount(g)),
#'   vertex.size = seq(10, 20, length.out = vcount(g)),
#'   vertex.norays = rep(4:8, length.out = vcount(g))
#' )
shapes <- function(shape = NULL) {
  if (is.null(shape)) {
    ls(.igraph.shapes)
  } else {
    ## checkScalarString(shape)
    .igraph.shapes[[shape]]
  }
}

#' @rdname shapes
#' @export
shape_noclip <- function(coords, el, params, end = c("both", "from", "to")) {
  end <- igraph.match.arg(end)

  if (end == "both") {
    coords
  } else if (end == "from") {
    coords[, 1:2, drop = FALSE]
  } else {
    coords[, 3:4, drop = FALSE]
  }
}

#' @rdname shapes
#' @export
shape_noplot <- function(coords, v = NULL, params) {
  invisible(NULL)
}

#' @rdname shapes
#' @export
add_shape <- function(
  shape,
  clip = shape_noclip,
  plot = shape_noplot,
  parameters = list()
) {
  if (!is.character(shape) || length(shape) != 1) {
    cli::cli_abort(c(
      "{.arg shape} must be a character of length 1.",
      i = "See {.help add_shape} for details."
    ))
  }

  if (!rlang::is_missing(clip) && !inherits(clip, "function")) {
    cli::cli_abort(c(
      "{.arg clip} must be a function.",
      i = "See {.help add_shape} for details."
    ))
  }

  if (!rlang::is_missing(plot) && !inherits(plot, "function")) {
    cli::cli_abort(c(
      "{.arg plot} must be a function.",
      i = "See {.help add_shape} for details."
    ))
  }

  if (
    length(parameters) > 0 &&
      (!inherits(parameters, "list") || !rlang::is_named(parameters))
  ) {
    cli::cli_abort(c(
      "{.arg parameters} must be a named list.",
      i = "See {.help add_shape} for details."
    ))
  }

  assign(shape, value = list(clip = clip, plot = plot), envir = .igraph.shapes)
  do.call(igraph.options, parameters)
  invisible(TRUE)
}

## These are the predefined shapes

.igraph.shape.circle.clip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  end <- match.arg(end)

  if (length(coords) == 0) {
    return(coords)
  }

  vertex.size <- params("vertex", "size")

  if (end == "from") {
    phi <- atan2(coords[, 4] - coords[, 2], coords[, 3] - coords[, 1])
    vsize.from <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    res <- cbind(
      coords[, 1] + vsize.from * cos(phi),
      coords[, 2] + vsize.from * sin(phi)
    )
  } else if (end == "to") {
    phi <- atan2(coords[, 4] - coords[, 2], coords[, 3] - coords[, 1])
    r <- sqrt((coords[, 3] - coords[, 1])^2 + (coords[, 4] - coords[, 2])^2)
    vsize.to <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    res <- cbind(
      coords[, 1] + (r - vsize.to) * cos(phi),
      coords[, 2] + (r - vsize.to) * sin(phi)
    )
  } else if (end == "both") {
    phi <- atan2(coords[, 4] - coords[, 2], coords[, 3] - coords[, 1])
    r <- sqrt((coords[, 3] - coords[, 1])^2 + (coords[, 4] - coords[, 2])^2)
    vsize.from <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    vsize.to <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    res <- cbind(
      coords[, 1] + vsize.from * cos(phi),
      coords[, 2] + vsize.from * sin(phi),
      coords[, 1] + (r - vsize.to) * cos(phi),
      coords[, 2] + (r - vsize.to) * sin(phi)
    )
  }
  res
}

#' @importFrom graphics symbols
.igraph.shape.circle.plot <- function(coords, v = NULL, params) {
  vertex.color <- params("vertex", "color")
  if (length(vertex.color) != 1 && !is.null(v)) {
    vertex.color <- vertex.color[v]
  }
  vertex.frame.color <- params("vertex", "frame.color")
  if (length(vertex.frame.color) != 1 && !is.null(v)) {
    vertex.frame.color <- vertex.frame.color[v]
  }
  vertex.frame.width <- params("vertex", "frame.width")
  if (length(vertex.frame.width) != 1 && !is.null(v)) {
    vertex.frame.width <- vertex.frame.width[v]
  }
  vertex.size <- params("vertex", "size")

  if (length(vertex.size) != 1 && !is.null(v)) {
    vertex.size <- vertex.size[v]
  }
  vertex.size <- rep(vertex.size, length.out = nrow(coords))

  # Handle vertex.frame.width <= 0 by hiding the border
  vertex.frame.color[vertex.frame.width <= 0] <- NA
  vertex.frame.width[vertex.frame.width <= 0] <- 1

  if (length(vertex.frame.width) == 1) {
    symbols(
      x = coords[, 1],
      y = coords[, 2],
      bg = vertex.color,
      fg = vertex.frame.color,
      circles = vertex.size,
      lwd = vertex.frame.width,
      add = TRUE,
      inches = FALSE
    )
  } else {
    mapply(
      coords[, 1],
      coords[, 2],
      vertex.color,
      vertex.frame.color,
      vertex.size,
      vertex.frame.width,
      FUN = function(x, y, bg, fg, size, lwd) {
        symbols(
          x = x,
          y = y,
          bg = bg,
          fg = fg,
          lwd = lwd,
          circles = size,
          add = TRUE,
          inches = FALSE
        )
      }
    )
  }
}

.igraph.shape.square.clip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  end <- match.arg(end)

  if (length(coords) == 0) {
    return(coords)
  }

  vertex.size <- params("vertex", "size")

  square.shift <- function(x0, y0, x1, y1, vsize) {
    m <- (y0 - y1) / (x0 - x1)
    l <- cbind(
      x1 - vsize / m,
      y1 - vsize,
      x1 - vsize,
      y1 - vsize * m,
      x1 + vsize / m,
      y1 + vsize,
      x1 + vsize,
      y1 + vsize * m
    )

    v <- cbind(
      x1 - vsize <= l[, 1] &
        l[, 1] <= x1 + vsize &
        y1 - vsize <= l[, 2] &
        l[, 2] <= y1 + vsize,
      x1 - vsize <= l[, 3] &
        l[, 3] <= x1 + vsize &
        y1 - vsize <= l[, 4] &
        l[, 4] <= y1 + vsize,
      x1 - vsize <= l[, 5] &
        l[, 5] <= x1 + vsize &
        y1 - vsize <= l[, 6] &
        l[, 6] <= y1 + vsize,
      x1 - vsize <= l[, 7] &
        l[, 7] <= x1 + vsize &
        y1 - vsize <= l[, 8] &
        l[, 8] <= y1 + vsize
    )

    d <- cbind(
      (l[, 1] - x0)^2 + (l[, 2] - y0)^2,
      (l[, 3] - x0)^2 + (l[, 4] - y0)^2,
      (l[, 5] - x0)^2 + (l[, 6] - y0)^2,
      (l[, 7] - x0)^2 + (l[, 8] - y0)^2
    )

    t(sapply(seq(length.out = nrow(l)), function(x) {
      d[x, ][!v[x, ]] <- Inf
      m <- which.min(d[x, ])
      l[x, c(m * 2 - 1, m * 2)]
    }))
  }

  if (end %in% c("from", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    res <- res1 <- square.shift(
      coords[, 3],
      coords[, 4],
      coords[, 1],
      coords[, 2],
      vsize
    )
  }
  if (end %in% c("to", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    res <- res2 <- square.shift(
      coords[, 1],
      coords[, 2],
      coords[, 3],
      coords[, 4],
      vsize
    )
  }
  if (end == "both") {
    res <- cbind(res1, res2)
  }

  res
}

#' @importFrom graphics symbols
.igraph.shape.square.plot <- function(coords, v = NULL, params) {
  vertex.color <- params("vertex", "color")
  if (length(vertex.color) != 1 && !is.null(v)) {
    vertex.color <- vertex.color[v]
  }
  vertex.frame.color <- params("vertex", "frame.color")
  if (length(vertex.frame.color) != 1 && !is.null(v)) {
    vertex.frame.color <- vertex.frame.color[v]
  }
  vertex.frame.width <- params("vertex", "frame.width")
  if (length(vertex.frame.width) != 1 && !is.null(v)) {
    vertex.frame.width <- vertex.frame.width[v]
  }
  vertex.size <- params("vertex", "size")

  if (length(vertex.size) != 1 && !is.null(v)) {
    vertex.size <- vertex.size[v]
  }
  vertex.size <- rep(vertex.size, length.out = nrow(coords))

  # Handle vertex.frame.width <= 0 by hiding the border
  vertex.frame.color[vertex.frame.width <= 0] <- NA
  vertex.frame.width[vertex.frame.width <= 0] <- 1

  if (length(vertex.frame.width) == 1) {
    symbols(
      x = coords[, 1],
      y = coords[, 2],
      bg = vertex.color,
      fg = vertex.frame.color,
      squares = 2 * vertex.size,
      lwd = vertex.frame.width,
      add = TRUE,
      inches = FALSE
    )
  } else {
    mapply(
      coords[, 1],
      coords[, 2],
      vertex.color,
      vertex.frame.color,
      vertex.size,
      vertex.frame.width,
      FUN = function(x, y, bg, fg, size, lwd) {
        symbols(
          x = x,
          y = y,
          bg = bg,
          fg = fg,
          lwd = lwd,
          squares = 2 * size,
          add = TRUE,
          inches = FALSE
        )
      }
    )
  }
}

.igraph.shape.csquare.clip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  end <- match.arg(end)

  if (length(coords) == 0) {
    return(coords)
  }

  vertex.size <- params("vertex", "size")

  square.shift <- function(x0, y0, x1, y1, vsize) {
    l <- cbind(
      x1,
      y1 - vsize,
      x1 - vsize,
      y1,
      x1,
      y1 + vsize,
      x1 + vsize,
      y1
    )

    d <- cbind(
      (l[, 1] - x0)^2 + (l[, 2] - y0)^2,
      (l[, 3] - x0)^2 + (l[, 4] - y0)^2,
      (l[, 5] - x0)^2 + (l[, 6] - y0)^2,
      (l[, 7] - x0)^2 + (l[, 8] - y0)^2
    )

    t(sapply(seq(length.out = nrow(l)), function(x) {
      m <- which.min(d[x, ])
      l[x, c(m * 2 - 1, m * 2)]
    }))
  }

  if (end %in% c("from", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    res <- res1 <- square.shift(
      coords[, 3],
      coords[, 4],
      coords[, 1],
      coords[, 2],
      vsize
    )
  }
  if (end %in% c("to", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    res <- res2 <- square.shift(
      coords[, 1],
      coords[, 2],
      coords[, 3],
      coords[, 4],
      vsize
    )
  }
  if (end == "both") {
    res <- cbind(res1, res2)
  }

  res
}

.igraph.shape.csquare.plot <- .igraph.shape.square.plot

.igraph.shape.rectangle.clip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  end <- match.arg(end)

  if (length(coords) == 0) {
    return(coords)
  }

  vertex.size <- params("vertex", "size")
  vertex.size2 <- params("vertex", "size2")

  rec.shift <- function(x0, y0, x1, y1, vsize, vsize2) {
    m <- (y0 - y1) / (x0 - x1)
    l <- cbind(
      x1 - vsize2 / m,
      y1 - vsize2,
      x1 - vsize,
      y1 - vsize * m,
      x1 + vsize2 / m,
      y1 + vsize2,
      x1 + vsize,
      y1 + vsize * m
    )

    v <- cbind(
      x1 - vsize <= l[, 1] &
        l[, 1] <= x1 + vsize &
        y1 - vsize2 <= l[, 2] &
        l[, 2] <= y1 + vsize2,
      x1 - vsize <= l[, 3] &
        l[, 3] <= x1 + vsize &
        y1 - vsize2 <= l[, 4] &
        l[, 4] <= y1 + vsize2,
      x1 - vsize <= l[, 5] &
        l[, 5] <= x1 + vsize &
        y1 - vsize2 <= l[, 6] &
        l[, 6] <= y1 + vsize2,
      x1 - vsize <= l[, 7] &
        l[, 7] <= x1 + vsize &
        y1 - vsize2 <= l[, 8] &
        l[, 8] <= y1 + vsize2
    )

    d <- cbind(
      (l[, 1] - x0)^2 + (l[, 2] - y0)^2,
      (l[, 3] - x0)^2 + (l[, 4] - y0)^2,
      (l[, 5] - x0)^2 + (l[, 6] - y0)^2,
      (l[, 7] - x0)^2 + (l[, 8] - y0)^2
    )

    t(sapply(seq(length.out = nrow(l)), function(x) {
      d[x, ][!v[x, ]] <- Inf
      m <- which.min(d[x, ])
      l[x, c(m * 2 - 1, m * 2)]
    }))
  }

  if (end %in% c("from", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    vsize2 <- if (length(vertex.size2) == 1) {
      vertex.size2
    } else {
      vertex.size2[el[, 1]]
    }
    res <- res1 <- rec.shift(
      coords[, 3],
      coords[, 4],
      coords[, 1],
      coords[, 2],
      vsize,
      vsize2
    )
  }
  if (end %in% c("to", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    vsize2 <- if (length(vertex.size2) == 1) {
      vertex.size2
    } else {
      vertex.size2[el[, 2]]
    }
    res <- res2 <- rec.shift(
      coords[, 1],
      coords[, 2],
      coords[, 3],
      coords[, 4],
      vsize,
      vsize2
    )
  }
  if (end == "both") {
    res <- cbind(res1, res2)
  }

  res
}

#' @importFrom graphics symbols
.igraph.shape.rectangle.plot <- function(coords, v = NULL, params) {
  vertex.color <- params("vertex", "color")
  if (length(vertex.color) != 1 && !is.null(v)) {
    vertex.color <- vertex.color[v]
  }
  vertex.frame.color <- params("vertex", "frame.color")
  if (length(vertex.frame.color) != 1 && !is.null(v)) {
    vertex.frame.color <- vertex.frame.color[v]
  }
  vertex.frame.width <- params("vertex", "frame.width")
  if (length(vertex.frame.width) != 1 && !is.null(v)) {
    vertex.frame.width <- vertex.frame.width[v]
  }
  vertex.size <- params("vertex", "size")
  if (length(vertex.size) != 1 && !is.null(v)) {
    vertex.size <- vertex.size[v]
  }
  vertex.size <- rep(vertex.size, length.out = nrow(coords))
  vertex.size2 <- params("vertex", "size2")

  if (length(vertex.size2) != 1 && !is.null(v)) {
    vertex.size2 <- vertex.size2[v]
  }
  vertex.size <- cbind(vertex.size, vertex.size2)

  # Handle vertex.frame.width <= 0 by hiding the border
  vertex.frame.color[vertex.frame.width <= 0] <- NA
  vertex.frame.width[vertex.frame.width <= 0] <- 1

  if (length(vertex.frame.width) == 1) {
    symbols(
      x = coords[, 1],
      y = coords[, 2],
      bg = vertex.color,
      fg = vertex.frame.color,
      rectangles = 2 * vertex.size,
      lwd = vertex.frame.width,
      add = TRUE,
      inches = FALSE
    )
  } else {
    mapply(
      coords[, 1],
      coords[, 2],
      vertex.color,
      vertex.frame.color,
      vertex.size[, 1],
      vertex.size[, 2],
      vertex.frame.width,
      FUN = function(x, y, bg, fg, size, size2, lwd) {
        symbols(
          x = x,
          y = y,
          bg = bg,
          fg = fg,
          lwd = lwd,
          rectangles = 2 * cbind(size, size2),
          add = TRUE,
          inches = FALSE
        )
      }
    )
  }
}

.igraph.shape.crectangle.clip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  end <- match.arg(end)

  if (length(coords) == 0) {
    return(coords)
  }

  vertex.size <- params("vertex", "size")
  vertex.size2 <- params("vertex", "size2")

  rec.shift <- function(x0, y0, x1, y1, vsize, vsize2) {
    l <- cbind(
      x1,
      y1 - vsize2,
      x1 - vsize,
      y1,
      x1,
      y1 + vsize2,
      x1 + vsize,
      y1
    )

    d <- cbind(
      (l[, 1] - x0)^2 + (l[, 2] - y0)^2,
      (l[, 3] - x0)^2 + (l[, 4] - y0)^2,
      (l[, 5] - x0)^2 + (l[, 6] - y0)^2,
      (l[, 7] - x0)^2 + (l[, 8] - y0)^2
    )

    t(sapply(seq(length.out = nrow(l)), function(x) {
      m <- which.min(d[x, ])
      l[x, c(m * 2 - 1, m * 2)]
    }))
  }

  if (end %in% c("from", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    vsize2 <- if (length(vertex.size2) == 1) {
      vertex.size2
    } else {
      vertex.size2[el[, 1]]
    }
    res <- res1 <- rec.shift(
      coords[, 3],
      coords[, 4],
      coords[, 1],
      coords[, 2],
      vsize,
      vsize2
    )
  }
  if (end %in% c("to", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    vsize2 <- if (length(vertex.size2) == 1) {
      vertex.size2
    } else {
      vertex.size2[el[, 2]]
    }
    res <- res2 <- rec.shift(
      coords[, 1],
      coords[, 2],
      coords[, 3],
      coords[, 4],
      vsize,
      vsize2
    )
  }
  if (end == "both") {
    res <- cbind(res1, res2)
  }

  res
}

.igraph.shape.crectangle.plot <- .igraph.shape.rectangle.plot

.igraph.shape.vrectangle.clip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  end <- match.arg(end)

  if (length(coords) == 0) {
    return(coords)
  }

  vertex.size <- params("vertex", "size")
  vertex.size2 <- params("vertex", "size2")

  rec.shift <- function(x0, y0, x1, y1, vsize, vsize2) {
    l <- cbind(x1 - vsize, y1, x1 + vsize, y1)

    d <- cbind(
      (l[, 1] - x0)^2 + (l[, 2] - y0)^2,
      (l[, 3] - x0)^2 + (l[, 4] - y0)^2
    )

    t(sapply(seq(length.out = nrow(l)), function(x) {
      m <- which.min(d[x, ])
      l[x, c(m * 2 - 1, m * 2)]
    }))
  }

  if (end %in% c("from", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    vsize2 <- if (length(vertex.size2) == 1) {
      vertex.size2
    } else {
      vertex.size2[el[, 1]]
    }
    res <- res1 <- rec.shift(
      coords[, 3],
      coords[, 4],
      coords[, 1],
      coords[, 2],
      vsize,
      vsize2
    )
  }
  if (end %in% c("to", "both")) {
    vsize <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    vsize2 <- if (length(vertex.size2) == 1) {
      vertex.size2
    } else {
      vertex.size2[el[, 2]]
    }
    res <- res2 <- rec.shift(
      coords[, 1],
      coords[, 2],
      coords[, 3],
      coords[, 4],
      vsize,
      vsize2
    )
  }
  if (end == "both") {
    res <- cbind(res1, res2)
  }

  res
}

.igraph.shape.vrectangle.plot <- .igraph.shape.rectangle.plot

.igraph.shape.none.clip <- .igraph.shape.circle.clip

.igraph.shape.none.plot <- function(coords, v = NULL, params) {
  ## does not plot anything at all
  invisible(NULL)
}

#' @importFrom graphics par polygon
mypie <- function(
  x,
  y,
  values,
  radius,
  edges = 200,
  col = NULL,
  angle = 45,
  density = NULL,
  border = NULL,
  lty = NULL,
  init.angle = 90,
  ...
) {
  values <- c(0, cumsum(values) / sum(values))
  dx <- diff(values)
  nx <- length(dx)
  twopi <- 2 * pi
  if (is.null(col)) {
    col <- if (is.null(density)) {
      c(
        "white",
        "lightblue",
        "mistyrose",
        "lightcyan",
        "lavender",
        "cornsilk"
      )
    } else {
      par("fg")
    }
  }
  col <- rep(col, length.out = nx)
  border <- rep(border, length.out = nx)
  lty <- rep(lty, length.out = nx)
  angle <- rep(angle, length.out = nx)
  density <- rep(density, length.out = nx)
  t2xy <- function(t) {
    t2p <- twopi * t + init.angle * pi / 180
    list(x = radius * cos(t2p), y = radius * sin(t2p))
  }
  for (i in 1:nx) {
    n <- max(2, floor(edges * dx[i]))
    P <- t2xy(seq.int(values[i], values[i + 1], length.out = n))
    polygon(
      x + c(P$x, 0),
      y + c(P$y, 0),
      density = density[i],
      angle = angle[i],
      border = border[i],
      col = col[i],
      lty = lty[i],
      ...
    )
  }
}

.igraph.shape.pie.clip <- function(
  coords,
  el,
  params,
  end = c("both", "from", "to")
) {
  end <- match.arg(end)

  if (length(coords) == 0) {
    return(coords)
  }

  vertex.size <- params("vertex", "size")

  if (end == "from") {
    phi <- atan2(coords[, 4] - coords[, 2], coords[, 3] - coords[, 1])
    vsize.from <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    res <- cbind(
      coords[, 1] + vsize.from * cos(phi),
      coords[, 2] + vsize.from * sin(phi)
    )
  } else if (end == "to") {
    phi <- atan2(coords[, 4] - coords[, 2], coords[, 3] - coords[, 1])
    r <- sqrt((coords[, 3] - coords[, 1])^2 + (coords[, 4] - coords[, 2])^2)
    vsize.to <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    res <- cbind(
      coords[, 1] + (r - vsize.to) * cos(phi),
      coords[, 2] + (r - vsize.to) * sin(phi)
    )
  } else if (end == "both") {
    phi <- atan2(coords[, 4] - coords[, 2], coords[, 3] - coords[, 1])
    r <- sqrt((coords[, 3] - coords[, 1])^2 + (coords[, 4] - coords[, 2])^2)
    vsize.from <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 1]]
    }
    vsize.to <- if (length(vertex.size) == 1) {
      vertex.size
    } else {
      vertex.size[el[, 2]]
    }
    res <- cbind(
      coords[, 1] + vsize.from * cos(phi),
      coords[, 2] + vsize.from * sin(phi),
      coords[, 1] + (r - vsize.to) * cos(phi),
      coords[, 2] + (r - vsize.to) * sin(phi)
    )
  }

  res
}

#' @importFrom stats na.omit
.igraph.shape.pie.plot <- function(coords, v = NULL, params) {
  getparam <- function(pname) {
    p <- params("vertex", pname)
    if (length(p) != 1 && !is.null(v)) {
      p <- p[v]
    }
    p
  }
  vertex.color <- getparam("color")
  vertex.frame.color <- getparam("frame.color")
  vertex.size <- rep(getparam("size"), length = nrow(coords))
  vertex.pie <- getparam("pie")
  vertex.pie.color <- getparam("pie.color")
  vertex.pie.angle <- getparam("pie.angle")
  vertex.pie.density <- getparam("pie.density")
  vertex.pie.lty <- getparam("pie.lty")

  for (i in seq_len(nrow(coords))) {
    pie <- if (length(vertex.pie) == 1) {
      vertex.pie[[1]]
    } else {
      vertex.pie[[i]]
    }
    col <- if (length(vertex.pie.color) == 1) {
      vertex.pie.color[[1]]
    } else {
      vertex.pie.color[[i]]
    }
    mypie(
      x = coords[i, 1],
      y = coords[i, 2],
      pie,
      radius = vertex.size[i],
      edges = 200,
      col = col,
      angle = na.omit(vertex.pie.angle[c(i, 1)])[1],
      density = na.omit(vertex.pie.density[c(i, 1)])[1],
      border = na.omit(vertex.frame.color[c(i, 1)])[1],
      lty = na.omit(vertex.pie.lty[c(i, 1)])[1]
    )
  }
}

.igraph.shape.sphere.clip <- .igraph.shape.circle.clip

#' @importFrom graphics rasterImage
#' @importFrom grDevices col2rgb as.raster
.igraph.shape.sphere.plot <- function(coords, v = NULL, params) {
  getparam <- function(pname) {
    p <- params("vertex", pname)
    if (length(p) != 1 && !is.null(v)) {
      p <- p[v]
    }
    p
  }

  vertex.color <- rep(getparam("color"), length = nrow(coords))
  vertex.size <- rep(getparam("size"), length = nrow(coords))

  ## Need to create a separate image for every different vertex color
  allcols <- unique(vertex.color)
  images <- lapply(allcols, function(col) {
    img <- getsphere(
      spos = c(0.0, 0.0, 10.0),
      sradius = 7.0,
      scolor = col2rgb(col) / 255,
      lightpos = list(c(-2, 2, 2)),
      lightcolor = list(c(1, 1, 1)),
      swidth = 100L,
      sheight = 100L
    )
    as.raster(img)
  })
  whichImage <- match(vertex.color, allcols)

  for (i in seq_len(nrow(coords))) {
    vsp2 <- vertex.size[i]
    rasterImage(
      images[[whichImage[i]]],
      coords[i, 1] - vsp2,
      coords[i, 2] - vsp2,
      coords[i, 1] + vsp2,
      coords[i, 2] + vsp2
    )
  }
}

.igraph.shape.raster.clip <- .igraph.shape.rectangle.clip

#' @importFrom graphics rasterImage
.igraph.shape.raster.plot <- function(coords, v = NULL, params) {
  getparam <- function(pname) {
    p <- params("vertex", pname)
    if (is.list(p) && length(p) != 1 && !is.null(v)) {
      p <- p[v]
    }
    p
  }

  size <- rep(getparam("size"), length = nrow(coords))
  size2 <- rep(getparam("size2"), length = nrow(coords))
  raster <- getparam("raster")

  for (i in seq_len(nrow(coords))) {
    ras <- if (!is.list(raster) || length(raster) == 1) raster else raster[[i]]
    rasterImage(
      ras,
      coords[i, 1] - size[i],
      coords[i, 2] - size2[i],
      coords[i, 1] + size[i],
      coords[i, 2] + size2[i]
    )
  }
}

.igraph.shapes <- new.env()
.igraph.shapes[["circle"]] <- list(
  clip = .igraph.shape.circle.clip,
  plot = .igraph.shape.circle.plot
)
.igraph.shapes[["square"]] <- list(
  clip = .igraph.shape.square.clip,
  plot = .igraph.shape.square.plot
)
.igraph.shapes[["csquare"]] <- list(
  clip = .igraph.shape.csquare.clip,
  plot = .igraph.shape.csquare.plot
)
.igraph.shapes[["rectangle"]] <- list(
  clip = .igraph.shape.rectangle.clip,
  plot = .igraph.shape.rectangle.plot
)
.igraph.shapes[["crectangle"]] <- list(
  clip = .igraph.shape.crectangle.clip,
  plot = .igraph.shape.crectangle.plot
)
.igraph.shapes[["vrectangle"]] <- list(
  clip = .igraph.shape.vrectangle.clip,
  plot = .igraph.shape.vrectangle.plot
)
.igraph.shapes[["none"]] <- list(
  clip = .igraph.shape.none.clip,
  plot = .igraph.shape.none.plot
)
.igraph.shapes[["pie"]] <- list(
  clip = .igraph.shape.pie.clip,
  plot = .igraph.shape.pie.plot
)
.igraph.shapes[["sphere"]] <- list(
  clip = .igraph.shape.sphere.clip,
  plot = .igraph.shape.sphere.plot
)
.igraph.shapes[["raster"]] <- list(
  clip = .igraph.shape.raster.clip,
  plot = .igraph.shape.raster.plot
)
igraph/rigraph documentation built on June 13, 2025, 1:44 p.m.