diff --git a/NAMESPACE b/NAMESPACE index b1c451a2dc7..ea622acdfa1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -162,6 +162,7 @@ export(as_long_data_frame) export(as_membership) export(as_phylo) export(as_star) +export(as_svg) export(as_tree) export(as_undirected) export(assortativity) @@ -593,6 +594,7 @@ export(kautz_graph) export(keeping_degseq) export(knn) export(label.propagation.community) +export(label_top) export(laplacian_matrix) export(largest.cliques) export(largest.independent.vertex.sets) @@ -803,6 +805,9 @@ export(sample_traits_callaway) export(sample_tree) export(sbm) export(sbm.game) +export(scale_color) +export(scale_colour) +export(scale_size) export(scan_stat) export(sequential_pal) export(set.edge.attribute) diff --git a/R/plot-aes.R b/R/plot-aes.R new file mode 100644 index 00000000000..3b8d38dbfb5 --- /dev/null +++ b/R/plot-aes.R @@ -0,0 +1,111 @@ +# IGraph R package +# Copyright (C) 2003-2012 Gabor Csardi +# +# 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. +################################################################### + +# Turn resolved plotting parameters into a typed, rectangular aesthetic table so +# that downstream code can slice it by vertex/edge index instead of +# re-implementing the `if (length(x) > 1) x[idx]` idiom for every parameter. +# +# Aesthetic resolution itself (the precedence chain +# argument > graph attribute > igraph option > default) still lives in +# `i.parse.plot.params()`; this layer only packages the already-resolved values. + +# Build an aesthetic table from a named list of columns, each recycled to `n` +# rows. Recycling uses `rep(length.out = n)` to match the historical (lenient) +# plotting behavior; downstream consumers (`mapply()`, `igraph.Arrows()`) recycle +# their arguments anyway, so a length-1 column behaves identically whether it is +# kept scalar or expanded here. +# +# Returns a vctrs data frame, which is type-stable (no factor coercion) and can +# be subset with `vctrs::vec_slice()`. +i.aes_table <- function(cols, n) { + cols <- lapply(cols, function(x) rep(x, length.out = n)) + vctrs::new_data_frame(cols, n = as.integer(n)) +} + +# igraph 3.0.0 breaking change: a per-element plotting aesthetic must have +# length 1 or exactly the number of vertices/edges. Previously a wrong-length +# vector (e.g. 3 colors for 5 vertices) was silently recycled, masking user +# mistakes; now it is an error. +# +# `vertex` and `edge` are named lists of resolved aesthetic vectors to check +# against `vc` / `ec`. Only unambiguous per-element aesthetics should be passed: +# aesthetics with special length semantics (vertex `label.adj`, list-valued +# `pie`/`raster`, the vertex-attribute `arrow.mode` "a:" form) are intentionally +# excluded by the caller. +i.check_aes_lengths <- function( + vertex, + edge, + vc, + ec, + call = rlang::caller_env() +) { + one_scope <- function(lst, n, scope, plural) { + for (nm in names(lst)) { + len <- length(lst[[nm]]) + if (len != 1L && len != n) { + cli::cli_abort( + c( + "Invalid length for {scope} aesthetic {.field {nm}}.", + "x" = "It has length {len}, but must be length 1 or {n}.", + "i" = "The graph has {n} {plural}." + ), + call = call + ) + } + } + } + one_scope(vertex, vc, "vertex", "vertices") + one_scope(edge, ec, "edge", "edges") + invisible(NULL) +} + +# Edge aesthetic table for the per-edge visual properties that are subset by +# edge index when drawing loop vs. non-loop edges. `loop.angle` (nullable) and +# vertex-scoped properties are handled separately by the caller. +i.edge_aes_table <- function( + color, + width, + lty, + arrow.mode, + arrow.size, + arrow.width, + curved, + label.color, + label.family, + label.font, + label.cex, + label.halo, + label.halo.width, + style, + alpha, + gradient, + n +) { + i.aes_table( + list( + color = color, + width = width, + lty = lty, + arrow.mode = arrow.mode, + arrow.size = arrow.size, + arrow.width = arrow.width, + curved = curved, + label.color = label.color, + label.family = label.family, + label.font = label.font, + label.cex = label.cex, + label.halo = label.halo, + label.halo.width = label.halo.width, + style = style, + alpha = alpha, + gradient = gradient + ), + n = n + ) +} diff --git a/R/plot-labels.R b/R/plot-labels.R new file mode 100644 index 00000000000..3869ecdc256 --- /dev/null +++ b/R/plot-labels.R @@ -0,0 +1,81 @@ +# IGraph R package +# Copyright (C) 2003-2012 Gabor Csardi +# +# 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. +################################################################### + +# Label decluttering: a helper that keeps only the most prominent labels and +# blanks the rest with NA. Because plot.igraph() omits NA labels, +# the result can be passed straight to a label argument, e.g. +# plot(g, vertex.label = label_top(degree(g), n = 10)). + +#' Keep only the most prominent labels +#' +#' `label_top()` returns a label vector with `NA` everywhere except the entries +#' that rank highest by `by`. Because [plot.igraph()] omits `NA` labels, this is +#' a convenient way to declutter dense graphs by labelling only the most +#' important vertices (or edges). Pass it to a label argument, e.g. +#' `plot(g, vertex.label = label_top(degree(g), n = 10))`. +#' +#' To label everything above a fixed cutoff instead of a fixed count, you do not +#' need this helper: `ifelse(metric > cutoff, labels, NA)` works directly. +#' +#' @param by A numeric vector of scores to rank by, e.g. `degree(g)` or +#' `betweenness(g)`. One score per vertex (or edge). +#' @param n Number of labels to keep (the top `n` by `by`). Give either `n` or +#' `prop`, not both. If neither is given, all labels are kept. +#' @param prop Proportion of labels to keep, between 0 and 1; rounded up. Give +#' either `n` or `prop`, not both. +#' @param labels The labels to thin. Defaults to `names(by)` if present, +#' otherwise the integer positions. Must have the same length as `by`. +#' @param decreasing Logical; if `TRUE` (the default) the highest `by` values +#' are kept, otherwise the lowest. +#' @return A character vector the same length as `by`, with `NA` in the +#' positions that are not kept. +#' @examples +#' g <- make_ring(10) +#' plot(g, vertex.label = label_top(degree(g), n = 3)) +#' @export +label_top <- function( + by, + n = NULL, + prop = NULL, + labels = NULL, + decreasing = TRUE +) { + if (!is.numeric(by)) { + cli::cli_abort("{.arg by} must be a numeric vector.") + } + if (!is.null(n) && !is.null(prop)) { + cli::cli_abort("Give either {.arg n} or {.arg prop}, not both.") + } + + labels <- labels %||% names(by) %||% as.character(seq_along(by)) + if (length(labels) != length(by)) { + cli::cli_abort("{.arg labels} must have the same length as {.arg by}.") + } + + k <- if (!is.null(n)) { + as.integer(n) + } else if (!is.null(prop)) { + if (prop < 0 || prop > 1) { + cli::cli_abort("{.arg prop} must be between 0 and 1.") + } + as.integer(ceiling(prop * length(by))) + } else { + length(by) + } + + keep <- rank( + if (decreasing) -by else by, + ties.method = "min", + na.last = TRUE + ) <= + k + out <- as.character(labels) + out[!keep] <- NA_character_ + out +} diff --git a/R/plot-render.R b/R/plot-render.R new file mode 100644 index 00000000000..4e69b783e0d --- /dev/null +++ b/R/plot-render.R @@ -0,0 +1,654 @@ +# IGraph R package +# Copyright (C) 2003-2012 Gabor Csardi +# +# 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. +################################################################### + +# Rendering indirection. +# +# Drawing code emits primitives through the i.r_*() dispatchers instead of +# calling base graphics directly. The dispatchers forward to the "current" +# renderer, a small list of closures held in i.render_state. The default base +# renderer simply calls the corresponding base-graphics function, so on-screen +# output is unchanged. A recording / SVG renderer (added in a later step) can +# capture the same primitive stream to build a backend-neutral draw list. + +i.render_state <- new.env(parent = emptyenv()) +i.render_state$cur <- NULL + +# The base renderer: each method draws with the matching base-graphics call. +i.renderer_base <- function() { + list( + init_canvas = function( + xlim, + ylim, + xlab, + ylab, + axes, + frame.plot, + asp, + main, + sub + ) { + graphics::plot( + 0, + 0, + type = "n", + xlab = xlab, + ylab = ylab, + xlim = xlim, + ylim = ylim, + axes = axes, + frame.plot = if (is.null(frame.plot)) axes else frame.plot, + asp = asp, + main = main, + sub = sub + ) + }, + segments = function( + x0, + y0, + x1, + y1, + col = graphics::par("fg"), + lwd = 1, + lty = 1 + ) { + graphics::segments(x0, y0, x1, y1, col = col, lwd = lwd, lty = lty) + }, + polyline = function( + x, + y = NULL, + col = graphics::par("fg"), + lwd = 1, + lty = 1 + ) { + graphics::lines(x, y, col = col, lwd = lwd, lty = lty) + }, + polygon = function( + x, + y, + col = NA, + border = NULL, + lwd = 1, + lty = 1, + density = NULL, + angle = 45, + ... + ) { + graphics::polygon( + x, + y, + col = col, + border = border, + lwd = lwd, + lty = lty, + density = density, + angle = angle, + ... + ) + }, + xspline = function(x, y = NULL, shape, open, col, border, lwd) { + graphics::xspline( + x, + y, + shape = shape, + open = open, + col = col, + border = border, + lwd = lwd + ) + }, + text = function(x, y, labels, col, family, font, cex, srt = 0, adj = NULL) { + graphics::text( + x, + y, + labels = labels, + col = col, + family = family, + font = font, + cex = cex, + srt = srt, + adj = adj + ) + }, + # One symbols() call. `kind` is "circles", "squares" or "rectangles"; `dim` + # is the matching size spec (vector, or 2-column matrix for rectangles). + symbols = function(kind, x, y, dim, bg, fg, lwd) { + args <- list( + x = x, + y = y, + bg = bg, + fg = fg, + lwd = lwd, + add = TRUE, + inches = FALSE + ) + args[[kind]] <- dim + do.call(graphics::symbols, args) + }, + raster = function(image, xleft, ybottom, xright, ytop) { + graphics::rasterImage(image, xleft, ybottom, xright, ytop) + }, + # Grouping hooks for backends that tag elements (e.g. SVG ids); the base + # renderer ignores them. + group_begin = function(type, id, title = NULL) invisible(NULL), + group_end = function() invisible(NULL) + ) +} + +# The current renderer, defaulting to base on first use. +i.cur_renderer <- function() { + if (is.null(i.render_state$cur)) { + i.render_state$cur <- i.renderer_base() + } + i.render_state$cur +} + +# Evaluate `expr` with `renderer` installed as the current renderer. +i.with_renderer <- function(renderer, expr) { + old <- i.render_state$cur + i.render_state$cur <- renderer + on.exit(i.render_state$cur <- old, add = TRUE) + force(expr) +} + +# --- dispatchers ------------------------------------------------------------- +i.r_init_canvas <- function(...) i.cur_renderer()$init_canvas(...) +i.r_segments <- function(...) i.cur_renderer()$segments(...) +i.r_polyline <- function(...) i.cur_renderer()$polyline(...) +i.r_polygon <- function(...) i.cur_renderer()$polygon(...) +i.r_xspline <- function(...) i.cur_renderer()$xspline(...) +i.r_text <- function(...) i.cur_renderer()$text(...) +i.r_symbols <- function(...) i.cur_renderer()$symbols(...) +i.r_raster <- function(...) i.cur_renderer()$raster(...) +i.r_group_begin <- function(...) i.cur_renderer()$group_begin(...) +i.r_group_end <- function(...) i.cur_renderer()$group_end(...) + +################################################################### +# Recording renderer + SVG writer +################################################################### + +# Canonicalise a colour vector to "#RRGGBBAA" hex (resolving palette indices and +# names against the active device), keeping NA as NA. Self-contained output. +i.col_to_hex <- function(col) { + if (is.null(col)) { + return(NA_character_) + } + out <- rep(NA_character_, length(col)) + ok <- !is.na(col) + if (any(ok)) { + m <- grDevices::col2rgb(col[ok], alpha = TRUE) + out[ok] <- grDevices::rgb( + m[1, ], + m[2, ], + m[3, ], + m[4, ], + maxColorValue = 255 + ) + } + out +} + +# A renderer that records primitives into a backend-neutral draw list instead of +# drawing. init_canvas still sets up the coordinate system on the (offscreen) +# device so geometry that reads par("usr")/xyinch() resolves correctly; nothing +# else is drawn. Colours are canonicalised to hex at record time. The current +# group (set by group_begin/end) is attached to each primitive. +i.renderer_record <- function() { + st <- new.env(parent = emptyenv()) + st$prims <- list() + st$canvas <- NULL + st$group <- NULL + add <- function(p) { + p$group <- st$group + st$prims[[length(st$prims) + 1L]] <- p + } + base <- i.renderer_base() + list( + .state = st, + init_canvas = function( + xlim, + ylim, + xlab, + ylab, + axes, + frame.plot, + asp, + main, + sub + ) { + # establish the coordinate system (discarded device); record the range + base$init_canvas(xlim, ylim, "", "", FALSE, FALSE, asp, "", "") + st$canvas <- list(usr = graphics::par("usr")) + }, + segments = function(x0, y0, x1, y1, col = NA, lwd = 1, lty = 1) { + add(list( + type = "segments", + x0 = x0, + y0 = y0, + x1 = x1, + y1 = y1, + col = i.col_to_hex(col), + lwd = lwd + )) + }, + polyline = function(x, y = NULL, col = NA, lwd = 1, lty = 1) { + xy <- grDevices::xy.coords(x, y) + add(list( + type = "polyline", + x = xy$x, + y = xy$y, + col = i.col_to_hex(col), + lwd = lwd + )) + }, + polygon = function( + x, + y = NULL, + col = NA, + border = NULL, + lwd = 1, + lty = 1, + density = NULL, + angle = 45, + ... + ) { + xy <- grDevices::xy.coords(x, y) + add(list( + type = "polygon", + x = xy$x, + y = xy$y, + col = i.col_to_hex(col), + border = i.col_to_hex(border), + lwd = lwd + )) + }, + xspline = function( + x, + y = NULL, + shape, + open, + col = NA, + border = NA, + lwd = 1 + ) { + pts <- grDevices::xspline(x, y, shape = shape, open = open, draw = FALSE) + add(list( + type = "polygon", + x = pts$x, + y = pts$y, + col = i.col_to_hex(col), + border = i.col_to_hex(border), + lwd = lwd + )) + }, + text = function( + x, + y, + labels, + col = NA, + family = "", + font = 1, + cex = 1, + srt = 0, + adj = NULL + ) { + add(list( + type = "text", + x = x, + y = y, + labels = labels, + col = i.col_to_hex(col), + cex = cex, + srt = srt, + adj = adj + )) + }, + symbols = function(kind, x, y, dim, bg = NA, fg = NA, lwd = 1) { + add(list( + type = "symbols", + kind = kind, + x = x, + y = y, + dim = dim, + bg = i.col_to_hex(bg), + fg = i.col_to_hex(fg), + lwd = lwd + )) + }, + raster = function(image, xleft, ybottom, xright, ytop) { + add(list( + type = "raster", + xleft = xleft, + ybottom = ybottom, + xright = xright, + ytop = ytop + )) + }, + group_begin = function(type, id = NULL, title = NULL) { + st$group <- list(type = type, id = id, title = title) + }, + group_end = function() { + st$group <- NULL + } + ) +} + +# ---- SVG writer ------------------------------------------------------------- + +i.svg_attr_esc <- function(x) { + x <- gsub("&", "&", x, fixed = TRUE) + x <- gsub("<", "<", x, fixed = TRUE) + x <- gsub(">", ">", x, fixed = TRUE) + gsub("'", "'", x, fixed = TRUE) +} + +# SVG colour + opacity from an "#RRGGBBAA" hex; returns c(fill, opacity). +i.svg_col <- function(hex) { + if (length(hex) == 0 || is.na(hex)) { + return(c("none", "1")) + } + if (nchar(hex) >= 9) { + a <- strtoi(substr(hex, 8, 9), 16L) / 255 + c(substr(hex, 1, 7), format(round(a, 3))) + } else { + c(hex, "1") + } +} + +# Build an SVG document string from a recorded draw list. `wpx`/`hpx` are the +# pixel canvas size; primitives (in user coords) are mapped via the recorded +# usr range with the y axis flipped. Vertices get per-element ids/titles; edges +# are wrapped per-edge group; everything else is grouped by phase. +i.svg_from_record <- function(state, wpx, hpx) { + usr <- state$canvas$usr + if (is.null(usr)) { + usr <- c(-1, 1, -1, 1) + } + sxr <- wpx / (usr[2] - usr[1]) + syr <- hpx / (usr[4] - usr[3]) + X <- function(x) (x - usr[1]) * sxr + Y <- function(y) hpx - (y - usr[3]) * syr + S <- function(s) s * sxr # user-length -> px (asp == 1) + + pts_str <- function(x, y) { + paste(sprintf("%.2f,%.2f", X(x), Y(y)), collapse = " ") + } + stroke <- function(hex, lwd) { + sc <- i.svg_col(hex) + sprintf( + "stroke='%s' stroke-opacity='%s' stroke-width='%.2f'", + sc[1], + sc[2], + max(lwd, 0.1) + ) + } + fillattr <- function(hex) { + fc <- i.svg_col(hex) + sprintf("fill='%s' fill-opacity='%s'", fc[1], fc[2]) + } + + one <- function(p, vtitle) { + # returns a character vector of SVG element strings + switch( + p$type, + segments = { + n <- length(p$x0) + col <- rep(p$col, length.out = n) + vapply( + seq_len(n), + function(k) { + sprintf( + "", + X(p$x0[k]), + Y(p$y0[k]), + X(p$x1[k]), + Y(p$y1[k]), + stroke(col[k], p$lwd) + ) + }, + character(1) + ) + }, + polyline = sprintf( + "", + pts_str(p$x, p$y), + stroke(p$col, p$lwd) + ), + polygon = sprintf( + "", + pts_str(p$x, p$y), + fillattr(p$col), + stroke(p$border, p$lwd) + ), + text = { + n <- length(p$x) + col <- rep(p$col, length.out = n) + adj <- if (is.null(p$adj)) 0.5 else p$adj[1] + anchor <- c("start", "middle", "end")[findInterval( + adj, + c(-Inf, 0.25, 0.75, Inf) + )] + fc <- i.svg_col(col[1]) + lab <- as.character(p$labels) + keep <- which(!is.na(lab) & nzchar(lab)) # base skips NA/empty labels + vapply( + keep, + function(k) { + rot <- if (p$srt != 0) { + sprintf( + " transform='rotate(%.2f %.2f %.2f)'", + -p$srt, + X(p$x[k]), + Y(p$y[k]) + ) + } else { + "" + } + sprintf( + "%s", + X(p$x[k]), + Y(p$y[k]), + p$cex * 12, + anchor, + fc[1], + fc[2], + rot, + i.svg_attr_esc(lab[k]) + ) + }, + character(1) + ) + }, + symbols = { + n <- length(p$x) + bg <- rep(p$bg, length.out = n) + fg <- rep(p$fg, length.out = n) + out <- character(n) + for (k in seq_len(n)) { + idattr <- "" + if (!is.null(vtitle)) { + kk <- vtitle$counter + idattr <- sprintf(" id='vertex-%d'", kk) + ttl <- if (!is.null(vtitle$titles)) { + sprintf("%s", i.svg_attr_esc(vtitle$titles[kk])) + } else { + "" + } + vtitle$counter <- kk + 1L + } else { + ttl <- "" + } + shp <- if (p$kind == "circles") { + sprintf( + "", + X(p$x[k]), + Y(p$y[k]), + S(p$dim[k]), + fillattr(bg[k]), + stroke(fg[k], p$lwd) + ) + } else if (p$kind == "squares") { + h <- p$dim[k] / 2 + sprintf( + "", + X(p$x[k] - h), + Y(p$y[k] + h), + S(p$dim[k]), + S(p$dim[k]), + fillattr(bg[k]), + stroke(fg[k], p$lwd) + ) + } else { + # rectangles: dim is n x 2 (full width, height) + w <- if (is.matrix(p$dim)) p$dim[k, 1] else p$dim[1] + hh <- if (is.matrix(p$dim)) p$dim[k, 2] else p$dim[2] + sprintf( + "", + X(p$x[k] - w / 2), + Y(p$y[k] + hh / 2), + S(w), + S(hh), + fillattr(bg[k]), + stroke(fg[k], p$lwd) + ) + } + out[k] <- paste0( + if (nzchar(idattr)) { + sprintf("%s%s", idattr, ttl, shp) + } else { + shp + } + ) + } + out + }, + raster = sprintf( + # v1 placeholder for sphere/raster shapes + "", + X(p$xleft), + Y(p$ytop), + S(p$xright - p$xleft), + S(p$ytop - p$ybottom) + ), + character(0) + ) + } + + body <- character(0) + prims <- state$prims + cur_key <- NA_character_ + open_g <- FALSE + vtitle <- NULL # environment-like tracker for vertex ids within a vertices group + + group_key <- function(g) { + if (is.null(g)) "" else paste0(g$type, ":", if (is.null(g$id)) "" else g$id) + } + + for (p in prims) { + g <- p$group + key <- group_key(g) + if (!identical(key, cur_key)) { + if (open_g) { + body <- c(body, "") + open_g <- FALSE + } + vtitle <- NULL + if (!is.null(g)) { + if (identical(g$type, "vertices")) { + body <- c(body, "") + vtitle <- new.env(parent = emptyenv()) + vtitle$counter <- 1L + vtitle$titles <- g$title + } else if (identical(g$type, "edge")) { + ttl <- if (!is.null(g$title)) { + sprintf("%s", i.svg_attr_esc(as.character(g$title))) + } else { + "" + } + body <- c(body, sprintf("%s", as.character(g$id), ttl)) + } else { + body <- c(body, sprintf("", g$type)) + } + open_g <- TRUE + } + cur_key <- key + } + body <- c(body, one(p, vtitle)) + } + if (open_g) { + body <- c(body, "") + } + + c( + sprintf("", wpx, hpx, wpx, hpx), + "", + body, + "" + ) +} + +#' Render a graph to SVG +#' +#' `as_svg()` draws a graph to a standalone SVG string using the same geometry +#' as [plot.igraph()], but emits per-vertex `` groups with +#' `` tooltips (and per-edge groups), giving lightweight interactivity +#' (hover) with no JavaScript. It accepts the usual plotting parameters via +#' `...`. +#' +#' Vertices, edges (all styles), arrowheads, labels, mark groups and pie shapes +#' are rendered; `sphere`/`raster` vertex shapes are drawn as a placeholder box +#' in this version. +#' +#' @param graph The graph to plot. +#' @param file Optional path to write the SVG to. If `NULL` (default) the SVG +#' string is returned invisibly. +#' @param width,height Size in inches (the SVG is `width*72` x `height*72` px). +#' @param tooltips Optional vertex attribute name to use for the `<title>` +#' tooltips; defaults to the vertex `name` attribute (or vertex index). +#' @param ... Further plotting parameters passed to [plot.igraph()]. +#' @return The SVG string, invisibly (also written to `file` if given). +#' @export +as_svg <- function( + graph, + file = NULL, + width = 7, + height = 7, + tooltips = NULL, + ... +) { + ensure_igraph(graph) + + titles <- if (!is.null(tooltips)) { + as.character(vertex_attr(graph, tooltips)) + } else if ("name" %in% vertex_attr_names(graph)) { + as.character(V(graph)$name) + } else { + as.character(seq_len(vcount(graph))) + } + + rec <- i.renderer_record() + grDevices::pdf(NULL, width = width, height = height) + on.exit(grDevices::dev.off(), add = TRUE) + + # plot.igraph() reads i.render_state$vertex_titles to title the per-vertex SVG + # groups; set it here (and reset on exit) rather than inside i.with_renderer(). + i.render_state$vertex_titles <- titles + on.exit(i.render_state$vertex_titles <- NULL, add = TRUE) + i.with_renderer(rec, plot(graph, ...)) + + svg <- i.svg_from_record( + rec$.state, + wpx = round(width * 72), + hpx = round(height * 72) + ) + svg <- paste(svg, collapse = "\n") + if (!is.null(file)) { + writeLines(svg, file) + } + invisible(svg) +} diff --git a/R/plot-scales.R b/R/plot-scales.R new file mode 100644 index 00000000000..37a0bcaccf4 --- /dev/null +++ b/R/plot-scales.R @@ -0,0 +1,381 @@ +# IGraph R package +# Copyright (C) 2003-2012 Gabor Csardi <csardi.gabor@gmail.com> +# +# 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. +################################################################### + +# Scales: map a data vector to a plotting aesthetic (colour or size) AND record +# a "guide" describing the mapping, so that plot.igraph() can +# draw a matching legend or colorbar. A scale is passed to an existing argument, +# e.g. plot(g, vertex.color = scale_color(V(g)$type)). + +new_igraph_scale <- function(values, guide) { + structure(list(values = values, guide = guide), class = "igraph_scale") +} + +is_igraph_scale <- function(x) inherits(x, "igraph_scale") + +#' Map data to a colour aesthetic with an automatic legend +#' +#' `scale_color()` (alias `scale_colour()`) maps a data vector to vertex or edge +#' colours and records the mapping so that [plot.igraph()] draws a matching +#' guide. Pass it to a colour argument, e.g. +#' `plot(g, vertex.color = scale_color(V(g)$group))`. +#' +#' A non-numeric `x` (factor, character, logical) produces a discrete mapping +#' and a categorical legend; a numeric `x` produces a continuous mapping (a +#' colour ramp) and a colorbar. +#' +#' @param x The data vector to map. Its length must be 1 or the number of +#' vertices/edges of the graph it is used with. +#' @param palette Colours to map to. For discrete `x`, a vector of colours (one +#' per level, recycled); defaults to [categorical_pal()]. For numeric `x`, the +#' anchor colours of the ramp; defaults to [sequential_pal()]. +#' @param na.value Colour used for `NA` entries in `x`. +#' @param name Optional guide title; defaults to the name of the argument the +#' scale is assigned to (e.g. `"vertex.color"`). +#' @return An `igraph_scale` object. +#' @family scales +#' @export +scale_color <- function(x, palette = NULL, na.value = "grey70", name = NULL) { + n <- length(x) + na <- is.na(x) + + if (is.numeric(x)) { + ramp_cols <- if (is.null(palette)) sequential_pal(9) else palette + rng <- range(x, na.rm = TRUE) + norm <- if (diff(rng) == 0) { + rep(0.5, n) + } else { + (x - rng[1]) / (rng[2] - rng[1]) + } + vals <- rep(na.value, n) + if (any(!na)) { + m <- grDevices::colorRamp(ramp_cols)(norm[!na]) + vals[!na] <- grDevices::rgb(m[, 1], m[, 2], m[, 3], maxColorValue = 255) + } + guide <- list( + aesthetic = "color", + type = "continuous", + name = name, + limits = rng, + ramp = ramp_cols + ) + } else { + xc <- as.character(x) + levels <- if (is.factor(x)) { + lv <- levels(x) + lv[lv %in% xc] + } else { + sort(unique(xc[!na])) + } + cols <- if (is.null(palette)) categorical_pal(length(levels)) else palette + cols <- rep(cols, length.out = length(levels)) + map <- stats::setNames(cols, levels) + vals <- unname(map[xc]) + vals[is.na(vals)] <- na.value + guide <- list( + aesthetic = "color", + type = "discrete", + name = name, + labels = levels, + colors = cols + ) + } + + new_igraph_scale(vals, guide) +} + +#' @rdname scale_color +#' @export +scale_colour <- scale_color + +#' Map data to a size aesthetic with an automatic legend +#' +#' `scale_size()` linearly maps a numeric data vector to a size range (suitable +#' for `vertex.size` or `edge.width`) and records the mapping so that +#' [plot.igraph()] draws a matching size legend. Pass it to a size argument, +#' e.g. `plot(g, vertex.size = scale_size(degree(g)))`. +#' +#' @param x A numeric data vector to map. Its length must be 1 or the number of +#' vertices/edges of the graph it is used with. +#' @param range Numeric length-2 vector giving the output size range. +#' @param na.value Size used for `NA` entries in `x`. +#' @param name Optional guide title; defaults to the argument name. +#' @param trans Optional transformation applied to `x` before rescaling, given +#' as a function or its name (e.g. `"sqrt"`, `"log"`). +#' @return An `igraph_scale` object. +#' @family scales +#' @export +scale_size <- function( + x, + range = c(2, 15), + na.value = NA, + name = NULL, + trans = NULL +) { + if (!is.numeric(x)) { + cli::cli_abort("{.arg x} must be numeric for {.fn scale_size}.") + } + n <- length(x) + na <- is.na(x) + tfun <- if (is.null(trans)) identity else match.fun(trans) + xt <- tfun(x) + rng <- range(xt, na.rm = TRUE) + + rescale <- function(v) { + if (diff(rng) == 0) { + rep(mean(range), length(v)) + } else { + (v - rng[1]) / (rng[2] - rng[1]) * (range[2] - range[1]) + range[1] + } + } + + vals <- rescale(xt) + vals[na] <- na.value + + breaks <- pretty(x[!na], n = 3) + breaks <- breaks[ + breaks >= min(x, na.rm = TRUE) & breaks <= max(x, na.rm = TRUE) + ] + guide <- list( + aesthetic = "size", + type = "discrete", + name = name, + labels = as.character(breaks), + sizes = rescale(tfun(breaks)) + ) + + new_igraph_scale(vals, guide) +} + +# Replace any igraph_scale arguments in `dots` with their resolved `values`, and +# collect the guides (titling each with the argument name unless the scale set a +# name). Must run before i.parse.plot.params(), whose rep() recycling would strip +# the scale class/attributes. +i.apply_scales <- function(dots) { + guides <- list() + for (nm in names(dots)) { + val <- dots[[nm]] + if (is_igraph_scale(val)) { + guide <- val$guide + if (is.null(guide$name)) { + guide$name <- nm + } + guides[[length(guides) + 1L]] <- guide + dots[[nm]] <- val$values + } + } + list(dots = dots, guides = guides) +} + + +# Map the `legend` argument to a margin side. TRUE -> "right"; "right"/"left"/ +# "top"/"bottom" are used directly; corner keywords map to the nearest side. +# Returns NULL when there is nothing to draw. +i.legend_side <- function(legend, guides) { + if (isFALSE(legend) || length(guides) == 0) { + return(NULL) + } + if (isTRUE(legend)) { + return("right") + } + pos <- as.character(legend)[1] + if (pos %in% c("right", "left", "top", "bottom")) { + return(pos) + } + if (grepl("left", pos)) { + return("left") + } + if (grepl("right", pos)) { + return("right") + } + if (grepl("top", pos)) { + return("top") + } + if (grepl("bottom", pos)) { + return("bottom") + } + "right" +} + +# Split the device into a plot region and a guide region, as device-relative +# (NDC) fractions for par("fig"). Using fig regions (rather than data-coordinate +# offsets) keeps the guide put and correctly sized when the device is resized. +i.legend_fig <- function(side) { + frac_v <- 0.22 # width fraction for a left/right guide + frac_h <- 0.18 # height fraction for a top/bottom guide + switch( + side, + right = list(plot = c(0, 1 - frac_v, 0, 1), guide = c(1 - frac_v, 1, 0, 1)), + left = list(plot = c(frac_v, 1, 0, 1), guide = c(0, frac_v, 0, 1)), + top = list(plot = c(0, 1, 0, 1 - frac_h), guide = c(0, 1, 1 - frac_h, 1)), + bottom = list(plot = c(0, 1, frac_h, 1), guide = c(0, 1, 0, frac_h)) + ) +} + +# Draw the guides in the current figure region (set by the caller via par(fig)). +# A fresh [0, 1] x [0, 1] window is set up and the guides are centred in it: +# stacked vertically for left/right, laid out in a row for top/bottom (with each +# guide's own entries arranged horizontally). +i.draw_guides_region <- function(guides, side) { + graphics::par(mar = c(0.4, 0.4, 0.4, 0.4)) + graphics::plot.new() + graphics::plot.window(xlim = c(0, 1), ylim = c(0, 1)) + graphics::par(xpd = NA) + + horiz <- side %in% c("top", "bottom") + # Measure each guide first so the whole stack can be centred. + rects <- lapply(guides, function(g) { + i.guide_draw(g, 0.5, 0.5, 0.5, 0.5, horiz, plot = FALSE) + }) + gap <- 0.04 + + if (horiz) { + ws <- vapply(rects, function(r) r$w, numeric(1)) + total <- sum(ws) + gap * max(0, length(guides) - 1) + x <- 0.5 - total / 2 + for (i in seq_along(guides)) { + i.guide_draw(guides[[i]], x, 0.5, 0, 0.5, horiz = TRUE, plot = TRUE) + x <- x + ws[i] + gap + } + } else { + hs <- vapply(rects, function(r) r$h, numeric(1)) + total <- sum(hs) + gap * max(0, length(guides) - 1) + y <- 0.5 + total / 2 + for (i in seq_along(guides)) { + i.guide_draw(guides[[i]], 0.5, y, 0.5, 1, horiz = FALSE, plot = TRUE) + y <- y - hs[i] - gap + } + } + invisible(NULL) +} + +# Draw (or, with plot = FALSE, just measure) one guide at the given anchor and +# justification. Returns list(left, top, w, h) in the current user coordinates. +i.guide_draw <- function(g, x, y, xjust, yjust, horiz, plot) { + if (g$type == "continuous") { + return(i.colorbar(g, x, y, xjust, yjust, horiz, plot)) + } + args <- list( + x = x, + y = y, + xjust = xjust, + yjust = yjust, + legend = g$labels, + title = g$name, + pch = 21, + bty = "n", + horiz = horiz, + plot = plot + ) + if (g$aesthetic == "color") { + args$pt.bg <- g$colors + args$pt.cex <- 1.8 + } else { + args$pt.bg <- "grey70" + args$pt.cex <- i.size_to_cex(g$sizes) + } + do.call(graphics::legend, args)$rect +} + +# Translate plotting sizes (vertex.size / edge.width scale) to a legend point +# cex. Sizes are normalised to a legible cex range; the legend is indicative, +# not a pixel-exact match to the drawn vertices (which use device units). +i.size_to_cex <- function(sizes) { + if (length(sizes) == 0 || all(!is.finite(sizes))) { + return(1.5) + } + mx <- max(sizes, na.rm = TRUE) + if (mx <= 0) { + return(rep(1.5, length(sizes))) + } + 0.8 + 2.2 * (sizes / mx) +} + +# Continuous colour guide (colorbar), in the current [0, 1] guide window. +# Vertical bar for left/right, horizontal bar for top/bottom. `plot = FALSE` +# measures only. Returns list(left, top, w, h). +i.colorbar <- function(g, x, y, xjust, yjust, horiz, plot) { + labs <- format(g$limits, digits = 3) + fill <- grDevices::rgb( + grDevices::colorRamp(g$ramp)(seq(0, 1, length.out = 50)), + maxColorValue = 255 + ) + lh <- 1.2 * graphics::strheight("M") + title_h <- if (is.null(g$name)) 0 else lh + + if (horiz) { + barw <- 0.5 + barh <- 0.12 + w <- barw + h <- barh + lh + title_h + } else { + barw <- 0.12 + label_w <- max(graphics::strwidth(labs, cex = 0.8)) + 0.02 + barh <- 0.5 + w <- barw + label_w + h <- barh + title_h + } + left <- x - xjust * w + top <- y + (1 - yjust) * h + + if (plot) { + bar_top <- top - title_h + if (horiz) { + xs <- seq(left, left + barw, length.out = 51) + graphics::rect( + xs[-51], + bar_top - barh, + xs[-1], + bar_top, + col = fill, + border = NA + ) + graphics::rect( + left, + bar_top - barh, + left + barw, + bar_top, + border = "grey40" + ) + ylab <- bar_top - barh - 0.2 * lh + graphics::text(left, ylab, labs[1], adj = c(0, 1), cex = 0.8) + graphics::text(left + barw, ylab, labs[2], adj = c(1, 1), cex = 0.8) + if (!is.null(g$name)) { + graphics::text(left + barw / 2, top, g$name, adj = c(0.5, 1)) + } + } else { + ys <- seq(bar_top - barh, bar_top, length.out = 51) + graphics::rect( + left, + ys[-51], + left + barw, + ys[-1], + col = fill, + border = NA + ) + graphics::rect( + left, + bar_top - barh, + left + barw, + bar_top, + border = "grey40" + ) + graphics::text( + left + barw + 0.02, + c(bar_top - barh, bar_top), + labels = labs, + adj = c(0, 0.5), + cex = 0.8 + ) + if (!is.null(g$name)) { + graphics::text(left, top, g$name, adj = c(0, 1)) + } + } + } + list(left = left, top = top, w = w, h = h) +} diff --git a/R/plot.R b/R/plot.R index 1b0f390e6c7..292cbf0472c 100644 --- a/R/plot.R +++ b/R/plot.R @@ -19,6 +19,493 @@ # ################################################################### +# Vertex sizes and edge widths are specified on a 0-200 scale (a `vertex.size` +# of 15 is the default); this factor converts them to user coordinates, where +# the plotting region spans [-1, 1] after rescaling. +VERTEX_SIZE_SCALE <- 1 / 200 + +# Arrowhead width scaling factor used by igraph.Arrows(); combined with the +# character size from par("cin") to size arrowheads relative to the device. +ARROW_WIDTH_FACTOR <- 1.2 / 4 + +# --- Self-loop / Bézier drawing helpers -------------------------------------- +# Hoisted out of plot.igraph()'s body (they capture no enclosing state). Named +# with an `i.` prefix; `i.plot.bezier` in particular must NOT be called +# `plot.bezier`, which R would treat as an S3 plot() method for class "bezier". + +# A single point on a cubic Bézier curve defined by control points `cp` (a 4x2 +# matrix) at parameter `t` in [0, 1]. +i.point.on.cubic.bezier <- function(cp, t) { + c <- 3 * (cp[2, ] - cp[1, ]) + b <- 3 * (cp[3, ] - cp[2, ]) - c + a <- cp[4, ] - cp[1, ] - c - b + + t2 <- t * t + t3 <- t * t * t + + a * t3 + b * t2 + c * t + cp[1, ] +} + +# `points` evenly spaced points along the cubic Bézier curve `cp`. +i.compute.bezier <- function(cp, points) { + dt <- seq(0, 1, by = 1 / (points - 1)) + sapply(dt, function(t) i.point.on.cubic.bezier(cp, t)) +} + +# Draw a Bézier curve with optional arrowheads at its ends. +i.plot.bezier <- function( + cp, + points, + color, + width, + arr, + lty, + arrow.size, + arr.w +) { + p <- i.compute.bezier(cp, points) + i.r_polygon(p[1, ], p[2, ], border = color, lwd = width, lty = lty) + if (arr == 1 || arr == 3) { + igraph.Arrows( + p[1, ncol(p) - 1], + p[2, ncol(p) - 1], + p[1, ncol(p)], + p[2, ncol(p)], + sh.col = color, + h.col = color, + size = arrow.size, + sh.lwd = width, + h.lwd = width, + open = FALSE, + code = 2, + width = arr.w + ) + } + if (arr == 2 || arr == 3) { + igraph.Arrows( + p[1, 2], + p[2, 2], + p[1, 1], + p[2, 1], + sh.col = color, + h.col = color, + size = arrow.size, + sh.lwd = width, + h.lwd = width, + open = FALSE, + code = 2, + width = arr.w + ) + } +} + +# Draw one self-loop as a rotated Bézier curve, plus its optional label. +# arrow.size/arr.w/loopSize defaults are placeholders only: every call site +# (the mapply() in plot.igraph) supplies them explicitly. +i.draw.loop <- function( + x0, + y0, + cx = x0, + cy = y0, + color, + angle = 0, + label = NA, + label.color, + label.font, + label.family, + label.cex, + label.halo = NA, + label.halo.width = 0.15, + width = 1, + arr = 2, + lty = 1, + arrow.size = 1, + arr.w = 1, + lab.x, + lab.y, + loopSize = 1, + narrowing = 1 +) { + rad <- angle + center <- c(cx, cy) + cp <- matrix( + c( + x0, + y0, + x0 + 0.4 * loopSize, + y0 + narrowing * 0.2 * loopSize, + x0 + 0.4 * loopSize, + y0 - narrowing * 0.2 * loopSize, + x0, + y0 + ), + ncol = 2, + byrow = TRUE + ) + cp_centered <- cp - + matrix(rep(center, each = nrow(cp)), ncol = 2, byrow = FALSE) + + rotation_matrix <- matrix(c(cos(rad), -sin(rad), sin(rad), cos(rad)), ncol = 2) + cp_rotated <- t(rotation_matrix %*% t(cp_centered)) + + cp <- cp_rotated + + matrix(rep(center, each = nrow(cp_rotated)), ncol = 2, byrow = FALSE) + + if (is.na(width)) { + width <- 1 + } + + i.plot.bezier( + cp, + 50, + color, + width, + arr = arr, + lty = lty, + arrow.size = arrow.size, + arr.w = arr.w + ) + + if (is.language(label) || !is.na(label)) { + # Get midpoint of the Bezier curve for label placement + p <- i.compute.bezier(cp, 50) + mid_index <- floor(ncol(p) / 2) + lx <- p[1, mid_index] + ly <- p[2, mid_index] + + # Override if label position explicitly given + if (!is.na(lab.x)) { + lx <- lab.x + } + if (!is.na(lab.y)) { + ly <- lab.y + } + + i.r_text_halo( + lx, + ly, + label, + col = label.color, + font = label.font, + family = label.family, + cex = label.cex, + halo = label.halo, + halo.width = label.halo.width + ) + } +} + +# Initialize the plotting canvas: an empty plot region +# with the requested limits, axes, aspect ratio and titles. Isolated from the +# drawing orchestration in plot.igraph() so the latter reads as +# setup -> edges -> vertices -> labels. +i.init_plot_canvas <- function( + xlim, + ylim, + xlab, + ylab, + axes, + frame.plot, + asp, + main, + sub +) { + i.r_init_canvas( + xlim = xlim, + ylim = ylim, + xlab = xlab, + ylab = ylab, + axes = axes, + frame.plot = frame.plot, + asp = asp, + main = main, + sub = sub + ) +} + +# Distribute self-loops around each vertex. For a vertex with +# k loops, place them evenly inside the largest angular gap between its incident +# (non-loop) edges, and compute a narrowing factor that compresses the loops +# when that gap is tight. Returns per-loop `angles` and `narrowing` vectors +# aligned to `loops.v`. +i.loop_angles <- function(graph, layout, loops.v) { + la_dyn <- numeric(length(loops.v)) + narrowing <- numeric(length(loops.v)) + + for (v in unique(loops.v)) { + idx <- which(loops.v == v) + n_loops <- length(idx) + + incident_edges <- incident(graph, v, mode = "all") + incident_edges <- incident_edges[!which_loop(graph)[incident_edges]] + + if (length(incident_edges) == 0) { + # Full circle available if no edges + loop_angles <- seq(0, 2 * pi, length.out = n_loops + 1)[-1] + gap_span <- 2 * pi + } else { + angles <- sapply(incident_edges, function(e) { + ends_e <- ends(graph, e, names = FALSE) + other <- if (as.numeric(ends_e[1]) == v) { + as.numeric(ends_e[2]) + } else { + as.numeric(ends_e[1]) + } + dx <- layout[other, 1] - layout[v, 1] + dy <- layout[other, 2] - layout[v, 2] + atan2(dy, dx) + }) + + angles <- (angles + 2 * pi) %% (2 * pi) + angles <- sort(angles) + gaps <- diff(c(angles, angles[1] + 2 * pi)) + max_gap_index <- which.max(gaps) + + gap_start <- angles[max_gap_index] + gap_span <- gaps[max_gap_index] + gap_end <- (gap_start + gap_span) %% (2 * pi) + + # Generate loop angles spaced inside the gap + if (gap_end > gap_start) { + loop_angles <- seq(gap_start, gap_end, length.out = n_loops + 2)[ + -c(1, n_loops + 2) + ] + } else { + # wrap around + gap_end <- gap_end + 2 * pi + loop_angles <- seq(gap_start, gap_end, length.out = n_loops + 2)[ + -c(1, n_loops + 2) + ] %% + (2 * pi) + } + } + + la_dyn[idx] <- loop_angles + + # Compute narrowing factor based on angular space + angle_per_loop <- gap_span / n_loops + # Scale narrowing between 1 (wide) and ~0.2 (tight) + narrowing_factor <- pmin(1, pmax(0.2, angle_per_loop / (pi / 4))) # full width if ≥45°, compress below + narrowing[idx] <- narrowing_factor + } + + list(angles = la_dyn, narrowing = narrowing) +} + +# Iteratively nudge overlapping text labels apart (ggrepel / Gephi "label +# adjust" style). Each label is repelled by other labels whose boxes overlap and +# gently sprung back toward its original anchor. Pure geometry given the label +# box half-sizes; deterministic (no randomness), so snapshots are stable. +# `hw`/`hh` are per-label half-width/height in user coordinates. +i.repel_labels <- function(x, y, hw, hh, iter = 200, spring = 0.04) { + n <- length(x) + if (n < 2) { + return(list(x = x, y = y)) + } + px <- x + py <- y + for (it in seq_len(iter)) { + fx <- numeric(n) + fy <- numeric(n) + for (i in seq_len(n - 1)) { + for (j in (i + 1):n) { + dx <- px[i] - px[j] + dy <- py[i] - py[j] + ox <- (hw[i] + hw[j]) - abs(dx) # overlap along x + oy <- (hh[i] + hh[j]) - abs(dy) # overlap along y + if (ox > 0 && oy > 0) { + # separate along the axis of smaller overlap (cheaper move) + if (ox <= oy) { + s <- if (dx >= 0) 1 else -1 + fx[i] <- fx[i] + s * ox * 0.5 + fx[j] <- fx[j] - s * ox * 0.5 + } else { + s <- if (dy >= 0) 1 else -1 + fy[i] <- fy[i] + s * oy * 0.5 + fy[j] <- fy[j] - s * oy * 0.5 + } + } + } + } + # spring back toward the original anchor + fx <- fx + (x - px) * spring + fy <- fy + (y - py) * spring + if (max(abs(c(fx, fy))) < 1e-4) { + break + } + px <- px + fx + py <- py + fy + } + list(x = px, y = py) +} + +# Draw vertex labels, offset from each vertex by label.dist along +# label.degree. xpd = TRUE is scoped to this call so labels may spill outside +# the plot region. With `repel = TRUE`, overlapping labels are nudged apart and +# a leader line connects each moved label to its anchor. No-op for an empty +# graph. +i.draw_vertex_labels <- function( + layout, + labels, + vertex.size, + label.dist, + label.degree, + label.color, + label.family, + label.font, + label.cex, + label.angle, + label.adj, + repel = FALSE, + label.halo = NA, + label.halo.width = 0.15 +) { + vc <- nrow(layout) + if (vc == 0) { + return(invisible(NULL)) + } + + old_xpd <- par(xpd = TRUE) + on.exit(par(old_xpd), add = TRUE) + + x <- layout[, 1] + + label.dist * + cos(-label.degree) * + (vertex.size + 6 * 8 * log10(2)) * + VERTEX_SIZE_SCALE + y <- layout[, 2] + + label.dist * + sin(-label.degree) * + (vertex.size + 6 * 8 * log10(2)) * + VERTEX_SIZE_SCALE + + label.col <- rep(label.color, length.out = vc) + label.fam <- rep(label.family, length.out = vc) + label.fnt <- rep(label.font, length.out = vc) + label.cex <- rep(label.cex, length.out = vc) + label.ang <- rep(label.angle, length.out = vc) + label.adj <- rep(list(label.adj), length.out = vc) + label.text <- rep(labels, length.out = vc) + label.halo <- rep(label.halo, length.out = vc) + label.halo.w <- rep(label.halo.width, length.out = vc) + + if (isTRUE(any(repel)) && vc > 1) { + drawn <- !is.na(label.text) & nzchar(as.character(label.text)) + if (sum(drawn) > 1) { + hw <- rep(0, vc) + hh <- rep(0, vc) + hw[drawn] <- strwidth(label.text[drawn], cex = label.cex[drawn]) / + 2 * + 1.15 + hh[drawn] <- strheight(label.text[drawn], cex = label.cex[drawn]) / + 2 * + 1.6 + moved <- i.repel_labels(x[drawn], y[drawn], hw[drawn], hh[drawn]) + nx <- x + ny <- y + nx[drawn] <- moved$x + ny[drawn] <- moved$y + # leader lines from the original anchor to labels that actually moved + shift <- sqrt((nx - x)^2 + (ny - y)^2) + lead <- drawn & shift > pmax(hh, 1e-6) + if (any(lead)) { + i.r_segments( + x[lead], + y[lead], + nx[lead], + ny[lead], + col = "grey60", + lwd = 0.5 + ) + } + x <- nx + y <- ny + } + } + + invisible(mapply( + function(x0, y0, lbl, col, fam, fnt, cex, srt, adj, halo, halo.w) { + i.r_text_halo( + x0, + y0, + labels = lbl, + col = col, + family = fam, + font = fnt, + cex = cex, + srt = srt, + adj = adj, + halo = halo, + halo.width = halo.w + ) + }, + x, + y, + label.text, + label.col, + label.fam, + label.fnt, + label.cex, + label.ang, + label.adj, + label.halo, + label.halo.w + )) +} + +# Draw one label with an optional shadowtext halo for legibility. +# `halo = NA` (the default) is exactly `i.r_text()` -> byte-identical to before. +# Otherwise the glyphs are drawn `halo.steps` times offset on a circle of radius +# (halo.width * strheight) in the `halo` colour, then the real text on top, which +# produces a tight outline that reads over edges. Operates on a single label. +i.r_text_halo <- function( + x, + y, + labels, + col, + family = "", + font = 1, + cex = 1, + srt = 0, + adj = NULL, + halo = NA, + halo.width = 0.15, + halo.steps = 16 +) { + if ( + !is.na(halo) && + !is.na(labels) && + nzchar(as.character(labels)) + ) { + r <- halo.width * strheight(labels, cex = cex) + th <- seq(0, 2 * pi, length.out = halo.steps + 1)[-1] + for (a in th) { + i.r_text( + x + r * cos(a), + y + r * sin(a), + labels = labels, + col = halo, + family = family, + font = font, + cex = cex, + srt = srt, + adj = adj + ) + } + } + i.r_text( + x, + y, + labels = labels, + col = col, + family = family, + font = font, + cex = cex, + srt = srt, + adj = adj + ) +} + #' Plotting of graphs #' #' `plot.igraph()` is able to plot graphs to any R device. It is the @@ -64,6 +551,12 @@ #' @param loop.size A numeric scalar that allows the user to scale the loop edges #' of the network. The default loop size is 1. Larger values will produce larger #' loops. +#' @param legend Controls drawing of legends/colorbars for any aesthetics +#' supplied via [scale_color()] / [scale_size()]. The guide is drawn in the +#' reserved outer margin on one side of the plot: `TRUE` (default) or +#' `"right"` places it to the right, `"left"`/`"top"`/`"bottom"` on the +#' corresponding side (`"top"`/`"bottom"` arrange entries horizontally); +#' `FALSE` suppresses it. Has no effect when no scale is used. #' @param \dots Additional plotting parameters. See [igraph.plotting] for #' the complete list. #' @return Returns `NULL`, invisibly. @@ -98,6 +591,7 @@ plot.igraph <- function( mark.expand = 15, mark.lwd = 1, loop.size = 1, + legend = TRUE, ... ) { graph <- x @@ -107,10 +601,17 @@ plot.igraph <- function( ################################################################ ## Visual parameters - params <- i.parse.plot.params(graph, list(...)) + # Resolve any scale_*() arguments to plain aesthetic vectors and collect their + # guides (legends/colorbars) to draw at the end. Must happen before + # i.parse.plot.params(), whose recycling strips the scale class. + scaled <- i.apply_scales(list(...)) + guides <- scaled$guides + legend_side <- i.legend_side(legend, guides) + params <- i.parse.plot.params(graph, scaled$dots) vertex.size <- params("vertex", "size") vertex.size.scaling <- params("vertex", "size.scaling") + vertex.alpha <- params("vertex", "alpha") label.family <- params("vertex", "label.family") label.font <- params("vertex", "label.font") label.cex <- params("vertex", "label.cex") @@ -119,10 +620,22 @@ plot.igraph <- function( label.dist <- params("vertex", "label.dist") label.angle <- params("vertex", "label.angle") label.adj <- params("vertex", "label.adj") + label.repel <- params("vertex", "label.repel") + label.halo <- params("vertex", "label.halo") + label.halo.width <- params("vertex", "label.halo.width") labels <- params("vertex", "label") shape <- igraph.check.shapes(params("vertex", "shape")) edge.color <- params("edge", "color") + edge.alpha <- params("edge", "alpha") + edge.color <- i.apply_alpha(edge.color, edge.alpha) + edge.gradient <- as.logical(params("edge", "gradient")) + # Base per-vertex fill colour (before vertex.alpha), only needed for gradients. + vcol_base <- if (any(edge.gradient)) { + rep(params("vertex", "color"), length.out = vc) + } else { + NULL + } edge.width <- params("edge", "width") edge.lty <- params("edge", "lty") arrow.mode <- params("edge", "arrow.mode") @@ -132,6 +645,8 @@ plot.igraph <- function( edge.label.family <- params("edge", "label.family") edge.label.cex <- params("edge", "label.cex") edge.label.color <- params("edge", "label.color") + edge.label.halo <- params("edge", "label.halo") + edge.label.halo.width <- params("edge", "label.halo.width") elab.x <- params("edge", "label.x") elab.y <- params("edge", "label.y") arrow.size <- params("edge", "arrow.size") @@ -140,6 +655,15 @@ plot.igraph <- function( if (is.function(curved)) { curved <- curved(graph) } + edge.style <- as.character(params("edge", "style")) + i.valid_edge_styles <- c("auto", "straight", "arc", "elbow", "diagonal") + bad.style <- setdiff(unique(edge.style), i.valid_edge_styles) + if (length(bad.style) > 0) { + cli::cli_abort(c( + "Invalid {.arg edge.style} value{?s}: {.val {bad.style}}.", + "i" = "Valid styles are {.val {i.valid_edge_styles}}." + )) + } layout <- i.postprocess.layout(params("plot", "layout")) if (nrow(layout) != vc) { @@ -167,6 +691,45 @@ plot.igraph <- function( # the new style parameters can't do this yet arrow.mode <- i.get.arrow.mode(graph, arrow.mode) + # igraph 3.0.0: per-element aesthetics must be length 1 or vcount()/ecount(). + # arrow.mode is excluded (its "a:" form reads a vertex attribute, so it can be + # vcount-long); label.adj / pie / raster have non-per-element length semantics. + i.check_aes_lengths( + vertex = list( + size = vertex.size, + color = params("vertex", "color"), + frame.color = params("vertex", "frame.color"), + frame.width = params("vertex", "frame.width"), + shape = shape, + label = labels, + label.color = label.color, + label.cex = label.cex, + label.dist = label.dist, + label.degree = label.degree, + label.angle = label.angle, + label.font = label.font, + label.family = label.family, + label.halo = label.halo, + label.halo.width = label.halo.width + ), + edge = list( + color = edge.color, + width = edge.width, + lty = edge.lty, + arrow.size = arrow.size, + arrow.width = arrow.width, + label = edge.labels, + label.color = edge.label.color, + label.cex = edge.label.cex, + label.font = edge.label.font, + label.family = edge.label.family, + label.halo = edge.label.halo, + label.halo.width = edge.label.halo.width + ), + vc = vc, + ec = ecount(graph) + ) + ################################################################ ## create the plot if (rescale) { @@ -178,7 +741,7 @@ plot.igraph <- function( } layout <- norm_coords(layout, -1, 1, -1, 1) fact <- (1 - vertex.size.scaling) - maxv <- 1 / 200 * max(vertex.size) + maxv <- VERTEX_SIZE_SCALE * max(vertex.size) xlim <- c( xlim[1] - margin[2] - fact * maxv, @@ -196,27 +759,42 @@ plot.igraph <- function( ylim <- range(layout[, 2]) + c(-margin[1], margin[3]) } } + # When a scale legend is drawn, split the device into a plot region and a + # guide region (device-relative, so it survives resizing). The graph is drawn + # in the plot region; the guides are drawn into the guide region at the end. + legend_fig <- NULL + if (!add && !is.null(legend_side)) { + legend_fig <- i.legend_fig(legend_side) + old_par <- graphics::par(no.readonly = TRUE) + on.exit(graphics::par(old_par), add = TRUE) + graphics::par(fig = legend_fig$plot) + } if (!add) { - plot( - 0, - 0, - type = "n", - xlab = xlab, - ylab = ylab, - xlim = xlim, - ylim = ylim, - axes = axes, - frame.plot = ifelse(is.null(frame.plot), axes, frame.plot), - asp = asp, - main = main, - sub = sub + i.init_plot_canvas( + xlim, + ylim, + xlab, + ylab, + axes, + frame.plot, + asp, + main, + sub ) } ################################################################ ## Rescaling vertices and updating params + # Fold vertex.alpha into the vertex fill colour so the shapes pick it up via + # the rebuilt params below (no-op when fully opaque). + if (!all(vertex.alpha == 1)) { + scaled$dots$vertex.color <- i.apply_alpha( + rep(params("vertex", "color"), length.out = vc), + vertex.alpha + ) + } if (vertex.size.scaling) { - newdots <- list(...) + newdots <- scaled$dots # vertex.size vertex.size <- i.rescale.vertex( @@ -254,15 +832,11 @@ plot.igraph <- function( params <- i.parse.plot.params(graph, newdots) } else { - params <- i.parse.plot.params( - graph, - list( - vertex.size = 1 / 200 * vertex.size, - vertex.size2 = 1 / 200 * params("vertex", "size2"), - ... - ) - ) - vertex.size <- 1 / 200 * vertex.size + newdots <- scaled$dots + newdots$vertex.size <- VERTEX_SIZE_SCALE * vertex.size + newdots$vertex.size2 <- VERTEX_SIZE_SCALE * params("vertex", "size2") + params <- i.parse.plot.params(graph, newdots) + vertex.size <- VERTEX_SIZE_SCALE * vertex.size } ################################################################ ## Mark vertex groups @@ -360,274 +934,61 @@ plot.igraph <- function( x1 <- ec[, 3] y1 <- ec[, 4] + # Resolve the per-edge aesthetics into one table (length ecount), + # then slice it by loop-edge / non-loop-edge index instead of repeating the + # `if (length(x) > 1) x[idx]` idiom for every parameter. + edge_aes <- i.edge_aes_table( + color = edge.color, + width = edge.width, + lty = edge.lty, + arrow.mode = arrow.mode, + arrow.size = arrow.size, + arrow.width = arrow.width, + curved = curved, + label.color = edge.label.color, + label.family = edge.label.family, + label.font = edge.label.font, + label.cex = edge.label.cex, + label.halo = edge.label.halo, + label.halo.width = edge.label.halo.width, + style = edge.style, + alpha = edge.alpha, + gradient = edge.gradient, + n = ecount(graph) + ) + ################################################################ ## add the loop edges if (length(loops.e) > 0) { - ec <- edge.color - if (length(ec) > 1) { - ec <- ec[loops.e] - } - - point.on.cubic.bezier <- function(cp, t) { - c <- 3 * (cp[2, ] - cp[1, ]) - b <- 3 * (cp[3, ] - cp[2, ]) - c - a <- cp[4, ] - cp[1, ] - c - b - - t2 <- t * t - t3 <- t * t * t - - a * t3 + b * t2 + c * t + cp[1, ] - } - - compute.bezier <- function(cp, points) { - dt <- seq(0, 1, by = 1 / (points - 1)) - sapply(dt, function(t) point.on.cubic.bezier(cp, t)) - } - - plot.bezier <- function( - cp, - points, - color, - width, - arr, - lty, - arrow.size, - arr.w - ) { - p <- compute.bezier(cp, points) - polygon(p[1, ], p[2, ], border = color, lwd = width, lty = lty) - if (arr == 1 || arr == 3) { - igraph.Arrows( - p[1, ncol(p) - 1], - p[2, ncol(p) - 1], - p[1, ncol(p)], - p[2, ncol(p)], - sh.col = color, - h.col = color, - size = arrow.size, - sh.lwd = width, - h.lwd = width, - open = FALSE, - code = 2, - width = arr.w - ) - } - if (arr == 2 || arr == 3) { - igraph.Arrows( - p[1, 2], - p[2, 2], - p[1, 1], - p[2, 1], - sh.col = color, - h.col = color, - size = arrow.size, - sh.lwd = width, - h.lwd = width, - open = FALSE, - code = 2, - width = arr.w - ) - } - } - - loop <- function( - x0, - y0, - cx = x0, - cy = y0, - color, - angle = 0, - label = NA, - label.color, - label.font, - label.family, - label.cex, - width = 1, - arr = 2, - lty = 1, - arrow.size = arrow.size, - arr.w = arr.w, - lab.x, - lab.y, - loopSize = loop.size, - narrowing = 1 - ) { - rad <- angle - center <- c(cx, cy) - cp <- matrix( - c( - x0, - y0, - x0 + 0.4 * loopSize, - y0 + narrowing * 0.2 * loopSize, - x0 + 0.4 * loopSize, - y0 - narrowing * 0.2 * loopSize, - x0, - y0 - ), - ncol = 2, - byrow = TRUE - ) - cp_centered <- cp - - matrix(rep(center, each = nrow(cp)), ncol = 2, byrow = FALSE) - - rotation_matrix <- matrix(c(cos(rad), -sin(rad), sin(rad), cos(rad)), ncol = 2) - cp_rotated <- t(rotation_matrix %*% t(cp_centered)) - - cp <- cp_rotated + - matrix(rep(center, each = nrow(cp_rotated)), ncol = 2, byrow = FALSE) - - if (is.na(width)) { - width <- 1 - } - - plot.bezier( - cp, - 50, - color, - width, - arr = arr, - lty = lty, - arrow.size = arrow.size, - arr.w = arr.w - ) - - if (is.language(label) || !is.na(label)) { - # Get midpoint of the Bezier curve for label placement - p <- compute.bezier(cp, 50) - mid_index <- floor(ncol(p) / 2) - lx <- p[1, mid_index] - ly <- p[2, mid_index] - - # Override if label position explicitly given - if (!is.na(lab.x)) { - lx <- lab.x - } - if (!is.na(lab.y)) { - ly <- lab.y - } - - text( - lx, - ly, - label, - col = label.color, - font = label.font, - family = label.family, - cex = label.cex - ) - } - } - - ec <- edge.color - if (length(ec) > 1) { - ec <- ec[loops.e] - } + # vertex.size is vertex-scoped (indexed by the loop's vertex) and loop.angle + # is nullable, so both are handled outside the edge aesthetic table. vs <- vertex.size if (length(vertex.size) > 1) { vs <- vs[loops.v] } - ew <- edge.width - if (length(edge.width) > 1) { - ew <- ew[loops.e] - } la <- loop.angle if (length(loop.angle) > 1) { la <- la[loops.e] } - lty <- edge.lty - if (length(edge.lty) > 1) { - lty <- lty[loops.e] - } - arr <- arrow.mode - if (length(arrow.mode) > 1) { - arr <- arrow.mode[loops.e] - } - asize <- arrow.size - if (length(arrow.size) > 1) { - asize <- arrow.size[loops.e] - } - lcol <- edge.label.color - if (length(lcol) > 1) { - lcol <- lcol[loops.e] - } - lfam <- edge.label.family - if (length(lfam) > 1) { - lfam <- lfam[loops.e] - } - lfon <- edge.label.font - if (length(lfon) > 1) { - lfon <- lfon[loops.e] - } - lcex <- edge.label.cex - if (length(lcex) > 1) { - lcex <- lcex[loops.e] - } - - # For each loop, assign unique angle within largest gap (flower petal style) - # depending on the number of loops and the available angular space - la_dyn <- numeric(length(loops.v)) - narrowing <- numeric(length(loops.v)) - - loop_table <- table(loops.v) - loop_idx <- ave(seq_along(loops.v), loops.v, FUN = seq_along) - - for (v in unique(loops.v)) { - idx <- which(loops.v == v) - n_loops <- length(idx) - - incident_edges <- incident(graph, v, mode = "all") - incident_edges <- incident_edges[!which_loop(graph)[incident_edges]] - - if (length(incident_edges) == 0) { - # Full circle available if no edges - loop_angles <- seq(0, 2 * pi, length.out = n_loops + 1)[-1] - gap_span <- 2 * pi - } else { - angles <- sapply(incident_edges, function(e) { - ends_e <- ends(graph, e, names = FALSE) - other <- if (as.numeric(ends_e[1]) == v) { - as.numeric(ends_e[2]) - } else { - as.numeric(ends_e[1]) - } - dx <- layout[other, 1] - layout[v, 1] - dy <- layout[other, 2] - layout[v, 2] - atan2(dy, dx) - }) - - angles <- (angles + 2 * pi) %% (2 * pi) - angles <- sort(angles) - gaps <- diff(c(angles, angles[1] + 2 * pi)) - max_gap_index <- which.max(gaps) - - gap_start <- angles[max_gap_index] - gap_span <- gaps[max_gap_index] - gap_end <- (gap_start + gap_span) %% (2 * pi) - - # Generate loop angles spaced inside the gap - if (gap_end > gap_start) { - loop_angles <- seq(gap_start, gap_end, length.out = n_loops + 2)[ - -c(1, n_loops + 2) - ] - } else { - # wrap around - gap_end <- gap_end + 2 * pi - loop_angles <- seq(gap_start, gap_end, length.out = n_loops + 2)[ - -c(1, n_loops + 2) - ] %% - (2 * pi) - } - } - - la_dyn[idx] <- loop_angles - # Compute narrowing factor based on angular space - angle_per_loop <- gap_span / n_loops - # Scale narrowing between 1 (wide) and ~0.2 (tight) - narrowing_factor <- pmin(1, pmax(0.2, angle_per_loop / (pi / 4))) # full width if ≥45°, compress below - narrowing[idx] <- narrowing_factor - } + loop_aes <- vctrs::vec_slice(edge_aes, loops.e) + ec <- loop_aes$color + ew <- loop_aes$width + lty <- loop_aes$lty + arr <- loop_aes$arrow.mode + asize <- loop_aes$arrow.size + aw <- loop_aes$arrow.width + lcol <- loop_aes$label.color + lfam <- loop_aes$label.family + lfon <- loop_aes$label.font + lcex <- loop_aes$label.cex + lhalo <- loop_aes$label.halo + lhalo.w <- loop_aes$label.halo.width + + # Place loops in the largest angular gap at each vertex (flower-petal style). + loop_geo <- i.loop_angles(graph, layout, loops.v) + la_dyn <- loop_geo$angles + narrowing <- loop_geo$narrowing if (is.null(la)) { la <- rep(NA, length(loops.v)) } @@ -645,7 +1006,7 @@ plot.igraph <- function( yy0 <- layout[loops.v, 2] + sin(la) * r_offset mapply( - loop, + i.draw.loop, xx0, yy0, color = ec, @@ -655,11 +1016,13 @@ plot.igraph <- function( label.family = lfam, label.font = lfon, label.cex = lcex, + label.halo = lhalo, + label.halo.width = lhalo.w, lty = lty, width = ew, arr = arr, arrow.size = asize, - arr.w = arrow.width, + arr.w = aw, lab.x = loop.labx, lab.y = loop.laby, loopSize = adjusted_loop_size, @@ -670,32 +1033,78 @@ plot.igraph <- function( ################################################################ ## non-loop edges if (length(x0) != 0) { - if (length(edge.color) > 1) { - edge.color <- edge.color[nonloops.e] - } - if (length(edge.width) > 1) { - edge.width <- edge.width[nonloops.e] - } - if (length(edge.lty) > 1) { - edge.lty <- edge.lty[nonloops.e] - } - if (length(arrow.mode) > 1) { - arrow.mode <- arrow.mode[nonloops.e] - } - if (length(arrow.size) > 1) { - arrow.size <- arrow.size[nonloops.e] + # Slice the edge aesthetic table to the non-loop edges; every column is now + # length(nonloops.e), so the per-arrow-code branch can index by `valid` + # directly. (This also fixes a former double-slice of `curved`.) + nl_aes <- vctrs::vec_slice(edge_aes, nonloops.e) + edge.color <- nl_aes$color + edge.width <- nl_aes$width + edge.lty <- nl_aes$lty + arrow.mode <- nl_aes$arrow.mode + arrow.size <- nl_aes$arrow.size + arrow.width <- nl_aes$arrow.width + curved <- nl_aes$curved + edge.style <- nl_aes$style + edge.gradient <- as.logical(nl_aes$gradient) + + # Axis-aware attachment for elbow/diagonal edges: re-clip their endpoints to + # the dominant-axis boundary (top/bottom- or left/right-centre) instead of + # the centre-to-centre diagonal point, so the orthogonal/diagonal routing + # meets each vertex on its centre axis. The chosen axis is passed to + # igraph.Arrows so the path builders route to match. Other styles keep the + # centre-to-centre clip from above. + eff.style <- ifelse( + edge.style == "auto", + ifelse(curved != 0, "arc", "straight"), + edge.style + ) + is.ed <- eff.style %in% c("elbow", "diagonal") + edge.axis <- rep(NA, length(x0)) + if (any(is.ed)) { + vert <- abs(edge.coords[, 4] - edge.coords[, 2]) >= + abs(edge.coords[, 3] - edge.coords[, 1]) + adj <- i.axis_clip_endpoints(edge.coords, el, shape, params, vert) + x0[is.ed] <- adj[is.ed, 1] + y0[is.ed] <- adj[is.ed, 2] + x1[is.ed] <- adj[is.ed, 3] + y1[is.ed] <- adj[is.ed, 4] + edge.axis[is.ed] <- vert[is.ed] } - if (length(curved) > 1) { - curved <- curved[nonloops.e] + + # Gradient edges: shaft colour runs from the source vertex colour to the + # target vertex colour; the arrowhead uses the target colour. Only touch the + # colour vectors when a gradient is actually requested, so plain plots are + # byte-identical. + sh.col.e <- edge.color + h.col.e <- edge.color + col.to.e <- edge.color + if (any(edge.gradient)) { + to_hex <- function(x) { + grDevices::rgb( + t(grDevices::col2rgb(x, alpha = TRUE)), + maxColorValue = 255 + ) + } + ealpha <- nl_aes$alpha + grad_from <- i.apply_alpha(to_hex(vcol_base[el[, 1]]), ealpha) + grad_to <- i.apply_alpha(to_hex(vcol_base[el[, 2]]), ealpha) + base_hex <- to_hex(edge.color) + sh.col.e <- base_hex + h.col.e <- base_hex + col.to.e <- base_hex + sh.col.e[edge.gradient] <- grad_from[edge.gradient] + h.col.e[edge.gradient] <- grad_to[edge.gradient] + col.to.e[edge.gradient] <- grad_to[edge.gradient] } + if (length(unique(arrow.mode)) == 1) { lc <- igraph.Arrows( x0, y0, x1, y1, - h.col = edge.color, - sh.col = edge.color, + h.col = h.col.e, + sh.col = sh.col.e, sh.lwd = edge.width, h.lwd = 1, open = FALSE, @@ -704,48 +1113,46 @@ plot.igraph <- function( h.lty = 1, size = arrow.size, width = arrow.width, - curved = curved + curved = curved, + style = edge.style, + gradient = edge.gradient, + col.to = col.to.e, + ids = nonloops.e, + axis = edge.axis ) lc.x <- lc$lab.x lc.y <- lc$lab.y } else { ## different kinds of arrows drawn separately as 'arrows' cannot - ## handle a vector as the 'code' argument - curved <- rep(curved, length.out = ecount(graph))[nonloops.e] - lc.x <- lc.y <- numeric(length(curved)) + ## handle a vector as the 'code' argument. Every aesthetic is already + ## length(nonloops.e), so subset each by `valid` consistently. + lc.x <- lc.y <- numeric(length(nonloops.e)) for (code in 0:3) { valid <- arrow.mode == code if (!any(valid)) { next } - ec <- edge.color - if (length(ec) > 1) { - ec <- ec[valid] - } - ew <- edge.width - if (length(ew) > 1) { - ew <- ew[valid] - } - el <- edge.lty - if (length(el) > 1) { - el <- el[valid] - } lc <- igraph.Arrows( x0[valid], y0[valid], x1[valid], y1[valid], code = code, - sh.col = ec, - h.col = ec, - sh.lwd = ew, + sh.col = sh.col.e[valid], + h.col = h.col.e[valid], + sh.lwd = edge.width[valid], h.lwd = 1, h.lty = 1, - sh.lty = el, + sh.lty = edge.lty[valid], open = FALSE, - size = arrow.size, - width = arrow.width, - curved = curved[valid] + size = arrow.size[valid], + width = arrow.width[valid], + curved = curved[valid], + style = edge.style[valid], + gradient = edge.gradient[valid], + col.to = col.to.e[valid], + ids = nonloops.e[valid], + axis = edge.axis[valid] ) lc.x[valid] <- lc$lab.x lc.y[valid] <- lc$lab.y @@ -758,39 +1165,27 @@ plot.igraph <- function( lc.y <- ifelse(is.na(elab.y), lc.y, elab.y) } - ecol <- edge.label.color - if (length(ecol) > 1) { - ecol <- ecol[nonloops.e] - } - efam <- edge.label.family - if (length(efam) > 1) { - efam <- efam[nonloops.e] - } - - efon <- edge.label.font - if (length(efon) > 1) { - efon <- efon[nonloops.e] - } - ecex <- edge.label.cex - if (length(ecex) > 1) { - ecex <- ecex[nonloops.e] - } - en <- length(nonloops.e) - ecol <- rep(ecol, length.out = en) - efam <- rep(efam, length.out = en) - efon <- rep(efon, length.out = en) - ecex <- rep(ecex, length.out = en) + # Edge-label aesthetics come from the same non-loop slice (already recycled + # to length(nonloops.e)). + ecol <- nl_aes$label.color + efam <- nl_aes$label.family + efon <- nl_aes$label.font + ecex <- nl_aes$label.cex + ehalo <- nl_aes$label.halo + ehalo.w <- nl_aes$label.halo.width invisible(mapply( - function(x, y, label, col, family, font, cex) { - text( + function(x, y, label, col, family, font, cex, halo, halo.w) { + i.r_text_halo( x, y, labels = label, col = col, family = family, font = font, - cex = cex + cex = cex, + halo = halo, + halo.width = halo.w ) }, lc.x, @@ -799,7 +1194,9 @@ plot.igraph <- function( ecol, efam, efon, - ecex + ecex, + ehalo, + ehalo.w )) } @@ -808,6 +1205,14 @@ plot.igraph <- function( ################################################################ # add the vertices if (vc > 0) { + vtitles <- if (!is.null(i.render_state$vertex_titles)) { + i.render_state$vertex_titles + } else if ("name" %in% vertex_attr_names(graph)) { + as.character(V(graph)$name) + } else { + as.character(seq_len(vc)) + } + i.r_group_begin("vertices", title = vtitles) if (length(unique(shape)) == 1) { .igraph.shapes[[shape[1]]]$plot(layout, params = params) } else { @@ -819,52 +1224,35 @@ plot.igraph <- function( ) }) } + i.r_group_end() } ################################################################ # add the labels - old_xpd <- par(xpd = TRUE) - on.exit(par(old_xpd), add = TRUE) - x <- layout[, 1] + - label.dist * cos(-label.degree) * (vertex.size + 6 * 8 * log10(2)) / 200 - y <- layout[, 2] + - label.dist * sin(-label.degree) * (vertex.size + 6 * 8 * log10(2)) / 200 - if (vc > 0) { - label.col <- rep(label.color, length.out = vc) - label.fam <- rep(label.family, length.out = vc) - label.fnt <- rep(label.font, length.out = vc) - label.cex <- rep(label.cex, length.out = vc) - label.ang <- rep(label.angle, length.out = vc) - label.adj <- rep(list(label.adj), length.out = vc) - label.text <- rep(labels, length.out = vc) - - # Draw vertex labels - invisible(mapply( - function(x0, y0, lbl, col, fam, fnt, cex, srt, adj) { - text( - x0, - y0, - labels = lbl, - col = col, - family = fam, - font = fnt, - cex = cex, - srt = srt, - adj = adj - ) - }, - x, - y, - label.text, - label.col, - label.fam, - label.fnt, - label.cex, - label.ang, - label.adj - )) + i.draw_vertex_labels( + layout, + labels, + vertex.size, + label.dist, + label.degree, + label.color, + label.family, + label.font, + label.cex, + label.angle, + label.adj, + repel = label.repel, + label.halo = label.halo, + label.halo.width = label.halo.width + ) + + ################################################################ + # draw legends / colorbars for any scale_*() aesthetics, in the guide region + if (!is.null(legend_fig)) { + graphics::par(fig = legend_fig$guide, new = TRUE) + i.draw_guides_region(guides, legend_side) } - rm(x, y) + invisible(NULL) } @@ -1725,6 +2113,194 @@ rglplot.igraph <- function(x, ...) { # This is taken from the IDPmisc package, # slightly modified: code argument added +# Pure geometry: the outline of an arrowhead in polar coordinates +# (angle + radius from the tip), used by igraph.Arrows() to draw or outline the +# head. Depends only on scalar inputs, so it is testable without a device. +# cin arrow length, already scaled by the character size (par("cin")) +# w arrow width factor +# delta line-width-dependent padding +i.arrowhead_shape <- function(cin, w, delta) { + x <- sqrt(seq(0, cin^2, length.out = floor(35 * cin) + 2)) + x.arr <- c(-rev(x), -x) + wx2 <- w * x^2 + y.arr <- c(-rev(wx2 + delta), wx2 + delta) + list( + deg.arr = c(atan2(y.arr, x.arr), NA), + r.arr = c(sqrt(x.arr^2 + y.arr^2), NA) + ) +} + +# Pure geometry: shaft segment endpoints for a single edge, pulled +# back from the vertices by `r.seg` at whichever end carries an arrowhead (per +# `code`) so the shaft does not poke through the head. `uin` is the +# inches-per-user-unit scale from 1/xyinch(). Returns sx1/sy1/sx2/sy2. +i.arrow_shaft_endpoints <- function(x1, y1, x2, y2, code, r.seg, uin) { + theta1 <- atan2((y1 - y2) * uin[2], (x1 - x2) * uin[1]) + theta2 <- atan2((y2 - y1) * uin[2], (x2 - x1) * uin[1]) + x1d <- y1d <- x2d <- y2d <- 0 + if (code %in% c(1, 3)) { + x2d <- r.seg * cos(theta2) / uin[1] + y2d <- r.seg * sin(theta2) / uin[2] + } + if (code %in% c(2, 3)) { + x1d <- r.seg * cos(theta1) / uin[1] + y1d <- r.seg * sin(theta1) / uin[2] + } + list(sx1 = x1 + x1d, sy1 = y1 + y1d, sx2 = x2 + x2d, sy2 = y2 + y2d) +} + +# Pure geometry: label anchor two thirds of the way along a straight +# edge from (x2, y2) toward (x1, y1). +i.edge_label_pos <- function(x1, y1, x2, y2) { + phi <- atan2(y1 - y2, x1 - x2) + r <- sqrt((x1 - x2)^2 + (y1 - y2)^2) + # unname() the components: when the coordinates carry names (e.g. a named + # vertex.size such as scale_size(degree(g)) propagates names through edge + # clipping), `c(x = <named>, y = <named>)` would yield names like "x.Alice" + # instead of "x"/"y", breaking the lab[["x"]] / lab[["y"]] lookups downstream. + c( + x = unname(x2 + 2 / 3 * r * cos(phi)), + y = unname(y2 + 2 / 3 * r * sin(phi)) + ) +} + +# Geometry: the X-spline of a curved edge. The control point is offset +# from the edge midpoint perpendicular to the shaft by `lambda`. Returns the +# xspline() coordinate list (draw = FALSE; needs an active device). +i.curved_spline <- function(x1, y1, x2, y2, sx1, sy1, sx2, sy2, lambda) { + midx <- (x1 + x2) / 2 + midy <- (y1 + y2) / 2 + spx <- midx - lambda * 1 / 2 * (sy2 - sy1) + spy <- midy + lambda * 1 / 2 * (sx2 - sx1) + xspline( + x = c(sx1, spx, sx2), + y = c(sy1, spy, sy2), + shape = 1, + draw = FALSE + ) +} + +# Geometry: two-corner orthogonal ("elbow") path between two points. +# Leaves along the dominant axis, turns at the midpoint of that axis, crosses, +# then turns into the target. `vertical` forces the leaving axis (TRUE = leave +# vertically); when NULL the dominant axis is inferred from the endpoints. The +# caller passes it explicitly so it matches the axis used to attach the +# endpoints. Returns list(x, y) of the four polyline vertices. +i.elbow_path <- function(x0, y0, x1, y1, vertical = NULL) { + vert <- if (is.null(vertical)) abs(y1 - y0) > abs(x1 - x0) else vertical + if (!vert) { + mid <- (x0 + x1) / 2 + list(x = c(x0, mid, mid, x1), y = c(y0, y0, y1, y1)) + } else { + mid <- (y0 + y1) / 2 + list(x = c(x0, x0, x1, x1), y = c(y0, mid, mid, y1)) + } +} + +# Geometry: smooth "diagonal" S-curve between two points, a cubic Bezier whose +# control points sit on the dominant axis so the curve leaves and enters along +# that axis. `vertical` forces the leaving axis (see i.elbow_path); when NULL it +# is inferred from the endpoints. Returns list(x, y) sampled at `n` points. +i.diagonal_path <- function(x0, y0, x1, y1, n = 30, vertical = NULL) { + vert <- if (is.null(vertical)) abs(y1 - y0) > abs(x1 - x0) else vertical + if (!vert) { + mid <- (x0 + x1) / 2 + cp <- rbind(c(x0, y0), c(mid, y0), c(mid, y1), c(x1, y1)) + } else { + mid <- (y0 + y1) / 2 + cp <- rbind(c(x0, y0), c(x0, mid), c(x1, mid), c(x1, y1)) + } + p <- i.compute.bezier(cp, n) + list(x = p[1, ], y = p[2, ]) +} + +# Re-clip edge endpoints along the dominant axis instead of the centre-to-centre +# line, for elbow/diagonal edges. `edge.coords` holds the centre-to-centre +# coordinates (columns from.x, from.y, to.x, to.y); `el` is the edge list, +# `shape` the per-vertex shape vector, `params` the resolved plotting params, and +# `vertical` a per-edge flag (TRUE = leave/enter vertically). Returns a 4-column +# matrix of axis-aligned boundary points. It reuses the shape clip functions by +# feeding them axis-aligned synthetic segments, so the boundary is correct for +# every shape (circle/square/rectangle). +i.axis_clip_endpoints <- function(edge.coords, el, shape, params, vertical) { + cx1 <- edge.coords[, 1] + cy1 <- edge.coords[, 2] + cx2 <- edge.coords[, 3] + cy2 <- edge.coords[, 4] + + # Synthetic segments whose direction at each end is axis-aligned: the "from" + # end leaves toward the target along the axis, the "to" end enters from the + # source side along the axis. + synth_from <- cbind( + cx1, + cy1, + ifelse(vertical, cx1, cx2), + ifelse(vertical, cy2, cy1) + ) + synth_to <- cbind( + ifelse(vertical, cx2, cx1), + ifelse(vertical, cy1, cy2), + cx2, + cy2 + ) + + clip_end <- function(coords, end) { + if (length(unique(shape)) == 1) { + .igraph.shapes[[shape[1]]]$clip(coords, el, params = params, end = end) + } else { + idx <- if (end == "from") el[, 1] else el[, 2] + t(sapply(seq_len(nrow(coords)), function(x) { + .igraph.shapes[[shape[idx[x]]]]$clip( + coords[x, , drop = FALSE], + el[x, , drop = FALSE], + params = params, + end = end + ) + })) + } + } + + from <- clip_end(synth_from, "from") + to <- clip_end(synth_to, "to") + cbind(from[, 1], from[, 2], to[, 1], to[, 2]) +} + +# Apply a per-element alpha (transparency, in [0, 1]) to a colour vector by +# multiplying any existing alpha. A no-op when every alpha is 1, so the default +# leaves colours — and snapshots — byte-identical. +i.apply_alpha <- function(col, alpha) { + if (length(col) == 0 || all(alpha == 1)) { + return(col) + } + rgba <- grDevices::col2rgb(col, alpha = TRUE) / 255 + a <- rep(alpha, length.out = ncol(rgba)) + grDevices::rgb(rgba[1, ], rgba[2, ], rgba[3, ], alpha = rgba[4, ] * a) +} + +# Draw a polyline (px, py) as a colour gradient from `col_from` to `col_to`: +# resample to `n` points by cumulative arc length, then draw the n-1 pieces with +# interpolated colours. Used for source->target edge gradients. +i.draw_gradient_path <- function(px, py, col_from, col_to, lwd, lty, n = 40) { + d <- c(0, cumsum(sqrt(diff(px)^2 + diff(py)^2))) + if (length(d) < 2 || max(d) == 0) { + return(invisible(NULL)) + } + at <- seq(0, max(d), length.out = n) + rx <- stats::approx(d, px, at)$y + ry <- stats::approx(d, py, at)$y + ramp <- grDevices::colorRamp(c(col_from, col_to), alpha = TRUE) + m <- ramp(seq(0, 1, length.out = n - 1)) # one RGBA row per segment + cols <- grDevices::rgb( + m[, 1], + m[, 2], + m[, 3], + alpha = m[, 4], + maxColorValue = 255 + ) + i.r_segments(rx[-n], ry[-n], rx[-1], ry[-1], col = cols, lwd = lwd, lty = lty) + invisible(NULL) +} + #' @importFrom graphics par xyinch segments xspline lines polygon # Vectorized and modular igraph.Arrows refactor igraph.Arrows <- function( @@ -1734,7 +2310,7 @@ igraph.Arrows <- function( y2, code = 2, size = 1, - width = 1.2 / 4 / par("cin")[2], + width = ARROW_WIDTH_FACTOR / par("cin")[2], open = TRUE, sh.adj = 0.1, sh.lwd = 1, @@ -1744,12 +2320,22 @@ igraph.Arrows <- function( h.col.bo = sh.col, h.lwd = sh.lwd, h.lty = sh.lty, - curved = FALSE + curved = FALSE, + style = "auto", + gradient = FALSE, + col.to = sh.col, + ids = NULL, + axis = NULL ) { n <- length(x1) recycle <- function(x) rep(x, length.out = n) + # Per-edge leaving axis for elbow/diagonal styles (TRUE = vertical). When the + # caller supplies it (from the vertex centres) the path builders route to match + # the axis-aligned endpoint attachment; NULL leaves the auto inference. + axis <- if (is.null(axis)) rep(NA, n) else recycle(axis) + x1 <- recycle(x1) y1 <- recycle(y1) x2 <- recycle(x2) @@ -1757,6 +2343,9 @@ igraph.Arrows <- function( size <- recycle(size) width <- recycle(width) curved <- recycle(curved) + style <- recycle(as.character(style)) + gradient <- recycle(gradient) + col.to <- recycle(col.to) sh.lwd <- recycle(sh.lwd) sh.col <- recycle(sh.col) sh.lty <- recycle(sh.lty) @@ -1771,65 +2360,83 @@ igraph.Arrows <- function( label_y <- numeric(n) for (i in seq_len(n)) { + if (!is.null(ids)) { + i.r_group_begin("edge", id = ids[i]) + } cin <- size[i] * par("cin")[2] - w <- width[i] * (1.2 / 4 / cin) + w <- width[i] * (ARROW_WIDTH_FACTOR / cin) delta <- sqrt(h.lwd[i]) * par("cin")[2] * 0.005 - # Arrowhead shape - x <- sqrt(seq(0, cin^2, length.out = floor(35 * cin) + 2)) - x.arr <- c(-rev(x), -x) - wx2 <- w * x^2 - y.arr <- c(-rev(wx2 + delta), wx2 + delta) - deg.arr <- c(atan2(y.arr, x.arr), NA) - r.arr <- c(sqrt(x.arr^2 + y.arr^2), NA) + # Arrowhead shape (pure geometry, see i.arrowhead_shape) + head <- i.arrowhead_shape(cin, w, delta) + deg.arr <- head$deg.arr + r.arr <- head$r.arr - theta1 <- atan2((y1[i] - y2[i]) * uin[2], (x1[i] - x2[i]) * uin[1]) - theta2 <- atan2((y2[i] - y1[i]) * uin[2], (x2[i] - x1[i]) * uin[1]) r.seg <- cin * sh.adj - - x1d <- y1d <- x2d <- y2d <- 0 - if (code %in% c(1, 3)) { - x2d <- r.seg * cos(theta2) / uin[1] - y2d <- r.seg * sin(theta2) / uin[2] - } - if (code %in% c(2, 3)) { - x1d <- r.seg * cos(theta1) / uin[1] - y1d <- r.seg * sin(theta1) / uin[2] + sh <- i.arrow_shaft_endpoints(x1[i], y1[i], x2[i], y2[i], code, r.seg, uin) + sx1 <- sh$sx1 + sy1 <- sh$sy1 + sx2 <- sh$sx2 + sy2 <- sh$sy2 + + eff_style <- style[i] + if (eff_style == "auto") { + eff_style <- if (!curved[i]) "straight" else "arc" } - sx1 <- x1[i] + x1d - sy1 <- y1[i] + y1d - sx2 <- x2[i] + x2d - sy2 <- y2[i] + y2d - - if (!curved[i]) { - segments( + if (eff_style == "straight") { + if (gradient[i]) { + i.draw_gradient_path( + c(sx1, sx2), + c(sy1, sy2), + sh.col[i], + col.to[i], + sh.lwd[i], + sh.lty[i] + ) + } else { + i.r_segments( + sx1, + sy1, + sx2, + sy2, + lwd = sh.lwd[i], + col = sh.col[i], + lty = sh.lty[i] + ) + } + lab <- i.edge_label_pos(x1[i], y1[i], x2[i], y2[i]) + label_x[i] <- lab[["x"]] + label_y[i] <- lab[["y"]] + } else if (eff_style == "arc") { + lambda <- if (is.numeric(curved)) curved[i] else 0.5 + if (style[i] == "arc" && lambda == 0) { + # an explicit arc on an otherwise-straight edge needs a strength + lambda <- 0.3 + } + spl <- i.curved_spline( + x1[i], + y1[i], + x2[i], + y2[i], sx1, sy1, sx2, sy2, - lwd = sh.lwd[i], - col = sh.col[i], - lty = sh.lty[i] - ) - phi <- atan2(y1[i] - y2[i], x1[i] - x2[i]) - r <- sqrt((x1[i] - x2[i])^2 + (y1[i] - y2[i])^2) - label_x[i] <- x2[i] + 2 / 3 * r * cos(phi) - label_y[i] <- y2[i] + 2 / 3 * r * sin(phi) - } else { - lambda <- if (is.numeric(curved)) curved[i] else 0.5 - midx <- (x1[i] + x2[i]) / 2 - midy <- (y1[i] + y2[i]) / 2 - spx <- midx - lambda * 1 / 2 * (sy2 - sy1) - spy <- midy + lambda * 1 / 2 * (sx2 - sx1) - - spl <- xspline( - x = c(sx1, spx, sx2), - y = c(sy1, spy, sy2), - shape = 1, - draw = FALSE + lambda ) - lines(spl, lwd = sh.lwd[i], col = sh.col[i], lty = sh.lty[i]) + if (gradient[i]) { + i.draw_gradient_path( + spl$x, + spl$y, + sh.col[i], + col.to[i], + sh.lwd[i], + sh.lty[i] + ) + } else { + i.r_polyline(spl, col = sh.col[i], lwd = sh.lwd[i], lty = sh.lty[i]) + } label_x[i] <- spl$x[round(2 / 3 * length(spl$x))] label_y[i] <- spl$y[round(2 / 3 * length(spl$y))] @@ -1841,6 +2448,47 @@ igraph.Arrows <- function( x2[i] <- spl$x[round(1 / 4 * length(spl$x))] y2[i] <- spl$y[round(1 / 4 * length(spl$y))] } + } else { + # elbow or diagonal: a polyline between the shaft endpoints + vert <- if (is.na(axis[i])) NULL else axis[i] + path <- if (eff_style == "elbow") { + i.elbow_path(sx1, sy1, sx2, sy2, vertical = vert) + } else { + i.diagonal_path(sx1, sy1, sx2, sy2, vertical = vert) + } + if (gradient[i]) { + i.draw_gradient_path( + path$x, + path$y, + sh.col[i], + col.to[i], + sh.lwd[i], + sh.lty[i] + ) + } else { + i.r_polyline( + path$x, + path$y, + col = sh.col[i], + lwd = sh.lwd[i], + lty = sh.lty[i] + ) + } + np <- length(path$x) + mid <- max(1L, round(np / 2)) + label_x[i] <- path$x[mid] + label_y[i] <- path$y[mid] + + # arrowhead end-tangents: align the head with the path's final/first + # segment (mirrors the arc branch's near-end reassignment) + if (code %in% c(2, 3)) { + x1[i] <- path$x[np - 1] + y1[i] <- path$y[np - 1] + } + if (code %in% c(1, 3)) { + x2[i] <- path$x[2] + y2[i] <- path$y[2] + } } draw_arrowhead <- function(px, py, theta) { @@ -1852,9 +2500,15 @@ igraph.Arrows <- function( yhead <- py2 + r.arr * sin(ttheta) / uin[2] if (open) { - lines(xhead, yhead, lwd = h.lwd[i], col = h.col.bo[i], lty = h.lty[i]) + i.r_polyline( + xhead, + yhead, + col = h.col.bo[i], + lwd = h.lwd[i], + lty = h.lty[i] + ) } else { - polygon( + i.r_polygon( xhead, yhead, col = h.col[i], @@ -1879,6 +2533,9 @@ igraph.Arrows <- function( atan2((y1[i] - y2[i]) * uin[2], (x1[i] - x2[i]) * uin[1]) ) } + if (!is.null(ids)) { + i.r_group_end() + } } list(lab.x = label_x, lab.y = label_y) @@ -1905,7 +2562,7 @@ igraph.polygon <- function( cl <- convex_hull(pp) - xspline( + i.r_xspline( cl$rescoords, shape = shape, open = FALSE, diff --git a/R/plot.common.R b/R/plot.common.R index d5beec08456..9f81b82635f 100644 --- a/R/plot.common.R +++ b/R/plot.common.R @@ -115,6 +115,11 @@ #' #' The default value is \dQuote{\code{SkyBlue2}}. #' } +#' \item{alpha}{ +#' Opacity of the vertex fill, a number (or vector) in `[0, 1]`, multiplied +#' into any alpha already present in `color`. `1` (the default) means fully +#' opaque. Frame colour, pie slices and labels are not affected. +#' } #' \item{frame.color}{ #' The color of the frame of the vertices, the same formats are allowed as for the fill color. #' @@ -231,6 +236,21 @@ #' \item{label.adj}{ #' one or two numeric values, giving the horizontal and vertical adjustment of the vertex labels. See also `adj` in [graphics::text()]. #' } +#' \item{label.repel}{ +#' Logical scalar. If `TRUE`, overlapping vertex labels are iteratively nudged +#' apart (in the spirit of \pkg{ggrepel}) and a thin leader line connects each +#' moved label to its original position. The default is `FALSE`. +#' } +#' \item{label.halo}{ +#' The colour of a legibility halo (outline) drawn behind the vertex label +#' text, so labels remain readable over edges and other vertices. The halo is +#' drawn shadowtext-style: the glyphs are repeated, offset in a ring, in this +#' colour, with the real label on top. `NA` (the default) draws no halo. +#' } +#' \item{label.halo.width}{ +#' The width of the label halo, as a fraction of the label height. Only has an +#' effect when `label.halo` is not `NA`. The default is `0.15`. +#' } #' \item{size.scaling}{ #' Switches between absolute vertex sizing (FALSE,default) and relative (TRUE). @@ -298,6 +318,15 @@ #' \item{label.color}{ #' The color of the edge labels, see the `color` vertex parameters on how to specify colors. #' } +#' \item{label.halo}{ +#' The colour of a legibility halo (outline) drawn behind the edge label text. +#' See the vertex parameter with the same name for details. `NA` (the default) +#' draws no halo. +#' } +#' \item{label.halo.width}{ +#' The width of the edge label halo, as a fraction of the label height. Only +#' has an effect when `label.halo` is not `NA`. The default is `0.15`. +#' } #' \item{label.x}{ #' The horizontal `NA` elements will be replaced by automatically calculated coordinates. #' If `NULL`, then all edge horizontal coordinates are calculated automatically. @@ -324,6 +353,32 @@ #' #' This parameter is currently ignored by [rglplot()]. #' } +#' \item{style}{ +#' The routing style for (non-loop) edges, a character scalar or vector, +#' replicated to the number of edges. One of: +#' \describe{ +#' \item{`"auto"`}{(default) straight, unless `curved` is non-zero (in which +#' case an arc), reproducing the historical behaviour.} +#' \item{`"straight"`}{a straight segment.} +#' \item{`"arc"`}{a curved arc; the strength is taken from `curved` if it is +#' non-zero, otherwise a default is used.} +#' \item{`"elbow"`}{a two-corner orthogonal (right-angle) connector.} +#' \item{`"diagonal"`}{a smooth S-curve with axis-aligned ends.} +#' } +#' This parameter is ignored for loop edges and by [rglplot()]. +#' } +#' \item{alpha}{ +#' Opacity of the edge, a number (or vector) in `[0, 1]`, multiplied into any +#' alpha already present in `color` (and in the gradient endpoint colours). +#' `1` (the default) means fully opaque. +#' } +#' \item{gradient}{ +#' Logical scalar or vector. If `TRUE`, the edge is drawn as a colour gradient +#' running from its source vertex's colour to its target vertex's colour (a +#' direction cue), and the arrowhead takes the target colour; `color` is then +#' ignored for that edge's shaft. The default is `FALSE`. Ignored for loop +#' edges and by [rglplot()]. +#' } #' \item{arrow.mode}{ #' This parameter can be used to specify for which edges should arrows be drawn. #' If this parameter is given by the user (in either of the three ways) @@ -527,6 +582,17 @@ autocurve.edges <- function(graph, start = 0.5) { # Common functions for plot and tkplot ################################################################### +# Resolve plotting parameters on demand. The returned closure `func(type, name)` +# looks a parameter up with the following precedence, highest first: +# 1. an explicit argument passed to plot() (vertex./edge./plain prefix) +# 2. a matching graph attribute (vertex_attr / edge_attr / graph_attr) +# 3. the corresponding igraph option (igraph_opt("<type>.<name>")) +# 4. the hard-coded default in i.default.values +# Function-valued defaults are evaluated with the graph; NAs in a resolved +# attribute are replaced with the default (or "" for labels). +# +# This closure is part of the public contract: user shapes registered via +# add_shape() receive it as their `params` argument and call params("vertex", .). i.parse.plot.params <- function(graph, params) { ## store the arguments p <- list(vertex = list(), edge = list(), plot = list()) @@ -4859,6 +4925,10 @@ i.vertex.default <- list( label.cex = 1, label.angle = 0, label.adj = NULL, + label.repel = FALSE, + label.halo = NA, + label.halo.width = 0.15, + alpha = 1, frame.color = "black", frame.width = 1, shape = "circle", @@ -4890,21 +4960,29 @@ i.edge.default <- list( label.font = 1, label.cex = 1, label.color = "darkblue", + label.halo = NA, + label.halo.width = 0.15, label.x = NULL, label.y = NULL, arrow.size = 1, arrow.mode = i.get.arrow.mode, curved = curve_multiple, - arrow.width = 1 + arrow.width = 1, + style = "auto", + alpha = 1, + gradient = FALSE ) +# Note: there is intentionally no `frame` default. plot.igraph() reads +# `frame.plot`, which falls back to `axes` when unset +# (ifelse(is.null(frame.plot), axes, frame.plot)); a `frame = FALSE` entry here +# was dead config that was never read. i.plot.default <- list( palette = categorical_pal(8), layout = layout_nicely, margin = c(0, 0, 0, 0), rescale = TRUE, asp = 1, - frame = FALSE, main = i.get.main, sub = "", xlab = i.get.xlab, @@ -4946,9 +5024,6 @@ i.rescale.vertex <- function( return(size) } -i.default.values[["edge"]] <- i.edge.default -i.default.values[["plot"]] <- i.plot.default - #' Using pie charts as vertices in graph plots #' #' More complex vertex images can be used to express addtional information diff --git a/R/plot.shapes.R b/R/plot.shapes.R index a5b5e06529e..74cbf3a34b6 100644 --- a/R/plot.shapes.R +++ b/R/plot.shapes.R @@ -368,6 +368,23 @@ shape_noplot <- function(coords, v = NULL, params) { invisible(NULL) } +# Check that a shape clip/plot function accepts the arguments igraph calls it +# with. Functions that take `...` are assumed to forward everything and pass. +i.check_shape_fun <- function(fn, arg, required) { + fmls <- names(formals(fn)) + if ("..." %in% fmls) { + return(invisible()) + } + missing <- setdiff(required, fmls) + if (length(missing) > 0) { + cli::cli_abort(c( + "Shape {.arg {arg}} function is missing required argument{?s} {.arg {missing}}.", + i = "It is called as {.code {arg}(coords, {if (arg == 'clip') 'el, ' else ''}...)} with {.arg {required}}; see {.help add_shape}." + )) + } + invisible() +} + #' @rdname shapes #' @export add_shape <- function( @@ -407,6 +424,13 @@ add_shape <- function( )) } + # Validate the clip/plot signatures up front so a malformed shape fails here + # rather than cryptically at plot time. A clip function is called as + # clip(coords, el, params =, end =) and a plot function as + # plot(coords, v =, params =); functions taking `...` are exempt. + i.check_shape_fun(clip, "clip", c("params", "end")) + i.check_shape_fun(plot, "plot", c("params", "v")) + assign(shape, value = list(clip = clip, plot = plot), envir = .igraph.shapes) do.call(igraph_options, parameters) invisible(TRUE) @@ -414,6 +438,16 @@ add_shape <- function( ## These are the predefined shapes #nocov start + +# A non-positive frame width means "draw no border": blank the frame colour and +# reset the width to a drawable value. Shared by the vertex shape plot functions +# so the rule lives in one place. +i.hide_zero_frame <- function(color, width) { + color[width <= 0] <- NA + width[width <= 0] <- 1 + list(color = color, width = width) +} + .igraph.shape.circle.clip <- function( coords, el, @@ -495,20 +529,19 @@ add_shape <- function( } 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 + frame <- i.hide_zero_frame(vertex.frame.color, vertex.frame.width) + vertex.frame.color <- frame$color + vertex.frame.width <- frame$width if (length(vertex.frame.width) == 1) { - symbols( + i.r_symbols( + "circles", x = coords[, 1], y = coords[, 2], + dim = vertex.size, bg = vertex.color, fg = vertex.frame.color, - circles = vertex.size, - lwd = vertex.frame.width, - add = TRUE, - inches = FALSE + lwd = vertex.frame.width ) } else { mapply( @@ -519,15 +552,14 @@ add_shape <- function( vertex.size, vertex.frame.width, FUN = function(x, y, bg, fg, size, lwd) { - symbols( + i.r_symbols( + "circles", x = x, y = y, + dim = size, bg = bg, fg = fg, - lwd = lwd, - circles = size, - add = TRUE, - inches = FALSE + lwd = lwd ) } ) @@ -650,20 +682,19 @@ add_shape <- function( } 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 + frame <- i.hide_zero_frame(vertex.frame.color, vertex.frame.width) + vertex.frame.color <- frame$color + vertex.frame.width <- frame$width if (length(vertex.frame.width) == 1) { - symbols( + i.r_symbols( + "squares", x = coords[, 1], y = coords[, 2], + dim = 2 * vertex.size, bg = vertex.color, fg = vertex.frame.color, - squares = 2 * vertex.size, - lwd = vertex.frame.width, - add = TRUE, - inches = FALSE + lwd = vertex.frame.width ) } else { mapply( @@ -674,15 +705,14 @@ add_shape <- function( vertex.size, vertex.frame.width, FUN = function(x, y, bg, fg, size, lwd) { - symbols( + i.r_symbols( + "squares", x = x, y = y, + dim = 2 * size, bg = bg, fg = fg, - lwd = lwd, - squares = 2 * size, - add = TRUE, - inches = FALSE + lwd = lwd ) } ) @@ -899,20 +929,19 @@ add_shape <- function( } 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 + frame <- i.hide_zero_frame(vertex.frame.color, vertex.frame.width) + vertex.frame.color <- frame$color + vertex.frame.width <- frame$width if (length(vertex.frame.width) == 1) { - symbols( + i.r_symbols( + "rectangles", x = coords[, 1], y = coords[, 2], + dim = 2 * vertex.size, bg = vertex.color, fg = vertex.frame.color, - rectangles = 2 * vertex.size, - lwd = vertex.frame.width, - add = TRUE, - inches = FALSE + lwd = vertex.frame.width ) } else { mapply( @@ -924,15 +953,14 @@ add_shape <- function( vertex.size[, 2], vertex.frame.width, FUN = function(x, y, bg, fg, size, size2, lwd) { - symbols( + i.r_symbols( + "rectangles", x = x, y = y, + dim = 2 * cbind(size, size2), bg = bg, fg = fg, - lwd = lwd, - rectangles = 2 * cbind(size, size2), - add = TRUE, - inches = FALSE + lwd = lwd ) } ) @@ -1158,7 +1186,7 @@ mypie <- function( 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( + i.r_polygon( x + c(P$x, 0), y + c(P$y, 0), density = density[i], @@ -1314,7 +1342,7 @@ mypie <- function( for (i in seq_len(nrow(coords))) { vsp2 <- vertex.size[i] - rasterImage( + i.r_raster( images[[whichImage[i]]], coords[i, 1] - vsp2, coords[i, 2] - vsp2, @@ -1342,7 +1370,7 @@ mypie <- function( for (i in seq_len(nrow(coords))) { ras <- if (!is.list(raster) || length(raster) == 1) raster else raster[[i]] - rasterImage( + i.r_raster( ras, coords[i, 1] - size[i], coords[i, 2] - size2[i], diff --git a/_pkgdown.yml b/_pkgdown.yml index c4502f3b72b..dbdbfc14d8a 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -19,6 +19,8 @@ navbar: articles: text: Articles menu: + - text: Plotting graphs + href: articles/plotting.html - text: Installation FAQs href: articles/installation-troubleshooting.html - text: ------- diff --git a/man/as_svg.Rd b/man/as_svg.Rd new file mode 100644 index 00000000000..039b7e37730 --- /dev/null +++ b/man/as_svg.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot-render.R +\name{as_svg} +\alias{as_svg} +\title{Render a graph to SVG} +\usage{ +as_svg(graph, file = NULL, width = 7, height = 7, tooltips = NULL, ...) +} +\arguments{ +\item{graph}{The graph to plot.} + +\item{file}{Optional path to write the SVG to. If \code{NULL} (default) the SVG +string is returned invisibly.} + +\item{width, height}{Size in inches (the SVG is \code{width*72} x \code{height*72} px).} + +\item{tooltips}{Optional vertex attribute name to use for the \verb{<title>} +tooltips; defaults to the vertex \code{name} attribute (or vertex index).} + +\item{...}{Further plotting parameters passed to \code{\link[=plot.igraph]{plot.igraph()}}.} +} +\value{ +The SVG string, invisibly (also written to \code{file} if given). +} +\description{ +\code{as_svg()} draws a graph to a standalone SVG string using the same geometry +as \code{\link[=plot.igraph]{plot.igraph()}}, but emits per-vertex \verb{<g id="vertex-N">} groups with +\verb{<title>} tooltips (and per-edge groups), giving lightweight interactivity +(hover) with no JavaScript. It accepts the usual plotting parameters via +\code{...}. +} +\details{ +Vertices, edges (all styles), arrowheads, labels, mark groups and pie shapes +are rendered; \code{sphere}/\code{raster} vertex shapes are drawn as a placeholder box +in this version. +} +\section{Related documentation in the C library}{ +\href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}} +} + diff --git a/man/label_top.Rd b/man/label_top.Rd new file mode 100644 index 00000000000..28f7d6cfb27 --- /dev/null +++ b/man/label_top.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot-labels.R +\name{label_top} +\alias{label_top} +\title{Keep only the most prominent labels} +\usage{ +label_top(by, n = NULL, prop = NULL, labels = NULL, decreasing = TRUE) +} +\arguments{ +\item{by}{A numeric vector of scores to rank by, e.g. \code{degree(g)} or +\code{betweenness(g)}. One score per vertex (or edge).} + +\item{n}{Number of labels to keep (the top \code{n} by \code{by}). Give either \code{n} or +\code{prop}, not both. If neither is given, all labels are kept.} + +\item{prop}{Proportion of labels to keep, between 0 and 1; rounded up. Give +either \code{n} or \code{prop}, not both.} + +\item{labels}{The labels to thin. Defaults to \code{names(by)} if present, +otherwise the integer positions. Must have the same length as \code{by}.} + +\item{decreasing}{Logical; if \code{TRUE} (the default) the highest \code{by} values +are kept, otherwise the lowest.} +} +\value{ +A character vector the same length as \code{by}, with \code{NA} in the +positions that are not kept. +} +\description{ +\code{label_top()} returns a label vector with \code{NA} everywhere except the entries +that rank highest by \code{by}. Because \code{\link[=plot.igraph]{plot.igraph()}} omits \code{NA} labels, this is +a convenient way to declutter dense graphs by labelling only the most +important vertices (or edges). Pass it to a label argument, e.g. +\code{plot(g, vertex.label = label_top(degree(g), n = 10))}. +} +\details{ +To label everything above a fixed cutoff instead of a fixed count, you do not +need this helper: \code{ifelse(metric > cutoff, labels, NA)} works directly. +} +\examples{ +g <- make_ring(10) +plot(g, vertex.label = label_top(degree(g), n = 3)) +} diff --git a/man/plot.common.Rd b/man/plot.common.Rd index 5cd7128d499..3bc7775585d 100644 --- a/man/plot.common.Rd +++ b/man/plot.common.Rd @@ -124,6 +124,11 @@ color name. The default value is \dQuote{\code{SkyBlue2}}. } +\item{alpha}{ +Opacity of the vertex fill, a number (or vector) in \verb{[0, 1]}, multiplied +into any alpha already present in \code{color}. \code{1} (the default) means fully +opaque. Frame colour, pie slices and labels are not affected. +} \item{frame.color}{ The color of the frame of the vertices, the same formats are allowed as for the fill color. @@ -240,6 +245,21 @@ The rotation of the vertex labels, in degrees. Corresponds to the \code{srt} par \item{label.adj}{ one or two numeric values, giving the horizontal and vertical adjustment of the vertex labels. See also \code{adj} in \code{\link[graphics:text]{graphics::text()}}. } +\item{label.repel}{ +Logical scalar. If \code{TRUE}, overlapping vertex labels are iteratively nudged +apart (in the spirit of \pkg{ggrepel}) and a thin leader line connects each +moved label to its original position. The default is \code{FALSE}. +} +\item{label.halo}{ +The colour of a legibility halo (outline) drawn behind the vertex label +text, so labels remain readable over edges and other vertices. The halo is +drawn shadowtext-style: the glyphs are repeated, offset in a ring, in this +colour, with the real label on top. \code{NA} (the default) draws no halo. +} +\item{label.halo.width}{ +The width of the label halo, as a fraction of the label height. Only has an +effect when \code{label.halo} is not \code{NA}. The default is \code{0.15}. +} \item{size.scaling}{ Switches between absolute vertex sizing (FALSE,default) and relative (TRUE). If FALSE, \code{vertex.size} and \code{vertex.size2} are used as is. @@ -306,6 +326,15 @@ The font size for the edge labels, see the corresponding vertex parameter for de \item{label.color}{ The color of the edge labels, see the \code{color} vertex parameters on how to specify colors. } +\item{label.halo}{ +The colour of a legibility halo (outline) drawn behind the edge label text. +See the vertex parameter with the same name for details. \code{NA} (the default) +draws no halo. +} +\item{label.halo.width}{ +The width of the edge label halo, as a fraction of the label height. Only +has an effect when \code{label.halo} is not \code{NA}. The default is \code{0.15}. +} \item{label.x}{ The horizontal \code{NA} elements will be replaced by automatically calculated coordinates. If \code{NULL}, then all edge horizontal coordinates are calculated automatically. @@ -332,6 +361,32 @@ The default value is \code{FALSE}. This parameter is currently ignored by \code{\link[=rglplot]{rglplot()}}. } +\item{style}{ +The routing style for (non-loop) edges, a character scalar or vector, +replicated to the number of edges. One of: +\describe{ +\item{\code{"auto"}}{(default) straight, unless \code{curved} is non-zero (in which +case an arc), reproducing the historical behaviour.} +\item{\code{"straight"}}{a straight segment.} +\item{\code{"arc"}}{a curved arc; the strength is taken from \code{curved} if it is +non-zero, otherwise a default is used.} +\item{\code{"elbow"}}{a two-corner orthogonal (right-angle) connector.} +\item{\code{"diagonal"}}{a smooth S-curve with axis-aligned ends.} +} +This parameter is ignored for loop edges and by \code{\link[=rglplot]{rglplot()}}. +} +\item{alpha}{ +Opacity of the edge, a number (or vector) in \verb{[0, 1]}, multiplied into any +alpha already present in \code{color} (and in the gradient endpoint colours). +\code{1} (the default) means fully opaque. +} +\item{gradient}{ +Logical scalar or vector. If \code{TRUE}, the edge is drawn as a colour gradient +running from its source vertex's colour to its target vertex's colour (a +direction cue), and the arrowhead takes the target colour; \code{color} is then +ignored for that edge's shaft. The default is \code{FALSE}. Ignored for loop +edges and by \code{\link[=rglplot]{rglplot()}}. +} \item{arrow.mode}{ This parameter can be used to specify for which edges should arrows be drawn. If this parameter is given by the user (in either of the three ways) diff --git a/man/plot.igraph.Rd b/man/plot.igraph.Rd index a1c56863940..72353f47477 100644 --- a/man/plot.igraph.Rd +++ b/man/plot.igraph.Rd @@ -18,6 +18,7 @@ mark.expand = 15, mark.lwd = 1, loop.size = 1, + legend = TRUE, ... ) } @@ -68,6 +69,13 @@ groups.} of the network. The default loop size is 1. Larger values will produce larger loops.} +\item{legend}{Controls drawing of legends/colorbars for any aesthetics +supplied via \code{\link[=scale_color]{scale_color()}} / \code{\link[=scale_size]{scale_size()}}. The guide is drawn in the +reserved outer margin on one side of the plot: \code{TRUE} (default) or +\code{"right"} places it to the right, \code{"left"}/\code{"top"}/\code{"bottom"} on the +corresponding side (\code{"top"}/\code{"bottom"} arrange entries horizontally); +\code{FALSE} suppresses it. Has no effect when no scale is used.} + \item{\dots}{Additional plotting parameters. See \link{igraph.plotting} for the complete list.} } @@ -85,7 +93,7 @@ first, handtune the placement of the vertices, query the coordinates by the plot the graph to any R device. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_get_edgelist}{\code{get_edgelist()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_incident}{\code{incident()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_is_loop}{\code{is_loop()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_is_directed}{\code{is_directed()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Nongraph.html#igraph_convex_hull_2d}{\code{convex_hull_2d()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_get_edgelist}{\code{get_edgelist()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_incident}{\code{incident()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_is_loop}{\code{is_loop()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Nongraph.html#igraph_convex_hull_2d}{\code{convex_hull_2d()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_is_directed}{\code{is_directed()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \examples{ diff --git a/man/scale_color.Rd b/man/scale_color.Rd new file mode 100644 index 00000000000..d3c60fa1b03 --- /dev/null +++ b/man/scale_color.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot-scales.R +\name{scale_color} +\alias{scale_color} +\alias{scale_colour} +\title{Map data to a colour aesthetic with an automatic legend} +\usage{ +scale_color(x, palette = NULL, na.value = "grey70", name = NULL) + +scale_colour(x, palette = NULL, na.value = "grey70", name = NULL) +} +\arguments{ +\item{x}{The data vector to map. Its length must be 1 or the number of +vertices/edges of the graph it is used with.} + +\item{palette}{Colours to map to. For discrete \code{x}, a vector of colours (one +per level, recycled); defaults to \code{\link[=categorical_pal]{categorical_pal()}}. For numeric \code{x}, the +anchor colours of the ramp; defaults to \code{\link[=sequential_pal]{sequential_pal()}}.} + +\item{na.value}{Colour used for \code{NA} entries in \code{x}.} + +\item{name}{Optional guide title; defaults to the name of the argument the +scale is assigned to (e.g. \code{"vertex.color"}).} +} +\value{ +An \code{igraph_scale} object. +} +\description{ +\code{scale_color()} (alias \code{scale_colour()}) maps a data vector to vertex or edge +colours and records the mapping so that \code{\link[=plot.igraph]{plot.igraph()}} draws a matching +guide. Pass it to a colour argument, e.g. +\code{plot(g, vertex.color = scale_color(V(g)$group))}. +} +\details{ +A non-numeric \code{x} (factor, character, logical) produces a discrete mapping +and a categorical legend; a numeric \code{x} produces a continuous mapping (a +colour ramp) and a colorbar. +} +\seealso{ +Other scales: +\code{\link[=scale_size]{scale_size()}} +} +\concept{scales} diff --git a/man/scale_size.Rd b/man/scale_size.Rd new file mode 100644 index 00000000000..95046b1fc74 --- /dev/null +++ b/man/scale_size.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot-scales.R +\name{scale_size} +\alias{scale_size} +\title{Map data to a size aesthetic with an automatic legend} +\usage{ +scale_size(x, range = c(2, 15), na.value = NA, name = NULL, trans = NULL) +} +\arguments{ +\item{x}{A numeric data vector to map. Its length must be 1 or the number of +vertices/edges of the graph it is used with.} + +\item{range}{Numeric length-2 vector giving the output size range.} + +\item{na.value}{Size used for \code{NA} entries in \code{x}.} + +\item{name}{Optional guide title; defaults to the argument name.} + +\item{trans}{Optional transformation applied to \code{x} before rescaling, given +as a function or its name (e.g. \code{"sqrt"}, \code{"log"}).} +} +\value{ +An \code{igraph_scale} object. +} +\description{ +\code{scale_size()} linearly maps a numeric data vector to a size range (suitable +for \code{vertex.size} or \code{edge.width}) and records the mapping so that +\code{\link[=plot.igraph]{plot.igraph()}} draws a matching size legend. Pass it to a size argument, +e.g. \code{plot(g, vertex.size = scale_size(degree(g)))}. +} +\seealso{ +Other scales: +\code{\link[=scale_color]{scale_color()}} +} +\concept{scales} diff --git a/tests/testthat/_snaps/plot-params.md b/tests/testthat/_snaps/plot-params.md new file mode 100644 index 00000000000..05d7429192d --- /dev/null +++ b/tests/testthat/_snaps/plot-params.md @@ -0,0 +1,30 @@ +# i.check_aes_lengths rejects mismatched vertex lengths + + Code + i.check_aes_lengths(vertex = list(color = c("red", "green")), edge = list(), + vc = 5, ec = 4) + Condition + Error: + ! Invalid length for vertex aesthetic color. + x It has length 2, but must be length 1 or 5. + i The graph has 5 vertices. + +# i.check_aes_lengths rejects mismatched edge lengths + + Code + i.check_aes_lengths(vertex = list(), edge = list(width = c(1, 2, 3)), vc = 5, + ec = 5) + Condition + Error: + ! Invalid length for edge aesthetic width. + x It has length 3, but must be length 1 or 5. + i The graph has 5 edges. + +# igraph.check.shapes() aborts on unknown shapes + + Code + igraph.check.shapes(c("circle", "not_a_shape")) + Condition + Error in `igraph.check.shapes()`: + ! Bad vertex shapes: not_a_shape. + diff --git a/tests/testthat/_snaps/plot/add-overlay.svg b/tests/testthat/_snaps/plot/add-overlay.svg new file mode 100644 index 00000000000..389bb1a98ef --- /dev/null +++ b/tests/testthat/_snaps/plot/add-overlay.svg @@ -0,0 +1,53 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='247.78' y1='349.24' x2='364.13' y2='349.24' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='384.67' y1='349.24' x2='501.02' y2='349.24' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='247.78' y1='349.24' x2='501.02' y2='349.24' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='237.51' cy='349.24' r='10.27' style='stroke-width: 0.75; fill: #FF0000;' /> +<circle cx='374.40' cy='349.24' r='10.27' style='stroke-width: 0.75; fill: #FF0000;' /> +<circle cx='511.29' cy='349.24' r='10.27' style='stroke-width: 0.75; fill: #FF0000;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='237.51' y='353.37' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='353.43' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='511.29' y='353.38' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +</g> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='247.78' y1='212.36' x2='364.13' y2='212.36' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='384.67' y1='212.36' x2='501.02' y2='212.36' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='247.78' y1='212.36' x2='501.02' y2='212.36' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='237.51' cy='212.36' r='10.27' style='stroke-width: 0.75; fill: #0000FF;' /> +<circle cx='374.40' cy='212.36' r='10.27' style='stroke-width: 0.75; fill: #0000FF;' /> +<circle cx='511.29' cy='212.36' r='10.27' style='stroke-width: 0.75; fill: #0000FF;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='237.51' y='216.48' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='216.55' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='511.29' y='216.49' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/edge-gradient-arc.svg b/tests/testthat/_snaps/plot/edge-gradient-arc.svg new file mode 100644 index 00000000000..ac5a80d06a4 --- /dev/null +++ b/tests/testthat/_snaps/plot/edge-gradient-arc.svg @@ -0,0 +1,157 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='539.34' y1='271.61' x2='529.89' y2='269.74' style='stroke-width: 2.25; stroke: #FF0000;' /> +<line x1='529.89' y1='269.74' x2='520.45' y2='267.83' style='stroke-width: 2.25; stroke: #F80600;' /> +<line x1='520.45' y1='267.83' x2='511.02' y2='265.88' style='stroke-width: 2.25; stroke: #F10D00;' /> +<line x1='511.02' y1='265.88' x2='501.59' y2='263.91' style='stroke-width: 2.25; stroke: #EA1400;' /> +<line x1='501.59' y1='263.91' x2='492.17' y2='261.90' style='stroke-width: 2.25; stroke: #E41A00;' /> +<line x1='492.17' y1='261.90' x2='482.76' y2='259.86' style='stroke-width: 2.25; stroke: #DD2100;' /> +<line x1='482.76' y1='259.86' x2='473.35' y2='257.78' style='stroke-width: 2.25; stroke: #D62800;' /> +<line x1='473.35' y1='257.78' x2='463.96' y2='255.66' style='stroke-width: 2.25; stroke: #D02E00;' /> +<line x1='463.96' y1='255.66' x2='454.57' y2='253.48' style='stroke-width: 2.25; stroke: #C93500;' /> +<line x1='454.57' y1='253.48' x2='445.20' y2='251.24' style='stroke-width: 2.25; stroke: #C23C00;' /> +<line x1='445.20' y1='251.24' x2='435.85' y2='248.93' style='stroke-width: 2.25; stroke: #BB4300;' /> +<line x1='435.85' y1='248.93' x2='426.53' y2='246.52' style='stroke-width: 2.25; stroke: #B54900;' /> +<line x1='426.53' y1='246.52' x2='417.23' y2='243.99' style='stroke-width: 2.25; stroke: #AE5000;' /> +<line x1='417.23' y1='243.99' x2='407.98' y2='241.33' style='stroke-width: 2.25; stroke: #A75700;' /> +<line x1='407.98' y1='241.33' x2='398.77' y2='238.48' style='stroke-width: 2.25; stroke: #A15D00;' /> +<line x1='398.77' y1='238.48' x2='389.65' y2='235.39' style='stroke-width: 2.25; stroke: #9A6400;' /> +<line x1='389.65' y1='235.39' x2='380.61' y2='232.06' style='stroke-width: 2.25; stroke: #936B00;' /> +<line x1='380.61' y1='232.06' x2='371.71' y2='228.39' style='stroke-width: 2.25; stroke: #8C7200;' /> +<line x1='371.71' y1='228.39' x2='362.93' y2='224.41' style='stroke-width: 2.25; stroke: #867800;' /> +<line x1='362.93' y1='224.41' x2='354.32' y2='220.11' style='stroke-width: 2.25; stroke: #7F7F00;' /> +<line x1='354.32' y1='220.11' x2='345.85' y2='215.52' style='stroke-width: 2.25; stroke: #788600;' /> +<line x1='345.85' y1='215.52' x2='337.56' y2='210.62' style='stroke-width: 2.25; stroke: #728C00;' /> +<line x1='337.56' y1='210.62' x2='329.45' y2='205.42' style='stroke-width: 2.25; stroke: #6B9300;' /> +<line x1='329.45' y1='205.42' x2='321.50' y2='199.98' style='stroke-width: 2.25; stroke: #649A00;' /> +<line x1='321.50' y1='199.98' x2='313.69' y2='194.35' style='stroke-width: 2.25; stroke: #5DA100;' /> +<line x1='313.69' y1='194.35' x2='306.00' y2='188.54' style='stroke-width: 2.25; stroke: #57A700;' /> +<line x1='306.00' y1='188.54' x2='298.41' y2='182.62' style='stroke-width: 2.25; stroke: #50AE00;' /> +<line x1='298.41' y1='182.62' x2='290.89' y2='176.60' style='stroke-width: 2.25; stroke: #49B500;' /> +<line x1='290.89' y1='176.60' x2='283.43' y2='170.50' style='stroke-width: 2.25; stroke: #43BB00;' /> +<line x1='283.43' y1='170.50' x2='276.02' y2='164.35' style='stroke-width: 2.25; stroke: #3CC200;' /> +<line x1='276.02' y1='164.35' x2='268.66' y2='158.14' style='stroke-width: 2.25; stroke: #35C900;' /> +<line x1='268.66' y1='158.14' x2='261.33' y2='151.89' style='stroke-width: 2.25; stroke: #2ED000;' /> +<line x1='261.33' y1='151.89' x2='254.03' y2='145.60' style='stroke-width: 2.25; stroke: #28D600;' /> +<line x1='254.03' y1='145.60' x2='246.76' y2='139.29' style='stroke-width: 2.25; stroke: #21DD00;' /> +<line x1='246.76' y1='139.29' x2='239.51' y2='132.94' style='stroke-width: 2.25; stroke: #1AE400;' /> +<line x1='239.51' y1='132.94' x2='232.28' y2='126.57' style='stroke-width: 2.25; stroke: #14EA00;' /> +<line x1='232.28' y1='126.57' x2='225.08' y2='120.18' style='stroke-width: 2.25; stroke: #0DF100;' /> +<line x1='225.08' y1='120.18' x2='217.90' y2='113.76' style='stroke-width: 2.25; stroke: #06F800;' /> +<line x1='217.90' y1='113.76' x2='210.74' y2='107.31' style='stroke-width: 2.25; stroke: #00FF00;' /> +<polygon points='224.47,113.49 223.26,113.25 221.98,112.95 220.61,112.57 219.10,112.07 217.39,111.39 215.29,110.37 210.79,107.25 210.70,107.36 214.38,111.41 215.67,113.36 216.57,114.96 217.26,116.39 217.82,117.70 218.28,118.93 218.68,120.10 ' style='stroke-width: 0.75; stroke: #FF0000; fill: #00FF00;' /> +<line x1='191.07' y1='118.03' x2='193.32' y2='126.22' style='stroke-width: 2.25; stroke: #00FF00;' /> +<line x1='193.32' y1='126.22' x2='195.53' y2='134.42' style='stroke-width: 2.25; stroke: #00F806;' /> +<line x1='195.53' y1='134.42' x2='197.72' y2='142.63' style='stroke-width: 2.25; stroke: #00F10D;' /> +<line x1='197.72' y1='142.63' x2='199.89' y2='150.85' style='stroke-width: 2.25; stroke: #00EA14;' /> +<line x1='199.89' y1='150.85' x2='202.02' y2='159.08' style='stroke-width: 2.25; stroke: #00E41A;' /> +<line x1='202.02' y1='159.08' x2='204.12' y2='167.31' style='stroke-width: 2.25; stroke: #00DD21;' /> +<line x1='204.12' y1='167.31' x2='206.19' y2='175.55' style='stroke-width: 2.25; stroke: #00D628;' /> +<line x1='206.19' y1='175.55' x2='208.22' y2='183.81' style='stroke-width: 2.25; stroke: #00D02E;' /> +<line x1='208.22' y1='183.81' x2='210.20' y2='192.07' style='stroke-width: 2.25; stroke: #00C935;' /> +<line x1='210.20' y1='192.07' x2='212.13' y2='200.35' style='stroke-width: 2.25; stroke: #00C23C;' /> +<line x1='212.13' y1='200.35' x2='213.99' y2='208.64' style='stroke-width: 2.25; stroke: #00BB43;' /> +<line x1='213.99' y1='208.64' x2='215.77' y2='216.95' style='stroke-width: 2.25; stroke: #00B549;' /> +<line x1='215.77' y1='216.95' x2='217.45' y2='225.28' style='stroke-width: 2.25; stroke: #00AE50;' /> +<line x1='217.45' y1='225.28' x2='218.99' y2='233.63' style='stroke-width: 2.25; stroke: #00A757;' /> +<line x1='218.99' y1='233.63' x2='220.38' y2='242.02' style='stroke-width: 2.25; stroke: #00A15D;' /> +<line x1='220.38' y1='242.02' x2='221.55' y2='250.43' style='stroke-width: 2.25; stroke: #009A64;' /> +<line x1='221.55' y1='250.43' x2='222.46' y2='258.88' style='stroke-width: 2.25; stroke: #00936B;' /> +<line x1='222.46' y1='258.88' x2='223.11' y2='267.35' style='stroke-width: 2.25; stroke: #008C72;' /> +<line x1='223.11' y1='267.35' x2='223.42' y2='275.84' style='stroke-width: 2.25; stroke: #008678;' /> +<line x1='223.42' y1='275.84' x2='223.43' y2='284.34' style='stroke-width: 2.25; stroke: #007F7F;' /> +<line x1='223.43' y1='284.34' x2='223.15' y2='292.83' style='stroke-width: 2.25; stroke: #007886;' /> +<line x1='223.15' y1='292.83' x2='222.55' y2='301.31' style='stroke-width: 2.25; stroke: #00728C;' /> +<line x1='222.55' y1='301.31' x2='221.67' y2='309.76' style='stroke-width: 2.25; stroke: #006B93;' /> +<line x1='221.67' y1='309.76' x2='220.50' y2='318.18' style='stroke-width: 2.25; stroke: #00649A;' /> +<line x1='220.50' y1='318.18' x2='219.13' y2='326.56' style='stroke-width: 2.25; stroke: #005DA1;' /> +<line x1='219.13' y1='326.56' x2='217.59' y2='334.92' style='stroke-width: 2.25; stroke: #0057A7;' /> +<line x1='217.59' y1='334.92' x2='215.92' y2='343.25' style='stroke-width: 2.25; stroke: #0050AE;' /> +<line x1='215.92' y1='343.25' x2='214.14' y2='351.56' style='stroke-width: 2.25; stroke: #0049B5;' /> +<line x1='214.14' y1='351.56' x2='212.27' y2='359.85' style='stroke-width: 2.25; stroke: #0043BB;' /> +<line x1='212.27' y1='359.85' x2='210.33' y2='368.12' style='stroke-width: 2.25; stroke: #003CC2;' /> +<line x1='210.33' y1='368.12' x2='208.34' y2='376.38' style='stroke-width: 2.25; stroke: #0035C9;' /> +<line x1='208.34' y1='376.38' x2='206.29' y2='384.63' style='stroke-width: 2.25; stroke: #002ED0;' /> +<line x1='206.29' y1='384.63' x2='204.21' y2='392.87' style='stroke-width: 2.25; stroke: #0028D6;' /> +<line x1='204.21' y1='392.87' x2='202.10' y2='401.10' style='stroke-width: 2.25; stroke: #0021DD;' /> +<line x1='202.10' y1='401.10' x2='199.95' y2='409.32' style='stroke-width: 2.25; stroke: #001AE4;' /> +<line x1='199.95' y1='409.32' x2='197.77' y2='417.54' style='stroke-width: 2.25; stroke: #0014EA;' /> +<line x1='197.77' y1='417.54' x2='195.57' y2='425.74' style='stroke-width: 2.25; stroke: #000DF1;' /> +<line x1='195.57' y1='425.74' x2='193.34' y2='433.94' style='stroke-width: 2.25; stroke: #0006F8;' /> +<line x1='193.34' y1='433.94' x2='191.07' y2='442.13' style='stroke-width: 2.25; stroke: #0000FF;' /> +<polygon points='190.46,427.09 190.78,428.28 191.09,429.56 191.36,430.96 191.59,432.53 191.75,434.36 191.78,436.69 191.00,442.12 191.14,442.15 193.11,437.04 194.28,435.02 195.31,433.50 196.28,432.24 197.20,431.16 198.09,430.19 198.96,429.31 ' style='stroke-width: 0.75; stroke: #00FF00; fill: #0000FF;' /> +<line x1='209.46' y1='454.94' x2='216.62' y2='448.50' style='stroke-width: 2.25; stroke: #0000FF;' /> +<line x1='216.62' y1='448.50' x2='223.81' y2='442.09' style='stroke-width: 2.25; stroke: #0600F8;' /> +<line x1='223.81' y1='442.09' x2='231.03' y2='435.71' style='stroke-width: 2.25; stroke: #0D00F1;' /> +<line x1='231.03' y1='435.71' x2='238.26' y2='429.35' style='stroke-width: 2.25; stroke: #1400EA;' /> +<line x1='238.26' y1='429.35' x2='245.52' y2='423.02' style='stroke-width: 2.25; stroke: #1A00E4;' /> +<line x1='245.52' y1='423.02' x2='252.80' y2='416.71' style='stroke-width: 2.25; stroke: #2100DD;' /> +<line x1='252.80' y1='416.71' x2='260.11' y2='410.44' style='stroke-width: 2.25; stroke: #2800D6;' /> +<line x1='260.11' y1='410.44' x2='267.45' y2='404.20' style='stroke-width: 2.25; stroke: #2E00D0;' /> +<line x1='267.45' y1='404.20' x2='274.82' y2='398.00' style='stroke-width: 2.25; stroke: #3500C9;' /> +<line x1='274.82' y1='398.00' x2='282.23' y2='391.84' style='stroke-width: 2.25; stroke: #3C00C2;' /> +<line x1='282.23' y1='391.84' x2='289.69' y2='385.75' style='stroke-width: 2.25; stroke: #4300BB;' /> +<line x1='289.69' y1='385.75' x2='297.22' y2='379.74' style='stroke-width: 2.25; stroke: #4900B5;' /> +<line x1='297.22' y1='379.74' x2='304.81' y2='373.82' style='stroke-width: 2.25; stroke: #5000AE;' /> +<line x1='304.81' y1='373.82' x2='312.50' y2='368.01' style='stroke-width: 2.25; stroke: #5700A7;' /> +<line x1='312.50' y1='368.01' x2='320.30' y2='362.36' style='stroke-width: 2.25; stroke: #5D00A1;' /> +<line x1='320.30' y1='362.36' x2='328.24' y2='356.91' style='stroke-width: 2.25; stroke: #64009A;' /> +<line x1='328.24' y1='356.91' x2='336.33' y2='351.68' style='stroke-width: 2.25; stroke: #6B0093;' /> +<line x1='336.33' y1='351.68' x2='344.61' y2='346.76' style='stroke-width: 2.25; stroke: #72008C;' /> +<line x1='344.61' y1='346.76' x2='353.05' y2='342.13' style='stroke-width: 2.25; stroke: #780086;' /> +<line x1='353.05' y1='342.13' x2='361.67' y2='337.81' style='stroke-width: 2.25; stroke: #7F007F;' /> +<line x1='361.67' y1='337.81' x2='370.42' y2='333.80' style='stroke-width: 2.25; stroke: #860078;' /> +<line x1='370.42' y1='333.80' x2='379.32' y2='330.11' style='stroke-width: 2.25; stroke: #8C0072;' /> +<line x1='379.32' y1='330.11' x2='388.34' y2='326.74' style='stroke-width: 2.25; stroke: #93006B;' /> +<line x1='388.34' y1='326.74' x2='397.46' y2='323.64' style='stroke-width: 2.25; stroke: #9A0064;' /> +<line x1='397.46' y1='323.64' x2='406.66' y2='320.77' style='stroke-width: 2.25; stroke: #A1005D;' /> +<line x1='406.66' y1='320.77' x2='415.91' y2='318.10' style='stroke-width: 2.25; stroke: #A70057;' /> +<line x1='415.91' y1='318.10' x2='425.21' y2='315.58' style='stroke-width: 2.25; stroke: #AE0050;' /> +<line x1='425.21' y1='315.58' x2='434.54' y2='313.18' style='stroke-width: 2.25; stroke: #B50049;' /> +<line x1='434.54' y1='313.18' x2='443.89' y2='310.87' style='stroke-width: 2.25; stroke: #BB0043;' /> +<line x1='443.89' y1='310.87' x2='453.26' y2='308.63' style='stroke-width: 2.25; stroke: #C2003C;' /> +<line x1='453.26' y1='308.63' x2='462.64' y2='306.47' style='stroke-width: 2.25; stroke: #C90035;' /> +<line x1='462.64' y1='306.47' x2='472.04' y2='304.35' style='stroke-width: 2.25; stroke: #D0002E;' /> +<line x1='472.04' y1='304.35' x2='481.45' y2='302.29' style='stroke-width: 2.25; stroke: #D60028;' /> +<line x1='481.45' y1='302.29' x2='490.87' y2='300.26' style='stroke-width: 2.25; stroke: #DD0021;' /> +<line x1='490.87' y1='300.26' x2='500.29' y2='298.27' style='stroke-width: 2.25; stroke: #E4001A;' /> +<line x1='500.29' y1='298.27' x2='509.72' y2='296.31' style='stroke-width: 2.25; stroke: #EA0014;' /> +<line x1='509.72' y1='296.31' x2='519.16' y2='294.38' style='stroke-width: 2.25; stroke: #F1000D;' /> +<line x1='519.16' y1='294.38' x2='528.60' y2='292.49' style='stroke-width: 2.25; stroke: #F80006;' /> +<line x1='528.60' y1='292.49' x2='538.06' y2='290.64' style='stroke-width: 2.25; stroke: #FF0000;' /> +<polygon points='524.87,297.91 525.79,297.08 526.80,296.24 527.93,295.37 529.23,294.47 530.79,293.51 532.87,292.44 538.07,290.71 538.04,290.57 532.59,291.09 530.25,290.95 528.43,290.70 526.88,290.40 525.50,290.06 524.23,289.70 523.06,289.31 ' style='stroke-width: 0.75; stroke: #0000FF; fill: #FF0000;' /> +<circle cx='557.73' cy='280.80' r='22.00' style='stroke-width: 0.75; fill: #FF0000;' /> +<circle cx='191.07' cy='97.47' r='22.00' style='stroke-width: 0.75; fill: #00FF00;' /> +<circle cx='191.07' cy='464.13' r='22.00' style='stroke-width: 0.75; fill: #0000FF;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='557.73' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='191.07' y='101.66' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='191.07' y='468.27' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/edge-gradient.svg b/tests/testthat/_snaps/plot/edge-gradient.svg new file mode 100644 index 00000000000..9e197ef9dc9 --- /dev/null +++ b/tests/testthat/_snaps/plot/edge-gradient.svg @@ -0,0 +1,199 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='542.79' y1='265.85' x2='538.87' y2='261.94' style='stroke-width: 2.25; stroke: #FF0000;' /> +<line x1='538.87' y1='261.94' x2='534.95' y2='258.02' style='stroke-width: 2.25; stroke: #F80600;' /> +<line x1='534.95' y1='258.02' x2='531.03' y2='254.10' style='stroke-width: 2.25; stroke: #F10D00;' /> +<line x1='531.03' y1='254.10' x2='527.11' y2='250.18' style='stroke-width: 2.25; stroke: #EA1400;' /> +<line x1='527.11' y1='250.18' x2='523.19' y2='246.26' style='stroke-width: 2.25; stroke: #E41A00;' /> +<line x1='523.19' y1='246.26' x2='519.28' y2='242.34' style='stroke-width: 2.25; stroke: #DD2100;' /> +<line x1='519.28' y1='242.34' x2='515.36' y2='238.42' style='stroke-width: 2.25; stroke: #D62800;' /> +<line x1='515.36' y1='238.42' x2='511.44' y2='234.50' style='stroke-width: 2.25; stroke: #D02E00;' /> +<line x1='511.44' y1='234.50' x2='507.52' y2='230.59' style='stroke-width: 2.25; stroke: #C93500;' /> +<line x1='507.52' y1='230.59' x2='503.60' y2='226.67' style='stroke-width: 2.25; stroke: #C23C00;' /> +<line x1='503.60' y1='226.67' x2='499.68' y2='222.75' style='stroke-width: 2.25; stroke: #BB4300;' /> +<line x1='499.68' y1='222.75' x2='495.76' y2='218.83' style='stroke-width: 2.25; stroke: #B54900;' /> +<line x1='495.76' y1='218.83' x2='491.84' y2='214.91' style='stroke-width: 2.25; stroke: #AE5000;' /> +<line x1='491.84' y1='214.91' x2='487.93' y2='210.99' style='stroke-width: 2.25; stroke: #A75700;' /> +<line x1='487.93' y1='210.99' x2='484.01' y2='207.07' style='stroke-width: 2.25; stroke: #A15D00;' /> +<line x1='484.01' y1='207.07' x2='480.09' y2='203.15' style='stroke-width: 2.25; stroke: #9A6400;' /> +<line x1='480.09' y1='203.15' x2='476.17' y2='199.24' style='stroke-width: 2.25; stroke: #936B00;' /> +<line x1='476.17' y1='199.24' x2='472.25' y2='195.32' style='stroke-width: 2.25; stroke: #8C7200;' /> +<line x1='472.25' y1='195.32' x2='468.33' y2='191.40' style='stroke-width: 2.25; stroke: #867800;' /> +<line x1='468.33' y1='191.40' x2='464.41' y2='187.48' style='stroke-width: 2.25; stroke: #7F7F00;' /> +<line x1='464.41' y1='187.48' x2='460.49' y2='183.56' style='stroke-width: 2.25; stroke: #788600;' /> +<line x1='460.49' y1='183.56' x2='456.58' y2='179.64' style='stroke-width: 2.25; stroke: #728C00;' /> +<line x1='456.58' y1='179.64' x2='452.66' y2='175.72' style='stroke-width: 2.25; stroke: #6B9300;' /> +<line x1='452.66' y1='175.72' x2='448.74' y2='171.80' style='stroke-width: 2.25; stroke: #649A00;' /> +<line x1='448.74' y1='171.80' x2='444.82' y2='167.89' style='stroke-width: 2.25; stroke: #5DA100;' /> +<line x1='444.82' y1='167.89' x2='440.90' y2='163.97' style='stroke-width: 2.25; stroke: #57A700;' /> +<line x1='440.90' y1='163.97' x2='436.98' y2='160.05' style='stroke-width: 2.25; stroke: #50AE00;' /> +<line x1='436.98' y1='160.05' x2='433.06' y2='156.13' style='stroke-width: 2.25; stroke: #49B500;' /> +<line x1='433.06' y1='156.13' x2='429.14' y2='152.21' style='stroke-width: 2.25; stroke: #43BB00;' /> +<line x1='429.14' y1='152.21' x2='425.23' y2='148.29' style='stroke-width: 2.25; stroke: #3CC200;' /> +<line x1='425.23' y1='148.29' x2='421.31' y2='144.37' style='stroke-width: 2.25; stroke: #35C900;' /> +<line x1='421.31' y1='144.37' x2='417.39' y2='140.45' style='stroke-width: 2.25; stroke: #2ED000;' /> +<line x1='417.39' y1='140.45' x2='413.47' y2='136.54' style='stroke-width: 2.25; stroke: #28D600;' /> +<line x1='413.47' y1='136.54' x2='409.55' y2='132.62' style='stroke-width: 2.25; stroke: #21DD00;' /> +<line x1='409.55' y1='132.62' x2='405.63' y2='128.70' style='stroke-width: 2.25; stroke: #1AE400;' /> +<line x1='405.63' y1='128.70' x2='401.71' y2='124.78' style='stroke-width: 2.25; stroke: #14EA00;' /> +<line x1='401.71' y1='124.78' x2='397.79' y2='120.86' style='stroke-width: 2.25; stroke: #0DF100;' /> +<line x1='397.79' y1='120.86' x2='393.88' y2='116.94' style='stroke-width: 2.25; stroke: #06F800;' /> +<line x1='393.88' y1='116.94' x2='389.96' y2='113.02' style='stroke-width: 2.25; stroke: #00FF00;' /> +<polygon points='397.95,117.25 396.94,116.97 395.84,116.60 394.60,116.10 393.11,115.34 390.01,112.97 389.91,113.07 392.27,116.17 393.04,117.67 393.54,118.91 393.90,120.00 394.18,121.02 ' style='stroke-width: 0.75; stroke: #FF0000; fill: #00FF00;' /> +<line x1='359.45' y1='112.41' x2='355.54' y2='116.33' style='stroke-width: 2.25; stroke: #00FF00;' /> +<line x1='355.54' y1='116.33' x2='351.62' y2='120.25' style='stroke-width: 2.25; stroke: #00F806;' /> +<line x1='351.62' y1='120.25' x2='347.70' y2='124.17' style='stroke-width: 2.25; stroke: #00F10D;' /> +<line x1='347.70' y1='124.17' x2='343.78' y2='128.09' style='stroke-width: 2.25; stroke: #00EA14;' /> +<line x1='343.78' y1='128.09' x2='339.86' y2='132.01' style='stroke-width: 2.25; stroke: #00E41A;' /> +<line x1='339.86' y1='132.01' x2='335.94' y2='135.92' style='stroke-width: 2.25; stroke: #00DD21;' /> +<line x1='335.94' y1='135.92' x2='332.02' y2='139.84' style='stroke-width: 2.25; stroke: #00D628;' /> +<line x1='332.02' y1='139.84' x2='328.10' y2='143.76' style='stroke-width: 2.25; stroke: #00D02E;' /> +<line x1='328.10' y1='143.76' x2='324.19' y2='147.68' style='stroke-width: 2.25; stroke: #00C935;' /> +<line x1='324.19' y1='147.68' x2='320.27' y2='151.60' style='stroke-width: 2.25; stroke: #00C23C;' /> +<line x1='320.27' y1='151.60' x2='316.35' y2='155.52' style='stroke-width: 2.25; stroke: #00BB43;' /> +<line x1='316.35' y1='155.52' x2='312.43' y2='159.44' style='stroke-width: 2.25; stroke: #00B549;' /> +<line x1='312.43' y1='159.44' x2='308.51' y2='163.36' style='stroke-width: 2.25; stroke: #00AE50;' /> +<line x1='308.51' y1='163.36' x2='304.59' y2='167.27' style='stroke-width: 2.25; stroke: #00A757;' /> +<line x1='304.59' y1='167.27' x2='300.67' y2='171.19' style='stroke-width: 2.25; stroke: #00A15D;' /> +<line x1='300.67' y1='171.19' x2='296.75' y2='175.11' style='stroke-width: 2.25; stroke: #009A64;' /> +<line x1='296.75' y1='175.11' x2='292.84' y2='179.03' style='stroke-width: 2.25; stroke: #00936B;' /> +<line x1='292.84' y1='179.03' x2='288.92' y2='182.95' style='stroke-width: 2.25; stroke: #008C72;' /> +<line x1='288.92' y1='182.95' x2='285.00' y2='186.87' style='stroke-width: 2.25; stroke: #008678;' /> +<line x1='285.00' y1='186.87' x2='281.08' y2='190.79' style='stroke-width: 2.25; stroke: #007F7F;' /> +<line x1='281.08' y1='190.79' x2='277.16' y2='194.71' style='stroke-width: 2.25; stroke: #007886;' /> +<line x1='277.16' y1='194.71' x2='273.24' y2='198.62' style='stroke-width: 2.25; stroke: #00728C;' /> +<line x1='273.24' y1='198.62' x2='269.32' y2='202.54' style='stroke-width: 2.25; stroke: #006B93;' /> +<line x1='269.32' y1='202.54' x2='265.40' y2='206.46' style='stroke-width: 2.25; stroke: #00649A;' /> +<line x1='265.40' y1='206.46' x2='261.49' y2='210.38' style='stroke-width: 2.25; stroke: #005DA1;' /> +<line x1='261.49' y1='210.38' x2='257.57' y2='214.30' style='stroke-width: 2.25; stroke: #0057A7;' /> +<line x1='257.57' y1='214.30' x2='253.65' y2='218.22' style='stroke-width: 2.25; stroke: #0050AE;' /> +<line x1='253.65' y1='218.22' x2='249.73' y2='222.14' style='stroke-width: 2.25; stroke: #0049B5;' /> +<line x1='249.73' y1='222.14' x2='245.81' y2='226.06' style='stroke-width: 2.25; stroke: #0043BB;' /> +<line x1='245.81' y1='226.06' x2='241.89' y2='229.97' style='stroke-width: 2.25; stroke: #003CC2;' /> +<line x1='241.89' y1='229.97' x2='237.97' y2='233.89' style='stroke-width: 2.25; stroke: #0035C9;' /> +<line x1='237.97' y1='233.89' x2='234.05' y2='237.81' style='stroke-width: 2.25; stroke: #002ED0;' /> +<line x1='234.05' y1='237.81' x2='230.14' y2='241.73' style='stroke-width: 2.25; stroke: #0028D6;' /> +<line x1='230.14' y1='241.73' x2='226.22' y2='245.65' style='stroke-width: 2.25; stroke: #0021DD;' /> +<line x1='226.22' y1='245.65' x2='222.30' y2='249.57' style='stroke-width: 2.25; stroke: #001AE4;' /> +<line x1='222.30' y1='249.57' x2='218.38' y2='253.49' style='stroke-width: 2.25; stroke: #0014EA;' /> +<line x1='218.38' y1='253.49' x2='214.46' y2='257.41' style='stroke-width: 2.25; stroke: #000DF1;' /> +<line x1='214.46' y1='257.41' x2='210.54' y2='261.32' style='stroke-width: 2.25; stroke: #0006F8;' /> +<line x1='210.54' y1='261.32' x2='206.62' y2='265.24' style='stroke-width: 2.25; stroke: #0000FF;' /> +<polygon points='210.85,257.25 210.57,258.26 210.20,259.36 209.70,260.60 208.94,262.09 206.57,265.19 206.67,265.29 209.77,262.93 211.27,262.16 212.51,261.66 213.60,261.30 214.62,261.02 ' style='stroke-width: 0.75; stroke: #00FF00; fill: #0000FF;' /> +<line x1='212.20' y1='280.80' x2='220.50' y2='280.80' style='stroke-width: 2.25; stroke: #0000FF;' /> +<line x1='220.50' y1='280.80' x2='228.79' y2='280.80' style='stroke-width: 2.25; stroke: #0600F8;' /> +<line x1='228.79' y1='280.80' x2='237.09' y2='280.80' style='stroke-width: 2.25; stroke: #0D00F1;' /> +<line x1='237.09' y1='280.80' x2='245.39' y2='280.80' style='stroke-width: 2.25; stroke: #1400EA;' /> +<line x1='245.39' y1='280.80' x2='253.68' y2='280.80' style='stroke-width: 2.25; stroke: #1A00E4;' /> +<line x1='253.68' y1='280.80' x2='261.98' y2='280.80' style='stroke-width: 2.25; stroke: #2100DD;' /> +<line x1='261.98' y1='280.80' x2='270.27' y2='280.80' style='stroke-width: 2.25; stroke: #2800D6;' /> +<line x1='270.27' y1='280.80' x2='278.57' y2='280.80' style='stroke-width: 2.25; stroke: #2E00D0;' /> +<line x1='278.57' y1='280.80' x2='286.86' y2='280.80' style='stroke-width: 2.25; stroke: #3500C9;' /> +<line x1='286.86' y1='280.80' x2='295.16' y2='280.80' style='stroke-width: 2.25; stroke: #3C00C2;' /> +<line x1='295.16' y1='280.80' x2='303.45' y2='280.80' style='stroke-width: 2.25; stroke: #4300BB;' /> +<line x1='303.45' y1='280.80' x2='311.75' y2='280.80' style='stroke-width: 2.25; stroke: #4900B5;' /> +<line x1='311.75' y1='280.80' x2='320.05' y2='280.80' style='stroke-width: 2.25; stroke: #5000AE;' /> +<line x1='320.05' y1='280.80' x2='328.34' y2='280.80' style='stroke-width: 2.25; stroke: #5700A7;' /> +<line x1='328.34' y1='280.80' x2='336.64' y2='280.80' style='stroke-width: 2.25; stroke: #5D00A1;' /> +<line x1='336.64' y1='280.80' x2='344.93' y2='280.80' style='stroke-width: 2.25; stroke: #64009A;' /> +<line x1='344.93' y1='280.80' x2='353.23' y2='280.80' style='stroke-width: 2.25; stroke: #6B0093;' /> +<line x1='353.23' y1='280.80' x2='361.52' y2='280.80' style='stroke-width: 2.25; stroke: #72008C;' /> +<line x1='361.52' y1='280.80' x2='369.82' y2='280.80' style='stroke-width: 2.25; stroke: #780086;' /> +<line x1='369.82' y1='280.80' x2='378.12' y2='280.80' style='stroke-width: 2.25; stroke: #7F007F;' /> +<line x1='378.12' y1='280.80' x2='386.41' y2='280.80' style='stroke-width: 2.25; stroke: #860078;' /> +<line x1='386.41' y1='280.80' x2='394.71' y2='280.80' style='stroke-width: 2.25; stroke: #8C0072;' /> +<line x1='394.71' y1='280.80' x2='403.00' y2='280.80' style='stroke-width: 2.25; stroke: #93006B;' /> +<line x1='403.00' y1='280.80' x2='411.30' y2='280.80' style='stroke-width: 2.25; stroke: #9A0064;' /> +<line x1='411.30' y1='280.80' x2='419.59' y2='280.80' style='stroke-width: 2.25; stroke: #A1005D;' /> +<line x1='419.59' y1='280.80' x2='427.89' y2='280.80' style='stroke-width: 2.25; stroke: #A70057;' /> +<line x1='427.89' y1='280.80' x2='436.19' y2='280.80' style='stroke-width: 2.25; stroke: #AE0050;' /> +<line x1='436.19' y1='280.80' x2='444.48' y2='280.80' style='stroke-width: 2.25; stroke: #B50049;' /> +<line x1='444.48' y1='280.80' x2='452.78' y2='280.80' style='stroke-width: 2.25; stroke: #BB0043;' /> +<line x1='452.78' y1='280.80' x2='461.07' y2='280.80' style='stroke-width: 2.25; stroke: #C2003C;' /> +<line x1='461.07' y1='280.80' x2='469.37' y2='280.80' style='stroke-width: 2.25; stroke: #C90035;' /> +<line x1='469.37' y1='280.80' x2='477.66' y2='280.80' style='stroke-width: 2.25; stroke: #D0002E;' /> +<line x1='477.66' y1='280.80' x2='485.96' y2='280.80' style='stroke-width: 2.25; stroke: #D60028;' /> +<line x1='485.96' y1='280.80' x2='494.26' y2='280.80' style='stroke-width: 2.25; stroke: #DD0021;' /> +<line x1='494.26' y1='280.80' x2='502.55' y2='280.80' style='stroke-width: 2.25; stroke: #E4001A;' /> +<line x1='502.55' y1='280.80' x2='510.85' y2='280.80' style='stroke-width: 2.25; stroke: #EA0014;' /> +<line x1='510.85' y1='280.80' x2='519.14' y2='280.80' style='stroke-width: 2.25; stroke: #F1000D;' /> +<line x1='519.14' y1='280.80' x2='527.44' y2='280.80' style='stroke-width: 2.25; stroke: #F80006;' /> +<line x1='527.44' y1='280.80' x2='535.73' y2='280.80' style='stroke-width: 2.25; stroke: #FF0000;' /> +<polygon points='527.09,283.46 528.01,282.95 529.04,282.43 530.27,281.91 531.87,281.39 535.73,280.87 535.73,280.73 531.87,280.21 530.27,279.69 529.04,279.17 528.01,278.65 527.09,278.14 ' style='stroke-width: 0.75; stroke: #0000FF; fill: #FF0000;' /> +<line x1='542.79' y1='295.75' x2='538.87' y2='299.66' style='stroke-width: 2.25; stroke: #FF0000;' /> +<line x1='538.87' y1='299.66' x2='534.95' y2='303.58' style='stroke-width: 2.25; stroke: #FF0400;' /> +<line x1='534.95' y1='303.58' x2='531.03' y2='307.50' style='stroke-width: 2.25; stroke: #FF0800;' /> +<line x1='531.03' y1='307.50' x2='527.11' y2='311.42' style='stroke-width: 2.25; stroke: #FF0D00;' /> +<line x1='527.11' y1='311.42' x2='523.19' y2='315.34' style='stroke-width: 2.25; stroke: #FF1100;' /> +<line x1='523.19' y1='315.34' x2='519.28' y2='319.26' style='stroke-width: 2.25; stroke: #FF1500;' /> +<line x1='519.28' y1='319.26' x2='515.36' y2='323.18' style='stroke-width: 2.25; stroke: #FF1A00;' /> +<line x1='515.36' y1='323.18' x2='511.44' y2='327.10' style='stroke-width: 2.25; stroke: #FF1E00;' /> +<line x1='511.44' y1='327.10' x2='507.52' y2='331.01' style='stroke-width: 2.25; stroke: #FF2200;' /> +<line x1='507.52' y1='331.01' x2='503.60' y2='334.93' style='stroke-width: 2.25; stroke: #FF2700;' /> +<line x1='503.60' y1='334.93' x2='499.68' y2='338.85' style='stroke-width: 2.25; stroke: #FF2B00;' /> +<line x1='499.68' y1='338.85' x2='495.76' y2='342.77' style='stroke-width: 2.25; stroke: #FF2F00;' /> +<line x1='495.76' y1='342.77' x2='491.84' y2='346.69' style='stroke-width: 2.25; stroke: #FF3400;' /> +<line x1='491.84' y1='346.69' x2='487.93' y2='350.61' style='stroke-width: 2.25; stroke: #FF3800;' /> +<line x1='487.93' y1='350.61' x2='484.01' y2='354.53' style='stroke-width: 2.25; stroke: #FF3C00;' /> +<line x1='484.01' y1='354.53' x2='480.09' y2='358.45' style='stroke-width: 2.25; stroke: #FF4100;' /> +<line x1='480.09' y1='358.45' x2='476.17' y2='362.36' style='stroke-width: 2.25; stroke: #FF4500;' /> +<line x1='476.17' y1='362.36' x2='472.25' y2='366.28' style='stroke-width: 2.25; stroke: #FF4900;' /> +<line x1='472.25' y1='366.28' x2='468.33' y2='370.20' style='stroke-width: 2.25; stroke: #FF4E00;' /> +<line x1='468.33' y1='370.20' x2='464.41' y2='374.12' style='stroke-width: 2.25; stroke: #FF5200;' /> +<line x1='464.41' y1='374.12' x2='460.49' y2='378.04' style='stroke-width: 2.25; stroke: #FF5600;' /> +<line x1='460.49' y1='378.04' x2='456.58' y2='381.96' style='stroke-width: 2.25; stroke: #FF5B00;' /> +<line x1='456.58' y1='381.96' x2='452.66' y2='385.88' style='stroke-width: 2.25; stroke: #FF5F00;' /> +<line x1='452.66' y1='385.88' x2='448.74' y2='389.80' style='stroke-width: 2.25; stroke: #FF6300;' /> +<line x1='448.74' y1='389.80' x2='444.82' y2='393.71' style='stroke-width: 2.25; stroke: #FF6800;' /> +<line x1='444.82' y1='393.71' x2='440.90' y2='397.63' style='stroke-width: 2.25; stroke: #FF6C00;' /> +<line x1='440.90' y1='397.63' x2='436.98' y2='401.55' style='stroke-width: 2.25; stroke: #FF7000;' /> +<line x1='436.98' y1='401.55' x2='433.06' y2='405.47' style='stroke-width: 2.25; stroke: #FF7500;' /> +<line x1='433.06' y1='405.47' x2='429.14' y2='409.39' style='stroke-width: 2.25; stroke: #FF7900;' /> +<line x1='429.14' y1='409.39' x2='425.23' y2='413.31' style='stroke-width: 2.25; stroke: #FF7D00;' /> +<line x1='425.23' y1='413.31' x2='421.31' y2='417.23' style='stroke-width: 2.25; stroke: #FF8200;' /> +<line x1='421.31' y1='417.23' x2='417.39' y2='421.15' style='stroke-width: 2.25; stroke: #FF8600;' /> +<line x1='417.39' y1='421.15' x2='413.47' y2='425.06' style='stroke-width: 2.25; stroke: #FF8A00;' /> +<line x1='413.47' y1='425.06' x2='409.55' y2='428.98' style='stroke-width: 2.25; stroke: #FF8F00;' /> +<line x1='409.55' y1='428.98' x2='405.63' y2='432.90' style='stroke-width: 2.25; stroke: #FF9300;' /> +<line x1='405.63' y1='432.90' x2='401.71' y2='436.82' style='stroke-width: 2.25; stroke: #FF9700;' /> +<line x1='401.71' y1='436.82' x2='397.79' y2='440.74' style='stroke-width: 2.25; stroke: #FF9C00;' /> +<line x1='397.79' y1='440.74' x2='393.88' y2='444.66' style='stroke-width: 2.25; stroke: #FFA000;' /> +<line x1='393.88' y1='444.66' x2='389.96' y2='448.58' style='stroke-width: 2.25; stroke: #FFA500;' /> +<polygon points='394.18,440.58 393.90,441.60 393.54,442.69 393.04,443.93 392.27,445.43 389.91,448.53 390.01,448.63 393.11,446.26 394.60,445.50 395.84,445.00 396.94,444.63 397.95,444.35 ' style='stroke-width: 0.75; stroke: #FF0000; fill: #FFA500;' /> +<circle cx='557.73' cy='280.80' r='22.00' style='stroke-width: 0.75; fill: #FF0000;' /> +<circle cx='374.40' cy='97.47' r='22.00' style='stroke-width: 0.75; fill: #00FF00;' /> +<circle cx='191.07' cy='280.80' r='22.00' style='stroke-width: 0.75; fill: #0000FF;' /> +<circle cx='374.40' cy='464.13' r='22.00' style='stroke-width: 0.75; fill: #FFA500;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='557.73' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='101.66' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='191.07' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='374.40' y='468.26' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/edge-label-halo.svg b/tests/testthat/_snaps/plot/edge-label-halo.svg new file mode 100644 index 00000000000..a7593bb798f --- /dev/null +++ b/tests/testthat/_snaps/plot/edge-label-halo.svg @@ -0,0 +1,115 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='556.30' y1='271.69' x2='384.53' y2='99.92' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='397.82,107.00 396.63,106.68 395.37,106.29 394.02,105.82 392.56,105.23 390.90,104.44 388.87,103.28 384.58,99.87 384.48,99.97 387.89,104.26 389.05,106.29 389.84,107.95 390.43,109.42 390.90,110.76 391.29,112.02 391.61,113.21 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<line x1='365.29' y1='98.90' x2='193.52' y2='270.67' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='200.60,257.38 200.28,258.57 199.89,259.83 199.42,261.18 198.83,262.64 198.04,264.30 196.88,266.33 193.47,270.62 193.57,270.72 197.86,267.31 199.89,266.15 201.55,265.36 203.02,264.77 204.36,264.30 205.62,263.91 206.81,263.59 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<line x1='192.50' y1='289.91' x2='364.27' y2='461.68' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='350.98,454.60 352.17,454.92 353.43,455.31 354.78,455.78 356.24,456.37 357.90,457.16 359.93,458.32 364.22,461.73 364.32,461.63 360.91,457.34 359.75,455.31 358.96,453.65 358.37,452.18 357.90,450.84 357.51,449.58 357.19,448.39 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<line x1='383.51' y1='462.70' x2='555.28' y2='290.93' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='548.20,304.22 548.52,303.03 548.91,301.77 549.38,300.42 549.97,298.96 550.76,297.30 551.92,295.27 555.33,290.98 555.23,290.88 550.94,294.29 548.91,295.45 547.25,296.24 545.78,296.83 544.44,297.30 543.18,297.69 541.99,298.01 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<text x='500.08' y='219.15' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='499.68' y='218.55' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='499.07' y='218.14' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='498.36' y='218.00' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='497.65' y='218.14' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='497.05' y='218.55' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='496.65' y='219.15' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='496.50' y='219.86' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='496.65' y='220.57' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='497.05' y='221.18' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='497.65' y='221.58' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='498.36' y='221.72' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='499.07' y='221.58' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='499.68' y='221.18' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='500.08' y='220.57' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='500.22' y='219.86' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='498.36' y='219.86' text-anchor='middle' style='font-size: 18.00px; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e1</text> +<text x='309.07' y='162.33' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='308.67' y='161.72' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='308.07' y='161.32' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='307.35' y='161.18' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='306.64' y='161.32' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='306.04' y='161.72' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='305.64' y='162.33' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='305.50' y='163.04' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='305.64' y='163.75' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='306.04' y='164.35' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='306.64' y='164.75' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='307.35' y='164.90' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='308.07' y='164.75' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='308.67' y='164.35' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='309.07' y='163.75' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='309.21' y='163.04' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='307.35' y='163.04' text-anchor='middle' style='font-size: 18.00px; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e2</text> +<text x='252.15' y='353.33' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='251.75' y='352.73' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='251.15' y='352.33' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='250.44' y='352.19' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='249.73' y='352.33' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='249.12' y='352.73' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='248.72' y='353.33' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='248.58' y='354.05' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='248.72' y='354.76' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='249.12' y='355.36' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='249.73' y='355.76' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='250.44' y='355.90' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='251.15' y='355.76' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='251.75' y='355.36' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='252.15' y='354.76' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='252.30' y='354.05' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='250.44' y='354.05' text-anchor='middle' style='font-size: 18.00px; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e3</text> +<text x='443.16' y='410.16' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='442.76' y='409.56' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='442.16' y='409.15' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='441.45' y='409.01' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='440.73' y='409.15' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='440.13' y='409.56' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='439.73' y='410.16' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='439.59' y='410.87' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='439.73' y='411.58' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='440.13' y='412.18' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='440.73' y='412.59' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='441.45' y='412.73' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='442.16' y='412.59' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='442.76' y='412.18' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='443.16' y='411.58' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='443.30' y='410.87' text-anchor='middle' style='font-size: 18.00px; fill: #FFFF00; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<text x='441.45' y='410.87' text-anchor='middle' style='font-size: 18.00px; font-family: sans;' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>e4</text> +<circle cx='565.41' cy='280.80' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='89.79' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='183.39' cy='280.80' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='471.81' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='565.41' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='93.98' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='183.39' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='374.40' y='475.94' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/edge-style-arc-single.svg b/tests/testthat/_snaps/plot/edge-style-arc-single.svg new file mode 100644 index 00000000000..d54a93b2047 --- /dev/null +++ b/tests/testthat/_snaps/plot/edge-style-arc-single.svg @@ -0,0 +1,51 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<polyline points='550.17,266.54 550.10,266.48 549.60,266.10 548.35,265.16 546.15,263.48 542.91,261.00 538.69,257.75 533.69,253.86 528.14,249.51 522.33,244.90 516.49,240.21 510.84,235.58 505.49,231.12 500.53,226.86 495.97,222.83 491.81,219.01 488.03,215.36 484.56,211.84 481.36,208.39 478.39,204.97 475.57,201.52 474.08,199.60 471.44,196.03 468.88,192.34 466.38,188.48 463.90,184.37 461.41,179.95 458.86,175.16 456.23,169.95 453.50,164.30 450.67,158.19 447.74,151.70 444.77,144.94 441.82,138.08 439.00,131.40 436.41,125.19 434.18,119.77 432.39,115.40 431.11,112.23 430.30,110.24 429.91,109.26 429.80,108.98 429.80,108.97 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='434.64,114.74 433.77,114.06 432.83,113.21 431.76,112.05 429.86,108.94 429.73,108.99 430.63,112.52 430.71,114.11 430.65,115.37 430.53,116.47 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='401.32,99.43 401.24,99.48 400.72,99.82 399.42,100.65 397.11,102.12 393.68,104.29 389.19,107.12 383.81,110.48 377.77,114.21 371.36,118.12 364.84,122.05 358.41,125.86 352.22,129.44 346.35,132.74 340.83,135.74 335.64,138.44 330.76,140.84 326.13,142.98 321.68,144.88 317.35,146.57 313.06,148.10 309.26,149.32 304.93,150.59 300.52,151.74 295.96,152.77 291.19,153.70 286.12,154.54 280.69,155.29 274.85,155.98 268.55,156.60 261.80,157.17 254.63,157.69 247.17,158.16 239.59,158.57 232.15,158.94 225.17,159.24 218.99,159.48 213.88,159.67 210.04,159.79 207.50,159.87 206.13,159.91 205.62,159.92 205.57,159.92 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='212.66,157.37 211.72,157.95 210.60,158.54 209.14,159.15 205.56,159.85 205.57,159.99 209.19,160.37 210.71,160.84 211.87,161.33 212.86,161.82 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='187.73,183.38 187.76,183.47 187.93,184.08 188.35,185.60 189.08,188.29 190.16,192.25 191.53,197.40 193.15,203.53 194.90,210.32 196.70,217.45 198.47,224.63 200.12,231.64 201.62,238.32 202.95,244.59 204.08,250.43 205.02,255.88 205.78,260.97 206.36,265.78 206.77,270.37 207.03,274.83 207.14,279.23 207.15,280.68 207.08,285.07 206.87,289.51 206.51,294.05 205.99,298.78 205.29,303.77 204.40,309.09 203.33,314.79 202.06,320.91 200.61,327.44 198.99,334.33 197.25,341.46 195.45,348.61 193.67,355.51 192.00,361.84 190.54,367.29 189.37,371.59 188.53,374.64 188.02,376.47 187.79,377.31 187.73,377.50 187.73,377.50 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='187.39,369.97 187.67,371.04 187.90,372.28 188.05,373.86 187.66,377.48 187.80,377.52 189.23,374.17 190.13,372.86 190.94,371.89 191.71,371.10 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='204.88,401.46 204.97,401.47 205.59,401.48 207.13,401.53 209.88,401.62 213.93,401.76 219.23,401.96 225.57,402.23 232.66,402.55 240.16,402.93 247.76,403.37 255.22,403.85 262.35,404.39 269.06,404.97 275.31,405.61 281.11,406.31 286.50,407.08 291.53,407.93 296.27,408.87 300.81,409.92 305.20,411.08 309.03,412.21 313.33,413.60 317.62,415.14 321.96,416.86 326.43,418.79 331.08,420.96 335.99,423.40 341.20,426.13 346.75,429.17 352.65,432.50 358.86,436.12 365.29,439.94 371.78,443.88 378.12,447.77 384.06,451.46 389.30,454.75 393.62,457.48 396.87,459.54 399.01,460.91 400.16,461.65 400.58,461.92 400.63,461.96 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='393.33,460.07 394.44,460.11 395.70,460.26 397.25,460.58 400.59,462.02 400.67,461.89 397.89,459.54 396.91,458.30 396.22,457.24 395.68,456.27 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='429.36,453.21 429.40,453.12 429.63,452.54 430.21,451.09 431.25,448.52 432.80,444.74 434.83,439.82 437.26,433.96 440.00,427.47 442.92,420.65 445.91,413.79 448.88,407.12 451.78,400.78 454.58,394.87 457.28,389.42 459.87,384.40 462.40,379.79 464.88,375.52 467.37,371.53 469.89,367.75 472.48,364.13 473.94,362.19 476.68,358.70 479.57,355.27 482.64,351.84 485.96,348.37 489.56,344.80 493.51,341.08 497.85,337.17 502.59,333.05 507.73,328.71 513.22,324.19 518.97,319.54 524.81,314.89 530.51,310.41 535.82,306.28 540.46,302.70 544.21,299.83 546.93,297.75 548.64,296.46 549.48,295.82 549.72,295.64 549.73,295.63 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='545.44,301.83 545.87,300.81 546.43,299.68 547.28,298.33 549.78,295.69 549.69,295.58 546.52,297.37 545.01,297.86 543.78,298.14 542.69,298.31 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<circle cx='561.07' cy='280.80' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='418.47' cy='94.13' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='187.73' cy='165.43' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='187.73' cy='396.17' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='418.47' cy='467.47' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='561.07' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='418.47' y='98.32' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='187.73' y='169.57' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='187.73' y='400.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='418.47' y='471.54' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/edge-style-diagonal.svg b/tests/testthat/_snaps/plot/edge-style-diagonal.svg new file mode 100644 index 00000000000..9ad71946bfc --- /dev/null +++ b/tests/testthat/_snaps/plot/edge-style-diagonal.svg @@ -0,0 +1,89 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<polyline points='374.82,98.31 374.43,103.63 373.30,108.59 371.50,113.22 369.06,117.55 366.05,121.61 362.52,125.42 358.52,129.00 354.12,132.40 349.35,135.62 344.29,138.69 338.99,141.65 333.49,144.52 327.85,147.32 322.13,150.09 316.39,152.84 310.67,155.60 305.04,158.40 299.54,161.27 294.23,164.23 289.17,167.30 284.41,170.53 280.00,173.92 276.01,177.50 272.48,181.31 269.47,185.37 267.03,189.70 265.22,194.33 264.10,199.29 263.71,204.61 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='262.33,198.73 262.83,199.83 263.30,201.24 263.64,204.60 263.78,204.61 264.60,201.34 265.27,200.01 265.92,198.99 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='373.98,98.31 374.37,103.63 375.50,108.59 377.30,113.22 379.74,117.55 382.75,121.61 386.28,125.42 390.28,129.00 394.68,132.40 399.45,135.62 404.51,138.69 409.81,141.65 415.31,144.52 420.95,147.32 426.67,150.09 432.41,152.84 438.13,155.60 443.76,158.40 449.26,161.27 454.57,164.23 459.63,167.30 464.39,170.53 468.80,173.92 472.79,177.50 476.32,181.31 479.33,185.37 481.77,189.70 483.58,194.33 484.70,199.29 485.09,204.61 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='482.88,198.99 483.53,200.01 484.20,201.34 485.02,204.61 485.16,204.60 485.50,201.24 485.97,199.83 486.47,198.73 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='263.97,227.34 263.78,232.66 263.22,237.63 262.31,242.26 261.09,246.60 259.59,250.66 257.82,254.47 255.82,258.07 253.61,261.46 251.23,264.68 248.70,267.76 246.04,270.73 243.29,273.60 240.47,276.40 237.61,279.17 234.73,281.92 231.87,284.69 229.05,287.49 226.30,290.36 223.64,293.33 221.11,296.41 218.72,299.63 216.52,303.02 214.52,306.61 212.75,310.43 211.24,314.49 210.02,318.83 209.12,323.46 208.56,328.43 208.36,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='206.77,327.93 207.31,329.00 207.84,330.40 208.29,333.74 208.43,333.75 209.13,330.45 209.76,329.09 210.37,328.06 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='263.44,227.34 263.64,232.66 264.20,237.63 265.10,242.26 266.32,246.60 267.83,250.66 269.60,254.47 271.60,258.07 273.80,261.46 276.19,264.68 278.72,267.76 281.38,270.73 284.13,273.60 286.95,276.40 289.81,279.17 292.69,281.92 295.55,284.69 298.37,287.49 301.12,290.36 303.78,293.33 306.31,296.41 308.69,299.63 310.90,303.02 312.90,306.61 314.66,310.43 316.17,314.49 317.39,318.83 318.30,323.46 318.86,328.43 319.05,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='317.05,328.06 317.66,329.09 318.29,330.45 318.98,333.75 319.13,333.74 319.58,330.40 320.11,329.00 320.64,327.93 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='485.36,227.34 485.16,232.66 484.60,237.63 483.70,242.26 482.48,246.60 480.97,250.66 479.20,254.47 477.20,258.07 475.00,261.46 472.61,264.68 470.08,267.76 467.42,270.73 464.67,273.60 461.85,276.40 458.99,279.17 456.11,281.92 453.25,284.69 450.43,287.49 447.68,290.36 445.02,293.33 442.49,296.41 440.11,299.63 437.90,303.02 435.90,306.61 434.14,310.43 432.63,314.49 431.41,318.83 430.50,323.46 429.94,328.43 429.75,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='428.16,327.93 428.69,329.00 429.22,330.40 429.67,333.74 429.82,333.75 430.51,330.45 431.14,329.09 431.75,328.06 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='484.83,227.34 485.02,232.66 485.58,237.63 486.49,242.26 487.71,246.60 489.21,250.66 490.98,254.47 492.98,258.07 495.19,261.46 497.57,264.68 500.10,267.76 502.76,270.73 505.51,273.60 508.33,276.40 511.19,279.17 514.07,281.92 516.93,284.69 519.75,287.49 522.50,290.36 525.16,293.33 527.69,296.41 530.08,299.63 532.28,303.02 534.28,306.61 536.05,310.43 537.56,314.49 538.78,318.83 539.68,323.46 540.24,328.43 540.44,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='538.43,328.06 539.04,329.09 539.67,330.45 540.37,333.75 540.51,333.74 540.96,330.40 541.49,329.00 542.03,327.93 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='208.51,356.44 208.41,361.76 208.13,366.72 207.68,371.36 207.07,375.70 206.31,379.77 205.43,383.58 204.43,387.17 203.32,390.57 202.13,393.79 200.87,396.88 199.54,399.84 198.16,402.71 196.75,405.52 195.32,408.28 193.88,411.04 192.45,413.81 191.04,416.61 189.66,419.48 188.33,422.45 187.06,425.53 185.87,428.76 184.77,432.15 183.77,435.74 182.89,439.56 182.13,443.62 181.52,447.96 181.07,452.60 180.79,457.57 180.69,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='178.99,457.10 179.55,458.16 180.10,459.55 180.62,462.89 180.76,462.89 181.40,459.57 182.00,458.21 182.59,457.16 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='208.22,356.44 208.31,361.76 208.60,366.72 209.05,371.36 209.66,375.70 210.41,379.77 211.30,383.58 212.30,387.17 213.40,390.57 214.59,393.79 215.86,396.88 217.19,399.84 218.56,402.71 219.98,405.52 221.41,408.28 222.85,411.04 224.28,413.81 225.69,416.61 227.06,419.48 228.39,422.45 229.66,425.53 230.85,428.76 231.96,432.15 232.96,435.74 233.84,439.56 234.59,443.62 235.20,447.96 235.66,452.60 235.94,457.57 236.04,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='234.13,457.16 234.73,458.21 235.33,459.57 235.96,462.89 236.11,462.89 236.62,459.55 237.17,458.16 237.73,457.10 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='319.20,356.44 319.10,361.76 318.82,366.72 318.37,371.36 317.76,375.70 317.00,379.77 316.12,383.58 315.12,387.17 314.02,390.57 312.82,393.79 311.56,396.88 310.23,399.84 308.85,402.71 307.44,405.52 306.01,408.28 304.57,411.04 303.14,413.81 301.73,416.61 300.35,419.48 299.02,422.45 297.76,425.53 296.56,428.76 295.46,432.15 294.46,435.74 293.58,439.56 292.82,443.62 292.21,447.96 291.76,452.60 291.48,457.57 291.38,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='289.69,457.10 290.24,458.16 290.79,459.55 291.31,462.89 291.45,462.89 292.09,459.57 292.69,458.21 293.29,457.16 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='318.91,356.44 319.01,361.76 319.29,366.72 319.74,371.36 320.35,375.70 321.10,379.77 321.99,383.58 322.99,387.17 324.09,390.57 325.28,393.79 326.55,396.88 327.88,399.84 329.26,402.71 330.67,405.52 332.10,408.28 333.54,411.04 334.97,413.81 336.38,416.61 337.76,419.48 339.08,422.45 340.35,425.53 341.54,428.76 342.65,432.15 343.65,435.74 344.53,439.56 345.29,443.62 345.90,447.96 346.35,452.60 346.63,457.57 346.73,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='344.82,457.16 345.42,458.21 346.02,459.57 346.66,462.89 346.80,462.89 347.31,459.55 347.87,458.16 348.42,457.10 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='429.89,356.44 429.79,361.76 429.51,366.72 429.06,371.36 428.45,375.70 427.70,379.77 426.81,383.58 425.81,387.17 424.71,390.57 423.52,393.79 422.25,396.88 420.92,399.84 419.54,402.71 418.13,405.52 416.70,408.28 415.26,411.04 413.83,413.81 412.42,416.61 411.04,419.48 409.72,422.45 408.45,425.53 407.26,428.76 406.15,432.15 405.15,435.74 404.27,439.56 403.51,443.62 402.90,447.96 402.45,452.60 402.17,457.57 402.07,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='400.38,457.10 400.93,458.16 401.49,459.55 402.00,462.89 402.14,462.89 402.78,459.57 403.38,458.21 403.98,457.16 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='429.60,356.44 429.70,361.76 429.98,366.72 430.43,371.36 431.04,375.70 431.80,379.77 432.68,383.58 433.68,387.17 434.78,390.57 435.98,393.79 437.24,396.88 438.57,399.84 439.95,402.71 441.36,405.52 442.79,408.28 444.23,411.04 445.66,413.81 447.07,416.61 448.45,419.48 449.78,422.45 451.04,425.53 452.24,428.76 453.34,432.15 454.34,435.74 455.22,439.56 455.98,443.62 456.59,447.96 457.04,452.60 457.32,457.57 457.42,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='455.51,457.16 456.11,458.21 456.71,459.57 457.35,462.89 457.49,462.89 458.01,459.55 458.56,458.16 459.11,457.10 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='540.58,356.44 540.49,361.76 540.20,366.72 539.75,371.36 539.14,375.70 538.39,379.77 537.50,383.58 536.50,387.17 535.40,390.57 534.21,393.79 532.94,396.88 531.61,399.84 530.24,402.71 528.82,405.52 527.39,408.28 525.95,411.04 524.52,413.81 523.11,416.61 521.74,419.48 520.41,422.45 519.14,425.53 517.95,428.76 516.84,432.15 515.84,435.74 514.96,439.56 514.21,443.62 513.60,447.96 513.14,452.60 512.86,457.57 512.76,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='511.07,457.10 511.63,458.16 512.18,459.55 512.69,462.89 512.84,462.89 513.47,459.57 514.07,458.21 514.67,457.16 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='540.29,356.44 540.39,361.76 540.67,366.72 541.12,371.36 541.73,375.70 542.49,379.77 543.37,383.58 544.37,387.17 545.48,390.57 546.67,393.79 547.93,396.88 549.26,399.84 550.64,402.71 552.05,405.52 553.48,408.28 554.92,411.04 556.35,413.81 557.76,416.61 559.14,419.48 560.47,422.45 561.74,425.53 562.93,428.76 564.03,432.15 565.03,435.74 565.91,439.56 566.67,443.62 567.28,447.96 567.73,452.60 568.01,457.57 568.11,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='566.21,457.16 566.80,458.21 567.40,459.57 568.04,462.89 568.18,462.89 568.70,459.55 569.25,458.16 569.81,457.10 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<circle cx='374.40' cy='87.09' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='263.71' cy='216.23' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='485.09' cy='216.23' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='208.36' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='319.05' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='429.75' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='540.44' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='180.69' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='236.04' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='291.38' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='346.73' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='402.07' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='457.42' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='512.76' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='568.11' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='374.40' y='91.22' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='263.71' y='220.42' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='485.09' y='220.36' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='208.36' y='349.50' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='319.05' y='349.44' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='429.75' y='349.50' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='540.44' y='349.50' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='180.69' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='236.04' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='291.38' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +<text x='346.73' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>11</text> +<text x='402.07' y='478.70' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>12</text> +<text x='457.42' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>13</text> +<text x='512.76' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>14</text> +<text x='568.11' y='478.58' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>15</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/edge-style-elbow.svg b/tests/testthat/_snaps/plot/edge-style-elbow.svg new file mode 100644 index 00000000000..60b23b847db --- /dev/null +++ b/tests/testthat/_snaps/plot/edge-style-elbow.svg @@ -0,0 +1,89 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<polyline points='374.82,98.31 374.82,151.46 263.71,151.46 263.71,204.61 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='261.91,198.85 262.48,199.90 263.06,201.28 263.64,204.61 263.78,204.61 264.36,201.28 264.93,199.90 265.51,198.85 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='373.98,98.31 373.98,151.46 485.09,151.46 485.09,204.61 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='483.29,198.85 483.87,199.90 484.44,201.28 485.02,204.61 485.16,204.61 485.74,201.28 486.32,199.90 486.89,198.85 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='263.97,227.34 263.97,280.54 208.36,280.54 208.36,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='206.56,327.99 207.14,329.04 207.71,330.42 208.29,333.75 208.43,333.75 209.01,330.42 209.59,329.04 210.16,327.99 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='263.44,227.34 263.44,280.54 319.05,280.54 319.05,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='317.25,327.99 317.83,329.04 318.41,330.42 318.98,333.75 319.13,333.75 319.70,330.42 320.28,329.04 320.85,327.99 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='485.36,227.34 485.36,280.54 429.75,280.54 429.75,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='427.95,327.99 428.52,329.04 429.10,330.42 429.67,333.75 429.82,333.75 430.39,330.42 430.97,329.04 431.55,327.99 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='484.83,227.34 484.83,280.54 540.44,280.54 540.44,333.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='538.64,327.99 539.21,329.04 539.79,330.42 540.37,333.75 540.51,333.75 541.09,330.42 541.66,329.04 542.24,327.99 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='208.51,356.44 208.51,409.66 180.69,409.66 180.69,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='178.89,457.13 179.47,458.19 180.04,459.56 180.62,462.89 180.76,462.89 181.34,459.56 181.91,458.19 182.49,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='208.22,356.44 208.22,409.66 236.04,409.66 236.04,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='234.24,457.13 234.81,458.19 235.39,459.56 235.96,462.89 236.11,462.89 236.68,459.56 237.26,458.19 237.84,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='319.20,356.44 319.20,409.66 291.38,409.66 291.38,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='289.58,457.13 290.16,458.19 290.73,459.56 291.31,462.89 291.45,462.89 292.03,459.56 292.61,458.19 293.18,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='318.91,356.44 318.91,409.66 346.73,409.66 346.73,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='344.93,457.13 345.50,458.19 346.08,459.56 346.66,462.89 346.80,462.89 347.38,459.56 347.95,458.19 348.53,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='429.89,356.44 429.89,409.66 402.07,409.66 402.07,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='400.27,457.13 400.85,458.19 401.42,459.56 402.00,462.89 402.14,462.89 402.72,459.56 403.30,458.19 403.87,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='429.60,356.44 429.60,409.66 457.42,409.66 457.42,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='455.62,457.13 456.19,458.19 456.77,459.56 457.35,462.89 457.49,462.89 458.07,459.56 458.64,458.19 459.22,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='540.58,356.44 540.58,409.66 512.76,409.66 512.76,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='510.96,457.13 511.54,458.19 512.12,459.56 512.69,462.89 512.84,462.89 513.41,459.56 513.99,458.19 514.56,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='540.29,356.44 540.29,409.66 568.11,409.66 568.11,462.89 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='566.31,457.13 566.89,458.19 567.46,459.56 568.04,462.89 568.18,462.89 568.76,459.56 569.33,458.19 569.91,457.13 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<circle cx='374.40' cy='87.09' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='263.71' cy='216.23' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='485.09' cy='216.23' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='208.36' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='319.05' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='429.75' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='540.44' cy='345.37' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='180.69' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='236.04' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='291.38' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='346.73' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='402.07' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='457.42' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='512.76' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='568.11' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='374.40' y='91.22' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='263.71' y='220.42' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='485.09' y='220.36' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='208.36' y='349.50' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='319.05' y='349.44' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='429.75' y='349.50' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='540.44' y='349.50' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='180.69' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='236.04' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='291.38' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +<text x='346.73' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>11</text> +<text x='402.07' y='478.70' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>12</text> +<text x='457.42' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>13</text> +<text x='512.76' y='478.64' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>14</text> +<text x='568.11' y='478.58' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>15</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/edge-style-mixed.svg b/tests/testthat/_snaps/plot/edge-style-mixed.svg new file mode 100644 index 00000000000..aeaf32bb50b --- /dev/null +++ b/tests/testthat/_snaps/plot/edge-style-mixed.svg @@ -0,0 +1,47 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='231.08' y1='198.17' x2='517.03' y2='126.68' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='510.58,130.59 511.39,129.84 512.37,129.03 513.68,128.15 517.04,126.75 517.01,126.61 513.38,126.96 511.81,126.80 510.57,126.55 509.50,126.26 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='528.99,137.55 528.99,137.63 529.02,138.19 529.09,139.61 529.20,142.17 529.36,146.01 529.56,151.16 529.79,157.49 530.02,164.78 530.25,172.76 530.45,181.11 530.61,189.56 530.70,197.89 530.72,205.93 530.67,213.58 530.52,220.78 530.29,227.52 529.96,233.83 529.52,239.75 528.98,245.33 528.33,250.65 527.55,255.76 526.65,260.74 525.63,265.64 525.42,266.57 524.24,271.44 522.91,276.31 521.42,281.24 519.74,286.28 517.84,291.51 515.70,296.97 513.31,302.73 510.64,308.83 507.69,315.29 504.47,322.12 501.00,329.29 497.32,336.70 493.52,344.22 489.71,351.67 486.00,358.80 482.57,365.36 479.53,371.10 477.03,375.80 475.12,379.36 473.82,381.78 473.07,383.17 472.75,383.75 472.69,383.86 472.69,383.86 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='474.04,376.45 474.08,377.55 474.02,378.82 473.81,380.39 472.62,383.83 472.75,383.90 474.90,380.96 476.07,379.88 477.07,379.12 478.00,378.51 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='454.08,399.10 366.85,399.10 366.85,438.75 279.63,438.75 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='286.83,436.52 285.87,437.06 284.72,437.60 283.23,438.14 279.63,438.68 279.63,438.82 283.23,439.36 284.72,439.90 285.87,440.44 286.83,440.98 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='264.00,423.66 263.83,413.36 263.35,403.74 262.58,394.76 261.53,386.37 260.24,378.50 258.73,371.12 257.02,364.17 255.14,357.59 253.10,351.35 250.94,345.39 248.66,339.65 246.31,334.09 243.90,328.66 241.45,323.30 239.00,317.97 236.55,312.62 234.14,307.18 231.78,301.63 229.51,295.89 227.35,289.93 225.31,283.68 223.42,277.11 221.71,270.16 220.20,262.77 218.92,254.91 217.87,246.51 217.10,237.53 216.62,227.92 216.45,217.62 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='218.80,224.78 218.24,223.83 217.69,222.69 217.12,221.21 216.52,217.62 216.38,217.62 215.90,221.23 215.38,222.73 214.86,223.88 214.34,224.86 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<circle cx='216.45' cy='201.83' r='15.79' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='532.35' cy='122.85' r='15.79' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='469.17' cy='399.26' r='15.79' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='263.84' cy='438.75' r='15.79' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='216.45' y='205.95' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='532.35' y='127.04' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='469.17' y='403.39' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='263.84' y='442.88' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/label-dist-degree.svg b/tests/testthat/_snaps/plot/label-dist-degree.svg new file mode 100644 index 00000000000..1c33fe0c387 --- /dev/null +++ b/tests/testthat/_snaps/plot/label-dist-degree.svg @@ -0,0 +1,43 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='506.23' y1='273.42' x2='381.78' y2='148.97' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='367.02' y1='148.97' x2='242.57' y2='273.42' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='242.57' y1='288.18' x2='367.02' y2='412.63' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='506.23' y1='288.18' x2='381.78' y2='412.63' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='513.61' cy='280.80' r='10.44' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='141.59' r='10.44' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='235.19' cy='280.80' r='10.44' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='420.01' r='10.44' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='513.61' y='305.15' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='8.67px' lengthAdjust='spacingAndGlyphs'>N</text> +<text x='394.62' y='145.72' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='8.01px' lengthAdjust='spacingAndGlyphs'>E</text> +<text x='235.19' y='264.71' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='8.01px' lengthAdjust='spacingAndGlyphs'>S</text> +<text x='354.18' y='424.14' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='11.33px' lengthAdjust='spacingAndGlyphs'>W</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/label-repel.svg b/tests/testthat/_snaps/plot/label-repel.svg new file mode 100644 index 00000000000..65872883fde --- /dev/null +++ b/tests/testthat/_snaps/plot/label-repel.svg @@ -0,0 +1,47 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<circle cx='197.53' cy='456.90' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='214.38' cy='439.29' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='231.22' cy='474.51' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='180.69' cy='449.86' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='534.42' cy='104.70' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='551.27' cy='122.31' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='517.58' cy='87.09' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='568.11' cy='97.66' r='11.62' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='197.53' y='464.15' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='26.01px' lengthAdjust='spacingAndGlyphs'>Alice</text> +<text x='214.38' y='443.58' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='21.36px' lengthAdjust='spacingAndGlyphs'>Bob</text> +<text x='231.22' y='478.80' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='28.68px' lengthAdjust='spacingAndGlyphs'>Carol</text> +<text x='180.69' y='450.97' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='28.02px' lengthAdjust='spacingAndGlyphs'>Dave</text> +<text x='534.42' y='108.77' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='20.69px' lengthAdjust='spacingAndGlyphs'>Eve</text> +<text x='551.27' y='126.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='30.68px' lengthAdjust='spacingAndGlyphs'>Frank</text> +<text x='517.58' y='91.22' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='32.69px' lengthAdjust='spacingAndGlyphs'>Grace</text> +<text x='568.11' y='101.95' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='27.35px' lengthAdjust='spacingAndGlyphs'>Heidi</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/mark-groups-multi.svg b/tests/testthat/_snaps/plot/mark-groups-multi.svg new file mode 100644 index 00000000000..b5c25f1a881 --- /dev/null +++ b/tests/testthat/_snaps/plot/mark-groups-multi.svg @@ -0,0 +1,53 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<polygon points='255.86,428.64 250.32,427.78 245.67,426.79 241.92,425.67 239.03,424.48 236.89,423.24 235.31,421.97 234.00,420.70 232.75,419.41 231.56,418.07 230.51,416.67 229.66,415.20 229.04,413.67 228.71,412.12 228.65,411.17 228.77,409.82 229.11,408.47 229.65,407.15 230.38,405.87 231.27,404.64 232.26,403.46 233.33,402.32 234.43,401.22 235.66,400.05 237.41,398.63 240.04,396.77 243.83,394.35 248.94,391.31 255.48,387.58 263.48,383.18 269.79,379.78 274.86,377.08 280.28,374.22 286.03,371.21 292.09,368.06 298.44,364.79 305.03,361.40 311.84,357.92 318.82,354.37 325.96,350.75 333.20,347.09 340.53,343.40 347.90,339.70 355.30,335.99 362.71,332.28 370.11,328.57 377.52,324.87 384.92,321.17 392.33,317.48 399.73,313.81 407.11,310.17 414.44,306.58 421.70,303.06 428.86,299.62 435.87,296.30 442.71,293.10 449.34,290.04 455.72,287.16 461.83,284.45 467.63,281.94 473.11,279.65 478.24,277.56 479.01,277.26 486.50,274.40 493.01,272.15 498.53,270.49 503.08,269.40 506.71,268.84 509.48,268.76 511.52,269.07 512.97,269.70 514.03,270.52 514.95,271.42 515.84,272.34 516.71,273.28 517.52,274.25 518.25,275.25 518.89,276.29 519.41,277.36 519.80,278.45 520.05,279.56 520.15,280.67 520.15,280.80 519.97,282.47 519.46,284.13 518.65,285.73 517.59,287.27 516.35,288.72 515.01,290.11 513.63,291.50 512.00,293.12 509.84,295.29 506.95,298.18 503.23,301.90 498.64,306.49 495.02,310.10 491.85,313.28 488.44,316.69 484.81,320.31 481.01,324.12 477.05,328.08 472.97,332.15 468.81,336.31 464.60,340.52 460.37,344.76 456.13,349.00 451.89,353.23 447.67,357.46 443.44,361.69 439.20,365.93 434.96,370.17 430.74,374.38 426.57,378.56 422.47,382.65 418.49,386.64 414.65,390.48 410.99,394.14 407.54,397.59 404.31,400.82 403.70,401.42 399.09,406.04 395.14,409.99 391.87,413.26 389.25,415.87 387.22,417.90 385.66,419.47 384.38,420.75 383.14,421.95 381.66,423.16 379.68,424.34 377.05,425.48 373.65,426.55 369.45,427.53 364.43,428.38 362.57,428.64 358.52,429.11 354.13,429.52 349.44,429.86 344.49,430.14 339.33,430.35 334.02,430.51 328.61,430.62 323.14,430.69 317.66,430.72 312.18,430.73 306.71,430.73 301.24,430.72 295.75,430.69 290.29,430.63 284.87,430.52 279.54,430.37 274.37,430.16 269.40,429.88 264.68,429.55 260.27,429.15 256.20,428.68 ' style='stroke-width: 0.75; stroke: #FF0000; fill: #FFCCCC;' /> +<polygon points='479.01,284.34 473.94,282.30 468.52,280.03 462.77,277.56 456.71,274.88 450.36,272.02 443.77,269.00 436.96,265.82 429.98,262.51 422.84,259.09 415.60,255.58 408.27,252.00 400.90,248.37 393.50,244.70 386.09,241.01 378.69,237.31 371.28,233.61 363.88,229.91 356.47,226.20 349.07,222.49 341.69,218.78 334.36,215.09 327.10,211.42 319.94,207.80 312.93,204.23 306.09,200.74 299.46,197.34 293.08,194.05 286.97,190.88 281.17,187.84 275.69,184.96 270.56,182.23 269.79,181.82 260.79,176.96 253.26,172.77 247.17,169.26 242.49,166.42 239.10,164.19 236.77,162.48 235.23,161.16 234.08,160.03 232.98,158.92 231.94,157.77 230.97,156.58 230.13,155.33 229.46,154.03 228.97,152.70 228.71,151.36 228.65,150.43 228.80,148.87 229.25,147.33 229.97,145.82 230.91,144.37 232.02,142.99 233.23,141.68 234.50,140.40 235.88,139.13 237.65,137.87 240.07,136.64 243.28,135.48 247.38,134.41 252.39,133.47 255.86,132.96 259.91,132.49 264.29,132.08 268.99,131.74 273.94,131.46 279.09,131.25 284.41,131.09 289.82,130.98 295.29,130.91 300.77,130.88 306.25,130.87 311.72,130.87 317.19,130.88 322.68,130.91 328.14,130.97 333.56,131.08 338.89,131.23 344.06,131.44 349.03,131.72 353.75,132.05 358.16,132.45 362.23,132.92 362.57,132.96 367.85,133.77 372.33,134.71 376.00,135.75 378.88,136.87 381.06,138.04 382.68,139.24 383.97,140.45 385.21,141.68 386.65,143.13 388.51,144.98 390.92,147.39 393.97,150.44 397.69,154.16 402.08,158.55 403.70,160.18 406.88,163.35 410.29,166.76 413.91,170.39 417.72,174.19 421.68,178.15 425.75,182.23 429.91,186.39 434.12,190.60 438.36,194.83 442.60,199.07 446.83,203.31 451.06,207.53 455.29,211.76 459.53,216.00 463.77,220.24 467.98,224.46 472.16,228.63 476.25,232.73 480.24,236.71 484.08,240.55 487.74,244.21 491.19,247.66 494.42,250.89 495.02,251.50 500.19,256.67 504.51,260.98 507.96,264.43 510.60,267.07 512.57,269.04 514.08,270.55 515.44,271.92 516.76,273.33 517.95,274.81 518.93,276.37 519.66,278.00 520.07,279.66 520.15,280.80 520.07,281.91 519.84,283.02 519.47,284.11 518.96,285.18 518.33,286.23 517.61,287.24 516.80,288.21 515.95,289.16 515.05,290.07 514.14,290.98 513.11,291.82 511.71,292.47 509.75,292.83 507.07,292.79 503.55,292.29 499.11,291.26 493.70,289.67 487.31,287.49 479.93,284.71 ' style='stroke-width: 0.75; stroke: #0000FF; fill: #CCCCFF;' /> +<line x1='253.81' y1='411.17' x2='364.62' y2='411.17' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='252.78' y1='406.80' x2='496.02' y2='285.17' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='248.40' y1='402.42' x2='370.03' y2='159.18' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='244.03' y1='401.39' x2='244.03' y2='160.21' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='381.31' y1='404.26' x2='497.86' y2='287.71' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='374.40' y1='401.39' x2='374.40' y2='160.21' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='370.03' y1='402.42' x2='248.40' y2='159.18' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='497.86' y1='273.89' x2='381.31' y2='157.34' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='496.02' y1='276.43' x2='252.78' y2='154.80' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='364.62' y1='150.43' x2='253.81' y2='150.43' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='244.03' cy='411.17' r='9.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='411.17' r='9.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='504.77' cy='280.80' r='9.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='150.43' r='9.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='244.03' cy='150.43' r='9.78' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='244.03' y='415.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='415.36' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='504.77' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='374.40' y='154.56' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='244.03' y='154.50' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/mixed-modes-curved.svg b/tests/testthat/_snaps/plot/mixed-modes-curved.svg new file mode 100644 index 00000000000..59d8a0a2282 --- /dev/null +++ b/tests/testthat/_snaps/plot/mixed-modes-curved.svg @@ -0,0 +1,50 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<polygon points='215.54,436.02 213.40,439.37 211.27,442.45 209.15,445.28 207.07,447.87 205.01,450.21 202.98,452.33 200.99,454.22 199.05,455.90 197.15,457.38 195.31,458.65 193.52,459.73 191.79,460.63 190.14,461.36 188.55,461.91 187.04,462.31 185.61,462.55 184.27,462.65 183.02,462.62 181.86,462.45 180.81,462.16 179.86,461.76 179.02,461.25 178.29,460.65 177.69,459.95 177.21,459.18 176.85,458.32 176.64,457.40 176.56,456.43 176.63,455.40 176.84,454.33 177.21,453.22 177.73,452.08 178.42,450.92 179.28,449.76 180.31,448.58 181.52,447.41 182.91,446.25 184.48,445.11 186.25,444.00 188.22,442.92 190.38,441.88 192.75,440.89 195.34,439.96 198.14,439.10 201.16,438.30 204.40,437.59 207.88,436.97 211.59,436.44 215.54,436.02 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: none;' /> +<line x1='212.62' y1='440.58' x2='215.54' y2='436.02' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='211.47,450.51 211.52,449.28 211.63,447.97 211.80,446.56 212.07,445.00 212.49,443.21 213.18,440.98 215.60,436.06 215.48,435.98 212.02,440.23 210.29,441.80 208.83,442.92 207.53,443.81 206.31,444.56 205.17,445.21 204.07,445.77 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polygon points='533.26,436.02 537.21,436.44 540.92,436.97 544.40,437.59 547.64,438.30 550.66,439.10 553.46,439.96 556.05,440.89 558.42,441.88 560.58,442.92 562.55,444.00 564.32,445.11 565.89,446.25 567.28,447.41 568.49,448.58 569.52,449.76 570.38,450.92 571.07,452.08 571.59,453.22 571.96,454.33 572.17,455.40 572.24,456.43 572.16,457.40 571.95,458.32 571.59,459.18 571.11,459.95 570.51,460.65 569.78,461.25 568.94,461.76 567.99,462.16 566.94,462.45 565.78,462.62 564.53,462.65 563.19,462.55 561.76,462.31 560.25,461.91 558.66,461.36 557.01,460.63 555.28,459.73 553.49,458.65 551.65,457.38 549.75,455.90 547.81,454.22 545.82,452.33 543.79,450.21 541.73,447.87 539.65,445.28 537.53,442.45 535.40,439.37 533.26,436.02 ' style='stroke-width: 1.50; stroke: #A9A9A9; fill: none;' /> +<line x1='536.18' y1='440.58' x2='533.26' y2='436.02' style='stroke-width: 1.50; stroke: #A9A9A9;' /> +<polygon points='544.75,445.76 543.66,445.19 542.51,444.55 541.30,443.80 539.99,442.90 538.54,441.78 536.80,440.22 533.35,435.97 533.17,436.08 535.59,440.99 536.29,443.22 536.70,445.01 536.97,446.57 537.14,447.99 537.25,449.30 537.31,450.53 ' style='stroke-width: 1.50; stroke: #A9A9A9; fill: #A9A9A9;' /> +<line x1='538.64' y1='436.60' x2='533.26' y2='436.02' style='stroke-width: 1.50; stroke: #A9A9A9;' /> +<polygon points='548.05,433.15 546.92,433.65 545.70,434.14 544.36,434.62 542.84,435.08 541.06,435.51 538.75,435.88 533.27,435.92 533.25,436.12 538.60,437.31 540.77,438.17 542.43,438.96 543.81,439.73 545.02,440.48 546.11,441.22 547.11,441.95 ' style='stroke-width: 1.50; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='236.27,430.13 236.35,430.11 236.86,429.97 238.19,429.60 240.58,428.94 244.19,427.95 249.08,426.62 255.16,424.99 262.24,423.11 270.08,421.06 278.42,418.92 286.97,416.78 295.51,414.68 303.85,412.71 311.88,410.88 319.52,409.22 326.73,407.76 333.52,406.48 339.93,405.40 345.99,404.50 351.76,403.78 357.31,403.23 362.69,402.84 367.98,402.61 373.21,402.51 374.40,402.51 379.63,402.57 384.90,402.78 390.26,403.13 395.76,403.64 401.48,404.32 407.47,405.18 413.79,406.22 420.49,407.45 427.61,408.87 435.15,410.49 443.09,412.28 451.38,414.22 459.89,416.29 468.45,418.43 476.86,420.58 484.84,422.66 492.12,424.58 498.44,426.28 503.61,427.68 507.51,428.74 510.17,429.48 511.72,429.90 512.39,430.09 512.53,430.13 512.53,430.13 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polyline points='518.72,420.12 518.71,420.04 518.61,419.54 518.37,418.26 517.91,415.93 517.21,412.39 516.25,407.56 515.02,401.48 513.55,394.31 511.88,386.24 510.03,377.56 508.08,368.51 506.04,359.35 503.97,350.29 501.90,341.48 499.83,333.04 497.78,325.04 495.75,317.50 493.73,310.42 491.72,303.77 489.70,297.52 487.65,291.61 485.55,286.00 483.39,280.62 481.13,275.41 478.78,270.33 476.95,266.59 474.41,261.65 471.72,256.73 468.87,251.81 465.81,246.83 462.52,241.75 458.96,236.52 455.10,231.09 450.90,225.42 446.36,219.47 441.46,213.23 436.20,206.70 430.61,199.89 424.75,192.87 418.69,185.72 412.57,178.57 406.52,171.58 400.73,164.94 395.38,158.85 390.65,153.49 386.67,149.01 383.54,145.51 381.28,142.99 379.83,141.38 379.07,140.53 378.79,140.23 378.76,140.20 ' style='stroke-width: 1.50; stroke: #A9A9A9;' /> +<polygon points='511.48,406.92 512.31,407.84 513.15,408.85 514.01,409.98 514.92,411.28 515.87,412.85 516.94,414.93 518.65,420.13 518.80,420.10 518.29,414.65 518.43,412.31 518.68,410.49 518.99,408.94 519.33,407.56 519.70,406.29 520.08,405.12 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='370.36,139.55 370.36,139.63 370.34,140.15 370.30,141.46 370.22,143.86 370.08,147.53 369.88,152.53 369.61,158.85 369.26,166.34 368.82,174.79 368.31,183.93 367.71,193.50 367.02,203.23 366.27,212.89 365.43,222.31 364.52,231.35 363.53,239.94 362.45,248.02 361.29,255.61 360.02,262.70 358.65,269.33 357.15,275.55 355.52,281.42 353.74,286.98 351.80,292.29 349.68,297.41 347.38,302.39 346.65,303.86 344.10,308.72 341.34,313.52 338.33,318.27 335.03,323.02 331.42,327.81 327.44,332.69 323.08,337.71 318.28,342.89 313.05,348.28 307.35,353.89 301.20,359.73 294.62,365.79 287.68,372.01 280.47,378.34 273.12,384.67 265.80,390.88 258.73,396.80 252.11,402.28 246.17,407.16 241.08,411.29 236.99,414.60 233.92,417.06 231.87,418.70 230.68,419.64 230.18,420.04 230.08,420.12 230.08,420.12 ' style='stroke-width: 2.25; stroke: #A9A9A9;' /> +<polygon points='242.46,401.24 242.06,402.34 241.63,403.47 241.15,404.64 240.61,405.85 240.01,407.12 239.32,408.47 238.52,409.90 237.55,411.47 236.34,413.25 234.65,415.43 230.03,420.06 230.12,420.17 235.50,416.45 237.95,415.17 239.91,414.30 241.63,413.63 243.19,413.10 244.63,412.66 245.99,412.29 247.28,411.98 248.52,411.72 249.71,411.49 250.85,411.30 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<circle cx='225.07' cy='430.13' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='523.73' cy='430.13' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='131.47' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='225.07' y='434.26' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='523.73' y='434.32' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='374.40' y='135.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/multi-edge-curve.svg b/tests/testthat/_snaps/plot/multi-edge-curve.svg new file mode 100644 index 00000000000..1e66f8f19ba --- /dev/null +++ b/tests/testthat/_snaps/plot/multi-edge-curve.svg @@ -0,0 +1,45 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<polyline points='234.83,280.80 234.93,280.75 235.63,280.43 237.32,279.66 240.22,278.36 244.29,276.55 249.30,274.38 254.87,272.01 260.65,269.63 266.32,267.39 271.68,265.38 276.66,263.67 281.22,262.26 285.41,261.15 289.30,260.33 292.98,259.78 296.53,259.48 299.49,259.40 303.01,259.51 306.59,259.85 310.30,260.45 314.22,261.33 318.45,262.50 323.06,263.98 328.07,265.76 333.45,267.84 339.10,270.13 344.79,272.54 350.20,274.90 354.95,277.02 358.71,278.72 361.28,279.90 362.67,280.55 363.16,280.78 363.20,280.80 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='348.24,279.07 349.47,278.93 350.78,278.83 352.21,278.78 353.79,278.80 355.62,278.92 357.94,279.26 363.17,280.87 363.23,280.73 358.49,277.99 356.67,276.53 355.33,275.27 354.24,274.12 353.30,273.04 352.49,272.01 351.75,271.02 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='234.83,280.80 234.93,280.75 235.63,280.43 237.32,279.66 240.22,278.36 244.29,276.55 249.30,274.38 254.87,272.01 260.65,269.63 266.32,267.39 271.68,265.38 276.66,263.67 281.22,262.26 285.41,261.15 289.30,260.33 292.98,259.78 296.53,259.48 299.49,259.40 303.01,259.51 306.59,259.85 310.30,260.45 314.22,261.33 318.45,262.50 323.06,263.98 328.07,265.76 333.45,267.84 339.10,270.13 344.79,272.54 350.20,274.90 354.95,277.02 358.71,278.72 361.28,279.90 362.67,280.55 363.16,280.78 363.20,280.80 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='348.24,279.07 349.47,278.93 350.78,278.83 352.21,278.78 353.79,278.80 355.62,278.92 357.94,279.26 363.17,280.87 363.23,280.73 358.49,277.99 356.67,276.53 355.33,275.27 354.24,274.12 353.30,273.04 352.49,272.01 351.75,271.02 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='234.83,280.80 234.93,280.75 235.63,280.43 237.32,279.66 240.22,278.36 244.29,276.55 249.30,274.38 254.87,272.01 260.65,269.63 266.32,267.39 271.68,265.38 276.66,263.67 281.22,262.26 285.41,261.15 289.30,260.33 292.98,259.78 296.53,259.48 299.49,259.40 303.01,259.51 306.59,259.85 310.30,260.45 314.22,261.33 318.45,262.50 323.06,263.98 328.07,265.76 333.45,267.84 339.10,270.13 344.79,272.54 350.20,274.90 354.95,277.02 358.71,278.72 361.28,279.90 362.67,280.55 363.16,280.78 363.20,280.80 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='348.24,279.07 349.47,278.93 350.78,278.83 352.21,278.78 353.79,278.80 355.62,278.92 357.94,279.26 363.17,280.87 363.23,280.73 358.49,277.99 356.67,276.53 355.33,275.27 354.24,274.12 353.30,273.04 352.49,272.01 351.75,271.02 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<polyline points='384.16,280.80 384.27,280.75 384.96,280.43 386.65,279.66 389.55,278.36 393.63,276.55 398.63,274.38 404.21,272.01 409.98,269.63 415.65,267.39 421.02,265.38 425.99,263.67 430.55,262.26 434.74,261.15 438.64,260.33 442.31,259.78 445.86,259.48 448.83,259.40 452.35,259.51 455.92,259.85 459.63,260.45 463.56,261.33 467.79,262.50 472.39,263.98 477.40,265.76 482.78,267.84 488.43,270.13 494.12,272.54 499.53,274.90 504.29,277.02 508.05,278.72 510.61,279.90 512.00,280.55 512.49,280.78 512.53,280.80 ' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<polygon points='497.58,279.07 498.80,278.93 500.11,278.83 501.54,278.78 503.12,278.80 504.96,278.92 507.27,279.26 512.50,280.87 512.56,280.73 507.82,277.99 506.00,276.53 504.66,275.27 503.57,274.12 502.64,273.04 501.82,272.01 501.09,271.02 ' style='stroke-width: 0.75; stroke: #A9A9A9; fill: #A9A9A9;' /> +<circle cx='225.07' cy='280.80' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='280.80' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='523.73' cy='280.80' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='225.07' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='284.99' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='523.73' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/palette-index.svg b/tests/testthat/_snaps/plot/palette-index.svg new file mode 100644 index 00000000000..5cbdd07ea80 --- /dev/null +++ b/tests/testthat/_snaps/plot/palette-index.svg @@ -0,0 +1,43 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='534.01' y1='261.86' x2='393.34' y2='121.19' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='355.46' y1='121.19' x2='214.79' y2='261.86' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='214.79' y1='299.74' x2='355.46' y2='440.41' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='534.01' y1='299.74' x2='393.34' y2='440.41' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='552.95' cy='280.80' r='26.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='102.25' r='26.78' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='195.85' cy='280.80' r='26.78' style='stroke-width: 0.75; fill: #009E73;' /> +<circle cx='374.40' cy='459.35' r='26.78' style='stroke-width: 0.75; fill: #F0E442;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='552.95' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='106.44' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='195.85' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='374.40' y='463.48' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-color-and-size.svg b/tests/testthat/_snaps/plot/scale-color-and-size.svg new file mode 100644 index 00000000000..1152c8404dd --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-color-and-size.svg @@ -0,0 +1,80 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='561.60' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='472.32' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng==)'> +<line x1='476.37' y1='276.44' x2='444.81' y2='174.29' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='437.62' y1='164.00' x2='358.45' y2='103.51' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='342.98' y1='98.28' x2='249.45' y2='98.28' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='230.34' y1='104.74' x2='157.61' y2='160.30' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='143.80' y1='180.11' x2='117.02' y2='266.75' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='117.02' y1='294.85' x2='142.60' y2='377.62' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='160.83' y1='403.76' x2='223.89' y2='451.93' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='257.56' y1='463.32' x2='330.81' y2='463.32' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='368.12' y1='450.70' x2='424.73' y2='407.45' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='476.37' y1='285.16' x2='449.60' y2='371.80' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='477.72' cy='280.80' r='4.56' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='442.86' cy='168.00' r='6.59' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='351.60' cy='98.28' r='8.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='238.80' cy='98.28' r='10.65' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='147.54' cy='168.00' r='12.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='112.68' cy='280.80' r='14.70' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='147.54' cy='393.60' r='16.73' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='238.80' cy='463.32' r='18.76' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='351.60' cy='463.32' r='20.79' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='442.86' cy='393.60' r='22.81' style='stroke-width: 0.75; fill: #56B4E9;' /> +</g> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +<text x='477.72' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='442.86' y='172.19' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='351.60' y='102.41' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='238.80' y='102.41' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='147.54' y='172.07' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='112.68' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='147.54' y='397.73' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='238.80' y='467.45' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='351.60' y='467.45' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='442.86' y='397.74' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<circle cx='623.42' cy='248.75' r='4.86' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='623.42' cy='263.15' r='4.86' style='stroke-width: 0.75; fill: #56B4E9;' /> +<text x='640.80' y='234.35' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='62.04px' lengthAdjust='spacingAndGlyphs'>vertex.color</text> +<text x='634.22' y='252.88' style='font-size: 12.00px; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='634.22' y='267.28' style='font-size: 12.00px; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<circle cx='631.43' cy='327.25' r='5.46' style='stroke-width: 0.75; fill: #B3B3B3;' /> +<circle cx='631.43' cy='341.65' r='8.10' style='stroke-width: 0.75; fill: #B3B3B3;' /> +<text x='640.80' y='312.85' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='57.37px' lengthAdjust='spacingAndGlyphs'>vertex.size</text> +<text x='642.23' y='331.38' style='font-size: 12.00px; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='642.23' y='345.78' style='font-size: 12.00px; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-continuous-colorbar-top.svg b/tests/testthat/_snaps/plot/scale-continuous-colorbar-top.svg new file mode 100644 index 00000000000..b0be9dc8a1e --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-continuous-colorbar-top.svg @@ -0,0 +1,124 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MTAzLjY4fDU3Ni4wMA=='> + <rect x='0.00' y='103.68' width='720.00' height='472.32' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MTAzLjY4fDU3Ni4wMA==)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDE2Mi43Mnw1MDIuNTY='> + <rect x='59.04' y='162.72' width='630.72' height='339.84' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDE2Mi43Mnw1MDIuNTY=)'> +<line x1='513.21' y1='318.97' x2='494.34' y2='257.91' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='478.75' y1='235.56' x2='429.96' y2='198.29' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='404.30' y1='189.61' x2='344.50' y2='189.61' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='318.84' y1='198.29' x2='270.05' y2='235.56' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='254.46' y1='257.91' x2='235.59' y2='318.97' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='235.59' y1='346.31' x2='254.46' y2='407.37' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='270.05' y1='429.72' x2='318.84' y2='466.99' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='344.50' y1='475.67' x2='404.30' y2='475.67' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='429.96' y1='466.99' x2='478.75' y2='429.72' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='513.21' y1='346.31' x2='494.34' y2='407.37' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='517.43' cy='332.64' r='14.30' style='stroke-width: 0.75; fill: #FFF7EC;' /> +<circle cx='490.11' cy='244.24' r='14.30' style='stroke-width: 0.75; fill: #FEE9CC;' /> +<circle cx='418.60' cy='189.61' r='14.30' style='stroke-width: 0.75; fill: #FDD8A7;' /> +<circle cx='330.20' cy='189.61' r='14.30' style='stroke-width: 0.75; fill: #FDC38C;' /> +<circle cx='258.69' cy='244.24' r='14.30' style='stroke-width: 0.75; fill: #FCA16C;' /> +<circle cx='231.37' cy='332.64' r='14.30' style='stroke-width: 0.75; fill: #F67B51;' /> +<circle cx='258.69' cy='421.04' r='14.30' style='stroke-width: 0.75; fill: #E7533A;' /> +<circle cx='330.20' cy='475.67' r='14.30' style='stroke-width: 0.75; fill: #CF2518;' /> +<circle cx='418.60' cy='475.67' r='14.30' style='stroke-width: 0.75; fill: #AD0000;' /> +<circle cx='490.11' cy='421.04' r='14.30' style='stroke-width: 0.75; fill: #7F0000;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MTAzLjY4fDU3Ni4wMA==)'> +<text x='517.43' y='336.77' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='490.11' y='248.43' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='418.60' y='193.74' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='330.20' y='193.74' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='258.69' y='248.31' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='231.37' y='336.77' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='258.69' y='425.17' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='330.20' y='479.80' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='418.60' y='479.80' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='490.11' y='425.17' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<rect x='196.00' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FFF7EC;' /> +<rect x='202.56' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FEF4E6;' /> +<rect x='209.12' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FEF2E0;' /> +<rect x='215.68' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FEEFDA;' /> +<rect x='222.24' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FEEDD4;' /> +<rect x='228.80' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FEEACE;' /> +<rect x='235.36' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FEE8C8;' /> +<rect x='241.92' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDE5C2;' /> +<rect x='248.48' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDE1BB;' /> +<rect x='255.04' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDDEB4;' /> +<rect x='261.60' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDDBAD;' /> +<rect x='268.16' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDD8A6;' /> +<rect x='274.72' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDD49F;' /> +<rect x='281.28' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDD09A;' /> +<rect x='287.84' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDCC96;' /> +<rect x='294.40' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDC892;' /> +<rect x='300.96' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDC48E;' /> +<rect x='307.52' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDC089;' /> +<rect x='314.08' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FDBC85;' /> +<rect x='320.64' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FCB67F;' /> +<rect x='327.20' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FCAE78;' /> +<rect x='333.76' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FCA771;' /> +<rect x='340.32' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FC9F6A;' /> +<rect x='346.88' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FC9863;' /> +<rect x='353.44' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FC905C;' /> +<rect x='360.00' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #FA8957;' /> +<rect x='366.56' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #F88354;' /> +<rect x='373.12' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #F67C52;' /> +<rect x='379.68' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #F4764F;' /> +<rect x='386.24' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #F26F4C;' /> +<rect x='392.80' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #F06949;' /> +<rect x='399.36' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #ED6145;' /> +<rect x='405.92' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #E9593E;' /> +<rect x='412.48' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #E55038;' /> +<rect x='419.04' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #E14731;' /> +<rect x='425.60' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #DD3F2A;' /> +<rect x='432.16' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #D93624;' /> +<rect x='438.72' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #D52E1D;' /> +<rect x='445.28' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #CF2618;' /> +<rect x='451.84' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #C91E13;' /> +<rect x='458.40' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #C3160E;' /> +<rect x='464.96' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #BE0E09;' /> +<rect x='471.52' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #B80604;' /> +<rect x='478.08' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #B10000;' /> +<rect x='484.64' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #A90000;' /> +<rect x='491.20' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #A00000;' /> +<rect x='497.76' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #980000;' /> +<rect x='504.32' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #8F0000;' /> +<rect x='510.88' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #870000;' /> +<rect x='517.44' y='46.72' width='6.56' height='10.24' style='stroke-width: 0.75; stroke: none; fill: #7F0000;' /> +<rect x='196.00' y='46.72' width='328.00' height='10.24' style='stroke-width: 0.75; stroke: #666666;' /> +<text x='196.00' y='65.55' style='font-size: 9.60px; font-family: sans;' textLength='8.01px' lengthAdjust='spacingAndGlyphs'> 1</text> +<text x='524.00' y='65.55' text-anchor='end' style='font-size: 9.60px; font-family: sans;' textLength='10.68px' lengthAdjust='spacingAndGlyphs'>10</text> +<text x='360.00' y='45.07' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='62.04px' lengthAdjust='spacingAndGlyphs'>vertex.color</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-continuous-colorbar.svg b/tests/testthat/_snaps/plot/scale-continuous-colorbar.svg new file mode 100644 index 00000000000..314120473d0 --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-continuous-colorbar.svg @@ -0,0 +1,124 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='561.60' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='472.32' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng==)'> +<line x1='476.36' y1='262.97' x2='451.73' y2='183.27' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='431.38' y1='154.10' x2='367.72' y2='105.47' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='334.22' y1='94.13' x2='256.18' y2='94.13' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='222.68' y1='105.47' x2='159.02' y2='154.10' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='138.67' y1='183.27' x2='114.04' y2='262.97' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='114.04' y1='298.63' x2='138.67' y2='378.33' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='159.02' y1='407.50' x2='222.68' y2='456.13' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='256.18' y1='467.47' x2='334.22' y2='467.47' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='367.72' y1='456.13' x2='431.38' y2='407.50' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='476.36' y1='298.63' x2='451.73' y2='378.33' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='481.87' cy='280.80' r='18.67' style='stroke-width: 0.75; fill: #FFF7EC;' /> +<circle cx='446.22' cy='165.43' r='18.67' style='stroke-width: 0.75; fill: #FEE9CC;' /> +<circle cx='352.88' cy='94.13' r='18.67' style='stroke-width: 0.75; fill: #FDD8A7;' /> +<circle cx='237.52' cy='94.13' r='18.67' style='stroke-width: 0.75; fill: #FDC38C;' /> +<circle cx='144.18' cy='165.43' r='18.67' style='stroke-width: 0.75; fill: #FCA16C;' /> +<circle cx='108.53' cy='280.80' r='18.67' style='stroke-width: 0.75; fill: #F67B51;' /> +<circle cx='144.18' cy='396.17' r='18.67' style='stroke-width: 0.75; fill: #E7533A;' /> +<circle cx='237.52' cy='467.47' r='18.67' style='stroke-width: 0.75; fill: #CF2518;' /> +<circle cx='352.88' cy='467.47' r='18.67' style='stroke-width: 0.75; fill: #AD0000;' /> +<circle cx='446.22' cy='396.17' r='18.67' style='stroke-width: 0.75; fill: #7F0000;' /> +</g> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +<text x='481.87' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='446.22' y='169.62' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='352.88' y='98.27' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='237.52' y='98.26' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='144.18' y='169.51' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='108.53' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='144.18' y='400.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='237.52' y='471.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='352.88' y='471.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='446.22' y='400.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<rect x='625.94' y='418.40' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FFF7EC;' /> +<rect x='625.94' y='413.17' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FEF4E6;' /> +<rect x='625.94' y='407.94' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FEF2E0;' /> +<rect x='625.94' y='402.72' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FEEFDA;' /> +<rect x='625.94' y='397.49' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FEEDD4;' /> +<rect x='625.94' y='392.26' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FEEACE;' /> +<rect x='625.94' y='387.04' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FEE8C8;' /> +<rect x='625.94' y='381.81' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDE5C2;' /> +<rect x='625.94' y='376.58' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDE1BB;' /> +<rect x='625.94' y='371.36' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDDEB4;' /> +<rect x='625.94' y='366.13' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDDBAD;' /> +<rect x='625.94' y='360.90' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDD8A6;' /> +<rect x='625.94' y='355.68' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDD49F;' /> +<rect x='625.94' y='350.45' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDD09A;' /> +<rect x='625.94' y='345.22' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDCC96;' /> +<rect x='625.94' y='340.00' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDC892;' /> +<rect x='625.94' y='334.77' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDC48E;' /> +<rect x='625.94' y='329.54' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDC089;' /> +<rect x='625.94' y='324.32' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FDBC85;' /> +<rect x='625.94' y='319.09' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FCB67F;' /> +<rect x='625.94' y='313.86' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FCAE78;' /> +<rect x='625.94' y='308.64' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FCA771;' /> +<rect x='625.94' y='303.41' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FC9F6A;' /> +<rect x='625.94' y='298.18' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FC9863;' /> +<rect x='625.94' y='292.96' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FC905C;' /> +<rect x='625.94' y='287.73' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #FA8957;' /> +<rect x='625.94' y='282.50' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #F88354;' /> +<rect x='625.94' y='277.28' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #F67C52;' /> +<rect x='625.94' y='272.05' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #F4764F;' /> +<rect x='625.94' y='266.82' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #F26F4C;' /> +<rect x='625.94' y='261.60' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #F06949;' /> +<rect x='625.94' y='256.37' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #ED6145;' /> +<rect x='625.94' y='251.14' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #E9593E;' /> +<rect x='625.94' y='245.92' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #E55038;' /> +<rect x='625.94' y='240.69' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #E14731;' /> +<rect x='625.94' y='235.46' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #DD3F2A;' /> +<rect x='625.94' y='230.24' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #D93624;' /> +<rect x='625.94' y='225.01' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #D52E1D;' /> +<rect x='625.94' y='219.78' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #CF2618;' /> +<rect x='625.94' y='214.56' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #C91E13;' /> +<rect x='625.94' y='209.33' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #C3160E;' /> +<rect x='625.94' y='204.10' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #BE0E09;' /> +<rect x='625.94' y='198.88' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #B80604;' /> +<rect x='625.94' y='193.65' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #B10000;' /> +<rect x='625.94' y='188.42' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #A90000;' /> +<rect x='625.94' y='183.20' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #A00000;' /> +<rect x='625.94' y='177.97' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #980000;' /> +<rect x='625.94' y='172.74' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #8F0000;' /> +<rect x='625.94' y='167.52' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #870000;' /> +<rect x='625.94' y='162.29' width='16.32' height='5.23' style='stroke-width: 0.75; stroke: none; fill: #7F0000;' /> +<rect x='625.94' y='162.29' width='16.32' height='261.33' style='stroke-width: 0.75; stroke: #666666;' /> +<text x='644.98' y='426.93' style='font-size: 9.60px; font-family: sans;' textLength='8.01px' lengthAdjust='spacingAndGlyphs'> 1</text> +<text x='644.98' y='165.59' style='font-size: 9.60px; font-family: sans;' textLength='10.68px' lengthAdjust='spacingAndGlyphs'>10</text> +<text x='625.94' y='160.64' style='font-size: 12.00px; font-family: sans;' textLength='62.04px' lengthAdjust='spacingAndGlyphs'>vertex.color</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-discrete-color.svg b/tests/testthat/_snaps/plot/scale-discrete-color.svg new file mode 100644 index 00000000000..081510a9400 --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-discrete-color.svg @@ -0,0 +1,75 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='561.60' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='472.32' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng==)'> +<line x1='476.36' y1='262.97' x2='451.73' y2='183.27' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='431.38' y1='154.10' x2='367.72' y2='105.47' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='334.22' y1='94.13' x2='256.18' y2='94.13' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='222.68' y1='105.47' x2='159.02' y2='154.10' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='138.67' y1='183.27' x2='114.04' y2='262.97' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='114.04' y1='298.63' x2='138.67' y2='378.33' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='159.02' y1='407.50' x2='222.68' y2='456.13' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='256.18' y1='467.47' x2='334.22' y2='467.47' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='367.72' y1='456.13' x2='431.38' y2='407.50' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='476.36' y1='298.63' x2='451.73' y2='378.33' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='481.87' cy='280.80' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='446.22' cy='165.43' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='352.88' cy='94.13' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='237.52' cy='94.13' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='144.18' cy='165.43' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='108.53' cy='280.80' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='144.18' cy='396.17' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='237.52' cy='467.47' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='352.88' cy='467.47' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='446.22' cy='396.17' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +</g> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +<text x='481.87' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='446.22' y='169.62' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='352.88' y='98.27' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='237.52' y='98.26' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='144.18' y='169.51' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='108.53' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='144.18' y='400.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='237.52' y='471.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='352.88' y='471.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='446.22' y='400.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<circle cx='623.42' cy='288.00' r='4.86' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='623.42' cy='302.40' r='4.86' style='stroke-width: 0.75; fill: #56B4E9;' /> +<text x='640.80' y='273.60' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='62.04px' lengthAdjust='spacingAndGlyphs'>vertex.color</text> +<text x='634.22' y='292.13' style='font-size: 12.00px; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='634.22' y='306.53' style='font-size: 12.00px; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-edge-color.svg b/tests/testthat/_snaps/plot/scale-edge-color.svg new file mode 100644 index 00000000000..eade1e7b68b --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-edge-color.svg @@ -0,0 +1,75 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='561.60' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='472.32' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng==)'> +<line x1='481.98' y1='267.11' x2='453.96' y2='176.44' style='stroke-width: 1.50; stroke: #E69F00;' /> +<line x1='438.34' y1='154.05' x2='365.61' y2='98.49' style='stroke-width: 1.50; stroke: #56B4E9;' /> +<line x1='339.90' y1='89.79' x2='250.50' y2='89.79' style='stroke-width: 1.50; stroke: #E69F00;' /> +<line x1='224.79' y1='98.49' x2='152.06' y2='154.05' style='stroke-width: 1.50; stroke: #56B4E9;' /> +<line x1='136.44' y1='176.44' x2='108.42' y2='267.11' style='stroke-width: 1.50; stroke: #E69F00;' /> +<line x1='108.42' y1='294.49' x2='136.44' y2='385.16' style='stroke-width: 1.50; stroke: #56B4E9;' /> +<line x1='152.06' y1='407.55' x2='224.79' y2='463.11' style='stroke-width: 1.50; stroke: #E69F00;' /> +<line x1='250.50' y1='471.81' x2='339.90' y2='471.81' style='stroke-width: 1.50; stroke: #56B4E9;' /> +<line x1='365.61' y1='463.11' x2='438.34' y2='407.55' style='stroke-width: 1.50; stroke: #E69F00;' /> +<line x1='481.98' y1='294.49' x2='453.96' y2='385.16' style='stroke-width: 1.50; stroke: #56B4E9;' /> +<circle cx='486.21' cy='280.80' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='449.73' cy='162.75' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='354.22' cy='89.79' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='236.18' cy='89.79' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='140.67' cy='162.75' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='104.19' cy='280.80' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='140.67' cy='398.85' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='236.18' cy='471.81' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='354.22' cy='471.81' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='449.73' cy='398.85' r='14.33' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +<text x='486.21' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='449.73' y='166.94' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='354.22' y='93.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='236.18' y='93.92' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='140.67' y='166.82' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='104.19' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='140.67' y='402.98' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='236.18' y='475.94' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='354.22' y='475.94' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='449.73' y='402.98' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<circle cx='635.10' cy='288.00' r='4.86' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='635.10' cy='302.40' r='4.86' style='stroke-width: 0.75; fill: #56B4E9;' /> +<text x='640.80' y='273.60' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='56.04px' lengthAdjust='spacingAndGlyphs'>edge.color</text> +<text x='645.90' y='292.13' style='font-size: 12.00px; font-family: sans;' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>x</text> +<text x='645.90' y='306.53' style='font-size: 12.00px; font-family: sans;' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>y</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-legend-bottom-horizontal.svg b/tests/testthat/_snaps/plot/scale-legend-bottom-horizontal.svg new file mode 100644 index 00000000000..88849b98510 --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-legend-bottom-horizontal.svg @@ -0,0 +1,75 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw0NzIuMzI='> + <rect x='0.00' y='0.00' width='720.00' height='472.32' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw0NzIuMzI=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDM5OC44OA=='> + <rect x='59.04' y='59.04' width='630.72' height='339.84' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDM5OC44OA==)'> +<line x1='513.21' y1='215.29' x2='494.34' y2='154.23' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='478.75' y1='131.88' x2='429.96' y2='94.61' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='404.30' y1='85.93' x2='344.50' y2='85.93' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='318.84' y1='94.61' x2='270.05' y2='131.88' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='254.46' y1='154.23' x2='235.59' y2='215.29' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='235.59' y1='242.63' x2='254.46' y2='303.69' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='270.05' y1='326.04' x2='318.84' y2='363.31' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='344.50' y1='371.99' x2='404.30' y2='371.99' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='429.96' y1='363.31' x2='478.75' y2='326.04' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='513.21' y1='242.63' x2='494.34' y2='303.69' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='517.43' cy='228.96' r='14.30' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='490.11' cy='140.56' r='14.30' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='418.60' cy='85.93' r='14.30' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='330.20' cy='85.93' r='14.30' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='258.69' cy='140.56' r='14.30' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='231.37' cy='228.96' r='14.30' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='258.69' cy='317.36' r='14.30' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='330.20' cy='371.99' r='14.30' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='418.60' cy='371.99' r='14.30' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='490.11' cy='317.36' r='14.30' style='stroke-width: 0.75; fill: #56B4E9;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw0NzIuMzI=)'> +<text x='517.43' y='233.09' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='490.11' y='144.75' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='418.60' y='90.06' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='330.20' y='90.06' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='258.69' y='144.63' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='231.37' y='233.09' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='258.69' y='321.49' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='330.20' y='376.12' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='418.60' y='376.12' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='490.11' y='321.49' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<circle cx='317.14' cy='531.36' r='4.86' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='368.10' cy='531.36' r='4.86' style='stroke-width: 0.75; fill: #56B4E9;' /> +<text x='360.00' y='516.96' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='62.04px' lengthAdjust='spacingAndGlyphs'>vertex.color</text> +<text x='327.94' y='535.49' style='font-size: 12.00px; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='378.90' y='535.49' style='font-size: 12.00px; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-legend-false.svg b/tests/testthat/_snaps/plot/scale-legend-false.svg new file mode 100644 index 00000000000..28f4cba0f71 --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-legend-false.svg @@ -0,0 +1,61 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='555.56' y1='262.97' x2='530.93' y2='183.27' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='510.58' y1='154.10' x2='446.92' y2='105.47' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='413.42' y1='94.13' x2='335.38' y2='94.13' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='301.88' y1='105.47' x2='238.22' y2='154.10' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='217.87' y1='183.27' x2='193.24' y2='262.97' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='193.24' y1='298.63' x2='217.87' y2='378.33' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='238.22' y1='407.50' x2='301.88' y2='456.13' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='335.38' y1='467.47' x2='413.42' y2='467.47' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='446.92' y1='456.13' x2='510.58' y2='407.50' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='555.56' y1='298.63' x2='530.93' y2='378.33' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='561.07' cy='280.80' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='525.42' cy='165.43' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='432.08' cy='94.13' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='316.72' cy='94.13' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='223.38' cy='165.43' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='187.73' cy='280.80' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='223.38' cy='396.17' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='316.72' cy='467.47' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +<circle cx='432.08' cy='467.47' r='18.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='525.42' cy='396.17' r='18.67' style='stroke-width: 0.75; fill: #56B4E9;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='561.07' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='525.42' y='169.62' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='432.08' y='98.27' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='316.72' y='98.26' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='223.38' y='169.51' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='187.73' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='223.38' y='400.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='316.72' y='471.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='432.08' y='471.60' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='525.42' y='400.30' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/scale-size-legend.svg b/tests/testthat/_snaps/plot/scale-size-legend.svg new file mode 100644 index 00000000000..6d2ed01e347 --- /dev/null +++ b/tests/testthat/_snaps/plot/scale-size-legend.svg @@ -0,0 +1,75 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='561.60' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='472.32' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8NTMxLjM2fDU5LjA0fDUwMi41Ng==)'> +<line x1='476.37' y1='276.44' x2='444.81' y2='174.29' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='437.62' y1='164.00' x2='358.45' y2='103.51' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='342.98' y1='98.28' x2='249.45' y2='98.28' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='230.34' y1='104.74' x2='157.61' y2='160.30' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='143.80' y1='180.11' x2='117.02' y2='266.75' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='117.02' y1='294.85' x2='142.60' y2='377.62' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='160.83' y1='403.76' x2='223.89' y2='451.93' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='257.56' y1='463.32' x2='330.81' y2='463.32' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='368.12' y1='450.70' x2='424.73' y2='407.45' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='476.37' y1='285.16' x2='449.60' y2='371.80' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='477.72' cy='280.80' r='4.56' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='442.86' cy='168.00' r='6.59' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='351.60' cy='98.28' r='8.62' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='238.80' cy='98.28' r='10.65' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='147.54' cy='168.00' r='12.67' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='112.68' cy='280.80' r='14.70' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='147.54' cy='393.60' r='16.73' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='238.80' cy='463.32' r='18.76' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='351.60' cy='463.32' r='20.79' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='442.86' cy='393.60' r='22.81' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw1NjEuNjB8MC4wMHw1NzYuMDA=)'> +<text x='477.72' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='442.86' y='172.19' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='351.60' y='102.41' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='238.80' y='102.41' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='147.54' y='172.07' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='112.68' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +<text x='147.54' y='397.73' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>7</text> +<text x='238.80' y='467.45' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text> +<text x='351.60' y='467.45' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>9</text> +<text x='442.86' y='397.74' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<circle cx='631.43' cy='288.00' r='5.46' style='stroke-width: 0.75; fill: #B3B3B3;' /> +<circle cx='631.43' cy='302.40' r='8.10' style='stroke-width: 0.75; fill: #B3B3B3;' /> +<text x='640.80' y='273.60' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='57.37px' lengthAdjust='spacingAndGlyphs'>vertex.size</text> +<text x='642.23' y='292.13' style='font-size: 12.00px; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='642.23' y='306.53' style='font-size: 12.00px; font-family: sans;' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/vector-edge-params-loops.svg b/tests/testthat/_snaps/plot/vector-edge-params-loops.svg new file mode 100644 index 00000000000..0857150f5d5 --- /dev/null +++ b/tests/testthat/_snaps/plot/vector-edge-params-loops.svg @@ -0,0 +1,56 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<polygon points='220.06,440.15 219.99,444.12 219.80,447.87 219.49,451.38 219.08,454.68 218.56,457.76 217.95,460.63 217.25,463.28 216.48,465.73 215.64,467.98 214.74,470.04 213.79,471.90 212.80,473.57 211.77,475.06 210.71,476.37 209.64,477.50 208.55,478.46 207.46,479.25 206.37,479.87 205.30,480.34 204.25,480.65 203.24,480.81 202.25,480.82 201.32,480.68 200.44,480.41 199.62,480.00 198.88,479.46 198.21,478.79 197.63,478.00 197.14,477.09 196.76,476.07 196.49,474.93 196.34,473.69 196.32,472.34 196.43,470.90 196.69,469.36 197.10,467.73 197.68,466.01 198.42,464.21 199.34,462.33 200.44,460.38 201.74,458.36 203.24,456.27 204.94,454.12 206.87,451.91 209.02,449.65 211.41,447.34 214.04,444.99 216.92,442.59 220.06,440.15 ' style='stroke-width: 2.25; stroke: #0000FF; fill: none;' /> +<line x1='214.64' y1='444.35' x2='220.06' y2='440.15' style='stroke-width: 2.25; stroke: #0000FF;' /> +<polygon points='202.68,464.73 203.13,463.60 203.61,462.45 204.13,461.26 204.69,460.05 205.30,458.79 205.96,457.50 206.69,456.15 207.51,454.73 208.43,453.24 209.49,451.64 210.74,449.89 212.29,447.90 214.43,445.46 220.13,440.25 219.98,440.05 213.52,444.28 210.63,445.75 208.31,446.76 206.31,447.54 204.49,448.17 202.82,448.69 201.24,449.12 199.76,449.50 198.33,449.82 196.97,450.10 195.65,450.34 194.38,450.55 193.14,450.73 191.93,450.88 ' style='stroke-width: 2.25; stroke: #0000FF; fill: #0000FF;' /> +<line x1='219.95' y1='447.00' x2='220.06' y2='440.15' style='stroke-width: 2.25; stroke: #0000FF;' /> +<polygon points='228.35,469.09 227.75,468.03 227.15,466.93 226.55,465.79 225.95,464.59 225.36,463.33 224.76,462.00 224.17,460.59 223.57,459.07 222.98,457.41 222.40,455.59 221.81,453.51 221.24,451.06 220.67,447.86 220.18,440.15 219.93,440.15 219.19,447.83 218.52,451.01 217.86,453.45 217.21,455.50 216.56,457.31 215.92,458.94 215.28,460.44 214.64,461.84 214.00,463.15 213.36,464.38 212.73,465.56 212.09,466.69 211.45,467.77 210.82,468.80 ' style='stroke-width: 2.25; stroke: #0000FF; fill: #0000FF;' /> +<text x='200.44' y='483.58' text-anchor='middle' style='font-size: 12.00px; fill: #0000FF; font-family: sans;' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>c</text> +<polygon points='528.74,440.15 531.88,442.59 534.76,444.99 537.39,447.34 539.78,449.65 541.93,451.91 543.86,454.12 545.56,456.27 547.06,458.36 548.36,460.38 549.46,462.33 550.38,464.21 551.12,466.01 551.70,467.73 552.11,469.36 552.37,470.90 552.48,472.34 552.46,473.69 552.31,474.93 552.04,476.07 551.66,477.09 551.17,478.00 550.59,478.79 549.92,479.46 549.18,480.00 548.36,480.41 547.48,480.68 546.55,480.82 545.56,480.81 544.55,480.65 543.50,480.34 542.43,479.87 541.34,479.25 540.25,478.46 539.16,477.50 538.09,476.37 537.03,475.06 536.00,473.57 535.01,471.90 534.06,470.04 533.16,467.98 532.32,465.73 531.55,463.28 530.85,460.63 530.24,457.76 529.72,454.68 529.31,451.38 529.00,447.87 528.81,444.12 528.74,440.15 ' style='stroke-width: 3.00; stroke: #FFA500; stroke-dasharray: 16.00,16.00; fill: none;' /> +<line x1='533.59' y1='443.91' x2='528.74' y2='440.15' style='stroke-width: 3.00; stroke: #FFA500;' /> +<polygon points='549.87,448.16 548.71,448.01 547.52,447.83 546.27,447.61 544.97,447.34 543.60,447.03 542.14,446.64 540.57,446.16 538.83,445.56 536.83,444.75 534.34,443.56 528.83,440.04 528.65,440.26 533.44,444.72 535.21,446.84 536.48,448.58 537.50,450.11 538.35,451.52 539.09,452.84 539.74,454.09 540.32,455.28 540.84,456.43 541.31,457.54 541.75,458.63 ' style='stroke-width: 3.00; stroke: #FFA500; fill: #FFA500;' /> +<text x='549.18' y='484.29' text-anchor='middle' style='font-size: 12.00px; fill: #FFA500; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>d</text> +<line x1='230.08' y1='420.12' x2='370.04' y2='140.20' style='stroke-width: 0.75; stroke: #FF0000;' /> +<polygon points='232.59,405.27 232.66,406.50 232.69,407.82 232.67,409.24 232.57,410.82 232.35,412.65 231.89,414.94 230.01,420.08 230.14,420.15 233.13,415.56 234.69,413.82 236.01,412.54 237.22,411.52 238.34,410.64 239.41,409.88 240.44,409.20 ' style='stroke-width: 0.75; stroke: #FF0000; fill: #FF0000;' /> +<line x1='369.39' y1='141.48' x2='229.43' y2='421.40' style='stroke-width: 3.75; stroke: #A020F0;' /> +<polygon points='366.88,156.33 366.81,155.10 366.77,153.78 366.80,152.36 366.90,150.78 367.12,148.95 367.57,146.66 369.46,141.52 369.33,141.45 366.34,146.04 364.78,147.78 363.45,149.06 362.25,150.08 361.12,150.96 360.05,151.72 359.02,152.40 ' style='stroke-width: 0.75; stroke: #A020F0; fill: #A020F0;' /> +<line x1='378.44' y1='139.55' x2='518.72' y2='420.12' style='stroke-width: 1.50; stroke: #00FF00; stroke-dasharray: 8.00,8.00;' /> +<polygon points='503.20,403.73 504.18,404.36 505.18,405.04 506.21,405.78 507.27,406.58 508.36,407.46 509.51,408.44 510.73,409.55 512.03,410.85 513.49,412.44 515.22,414.59 518.66,420.15 518.79,420.08 516.40,414.00 515.72,411.32 515.32,409.20 515.07,407.38 514.91,405.74 514.82,404.23 514.77,402.83 514.77,401.50 514.79,400.24 514.85,399.03 514.93,397.87 ' style='stroke-width: 0.75; stroke: #00FF00; fill: #00FF00;' /> +<text x='276.51' y='330.41' text-anchor='middle' style='font-size: 12.00px; fill: #FF0000; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>a</text> +<text x='425.85' y='238.65' text-anchor='middle' style='font-size: 12.00px; fill: #00FF00; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>b</text> +<text x='322.95' y='237.53' text-anchor='middle' style='font-size: 12.00px; fill: #A020F0; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>e</text> +<circle cx='225.07' cy='430.13' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='374.40' cy='131.47' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='523.73' cy='430.13' r='11.20' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='225.07' y='434.26' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='374.40' y='135.66' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='523.73' y='434.27' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/vertex-edge-alpha.svg b/tests/testthat/_snaps/plot/vertex-edge-alpha.svg new file mode 100644 index 00000000000..5773907b7fe --- /dev/null +++ b/tests/testthat/_snaps/plot/vertex-edge-alpha.svg @@ -0,0 +1,58 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='540.97' y1='256.84' x2='475.65' y2='126.20' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='530.67' y1='265.94' x2='307.41' y2='117.11' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='526.17' y1='280.80' x2='222.63' y2='280.80' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='530.67' y1='295.66' x2='307.41' y2='444.49' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='540.97' y1='304.76' x2='475.65' y2='435.40' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='436.89' y1='102.25' x2='311.91' y2='102.25' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='441.39' y1='117.11' x2='218.13' y2='265.94' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='451.70' y1='126.20' x2='297.10' y2='435.40' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='463.68' y1='129.03' x2='463.68' y2='432.57' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='273.15' y1='126.20' x2='207.83' y2='256.84' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='285.12' y1='129.03' x2='285.12' y2='432.57' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='297.10' y1='126.20' x2='451.70' y2='435.40' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='207.83' y1='304.76' x2='273.15' y2='435.40' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='218.13' y1='295.66' x2='441.39' y2='444.49' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<line x1='311.91' y1='459.35' x2='436.89' y2='459.35' style='stroke-width: 2.25; stroke: #B22222; stroke-opacity: 0.40;' /> +<circle cx='552.95' cy='280.80' r='26.78' style='stroke-width: 0.75; fill: #4682B4; fill-opacity: 0.40;' /> +<circle cx='463.68' cy='102.25' r='26.78' style='stroke-width: 0.75; fill: #4682B4; fill-opacity: 0.40;' /> +<circle cx='285.12' cy='102.25' r='26.78' style='stroke-width: 0.75; fill: #4682B4; fill-opacity: 0.40;' /> +<circle cx='195.85' cy='280.80' r='26.78' style='stroke-width: 0.75; fill: #4682B4; fill-opacity: 0.40;' /> +<circle cx='285.12' cy='459.35' r='26.78' style='stroke-width: 0.75; fill: #4682B4; fill-opacity: 0.40;' /> +<circle cx='463.68' cy='459.35' r='26.78' style='stroke-width: 0.75; fill: #4682B4; fill-opacity: 0.40;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='552.95' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text> +<text x='463.68' y='106.44' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text> +<text x='285.12' y='106.38' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>3</text> +<text x='195.85' y='284.93' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text> +<text x='285.12' y='463.42' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>5</text> +<text x='463.68' y='463.48' text-anchor='middle' style='font-size: 12.00px; fill: #00008B; font-family: sans;' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text> +</g> +</svg> diff --git a/tests/testthat/_snaps/plot/vertex-label-halo.svg b/tests/testthat/_snaps/plot/vertex-label-halo.svg new file mode 100644 index 00000000000..df72cb7b93a --- /dev/null +++ b/tests/testthat/_snaps/plot/vertex-label-halo.svg @@ -0,0 +1,126 @@ +<?xml version='1.0' encoding='UTF-8' ?> +<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' data-engine-version='2.0' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'> +<defs> + <style type='text/css'><![CDATA[ + .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle { + fill: none; + stroke: #000000; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10.00; + } + ]]></style> +</defs> +<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/> +<defs> + <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='> + <rect x='0.00' y='0.00' width='720.00' height='576.00' /> + </clipPath> +</defs> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +</g> +<defs> + <clipPath id='cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng=='> + <rect x='59.04' y='59.04' width='630.72' height='443.52' /> + </clipPath> +</defs> +<g clip-path='url(#cpNTkuMDR8Njg5Ljc2fDU5LjA0fDUwMi41Ng==)'> +<line x1='536.69' y1='259.52' x2='432.81' y2='123.53' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='390.96' y1='110.16' x2='221.44' y2='162.54' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='195.85' y1='197.23' x2='195.85' y2='364.37' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='221.44' y1='399.06' x2='390.96' y2='451.44' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<line x1='536.69' y1='302.08' x2='432.81' y2='438.07' style='stroke-width: 0.75; stroke: #A9A9A9;' /> +<circle cx='552.95' cy='280.80' r='26.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='416.55' cy='102.25' r='26.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='195.85' cy='170.45' r='26.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='195.85' cy='391.15' r='26.78' style='stroke-width: 0.75; fill: #E69F00;' /> +<circle cx='416.55' cy='459.35' r='26.78' style='stroke-width: 0.75; fill: #E69F00;' /> +</g> +<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'> +<text x='554.86' y='283.11' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='554.41' y='282.44' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='553.74' y='282.00' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='552.95' y='281.84' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='552.16' y='282.00' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='551.49' y='282.44' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='551.04' y='283.11' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='550.89' y='283.90' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='551.04' y='284.69' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='551.49' y='285.36' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='552.16' y='285.81' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='552.95' y='285.97' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='553.74' y='285.81' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='554.41' y='285.36' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='554.86' y='284.69' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='555.02' y='283.90' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='552.95' y='283.90' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='29.36px' lengthAdjust='spacingAndGlyphs'>alpha</text> +<text x='418.46' y='105.75' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='418.01' y='105.08' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='417.34' y='104.63' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='416.55' y='104.48' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='415.76' y='104.63' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='415.09' y='105.08' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='414.64' y='105.75' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='414.49' y='106.54' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='414.64' y='107.33' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='415.09' y='108.00' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='415.76' y='108.45' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='416.55' y='108.61' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='417.34' y='108.45' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='418.01' y='108.00' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='418.46' y='107.33' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='418.61' y='106.54' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='416.55' y='106.54' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='23.36px' lengthAdjust='spacingAndGlyphs'>beta</text> +<text x='197.76' y='171.64' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='197.31' y='170.97' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='196.64' y='170.53' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='195.85' y='170.37' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='195.06' y='170.53' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='194.39' y='170.97' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='193.94' y='171.64' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='193.78' y='172.43' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='193.94' y='173.22' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='194.39' y='173.89' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='195.06' y='174.34' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='195.85' y='174.50' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='196.64' y='174.34' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='197.31' y='173.89' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='197.76' y='173.22' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='197.91' y='172.43' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='195.85' y='172.43' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='40.02px' lengthAdjust='spacingAndGlyphs'>gamma</text> +<text x='197.76' y='394.65' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='197.31' y='393.98' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='196.64' y='393.53' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='195.85' y='393.38' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='195.06' y='393.53' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='194.39' y='393.98' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='193.94' y='394.65' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='193.78' y='395.44' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='193.94' y='396.23' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='194.39' y='396.90' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='195.06' y='397.35' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='195.85' y='397.51' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='196.64' y='397.35' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='197.31' y='396.90' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='197.76' y='396.23' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='197.91' y='395.44' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='195.85' y='395.44' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='26.02px' lengthAdjust='spacingAndGlyphs'>delta</text> +<text x='418.46' y='461.66' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='418.01' y='460.99' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='417.34' y='460.55' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='416.55' y='460.39' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='415.76' y='460.55' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='415.09' y='460.99' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='414.64' y='461.66' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='414.49' y='462.45' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='414.64' y='463.24' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='415.09' y='463.91' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='415.76' y='464.36' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='416.55' y='464.52' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='417.34' y='464.36' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='418.01' y='463.91' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='418.46' y='463.24' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='418.61' y='462.45' text-anchor='middle' style='font-size: 12.00px; fill: #FFFFFF; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +<text x='416.55' y='462.45' text-anchor='middle' style='font-size: 12.00px; font-family: sans;' textLength='38.03px' lengthAdjust='spacingAndGlyphs'>epsilon</text> +</g> +</svg> diff --git a/tests/testthat/test-plot-labels.R b/tests/testthat/test-plot-labels.R new file mode 100644 index 00000000000..aa3707d1e4e --- /dev/null +++ b/tests/testthat/test-plot-labels.R @@ -0,0 +1,59 @@ +# Label decluttering helper: label_top() keeps the highest-ranked labels and +# blanks the rest with NA so plot.igraph() omits them. + +test_that("label_top keeps exactly n labels, the highest by `by`", { + by <- c(5, 1, 9, 3, 7) + out <- label_top(by, n = 2, labels = letters[1:5]) + expect_length(out, 5) + expect_equal(sum(!is.na(out)), 2) + # the two highest are 9 (pos 3, "c") and 7 (pos 5, "e") + expect_equal(which(!is.na(out)), c(3, 5)) + expect_equal(out[!is.na(out)], c("c", "e")) +}) + +test_that("label_top supports prop (rounded up)", { + by <- 1:10 + out <- label_top(by, prop = 0.25, labels = as.character(1:10)) + expect_equal(sum(!is.na(out)), 3) # ceiling(0.25 * 10) + expect_equal(out[!is.na(out)], c("8", "9", "10")) +}) + +test_that("label_top decreasing = FALSE keeps the lowest", { + by <- c(5, 1, 9, 3, 7) + out <- label_top(by, n = 2, labels = letters[1:5], decreasing = FALSE) + expect_equal(which(!is.na(out)), c(2, 4)) # values 1 and 3 +}) + +test_that("label_top defaults labels to names then indices", { + named <- c(a = 5, b = 1, c = 9) + expect_equal(label_top(named, n = 1), c(NA, NA, "c")) + + unnamed <- c(5, 1, 9) + expect_equal(label_top(unnamed, n = 1), c(NA, NA, "3")) +}) + +test_that("label_top keeps everything when neither n nor prop given", { + by <- c(5, 1, 9) + expect_equal(label_top(by, labels = letters[1:3]), c("a", "b", "c")) +}) + +test_that("label_top handles ties via rank ties.method = 'min'", { + by <- c(5, 5, 1) + out <- label_top(by, n = 1, labels = letters[1:3]) + # both 5s rank 1 (min), so both kept even though n = 1 + expect_equal(which(!is.na(out)), c(1, 2)) +}) + +test_that("label_top validates its arguments", { + expect_error(label_top("x", n = 1), "numeric") + expect_error(label_top(1:3, n = 1, prop = 0.5), "either") + expect_error(label_top(1:3, prop = 2), "between 0 and 1") + expect_error(label_top(1:3, labels = letters[1:2]), "same length") +}) + +test_that("label_top composes with plot.igraph (NA labels omitted)", { + g <- make_ring(6) + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + expect_silent(plot(g, vertex.label = label_top(degree(g), n = 2))) +}) diff --git a/tests/testthat/test-plot-params.R b/tests/testthat/test-plot-params.R new file mode 100644 index 00000000000..a9551d206c1 --- /dev/null +++ b/tests/testthat/test-plot-params.R @@ -0,0 +1,303 @@ +# Unit tests for the (non-exported) plotting parameter-resolution machinery and +# getter helpers in R/plot.common.R. These are fast, mostly device-free tests +# that pin down behavior before the planned plotting refactor. + +# --------------------------------------------------------------------------- +# i.parse.plot.params(): precedence, routing, recycling, NA handling +# --------------------------------------------------------------------------- + +test_that("i.parse.plot.params() resolves arg > attr > option > default", { + g <- make_ring(3) + + # default: nothing set anywhere -> hard-coded default (vertex color = 1) + p_default <- i.parse.plot.params(g, list()) + expect_equal(p_default("vertex", "color"), 1) + + # option beats default + withr::local_options(igraph_verbose = FALSE) + local_igraph_options(vertex.color = "green") + p_opt <- i.parse.plot.params(g, list()) + expect_equal(p_opt("vertex", "color"), "green") + + # graph attribute beats option + ga <- set_vertex_attr(g, "color", value = rep("red", 3)) + p_attr <- i.parse.plot.params(ga, list()) + expect_equal(p_attr("vertex", "color"), rep("red", 3)) + + # explicit argument beats everything + p_arg <- i.parse.plot.params(ga, list(vertex.color = "blue")) + expect_equal(p_arg("vertex", "color"), "blue") +}) + +test_that("i.parse.plot.params() routes vertex./edge./plain prefixes", { + g <- make_ring(3) + p <- i.parse.plot.params( + g, + list(vertex.size = 99, edge.width = 7, margin = 0.5) + ) + expect_equal(p("vertex", "size"), 99) + expect_equal(p("edge", "width"), 7) + expect_equal(p("plot", "margin"), 0.5) +}) + +test_that("i.parse.plot.params() calls function-valued defaults with the graph", { + g <- make_ring(3) + g <- set_vertex_attr(g, "name", value = c("a", "b", "c")) + p <- i.parse.plot.params(g, list()) + + # vertex label default is i.get.labels(), which returns the name attribute + expect_equal(p("vertex", "label"), c("a", "b", "c")) + + # dontcall = TRUE returns the function itself rather than calling it + expect_true(is.function(p("vertex", "label", dontcall = TRUE))) +}) + +test_that("i.parse.plot.params() selects a single recycled element via range", { + g <- make_ring(3) + + # scalar value: returned as-is for a scalar range + p_scalar <- i.parse.plot.params(g, list(vertex.size = 5)) + expect_equal(p_scalar("vertex", "size", range = 2), 5) + + # vector value: 0-based index into rep(v, length.out = range + 1) + p_vec <- i.parse.plot.params(g, list(vertex.size = c(10, 20))) + expect_equal(p_vec("vertex", "size", range = 0), 10) + expect_equal(p_vec("vertex", "size", range = 1), 20) + expect_equal(p_vec("vertex", "size", range = 2), 10) # recycles +}) + +test_that("i.parse.plot.params() warns and replaces NA in non-label attributes", { + g <- make_ring(3) + g <- set_vertex_attr(g, "color", value = c("red", NA, "blue")) + p <- i.parse.plot.params(g, list()) + + expect_warning(res <- p("vertex", "color"), "contains NAs") + # NA replaced with the default vertex color (1), coerced into the vector + expect_false(anyNA(res)) + expect_equal(res[c(1, 3)], c("red", "blue")) +}) + +test_that("i.parse.plot.params() silently replaces NA labels with empty string", { + g <- make_ring(3) + g <- set_vertex_attr(g, "label", value = c("a", NA, "c")) + p <- i.parse.plot.params(g, list()) + + expect_no_warning(res <- p("vertex", "label")) + expect_equal(res, c("a", "", "c")) +}) + +# --------------------------------------------------------------------------- +# Aesthetic tables (i.aes_table / i.edge_aes_table) +# --------------------------------------------------------------------------- + +test_that("i.aes_table recycles columns to n rows", { + tbl <- i.aes_table(list(a = 1, b = c("x", "y")), n = 4) + expect_s3_class(tbl, "data.frame") + expect_equal(nrow(tbl), 4) + expect_equal(tbl$a, rep(1, 4)) + expect_equal(tbl$b, c("x", "y", "x", "y")) +}) + +test_that("i.edge_aes_table expands scalars and is sliceable by edge index", { + tbl <- i.edge_aes_table( + color = "red", + width = c(1, 2, 3), + lty = 1, + arrow.mode = 2, + arrow.size = 1, + arrow.width = 1, + curved = 0, + label.color = "blue", + label.family = "serif", + label.font = 1, + label.cex = 1, + label.halo = NA, + label.halo.width = 0.15, + style = "auto", + alpha = 1, + gradient = FALSE, + n = 3 + ) + expect_equal(nrow(tbl), 3) + expect_equal(tbl$color, rep("red", 3)) # scalar expanded + expect_equal(tbl$width, c(1, 2, 3)) # vector preserved + expect_equal(tbl$style, rep("auto", 3)) + expect_equal(tbl$alpha, rep(1, 3)) + expect_equal(tbl$gradient, rep(FALSE, 3)) + + sliced <- vctrs::vec_slice(tbl, c(1, 3)) + expect_equal(nrow(sliced), 2) + expect_equal(sliced$width, c(1, 3)) +}) + +# --------------------------------------------------------------------------- +# i.check_aes_lengths() — strict recycling (igraph 3.0.0) +# --------------------------------------------------------------------------- + +test_that("i.check_aes_lengths accepts length 1 and length n", { + expect_silent( + i.check_aes_lengths( + vertex = list(color = "red", size = c(1, 2, 3)), + edge = list(width = 1), + vc = 3, + ec = 2 + ) + ) +}) + +test_that("i.check_aes_lengths rejects mismatched vertex lengths", { + expect_snapshot_igraph_error( + i.check_aes_lengths( + vertex = list(color = c("red", "green")), + edge = list(), + vc = 5, + ec = 4 + ) + ) +}) + +test_that("i.check_aes_lengths rejects mismatched edge lengths", { + expect_snapshot_igraph_error( + i.check_aes_lengths( + vertex = list(), + edge = list(width = c(1, 2, 3)), + vc = 5, + ec = 5 + ) + ) +}) + +test_that("plot() errors on a wrong-length vertex aesthetic (strict recycling)", { + g <- make_ring(5) + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + # 3 colors for 5 vertices: previously silently recycled, now an error + expect_error(plot(g, vertex.color = c("red", "green", "blue")), "length 3") +}) + +test_that("plot() still accepts length-1 and length-n aesthetics", { + g <- make_ring(5) + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + expect_no_error(plot(g, vertex.color = "red")) + expect_no_error(plot(g, vertex.color = rep("red", 5))) +}) + +# --------------------------------------------------------------------------- +# i.get.arrow.mode() +# --------------------------------------------------------------------------- + +test_that("i.get.arrow.mode() maps character arrow specs to numeric codes", { + g <- make_ring(3, directed = TRUE) + expect_equal( + i.get.arrow.mode(g, c("<", "<-", ">", "->", "<>", "<->", "x")), + c(1, 1, 2, 2, 3, 3, 0) + ) +}) + +test_that("i.get.arrow.mode() reads a vertex attribute via the 'a:' prefix", { + g <- make_ring(2, directed = TRUE) + g <- set_vertex_attr(g, "am", value = c("->", "<-")) + expect_equal(i.get.arrow.mode(g, "a:am"), c(2, 1)) +}) + +test_that("i.get.arrow.mode() defaults by graph directedness when NULL", { + expect_equal(i.get.arrow.mode(make_ring(3, directed = TRUE), NULL), 2) + expect_equal(i.get.arrow.mode(make_ring(3, directed = FALSE), NULL), 0) +}) + +# --------------------------------------------------------------------------- +# label getters +# --------------------------------------------------------------------------- + +test_that("i.get.labels() uses the name attribute, else vertex indices", { + g_named <- set_vertex_attr(make_ring(3), "name", value = c("x", "y", "z")) + expect_equal(i.get.labels(g_named), c("x", "y", "z")) + + expect_equal(i.get.labels(make_ring(3)), 1:3) +}) + +test_that("i.get.edge.labels() defaults to an NA vector of edge length", { + g <- make_ring(4) + res <- i.get.edge.labels(g) + expect_length(res, ecount(g)) + expect_true(all(is.na(res))) +}) + +# --------------------------------------------------------------------------- +# i.get.main() / i.get.xlab() and annotate.plot +# --------------------------------------------------------------------------- + +test_that("i.get.main()/i.get.xlab() respect the annotate.plot option", { + g <- make_ring(3) + g$name <- "my graph" + + # default: no annotation + expect_identical(i.get.main(g), "") + expect_identical(i.get.xlab(g), "") + + # with annotate.plot = TRUE, return graph metadata + local_igraph_options(annotate.plot = TRUE) + expect_identical(i.get.main(g), "my graph") + expect_match(i.get.xlab(g), "3 vertices") + expect_match(i.get.xlab(g), "3 edges") +}) + +# --------------------------------------------------------------------------- +# igraph.check.shapes() +# --------------------------------------------------------------------------- + +test_that("igraph.check.shapes() passes valid shapes through", { + expect_equal( + igraph.check.shapes(c("circle", "square")), + c("circle", "square") + ) +}) + +test_that("igraph.check.shapes() aborts on unknown shapes", { + expect_snapshot_igraph_error({ + igraph.check.shapes(c("circle", "not_a_shape")) + }) +}) + +# --------------------------------------------------------------------------- +# curve_multiple() +# --------------------------------------------------------------------------- + +test_that("curve_multiple() returns zero curvature for a simple graph", { + g <- make_ring(4) + expect_equal(curve_multiple(g), rep(0, ecount(g))) +}) + +test_that("curve_multiple() spreads symmetric curvature across multi-edges", { + # two parallel edges in the same direction share an edgelist key + g <- make_graph(c(1, 2, 1, 2), directed = TRUE) + expect_equal(curve_multiple(g), c(-0.5, 0.5)) + + # reciprocal edges (1->2, 2->1) are distinct keys, so each stays 0 + g2 <- make_graph(c(1, 2, 2, 1), directed = TRUE) + expect_equal(curve_multiple(g2), c(0, 0)) +}) + +# --------------------------------------------------------------------------- +# i.rescale.vertex() -- needs an open device for par("usr") +# --------------------------------------------------------------------------- + +test_that("i.rescale.vertex() clamps sizes to the relative-size range", { + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + plot(0, 0, type = "n", xlim = c(-1, 1), ylim = c(-1, 1)) + + minmax <- c(0.01, 0.025) + res <- i.rescale.vertex(c(10, 20, 30), minmax.relative.size = minmax) + + usr <- par("usr")[1:2] + scal <- (usr[2] - usr[1]) * minmax + + expect_length(res, 3) + # smallest input maps to the lower bound, largest to the upper bound + expect_equal(res[1], scal[1]) + expect_equal(res[3], scal[2]) + # monotonic increasing in between + expect_true(all(diff(res) > 0)) +}) diff --git a/tests/testthat/test-plot-render.R b/tests/testthat/test-plot-render.R new file mode 100644 index 00000000000..103ae42a273 --- /dev/null +++ b/tests/testthat/test-plot-render.R @@ -0,0 +1,134 @@ +# Rendering indirection: drawing is emitted through the i.r_*() dispatchers, +# which forward to the current renderer. + +test_that("the default renderer is the base renderer", { + r <- i.cur_renderer() + expect_type(r, "list") + expect_true(all( + c("segments", "polyline", "polygon", "xspline", "text", "symbols", "raster") %in% + names(r) + )) +}) + +test_that("i.with_renderer installs a renderer and restores the previous one", { + before <- i.cur_renderer() + marker <- list(tag = "fake") + fake <- c(before, list(tag = "fake")) + i.with_renderer(fake, { + expect_identical(i.cur_renderer()$tag, "fake") + }) + # restored afterwards + expect_null(i.cur_renderer()$tag) +}) + +test_that("dispatchers forward primitives to the current renderer", { + rec <- new.env() + rec$calls <- character() + capture <- function(name) { + function(...) { + rec$calls <- c(rec$calls, name) + invisible(NULL) + } + } + fake <- list( + init_canvas = capture("init_canvas"), + segments = capture("segments"), + polyline = capture("polyline"), + polygon = capture("polygon"), + xspline = capture("xspline"), + text = capture("text"), + symbols = capture("symbols"), + raster = capture("raster"), + group_begin = capture("group_begin"), + group_end = capture("group_end") + ) + i.with_renderer(fake, { + i.r_segments(0, 0, 1, 1) + i.r_polyline(c(0, 1), c(0, 1)) + i.r_polygon(c(0, 1, 1), c(0, 0, 1)) + i.r_text(0, 0, "x") + i.r_symbols("circles", 0, 0, 1, "red", "black", 1) + }) + expect_equal( + rec$calls, + c("segments", "polyline", "polygon", "text", "symbols") + ) +}) + +test_that("plot.igraph still works (drawing through the base renderer)", { + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + expect_silent(plot(make_ring(5))) +}) + +test_that("the record renderer captures a backend-neutral draw list", { + g <- make_ring(3) + V(g)$name <- c("x", "y", "z") + rec <- i.renderer_record() + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + i.with_renderer(rec, plot(g, vertex.size = 20)) + + prims <- rec$.state$prims + expect_gt(length(prims), 0) + types <- vapply(prims, function(p) p$type, character(1)) + expect_true("symbols" %in% types) # vertices + expect_true(any(types %in% c("segments", "polyline"))) # edges + grp <- vapply( + prims, + function(p) if (is.null(p$group)) "" else p$group$type, + character(1) + ) + expect_true("vertices" %in% grp) + expect_true("edge" %in% grp) + # colours are canonicalised to hex + sym <- prims[[which(types == "symbols")[1]]] + expect_match(sym$bg[1], "^#") +}) + +test_that("as_svg produces well-formed SVG with per-vertex/edge ids and titles", { + skip_if_not_installed("xml2") + g <- make_ring(4, directed = TRUE) + V(g)$name <- c("a", "b", "c", "d") + svg <- as_svg(g) + + # well-formed + expect_s3_class(xml2::read_xml(svg), "xml_document") + # one group per vertex and per edge + expect_length(gregexpr("id='vertex-", svg, fixed = TRUE)[[1]], 4) + expect_length(gregexpr("id='edge-", svg, fixed = TRUE)[[1]], 4) + # tooltip from the vertex name + expect_match(svg, "<title>a", fixed = TRUE) +}) + +test_that("as_svg writes to a file and honours the tooltips argument", { + g <- make_ring(3) + V(g)$kind <- c("p", "q", "r") + + f <- withr::local_tempfile(fileext = ".svg") + out <- as_svg(g, file = f, tooltips = "kind") + expect_true(file.exists(f)) + expect_match(paste(readLines(f), collapse = ""), "p", fixed = TRUE) +}) + +test_that("a label halo emits the offset copies plus the real label in SVG", { + skip_if_not_installed("xml2") + g <- make_ring(3) + V(g)$name <- c("aa", "bb", "cc") + + plain <- as_svg(g) + haloed <- as_svg( + g, + vertex.label.halo = "white", + vertex.label.halo.width = 0.2 + ) + + expect_s3_class(xml2::read_xml(haloed), "xml_document") + # the halo adds offset copies of every glyph, so there are strictly more + # elements than without it, and the real labels are still present. + n_plain <- length(gregexpr("aa<", fixed = TRUE) +}) diff --git a/tests/testthat/test-plot-scales.R b/tests/testthat/test-plot-scales.R new file mode 100644 index 00000000000..b0d4fb11d9e --- /dev/null +++ b/tests/testthat/test-plot-scales.R @@ -0,0 +1,123 @@ +# Unit tests for the scale layer: scale_color() / scale_size() and the internal +# i.apply_scales() that feeds plot.igraph(). + +test_that("scale_color() maps a discrete vector to categorical colours", { + s <- scale_color(c("a", "b", "a", "c")) + expect_s3_class(s, "igraph_scale") + expect_type(s$values, "character") + + pal <- categorical_pal(3) + # levels are sorted: a, b, c + expect_equal(s$values, pal[c(1, 2, 1, 3)]) + + expect_equal(s$guide$aesthetic, "color") + expect_equal(s$guide$type, "discrete") + expect_equal(s$guide$labels, c("a", "b", "c")) + expect_equal(s$guide$colors, pal) +}) + +test_that("scale_color() respects factor level order and a custom palette", { + x <- factor(c("lo", "hi", "lo"), levels = c("lo", "hi")) + s <- scale_color(x, palette = c("red", "blue")) + expect_equal(s$guide$labels, c("lo", "hi")) # factor order, not sorted + expect_equal(s$values, c("red", "blue", "red")) +}) + +test_that("scale_color() sends NA data to na.value and drops it from the guide", { + s <- scale_color(c("a", NA, "b"), na.value = "grey90") + expect_equal(s$values[2], "grey90") + expect_false("grey90" %in% s$guide$colors) + expect_equal(s$guide$labels, c("a", "b")) +}) + +test_that("scale_color() maps a numeric vector continuously with a colorbar guide", { + s <- scale_color(c(0, 5, 10)) + expect_equal(s$guide$type, "continuous") + expect_equal(s$guide$limits, c(0, 10)) + expect_match(s$values, "^#", all = TRUE) # valid hex + # endpoints differ (low vs high of the ramp) + expect_false(s$values[1] == s$values[3]) +}) + +test_that("scale_size() rescales numeric data to the size range", { + s <- scale_size(c(1, 2, 3), range = c(10, 30)) + expect_s3_class(s, "igraph_scale") + expect_equal(s$values, c(10, 20, 30)) + expect_equal(s$guide$aesthetic, "size") + expect_equal(s$guide$type, "discrete") +}) + +test_that("scale_size() supports a transform and constant input", { + s <- scale_size(c(1, 4, 9), range = c(0, 10), trans = "sqrt") + # sqrt -> 1,2,3 -> linear 0,5,10 + expect_equal(s$values, c(0, 5, 10)) + + flat <- scale_size(rep(5, 4), range = c(2, 8)) + expect_equal(flat$values, rep(5, 4)) # midpoint of range +}) + +test_that("scale_size() rejects non-numeric input", { + expect_error(scale_size(c("a", "b")), "must be numeric") +}) + +test_that("i.apply_scales replaces scale args and collects guides", { + dots <- list( + vertex.color = scale_color(c("a", "b")), + vertex.size = scale_size(c(1, 2)), + edge.width = 3 # plain arg untouched + ) + res <- i.apply_scales(dots) + + expect_type(res$dots$vertex.color, "character") # resolved + expect_equal(res$dots$edge.width, 3) # untouched + expect_length(res$guides, 2) + # title defaults to the argument name + names <- vapply(res$guides, function(g) g$name, character(1)) + expect_setequal(names, c("vertex.color", "vertex.size")) +}) + +test_that("a scale's explicit name overrides the argument-name default", { + res <- i.apply_scales(list( + vertex.color = scale_color(c("a", "b"), name = "Group") + )) + expect_equal(res$guides[[1]]$name, "Group") +}) + +test_that("a wrong-length scale is rejected by strict recycling at plot time", { + g <- make_ring(5) + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + # 3 colours mapped, but 5 vertices + expect_error( + plot(g, vertex.color = scale_color(c("a", "b", "c"))), + "length 3" + ) +}) + +test_that("named per-vertex aesthetics don't break edge drawing (#regression)", { + # A named vertex.size (e.g. scale_size(degree(g)) carries degree()'s names) + # used to propagate names into the clipped edge coordinates, where + # i.edge_label_pos()'s c(x = ..., y = ...) produced names like "x.Alice" + # instead of "x"/"y", crashing with "subscript out of bounds". + g <- make_graph(~ A - B, B - C, C - A, A - D) + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + + expect_silent(plot(g, vertex.size = scale_size(degree(g), range = c(10, 30)))) + expect_silent(plot( + g, + vertex.size = stats::setNames(c(10, 20, 30, 15), V(g)$name) + )) +}) + +test_that("i.edge_label_pos returns x/y names even for named inputs", { + pos <- i.edge_label_pos( + stats::setNames(0, "a"), + stats::setNames(0, "a"), + stats::setNames(1, "b"), + stats::setNames(1, "b") + ) + expect_named(pos, c("x", "y")) + expect_equal(pos[["x"]], 1 / 3) + expect_equal(pos[["y"]], 1 / 3) +}) diff --git a/tests/testthat/test-plot.R b/tests/testthat/test-plot.R index bbc29010ab8..3a5f8c10257 100644 --- a/tests/testthat/test-plot.R +++ b/tests/testthat/test-plot.R @@ -234,6 +234,517 @@ test_that("mark border linewidth", { vdiffr::expect_doppelganger("mark-border-lwd", mark_border_lwd) }) +test_that("i.repel_labels separates overlapping labels and is deterministic", { + # two boxes stacked at the same point should be pushed apart (here along y, + # the smaller-overlap axis) + r <- i.repel_labels( + x = c(0, 0), + y = c(0, 0), + hw = c(0.2, 0.2), + hh = c(0.1, 0.1) + ) + sep <- max(abs(r$x[1] - r$x[2]), abs(r$y[1] - r$y[2])) + expect_gt(sep, 0.15) # was 0; now nearly the box height sum (0.2) + + # deterministic + r2 <- i.repel_labels( + x = c(0, 0), + y = c(0, 0), + hw = c(0.2, 0.2), + hh = c(0.1, 0.1) + ) + expect_equal(r, r2) + + # a single label is returned unchanged + expect_equal(i.repel_labels(5, 7, 1, 1), list(x = 5, y = 7)) + + # non-overlapping labels are left where they are + far <- i.repel_labels(c(0, 10), c(0, 0), c(0.2, 0.2), c(0.1, 0.1)) + expect_equal(far$x, c(0, 10)) + expect_equal(far$y, c(0, 0)) +}) + +test_that("i.loop_angles distributes loops and returns aligned vectors", { + # Two vertices, vertex 1 has 2 loops, plus a 1-2 edge. + g <- make_graph(c(1, 2, 1, 1, 1, 1), directed = FALSE) + layout <- cbind(c(0, 1), c(0, 0)) + loops.v <- c(1, 1) # the two loop edges are both at vertex 1 + + res <- i.loop_angles(g, layout, loops.v) + expect_named(res, c("angles", "narrowing")) + expect_length(res$angles, 2) + expect_length(res$narrowing, 2) + # narrowing is bounded to [0.2, 1] + expect_true(all(res$narrowing >= 0.2 & res$narrowing <= 1)) + # the two loops get distinct angles + expect_false(res$angles[1] == res$angles[2]) +}) + +test_that("i.apply_alpha multiplies alpha and is a no-op at 1", { + # no-op: fully opaque returns the input unchanged (keeps snapshots stable) + expect_identical(i.apply_alpha(c("red", "blue"), 1), c("red", "blue")) + expect_identical(i.apply_alpha(c("red", "blue"), c(1, 1)), c("red", "blue")) + + # fractional alpha reduces the alpha channel + a <- i.apply_alpha("red", 0.5) + expect_equal(unname(grDevices::col2rgb(a, alpha = TRUE)["alpha", ]), 128) + + # vectorised over colour and alpha (recycled) + v <- i.apply_alpha(c("red", "green", "blue"), c(0.5, 1, 0.25)) + av <- grDevices::col2rgb(v, alpha = TRUE)["alpha", ] + expect_equal(unname(av), c(128, 255, 64)) + + # already-translucent input is further reduced (multiplicative) + half <- i.apply_alpha(grDevices::rgb(1, 0, 0, 0.5), 0.5) + expect_equal(unname(grDevices::col2rgb(half, alpha = TRUE)["alpha", ]), 64) + + expect_identical(i.apply_alpha(character(0), 0.5), character(0)) +}) + +test_that("i.elbow_path is a two-corner route along the dominant axis", { + # horizontal dominant: leave horizontally, turn at mid-x + e <- i.elbow_path(0, 0, 10, 4) + expect_equal(e$x, c(0, 5, 5, 10)) + expect_equal(e$y, c(0, 0, 4, 4)) + # vertical dominant: leave vertically, turn at mid-y + v <- i.elbow_path(0, 0, 4, 10) + expect_equal(v$x, c(0, 0, 4, 4)) + expect_equal(v$y, c(0, 5, 5, 10)) + # endpoints preserved + expect_equal(c(e$x[1], e$y[1]), c(0, 0)) + expect_equal(c(e$x[4], e$y[4]), c(10, 4)) +}) + +test_that("i.diagonal_path is a smooth path between the endpoints", { + d <- i.diagonal_path(0, 0, 10, 4, n = 30) + expect_length(d$x, 30) + expect_equal(c(d$x[1], d$y[1]), c(0, 0)) + expect_equal(c(d$x[30], d$y[30]), c(10, 4)) + expect_true(all(is.finite(d$x)) && all(is.finite(d$y))) + # deterministic + expect_equal(d, i.diagonal_path(0, 0, 10, 4, n = 30)) +}) + +test_that("an explicit `vertical` overrides the inferred dominant axis", { + # horizontal-dominant deltas, but vertical = TRUE forces a vertical leave + e <- i.elbow_path(0, 0, 10, 4, vertical = TRUE) + expect_equal(e$x, c(0, 0, 10, 10)) + expect_equal(e$y, c(0, 2, 2, 4)) + # and vertical = FALSE forces a horizontal leave on vertical-dominant deltas + h <- i.elbow_path(0, 0, 4, 10, vertical = FALSE) + expect_equal(h$x, c(0, 2, 2, 4)) + expect_equal(h$y, c(0, 0, 10, 10)) +}) + +test_that("elbow/diagonal edges attach on the vertex centre axis", { + # vertical-dominant edge whose endpoints have different x: a centre-to-centre + # clip would attach off the parent/child x; the axis-aware clip must keep the + # first/last shaft point on each vertex's centre x. + g <- make_graph(c(1, 2), directed = TRUE) + lay <- matrix(c(0, 1, 0.3, -1), ncol = 2, byrow = TRUE) + + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + + shaft_x <- function(style) { + rec <- i.renderer_record() + i.with_renderer( + rec, + plot( + g, + layout = lay, + rescale = FALSE, + xlim = c(-1, 1), + ylim = c(-1, 1), + edge.style = style, + vertex.size = 20, + # no arrowhead: avoids the shaft pull-back so the path starts/ends + # exactly at the clipped attachment points + edge.arrow.mode = 0 + ) + ) + prims <- rec$.state$prims + pl <- Filter( + function(p) { + identical(p$type, "polyline") && + !is.null(p$group) && + identical(p$group$type, "edge") + }, + prims + )[[1]] + c(first = pl$x[1], last = pl$x[length(pl$x)]) + } + + for (style in c("elbow", "diagonal")) { + xs <- shaft_x(style) + expect_equal(unname(xs["first"]), 0, tolerance = 1e-6) # parent centre x + expect_equal(unname(xs["last"]), 0.3, tolerance = 1e-6) # child centre x + } +}) + +test_that("i.arrowhead_shape returns matched polar arrays ending in NA", { + # Pure geometry helper extracted from igraph.Arrows; device-free. + head <- i.arrowhead_shape(cin = 0.2, w = 1.5, delta = 0.01) + expect_named(head, c("deg.arr", "r.arr")) + expect_equal(length(head$deg.arr), length(head$r.arr)) + # both arrays terminate in NA (the pen-up marker for the outline) + expect_true(is.na(tail(head$deg.arr, 1))) + expect_true(is.na(tail(head$r.arr, 1))) + # radii are non-negative where defined + expect_true(all(head$r.arr >= 0, na.rm = TRUE)) + # larger arrows (bigger cin) produce more outline points + bigger <- i.arrowhead_shape(cin = 0.4, w = 1.5, delta = 0.01) + expect_gt(length(bigger$r.arr), length(head$r.arr)) +}) + +test_that("i.arrow_shaft_endpoints adjusts only the arrowed end", { + uin <- c(1, 1) # square device units for a clean check + # horizontal edge (0,0) -> (10,0). code 2 adjusts the from-end (x1d), leaving + # the to-end at 10; the shift is r.seg along theta1 (= -1 here). + s2 <- i.arrow_shaft_endpoints(0, 0, 10, 0, code = 2, r.seg = 1, uin = uin) + expect_equal(s2$sx2, 10) # to-end unchanged for code 2 + expect_equal(s2$sx1, -1) # from-end shifted by r.seg + # code 0 (no arrows): both ends unchanged + s0 <- i.arrow_shaft_endpoints(0, 0, 10, 0, code = 0, r.seg = 1, uin = uin) + expect_equal(c(s0$sx1, s0$sx2), c(0, 10)) +}) + +test_that("i.edge_label_pos is two thirds from the target toward the source", { + pos <- i.edge_label_pos(0, 0, 9, 0) + expect_equal(unname(pos["x"]), 3) # 9 - 2/3*9, i.e. 2/3 from (9,0) toward (0,0) + expect_equal(unname(pos["y"]), 0) +}) + +test_that("scales draw matching legends and colorbars", { + skip_if_not_installed("vdiffr") + + ring10 <- function() { + g <- make_ring(10) + g$layout <- layout_in_circle(g) + g + } + + vdiffr::expect_doppelganger("scale-discrete-color", function() { + g <- ring10() + V(g)$grp <- rep(c("alpha", "beta"), 5) + plot(g, vertex.color = scale_color(V(g)$grp), vertex.size = 20) + }) + + vdiffr::expect_doppelganger("scale-continuous-colorbar", function() { + g <- ring10() + plot(g, vertex.color = scale_color(1:10), vertex.size = 20) + }) + + vdiffr::expect_doppelganger("scale-size-legend", function() { + g <- ring10() + plot(g, vertex.size = scale_size(1:10, range = c(5, 25))) + }) + + vdiffr::expect_doppelganger("scale-color-and-size", function() { + g <- ring10() + V(g)$grp <- rep(c("alpha", "beta"), 5) + plot( + g, + vertex.color = scale_color(V(g)$grp), + vertex.size = scale_size(1:10, range = c(5, 25)) + ) + }) + + vdiffr::expect_doppelganger("scale-legend-bottom-horizontal", function() { + g <- ring10() + V(g)$grp <- rep(c("alpha", "beta"), 5) + plot( + g, + vertex.color = scale_color(V(g)$grp), + vertex.size = 20, + legend = "bottom" + ) + }) + + vdiffr::expect_doppelganger("scale-continuous-colorbar-top", function() { + g <- ring10() + plot(g, vertex.color = scale_color(1:10), vertex.size = 20, legend = "top") + }) + + vdiffr::expect_doppelganger("scale-edge-color", function() { + g <- ring10() + E(g)$type <- rep(c("x", "y"), length.out = ecount(g)) + plot( + g, + edge.color = scale_color(E(g)$type), + edge.width = 2, + vertex.size = 15 + ) + }) +}) + +test_that("vertex.label.repel separates clustered labels", { + skip_if_not_installed("vdiffr") + + vdiffr::expect_doppelganger("label-repel", function() { + g <- make_empty_graph(8) + layout <- cbind( + c(0, 0.05, 0.1, -0.05, 1, 1.05, 0.95, 1.1), + c(0, 0.05, -0.05, 0.02, 1, 0.95, 1.05, 1.02) + ) + V(g)$label <- c( + "Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Heidi" + ) + plot(g, layout = layout, vertex.size = 12, vertex.label.repel = TRUE) + }) +}) + +test_that("legend = FALSE suppresses the guide", { + skip_if_not_installed("vdiffr") + # Same graph as scale-discrete-color but with the legend turned off; should + # render identically to a plain coloured plot (no guide box). + vdiffr::expect_doppelganger("scale-legend-false", function() { + g <- make_ring(10) + g$layout <- layout_in_circle(g) + V(g)$grp <- rep(c("alpha", "beta"), 5) + plot( + g, + vertex.color = scale_color(V(g)$grp), + vertex.size = 20, + legend = FALSE + ) + }) +}) + +test_that("vector edge params are subset correctly across loops and non-loops", { + # Guards the per-edge subsetting of loop vs non-loop edges in plot.igraph(). + skip_if_not_installed("vdiffr") + + vector_edge_params <- function() { + # edges: 1->2, 2->3, 1->1 (loop), 3->3 (loop), 2->1 + g <- make_graph(c(1, 2, 2, 3, 1, 1, 3, 3, 2, 1), directed = TRUE) + V(g)$x <- c(0, 1, 2) + V(g)$y <- c(0, 1, 0) + ne <- ecount(g) + plot( + g, + edge.color = c("red", "green", "blue", "orange", "purple"), + edge.width = c(1, 2, 3, 4, 5), + edge.lty = c(1, 2, 1, 2, 1), + edge.arrow.mode = c(1, 2, 3, 2, 1), + edge.arrow.size = c(1, 1.5, 2, 1.5, 1), + edge.label = letters[seq_len(ne)], + edge.label.color = c("red", "green", "blue", "orange", "purple"), + margin = 0.3 + ) + } + vdiffr::expect_doppelganger("vector-edge-params-loops", vector_edge_params) +}) + +test_that("edge.gradient blends source to target vertex colours", { + skip_if_not_installed("vdiffr") + + vdiffr::expect_doppelganger("edge-gradient", function() { + g <- make_graph(c(1, 2, 2, 3, 3, 1, 1, 4), directed = TRUE) + g$layout <- layout_in_circle(g) + V(g)$color <- c("red", "green", "blue", "orange") + plot( + g, + edge.gradient = TRUE, + vertex.size = 24, + edge.width = 3, + edge.arrow.size = 0.6 + ) + }) + + vdiffr::expect_doppelganger("edge-gradient-arc", function() { + g <- make_graph(c(1, 2, 2, 3, 3, 1), directed = TRUE) + g$layout <- layout_in_circle(g) + V(g)$color <- c("red", "green", "blue") + plot( + g, + edge.gradient = TRUE, + edge.style = "arc", + vertex.size = 24, + edge.width = 3 + ) + }) +}) + +test_that("vertex.alpha and edge.alpha render translucently", { + skip_if_not_installed("vdiffr") + + vdiffr::expect_doppelganger("vertex-edge-alpha", function() { + g <- make_full_graph(6) + g$layout <- layout_in_circle(g) + plot( + g, + vertex.alpha = 0.4, + edge.alpha = 0.4, + vertex.size = 30, + vertex.color = "steelblue", + edge.width = 3, + edge.color = "firebrick" + ) + }) +}) + +test_that("edge.style routes edges (elbow / diagonal / mixed / arc)", { + skip_if_not_installed("vdiffr") + + tree <- function() { + g <- make_tree(15, children = 2) + g$layout <- layout_as_tree(g) + g + } + + vdiffr::expect_doppelganger("edge-style-elbow", function() { + plot(tree(), edge.style = "elbow", vertex.size = 12, edge.arrow.size = 0.4) + }) + + vdiffr::expect_doppelganger("edge-style-diagonal", function() { + plot( + tree(), + edge.style = "diagonal", + vertex.size = 12, + edge.arrow.size = 0.4 + ) + }) + + vdiffr::expect_doppelganger("edge-style-mixed", function() { + g <- make_graph(c(1, 2, 2, 3, 3, 4, 4, 1), directed = TRUE) + # deliberately non-axis-aligned so elbow/diagonal visibly bend + g$layout <- cbind(c(0, 2, 1.6, 0.3), c(1, 1.4, 0, -0.2)) + plot( + g, + edge.style = c("straight", "arc", "elbow", "diagonal"), + vertex.size = 20, + edge.arrow.size = 0.5, + margin = 0.2 + ) + }) + + vdiffr::expect_doppelganger("edge-style-arc-single", function() { + # "arc" forces a curve on single (otherwise straight) edges + g <- make_ring(5, directed = TRUE) + g$layout <- layout_in_circle(g) + plot(g, edge.style = "arc", vertex.size = 20, edge.arrow.size = 0.5) + }) +}) + +test_that("mixed arrow modes with per-edge curved/size and loops render correctly", { + # Regression guard: the per-arrow-code branch used to double-slice + # `curved` and ignored per-edge arrow.size/width. Exercise that path with a + # graph that has loops, non-loop edges, mixed arrow modes, and per-edge + # curved + arrow.size vectors. + skip_if_not_installed("vdiffr") + + mixed_modes_curved <- function() { + # edges: 1->2, 2->3, 3->1, 1->1 (loop), 2->2 (loop) + g <- make_graph(c(1, 2, 2, 3, 3, 1, 1, 1, 2, 2), directed = TRUE) + V(g)$x <- c(0, 2, 1) + V(g)$y <- c(0, 0, 2) + ne <- ecount(g) + plot( + g, + edge.arrow.mode = c(0, 1, 2, 2, 3), + edge.curved = c(0.3, -0.3, 0.5, 0, 0), + edge.arrow.size = c(0.5, 1, 1.5, 1, 1), + edge.width = c(1, 2, 3, 1, 2), + margin = 0.3 + ) + } + vdiffr::expect_doppelganger("mixed-modes-curved", mixed_modes_curved) +}) + +test_that("multi-edges are auto-curved", { + skip_if_not_installed("vdiffr") + + multi_edge_curve <- function() { + g <- make_graph(c(1, 2, 1, 2, 1, 2, 2, 3), directed = TRUE) + V(g)$x <- c(0, 2, 4) + V(g)$y <- c(0, 0, 0) + plot(g, edge.curved = TRUE, margin = 0.3) + } + vdiffr::expect_doppelganger("multi-edge-curve", multi_edge_curve) +}) + +test_that("NA in a vertex attribute warns and still plots", { + g <- make_ring(3) + V(g)$color <- c("red", NA, "blue") + g$layout <- cbind(1:3, rep(0, 3)) + + grDevices::pdf(NULL) + withr::defer(grDevices::dev.off()) + expect_warning(plot(g), "contains NAs") +}) + +test_that("mark.groups draws multiple overlapping groups", { + skip_if_not_installed("vdiffr") + + mark_groups_multi <- function() { + g <- make_full_graph(5) + V(g)$x <- c(0, 1, 2, 1, 0) + V(g)$y <- c(0, 0, 1, 2, 2) + plot( + g, + mark.groups = list(c(1, 2, 3), c(3, 4, 5)), + mark.col = c("#ffcccc", "#ccccff"), + mark.border = c("red", "blue"), + margin = 0.5 + ) + } + vdiffr::expect_doppelganger("mark-groups-multi", mark_groups_multi) +}) + +test_that("label.dist and label.degree position labels", { + skip_if_not_installed("vdiffr") + + label_dist_degree <- function() { + g <- make_ring(4) + g$layout <- layout_in_circle(g) + plot( + g, + vertex.label = c("N", "E", "S", "W"), + vertex.label.dist = 2, + vertex.label.degree = c(pi / 2, 0, -pi / 2, pi), + margin = 0.4 + ) + } + vdiffr::expect_doppelganger("label-dist-degree", label_dist_degree) +}) + +test_that("add = TRUE overlays a second graph on the same device", { + skip_if_not_installed("vdiffr") + + overlay <- function() { + g1 <- make_ring(3) + g1$layout <- cbind(c(0, 1, 2), c(0, 0, 0)) + g2 <- make_ring(3) + g2$layout <- cbind(c(0, 1, 2), c(1, 1, 1)) + plot( + g1, + rescale = FALSE, + xlim = c(-1, 3), + ylim = c(-1, 2), + vertex.color = "red" + ) + plot(g2, rescale = FALSE, add = TRUE, vertex.color = "blue") + } + vdiffr::expect_doppelganger("add-overlay", overlay) +}) + +test_that("numeric vertex.color indexes into a custom palette", { + skip_if_not_installed("vdiffr") + + palette_index <- function() { + g <- make_ring(4) + g$layout <- layout_in_circle(g) + plot( + g, + vertex.color = c(1, 2, 3, 4), + palette = categorical_pal(4), + vertex.size = 30 + ) + } + vdiffr::expect_doppelganger("palette-index", palette_index) +}) + test_that("plot rescales correctly", { skip_if_not_installed("vdiffr") rescale_coords <- function() { @@ -265,3 +776,44 @@ test_that("plot rescales correctly", { } vdiffr::expect_doppelganger("rescale-coords", rescale_coords) }) + +test_that("vertex label halo draws an outline", { + skip_if_not_installed("vdiffr") + + g <- make_ring(5) + V(g)$name <- c("alpha", "beta", "gamma", "delta", "epsilon") + g$layout <- layout_in_circle(g) + + vdiffr::expect_doppelganger( + "vertex-label-halo", + function() { + plot( + g, + vertex.size = 30, + vertex.label.color = "black", + vertex.label.halo = "white", + vertex.label.halo.width = 0.25 + ) + } + ) +}) + +test_that("edge label halo draws an outline", { + skip_if_not_installed("vdiffr") + + g <- make_ring(4, directed = TRUE) + E(g)$label <- c("e1", "e2", "e3", "e4") + g$layout <- layout_in_circle(g) + + vdiffr::expect_doppelganger( + "edge-label-halo", + function() { + plot( + g, + edge.label.color = "black", + edge.label.halo = "yellow", + edge.label.cex = 1.5 + ) + } + ) +}) diff --git a/tests/testthat/test-plot.shapes.R b/tests/testthat/test-plot.shapes.R index 37ea9ae1f13..b070dc83c5a 100644 --- a/tests/testthat/test-plot.shapes.R +++ b/tests/testthat/test-plot.shapes.R @@ -49,6 +49,44 @@ test_that("add_shape() validates inputs correctly", { ) }) +test_that("add_shape() validates clip/plot signatures", { + # Remove the shapes registered below so they don't leak into other tests + # (e.g. the "render all shapes" snapshot test). + withr::defer({ + for (s in c("dots_shape", "good_shape")) { + if (exists(s, envir = .igraph.shapes)) { + rm(list = s, envir = .igraph.shapes) + } + } + }) + # clip missing `end` + expect_error( + add_shape("bad_clip", clip = function(coords, el, params) coords), + "missing required argument" + ) + # plot missing `params` + expect_error( + add_shape("bad_plot", plot = function(coords, v) invisible()), + "missing required argument" + ) + # functions taking ... are accepted + expect_true( + add_shape( + "dots_shape", + clip = function(...) NULL, + plot = function(...) invisible() + ) + ) + # a correctly-shaped custom shape is accepted + expect_true( + add_shape( + "good_shape", + clip = function(coords, el, params, end) coords, + plot = function(coords, v = NULL, params) invisible() + ) + ) +}) + test_that("add_shape() can override existing shapes", { original_circle <- shapes("circle") dummy_plot <- function(coords, v = NULL, params) invisible(NULL) @@ -119,13 +157,97 @@ test_that("clipping handles empty coordinates", { el <- matrix(numeric(0), nrow = 0, ncol = 2) params <- function(type, param) 1 - for (shape_name in c("circle", "square", "rectangle")) { + built_in <- c( + "circle", "square", "csquare", "rectangle", "crectangle", + "vrectangle", "pie" + ) + for (shape_name in built_in) { clip_func <- shapes(shape_name)$clip result <- clip_func(empty_coords, el, params, "both") expect_equal(nrow(result), 0) } }) +test_that("non-circle clip functions return the right column structure", { + # diagonal edge from (0,0) to (10,10) + coords <- matrix(c(0, 0, 10, 10), nrow = 1) + el <- matrix(c(1, 2), nrow = 1) + params <- function(type, param) { + switch(param, "size" = 2, "size2" = 2, 1) + } + + all_clip <- c("square", "csquare", "rectangle", "crectangle", "vrectangle", "pie") + for (shape_name in all_clip) { + clip_func <- shapes(shape_name)$clip + expect_equal( + ncol(clip_func(coords, el, params, "from")), + 2, + info = shape_name + ) + expect_equal( + ncol(clip_func(coords, el, params, "to")), + 2, + info = shape_name + ) + expect_equal( + ncol(clip_func(coords, el, params, "both")), + 4, + info = shape_name + ) + } +}) + +test_that("non-centered clip functions clip endpoints inward", { + # diagonal edge from (0,0) to (10,10) + coords <- matrix(c(0, 0, 10, 10), nrow = 1) + el <- matrix(c(1, 2), nrow = 1) + params <- function(type, param) { + switch(param, "size" = 2, "size2" = 2, 1) + } + + # csquare/crectangle clip to a face-center (can sit on an axis for a 45 deg + # edge), so the inward check applies only to the non-centered shapes. + for (shape_name in c("square", "rectangle", "vrectangle", "pie")) { + clip_func <- shapes(shape_name)$clip + expect_true( + clip_func(coords, el, params, "from")[1, 1] > 0, + info = shape_name + ) + expect_true( + clip_func(coords, el, params, "to")[1, 1] < 10, + info = shape_name + ) + } +}) + +test_that("clip functions select vertex.size per endpoint from a vector", { + # Two identical diagonal edges, distinct from/to vertex indices, so that a + # per-vertex size vector must be indexed by el[, 1] (from) and el[, 2] (to). + # This pins the exact per-endpoint selection that the planned refactor + # deduplicates across shapes. + coords <- rbind(c(0, 0, 10, 10), c(0, 0, 10, 10)) + el <- rbind(c(1, 2), c(3, 4)) + sizes <- c(2, 2, 8, 8) # vertices 3 and 4 are larger + params <- function(type, param) { + if (param == "size") { + return(sizes) + } + 1 + } + + clip_func <- shapes("circle")$clip + + # "from" uses size[el[, 1]] = c(2, 8): the larger from-vertex (row 2) is + # clipped further along the edge, so its x is greater. + res_from <- clip_func(coords, el, params, "from") + expect_gt(res_from[2, 1], res_from[1, 1]) + + # "to" uses size[el[, 2]] = c(2, 8): the larger to-vertex (row 2) clips the + # endpoint back more, so its x is smaller. + res_to <- clip_func(coords, el, params, "to") + expect_lt(res_to[2, 1], res_to[1, 1]) +}) + test_that("all built-in shapes render correctly", { skip_if_not_installed("vdiffr") diff --git a/vignettes/articles/plotting.Rmd b/vignettes/articles/plotting.Rmd new file mode 100644 index 00000000000..8518b19ec0e --- /dev/null +++ b/vignettes/articles/plotting.Rmd @@ -0,0 +1,622 @@ +--- +title: "Plotting graphs" +--- + +```{r include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + fig.width = 6, + fig.height = 6, + fig.align = "center", + dev = "png" +) +set.seed(42) # stable force-directed layouts across rebuilds +``` + +```{r setup} +library(igraph) +``` + +`igraph` ships a native, dependency-light plotting engine built on base R +graphics. A single call to `plot()` "just works", and every visual detail — +vertex shapes, edge routing, colours, labels, legends — is controllable through +arguments to `plot()` or through vertex/edge/graph attributes. + +This article is a complete tour of that engine: it walks through both the +long-standing features and the more recent additions (scales and legends, +label repelling and halos, richer edge styles, colour gradients and +transparency, and SVG export). Everything is shown at least once. + +```{r} +# A small attributed social network used throughout this article. +g <- make_graph( + ~ Alice - Boris:Himari:Moshe, Himari - Alice:Nang:Moshe:Samira, + Ibrahim - Nang:Moshe, Nang - Samira +) +V(g)$gender <- c("f", "m", "f", "m", "m", "f", "m") +E(g)$kind <- rep(c("friend", "colleague"), length.out = ecount(g)) +``` + +## Quick start + +The simplest possible call lays the graph out automatically and draws it: + +```{r} +plot(g) +``` + +There are two ways to control appearance: + +* **As an argument to `plot()`**, prefixed with `vertex.` / `edge.` / + (none for plot-level), e.g. `vertex.color = "tomato"`. +* **As a graph attribute**, e.g. `V(g)$color <- "tomato"`. + +Arguments to `plot()` take precedence over attributes, which in turn take +precedence over options and built-in defaults (see +[Setting defaults](#setting-defaults)). Keeping the styling in the `plot()` +call keeps the visual representation separate from the graph data: + +```{r} +plot( + g, + vertex.color = ifelse(V(g)$gender == "m", "skyblue", "pink"), + vertex.size = 30, + edge.width = ifelse(E(g)$kind == "colleague", 3, 1) +) +``` + +## Layouts + +A graph is an abstract object; to draw it we first map vertices to coordinates +with a *layout algorithm*. All layout functions start with `layout_`: + +| Method name | Algorithm description | +|--------------------|---------------------------------------------------------------| +| `layout_randomly` | Places vertices completely randomly | +| `layout_in_circle` | Deterministic; places vertices on a circle | +| `layout_on_sphere` | Deterministic; places vertices evenly on a sphere surface | +| `layout_with_drl` | DrL (Distributed Recursive Layout), for large graphs | +| `layout_with_fr` | Fruchterman–Reingold force-directed algorithm | +| `layout_with_kk` | Kamada–Kawai force-directed algorithm | +| `layout_with_lgl` | LGL (Large Graph Layout), for large graphs | +| `layout_as_tree` | Reingold–Tilford tree layout | +| `layout_nicely` | Picks a sensible algorithm automatically (the default) | + +A layout function returns a matrix with one row per vertex. You can pass either +a precomputed matrix or the function itself: + +```{r} +lay <- layout_with_kk(g) +plot(g, layout = lay, main = "Kamada-Kawai layout") +``` + +```{r} +plot(g, layout = layout_in_circle, main = "Circular layout") +``` + +By default coordinates are rescaled to fit a `[-1, 1]` square with aspect ratio +1. Disable rescaling and set the limits/aspect/margins yourself when you need +the layout's own coordinates: + +```{r} +plot( + g, + layout = lay, + rescale = FALSE, + xlim = range(lay[, 1]), + ylim = range(lay[, 2]), + asp = 1, + margin = 0.1 +) +``` + +## Vertices + +Vertex colour, size and frame are the basic knobs. `vertex.size2` sets the +second dimension for non-square shapes (e.g. rectangles). + +```{r} +plot( + g, + vertex.color = "gold", + vertex.size = 35, + vertex.frame.color = "grey30", + vertex.frame.width = 2 +) +``` + +### Transparency + +`vertex.alpha` (and `edge.alpha`, later) set opacity in `[0, 1]`, folded into +the fill colour: + +```{r} +plot(g, vertex.color = "purple", vertex.alpha = 0.4, vertex.size = 40) +``` + +### Shape gallery + +The built-in shapes are `circle`, `square`, `csquare` (centred square), +`rectangle`, `crectangle` (centred rectangle), `vrectangle` (vertical +rectangle), `none` (labels only), `pie`, `sphere`, and `raster`. Use `shapes()` +to list everything currently registered: + +```{r} +shapes() +``` + +```{r fig.width = 7, fig.height = 7} +shp <- c( + "circle", "square", "csquare", "rectangle", + "crectangle", "vrectangle", "none", "pie", "sphere", "raster" +) +ring <- make_ring(length(shp)) +plot( + ring, + vertex.shape = shp, + vertex.label = shp, + vertex.label.dist = 1.4, + vertex.size = 26, + vertex.size2 = 18, + # pie data is only used by the "pie" shape + vertex.pie = lapply(shp, function(s) if (s == "pie") c(2, 3, 1) else 0), + vertex.pie.color = list(c("tomato", "gold", "skyblue")) +) +``` + +The `pie` shape draws a small pie chart per vertex; its slices, colours, shading +angle, shading density and line type are controlled by `vertex.pie`, +`vertex.pie.color`, `vertex.pie.angle`, `vertex.pie.density` and +`vertex.pie.lty`. The `raster` shape draws an image (defaulting to the igraph +logo); supply your own with `vertex.raster`. + +### Relative sizing + +By default `vertex.size` is in absolute units. Set `vertex.size.scaling = TRUE` +to size vertices relative to the plot region, controlled by `relative.size`: + +```{r} +plot( + make_star(8), + vertex.size.scaling = TRUE, + vertex.relative.size = c(0.02, 0.06) +) +``` + +## Vertex labels + +Labels default to vertex names (or IDs). They are positioned at a distance +`label.dist` from the vertex, in the direction `label.degree` (an angle in +radians), and styled with the usual font controls. `label.angle` rotates the +text and `label.adj` fine-tunes its anchor. + +```{r} +plot( + g, + vertex.size = 30, + vertex.label.color = "black", + vertex.label.family = "sans", + vertex.label.font = 2, # bold + vertex.label.cex = 1.1, + vertex.label.dist = 2.2, + vertex.label.degree = -pi / 2 # place labels above the vertices +) +``` + +`vertex.label.angle` rotates the label text (in degrees) and `vertex.label.adj` +adjusts its anchor, exactly like `srt` and `adj` in `graphics::text()`: + +```{r} +plot( + g, + vertex.size = 36, + vertex.label.angle = 30, + vertex.label.adj = c(0.5, 0.5) +) +``` + +### Avoiding overlaps with `label.repel` + +On dense graphs labels collide. Set `vertex.label.repel = TRUE` to iteratively +nudge overlapping labels apart, drawing a thin leader line from each moved label +back to its vertex: + +```{r} +gr <- make_full_graph(25) +V(gr)$name <- paste0("node-", seq_len(vcount(gr))) +plot(gr, vertex.size = 25, vertex.label.cex = 2, vertex.label.repel = TRUE) +``` + +### Legibility with `label.halo` + +`vertex.label.halo` draws a halo (outline) behind the label text so it stays +readable over edges and other vertices. `vertex.label.halo.width` controls the +outline thickness as a fraction of the label height. The default `NA` draws no +halo. + +```{r} +plot( + g, + vertex.size = 30, + vertex.color = "steelblue", + vertex.label.color = "white", + vertex.label.halo = "black", + vertex.label.halo.width = 0.3 +) +``` + +## Edges + +Edges are styled with colour, width and line type, and (for directed graphs) +arrowheads. `edge.arrow.mode` chooses the arrow direction (`0` none, `1` +backward, `2` forward, `3` both, or the symbolic `"<"`, `">"`, `"<>"`); it also +accepts `"a:attr"` to read the mode from a vertex attribute. + +```{r} +dg <- make_graph(~ A -+ B, B -+ C, C -+ A, A -+ D) +plot( + dg, + edge.color = "grey40", + edge.width = 2, + edge.lty = "dashed", + edge.arrow.size = 0.8, + edge.arrow.width = 1.2 +) +``` + +### Curved edges and multi-edge fans + +`edge.curved` bends edges: `TRUE` means curvature 0.5, numeric values set the +amount (negative bends the other way). For graphs with parallel edges, +`curve_multiple()` fans them apart so they don't overlap: + +```{r} +multi <- graph_from_edgelist( + matrix(c(1, 2, 1, 2, 1, 2, 2, 3), ncol = 2, byrow = TRUE) +) +plot(multi, edge.curved = curve_multiple(multi), edge.arrow.size = 0.6) +``` + +### Edge styles + +The `edge.style` argument selects an edge-routing style: + +* `"auto"` (default) — straight, or an arc when `edge.curved` is non-zero +* `"straight"` — always straight +* `"arc"` — a curved arc whose height comes from `edge.curved` +* `"elbow"` — a two-corner orthogonal (right-angle) connector +* `"diagonal"` — a smooth S-curve with axis-aligned ends + +The orthogonal and diagonal styles are well suited to tree- and grid-like +layouts: + +```{r fig.width = 7, fig.height = 3.2} +tree <- make_tree(7, children = 2) +op <- par(mfrow = c(1, 3), mar = c(1, 1, 2, 1)) +plot(tree, layout = layout_as_tree, edge.style = "arc", main = "arc") +plot(tree, layout = layout_as_tree, edge.style = "elbow", main = "elbow") +plot(tree, layout = layout_as_tree, edge.style = "diagonal", main = "diagonal") +par(op) +``` + +### Transparency and colour gradients + +`edge.alpha` sets edge opacity. `edge.gradient = TRUE` colours each edge with a +gradient running from the source vertex's colour to the target's — a way to show +direction without arrowheads: + +```{r} +ring_d <- make_ring(8, directed = TRUE) +V(ring_d)$color <- rainbow(vcount(ring_d)) +plot( + ring_d, + layout = layout_in_circle, + vertex.size = 18, + edge.gradient = TRUE, + edge.width = 4, + edge.arrow.mode = 0 +) +``` + +```{r} +plot(dg, edge.color = "navy", edge.alpha = 0.3, edge.width = 6) +``` + +### Self-loops + +Loops are drawn as small arcs. `edge.loop.angle` sets the direction of a loop +(in radians) and the plot-level `loop.size` scales them: + +```{r fig.width = 6, fig.height = 4} +# the formula interface drops self-loops, so build the edge list directly +loops <- make_graph(c("A", "A", "A", "B", "B", "B"), directed = TRUE) +plot( + loops, + layout = matrix(c(-0.5, 0.5, 0, 0), ncol = 2), + vertex.size = 36, + edge.loop.angle = pi / 2, + loop.size = 1.5, + edge.arrow.size = 0.6, + margin = 0.25 +) +``` + +When a single vertex carries *several* loops, igraph spreads them around the +vertex in a "flower-petal" arrangement so they don't overlap: + +```{r fig.width = 5, fig.height = 5} +# six self-loops on a single vertex +many <- make_graph(rep(1, 12), n = 1, directed = TRUE) +plot(many, vertex.size = 30, edge.arrow.size = 0.5, margin = 0.2) +``` + +## Edge labels + +Edge labels are added with `edge.label` and styled with the matching +`edge.label.*` controls, including a halo (`edge.label.halo`) just like vertex +labels. `edge.label.x` / `edge.label.y` override the automatic placement. + +```{r} +plot( + dg, + edge.label = c("a", "b", "c", "d"), + edge.label.color = "black", + edge.label.cex = 1.2, + edge.label.family = "sans", + edge.label.halo = "yellow", + edge.label.halo.width = 0.4, + edge.arrow.size = 0.6 +) +``` + +## Colours and palettes + +igraph provides perceptually sensible palettes: +`categorical_pal()` for unordered categories, `sequential_pal()` for ordered +magnitudes, `diverging_pal()` for signed data, and `r_pal()` for R's classic +palette. + +```{r fig.width = 8, fig.height = 2.4} +op <- par(mfrow = c(1, 4), mar = c(0, 1, 2, 1)) +pie(rep(1, 8), col = categorical_pal(8), main = "categorical_pal(8)") +pie(rep(1, 9), col = sequential_pal(9), main = "sequential_pal(9)") +pie(rep(1, 9), col = diverging_pal(9), main = "diverging_pal(9)") +pie(rep(1, 8), col = r_pal(8), main = "r_pal(8)") +par(op) +``` + +When a vertex/edge colour is given as a factor or integer, it indexes into the +active `palette`: + +```{r} +plot( + g, + vertex.color = as.factor(V(g)$gender), + palette = categorical_pal(8), + vertex.size = 30 +) +``` + +## Scales and legends + +Passing a raw colour vector means igraph never learns what the colours *mean*, +so it cannot explain them. The `scale_*()` helpers fix this: they map a data +column to an aesthetic **and record the mapping**, so `plot()` can draw a +matching guide automatically. + +`scale_color()` on a discrete variable produces a categorical legend: + +```{r} +plot( + g, + vertex.color = scale_color(V(g)$gender, name = "Gender"), + vertex.size = 30 +) +``` + +On a numeric variable it produces a continuous **colourbar**: + +```{r} +plot( + g, + vertex.color = scale_color(degree(g), name = "Degree"), + vertex.size = 30 +) +``` + +`scale_size()` maps a numeric variable to a size range and draws a size legend: + +```{r} +plot( + g, + vertex.size = scale_size(degree(g), range = c(10, 35), name = "Degree"), + vertex.color = "lightgrey" +) +``` + +The `legend` argument controls placement: `TRUE` (default, right side), +`"left"`, `"top"`, `"bottom"`, or `FALSE` to suppress the guide. Multiple scales +combine into one guide region: + +```{r} +plot( + g, + vertex.color = scale_color(V(g)$gender, name = "Gender"), + vertex.size = scale_size(degree(g), range = c(12, 34), name = "Degree"), + legend = "bottom" +) +``` + +## Decluttering with `label_top()` + +On large graphs, labelling every vertex is unreadable. `label_top()` keeps only +the highest-ranking labels by some metric and blanks the rest with `NA` (which +`plot()` omits): + +```{r} +big <- sample_gnp(40, 0.08) +plot( + big, + vertex.size = 6, + vertex.label = label_top(degree(big), n = 5), + vertex.label.dist = 1, + vertex.label.color = "black" +) +``` + +Use `prop` to keep a fraction instead of a count, and `decreasing = FALSE` to +keep the lowest: + +```{r} +plot( + big, + vertex.size = 6, + vertex.label = label_top(degree(big), prop = 0.1, decreasing = FALSE), + vertex.label.dist = 1, + vertex.label.color = "black" +) +``` + +To label everything above a fixed cutoff you don't need the helper — +`ifelse(metric > cutoff, labels, NA)` works directly. + +## Highlighting groups + +`mark.groups` draws a smoothed, filled polygon "under" a set of vertices. The +fill, border, smoothness, padding and border width are set by `mark.col`, +`mark.border`, `mark.shape`, `mark.expand` and `mark.lwd`: + +```{r} +plot( + g, + vertex.size = 25, + mark.groups = list(c("Alice", "Himari", "Moshe"), c("Nang", "Samira")), + mark.col = c("#ffd9d9", "#d9ecff"), + mark.border = c("tomato", "steelblue"), + mark.shape = 1 / 2, + mark.expand = 20, + mark.lwd = 2 +) +``` + +## Annotations and frame + +Plot-level titles and axes follow base graphics conventions: `main`, `sub`, +`xlab`, `ylab`, `axes` and `frame.plot`. + +```{r} +plot( + g, + vertex.size = 25, + main = "Friendship network", + sub = "an igraph example", + xlab = "x", + ylab = "y", + axes = TRUE, + frame.plot = TRUE +) +``` + +## Custom vertex shapes + +You can register your own shape with `add_shape()`. A shape is defined by a +*clip* function (where edges stop) and a *plot* function (how the vertex is +drawn); `shape_noclip` and `shape_noplot` are no-op building blocks, and +`shapes("circle")$clip` reuses an existing clipper. + +```{r} +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] + } + # When all vertices share a shape, the plot function is called once for the + # whole layout (with v = NULL), so recycle scalars to one value per row. + vertex.size <- rep(vertex.size, length.out = nrow(coords)) + symbols( + x = coords[, 1], y = coords[, 2], bg = vertex.color, + stars = cbind(vertex.size, vertex.size, vertex.size), + add = TRUE, inches = FALSE + ) +} + +add_shape("triangle", clip = shapes("circle")$clip, plot = mytriangle) + +plot( + g, + vertex.shape = "triangle", + vertex.color = "seagreen", + vertex.size = 40 +) +``` + +`shape_noclip` (edges run all the way to the centre) and `shape_noplot` (draw +nothing) are the no-op building blocks; registering a shape from them gives an +"invisible" vertex that still carries a label: + +```{r} +add_shape("blank", clip = shape_noclip, plot = shape_noplot) +plot(g, vertex.shape = "blank", vertex.label.dist = 0) +``` + +## Setting defaults {#setting-defaults} + +`igraph_options()` sets defaults for every subsequent plot, and `igraph_opt()` +reads one back. Plotting options use the same `vertex.` / `edge.` prefixes: + +```{r} +old <- igraph_options( + vertex.color = "orange", + vertex.size = 30, + edge.color = "grey70" +) +plot(g) +igraph_opt("vertex.color") # read one option back +igraph_options(old) # restore +``` + +The full precedence order, from highest to lowest, is: + +1. an explicit argument to `plot()`, +2. a vertex/edge/graph attribute, +3. an `igraph_options()` setting, +4. the built-in default. + +Setting `igraph_options(annotate.plot = TRUE)` makes `plot()` annotate the +figure with the graph's name and its vertex/edge counts by default. + +## SVG export with tooltips + +`as_svg()` renders a graph to a standalone SVG. Beyond crisp vector output, it +tags each vertex and edge with element IDs and a ``, so the SVG has +hover tooltips with **no JavaScript dependency**. The `tooltips` argument picks +which vertex attribute to use for the titles (defaulting to the vertex name). + +```{r} +svg <- as_svg(g, width = 6, height = 6, tooltips = "gender") +# write to a file ... +tmp <- tempfile(fileext = ".svg") +cat(svg, file = tmp) +substr(svg, 1, 60) +``` + +Each vertex becomes `<g id="vertex-N"><title>......` and each edge a +matching group, so downstream tools (or a browser) can offer hover and click +interactions. Embedded directly into this page, hovering a vertex shows its +tooltip: + +```{r results = "asis", echo = TRUE} +cat(as_svg(g, width = 6, height = 6, tooltips = "gender", vertex.size = 30)) +``` + +## Where to go next + +* The per-function reference: , in particular + `?plot.igraph` and `?igraph.plotting` for the full list of parameters. +* The main [igraph](igraph.html) introduction for graph construction and + analysis. diff --git a/vignettes/igraph.Rmd b/vignettes/igraph.Rmd index 6682bc6b3fc..b32a7486f3e 100644 --- a/vignettes/igraph.Rmd +++ b/vignettes/igraph.Rmd @@ -576,46 +576,9 @@ plot(g, This latter approach is preferred if you want to keep the properties of the visual representation of your graph separate from the graph itself. -In summary, there are special vertex and edge properties that correspond to the visual representation of the graph. These attributes override the default settings of igraph (i.e color, weight, name, shape, layout, etc.). The following two tables summarise the most frequently used visual attributes for vertices and edges, respectively: - -### Vertex attributes controlling graph plots - -| Attribute name | Keyword argument | Purpose | -|----------------------|----------------------|-----------------------------| -| `color` | `vertex.color` | Color of the vertex | -| `label` | `vertex.label` | Label of the vertex. They will be converted to character. Specify NA to omit vertex labels. The default vertex labels are the vertex ids. | -| `label.cex` | `vertex.label.cex` | Font size of the vertex label, interpreted as a multiplicative factor, similarly to R's `text` function | -| `label.color` | `vertex.label.color` | Color of the vertex label | -| `label.degree` | `vertex.label.degree` | It defines the position of the vertex labels, relative to the center of the vertices. It is interpreted as an angle in radian, zero means 'to the right', and 'pi' means to the left, up is -pi/2 and down is pi/2. The default value is -pi/4 | -| `label.dist` | `vertex.label.dist` | Distance of the vertex label from the vertex itself, relative to the vertex size | -| `label.family` | `vertex.label.family` | Font family of the vertex, similarly to R's `text` function | -| `label.font` | `vertex.label.font` | Font within the font family of the vertex, similarly to R's `text` function | -| `shape` | `vertex.shape` | The shape of the vertex, currently "circle", "square", "csquare", "rectangle", "crectangle", "vrectangle", "pie" (see vertex.shape.pie), 'sphere', and "none" are supported, and only by the plot.igraph command. | -| `size` | `vertex.size` | The size of the vertex, a numeric scalar or vector, in the latter case each vertex sizes may differ | - -### Edge attributes controlling graph plots - -| Attribute name | Keyword argument | Purpose | -|-------------------------|-----------------------------|------------------| -| `color` | `edge.color` | Color of the edge | -| `curved` | `edge.curved` | A numeric value specifies the curvature of the edge; zero curvature means straight edges, negative values means the edge bends clockwise, positive values the opposite. TRUE means curvature 0.5, FALSE means curvature zero | -| `arrow.size` | `edge.arrow.size` | Currently this is a constant, so it is the same for every edge. If a vector is submitted then only the first element is used, that is to say if this is taken from an edge attribute then only the attribute of the first edge is used for all arrows. | -| `arrow.width` | `edge.arrow.width` | The width of the arrows. Currently this is a constant, so it is the same for every edge | -| `width` | `edge.width` | Width of the edge in pixels | -| `label` | `edge.label` | If specified, it adds a label to the edge. | -| `label.cex` | `edge.label.cex` | Font size of the edge label, interpreted as a multiplicative factor, similarly to R's `text` function | -| `label.color` | `edge.label.color` | Color of the edge label | -| `label.family` | `edge.label.family` | Font family of the edge, similarly to R's `text` function | -| `label.font` | `edge.label.font` | Font within the font family of the edge, similarly to R's `text` function | - -### Generic arguments of `plot()` - -These settings can be specified as arguments to the `plot` function to control the overall appearance of the plot. - -| Keyword argument | Purpose | -|--------------------------------|----------------------------------------| -| `layout` | The layout to be used. It can be an instance of `Layout`, a list of tuples containing X-Y coordinates, or the name of a layout algorithm. The default is `auto`, which selects a layout algorithm automatically based on the size and connectedness of the graph. | -| `margin` | The amount of empty space below, over, at the left and right of the plot, it is a numeric vector of length four. | +In summary, there are special vertex and edge properties that correspond to the visual representation of the graph. These attributes override the default settings of igraph (i.e color, weight, name, shape, layout, etc.). + +igraph's plotting engine is far richer than this short overview: vertex shapes, colour scales with automatic legends, edge styles and gradients, label repelling and halos, group highlighting, custom shapes and SVG export are all supported. For a complete, runnable tour of every plotting feature, see the [**Plotting graphs**](plotting.html) article. ## igraph and the outside world