Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: autograph
Title: Automatic Plotting and Theming of Many Graphs
Version: 1.2.1
Version: 1.2.2
Description: Visual exploration and presentation of networks should not be difficult.
This package includes functions for plotting networks and network-related metrics with sensible and pretty defaults.
It includes 'ggplot2'-based plot methods for many popular network package classes.
Expand Down
14 changes: 14 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# autograph 1.2.2

## Package

- Shortened the startup message of `.onAttach()`

## Graphing

- Improved how `node_color=` colours numeric node variables

## Tests

- Fixed the arc strength test depending on the BLAS

# autograph 1.2.1

## Layouts
Expand Down
21 changes: 19 additions & 2 deletions R/graph_aes.R
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,21 @@
if (!node_color %in% manynet::net_node_attributes(g)) return(NULL)
vals <- manynet::node_attribute(g, node_color)
if ("node_mark" %in% class(vals))
factor(as.character(vals), levels = c("FALSE", "TRUE")) else
as.factor(as.character(vals))
return(factor(as.character(vals), levels = c("FALSE", "TRUE")))
if (.is_continuous(vals)) return(as.numeric(vals))
as.factor(as.character(vals))
}

# A measure such as coreness or centrality has an order and a distance between
# its values that a set of categories does not, so it is drawn as a gradient
# rather than given one colour for each value it takes. Marks are logical and
# memberships are characters, so neither of them reaches this.
# Two values are a contrast rather than a gradient, and are left to the
# categorical palette, which draws such a pair in the theme's base and
# highlight colours anyway.
.is_continuous <- function(vals) {
if (!is.numeric(vals) || is.factor(vals)) return(FALSE)
length(unique(stats::na.omit(as.numeric(vals)))) > 2
}

# `levels` holds the categories that `graphs()` found across all of its panels.
Expand All @@ -179,6 +192,9 @@
.infer_ncolor <- function(g, node_color, levels = NULL) {
vals <- .ncolor_values(g, node_color)
if (is.null(vals)) return(if (!is.null(node_color)) node_color else ag_ink())
# A gradient has no categories to hold level for level across the panels of a
# `graphs()` plot; its panels are held together by a shared range instead.
if (is.numeric(vals)) return(vals)
if (!is.null(levels)) return(factor(as.character(vals), levels = levels))
if (length(unique(vals)) == 1) {
.inform_constant_color("node_color", node_color, "node")
Expand Down Expand Up @@ -352,6 +368,7 @@
.shared_range(gather(function(g) .infer_nsize(g, node_size, layout))),
ecolor = .shared_levels(gather(function(g) .ecolor_values(g, edge_color))),
ncolor = .shared_levels(gather(function(g) .ncolor_values(g, node_color))),
ncolor_range = .shared_range(gather(function(g) .ncolor_values(g, node_color))),
nshape = .shared_levels(gather(function(g) .nshape_values(g, node_shape))),
diffusion = .shared_levels(gather(.diffusion_states)),
nadopt = .shared_range(gather(.finite_adoption_time)))
Expand Down
74 changes: 48 additions & 26 deletions R/graph_legends.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,54 @@ graph_legends <- function(p, g,
node_color = NULL, node_shape = NULL, node_size = NULL,
edge_color = NULL, edge_size = NULL) {
.check_legend_size(g, node_color, node_shape, edge_color)
p +
ggplot2::guides(fill = ggplot2::guide_legend(order = 1,
title = ifelse(is.null(node_color),
"Color", node_color),
override.aes = list(shape = 21)),
color = ggplot2::guide_legend(order = 2),
shape = ggplot2::guide_legend(order = 3,
title = ifelse(is.null(node_shape),
ifelse(manynet::is_twomode(g), "Mode", "Shape"),
node_shape)),
size = ggplot2::guide_legend(order = 4,
title = ifelse(is.null(node_size),
"Size", node_size)),
linetype = ggplot2::guide_legend(order = 5),
# `.infer_ecolor_title()` decides this alongside the colours
# themselves in R/graph_aes.R, so that the two cannot
# disagree about what the colour is showing, as they did
# when this said "Sign" over colours that showed layers.
edge_colour = ggplot2::guide_legend(
order = 6, title = .infer_ecolor_title(g, edge_color)),
edge_size = ggplot2::guide_legend(order = 7,
title = ifelse(is.null(edge_size),
ifelse(manynet::is_weighted(g), "Weight", "Size"),
edge_size)),
alpha = ggplot2::guide_legend(order = 99,
override.aes = list( alpha = 0, size = 0, shape = NA )))
# A guide set here wins over the one the scale asked for, so a continuous
# fill -- a measure drawn as a gradient, or a time of adoption -- keeps the
# colourbar its scale gave it rather than being broken into keys. Where the
# user named the attribute, the bar is titled with it; otherwise the title
# the scale set is left alone.
fill_guide <- if (.fill_is_continuous(p)) {
if (is.null(node_color)) NULL else
ggplot2::guide_colourbar(order = 1, title = node_color)
} else {
ggplot2::guide_legend(order = 1,
title = ifelse(is.null(node_color),
"Color", node_color),
override.aes = list(shape = 21))
}
guides <- list(fill = fill_guide,
color = ggplot2::guide_legend(order = 2),
shape = ggplot2::guide_legend(order = 3,
title = ifelse(is.null(node_shape),
ifelse(manynet::is_twomode(g), "Mode", "Shape"),
node_shape)),
size = ggplot2::guide_legend(order = 4,
title = ifelse(is.null(node_size),
"Size", node_size)),
linetype = ggplot2::guide_legend(order = 5),
# `.infer_ecolor_title()` decides this alongside the colours
# themselves in R/graph_aes.R, so that the two cannot
# disagree about what the colour is showing, as they did
# when this said "Sign" over colours that showed layers.
edge_colour = ggplot2::guide_legend(
order = 6, title = .infer_ecolor_title(g, edge_color)),
edge_size = ggplot2::guide_legend(order = 7,
title = ifelse(is.null(edge_size),
ifelse(manynet::is_weighted(g), "Weight", "Size"),
edge_size)),
alpha = ggplot2::guide_legend(order = 99,
override.aes = list( alpha = 0, size = 0, shape = NA )))
p + do.call(ggplot2::guides, guides[!vapply(guides, is.null, logical(1))])
}

# Whether the plot's fill scale maps a continuous variable. Read from the
# scale the plot already carries rather than from the network, so that every
# gradient -- node colour, time of adoption -- is treated the same way.
.fill_is_continuous <- function(p) {
scales <- p[["scales"]][["scales"]]
if (!length(scales)) return(FALSE)
any(vapply(scales, function(s)
"fill" %in% s[["aesthetics"]] && inherits(s, "ScaleContinuous"),
logical(1)))
}

# A legend is read by matching a key against a mark, and a reader cannot hold
Expand Down
28 changes: 19 additions & 9 deletions R/graph_nodes.R
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,25 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size,
# Named values, shared limits and `drop = FALSE` for the same reason as the
# edge colours in R/graph_edges.R: a category keeps its colour and its key
# in every panel of a `graphs()` plot.
nlevels <- shared[["ncolor"]]
if (is.null(nlevels)) nlevels <- unique(as.character(out[["ncolor"]]))
if (length(nlevels) > 1){
nvalues <- if (length(nlevels) == 2)
getOption("snet_highlight", default = c("grey","black")) else
ag_qualitative(length(nlevels))
p <- p + ggplot2::scale_fill_manual(
values = stats::setNames(nvalues, nlevels), limits = nlevels,
drop = FALSE, guide = ggplot2::guide_legend(node_color))
if (is.numeric(out[["ncolor"]])) {
# A measure is drawn as a gradient from the theme's base colour to its
# highlight, so that the order of its values can be read off the plot.
# The limits are shared across the panels of a `graphs()` plot, so that
# one value keeps one colour throughout.
p <- p + ggplot2::scale_fill_gradientn(
colours = ag_sequential(9), limits = shared[["ncolor_range"]],
guide = ggplot2::guide_colourbar(title = node_color))
} else {
nlevels <- shared[["ncolor"]]
if (is.null(nlevels)) nlevels <- unique(as.character(out[["ncolor"]]))
if (length(nlevels) > 1){
nvalues <- if (length(nlevels) == 2)
getOption("snet_highlight", default = c("grey","black")) else
ag_qualitative(length(nlevels))
p <- p + ggplot2::scale_fill_manual(
values = stats::setNames(nvalues, nlevels), limits = nlevels,
drop = FALSE, guide = ggplot2::guide_legend(node_color))
}
}
}
# Consider rescaling nodes
Expand Down
4 changes: 4 additions & 0 deletions R/graphr.R
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@
#' @param node_color,node_colour Node variable to be used for coloring the nodes.
#' It is easiest if this is added as a node attribute to
#' the graph before plotting.
#' A categorical variable gives one colour to each category.
#' A measure, such as a centrality or coreness score, is drawn instead as a
#' gradient from the theme's base colour to its highlight colour,
#' with a colourbar in place of the legend.
#' Nodes can also be colored by declaring a color instead.
#' @param node_group Node variable to be used for grouping the nodes.
#' It is easiest if this is added as a hull over
Expand Down
15 changes: 15 additions & 0 deletions R/grapht.R
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,17 @@ print.grapht <- function(x, ...) {
}
if (!is.null(node_color) &&
node_color %in% manynet::net_node_attributes(waves[[1]])) {
# A measure is drawn as a gradient, as it is in `graphr()`; see
# `.is_continuous()` in R/graph_aes.R. Read over all of the waves at once,
# so that one value keeps one colour from frame to frame.
if (.is_continuous(unlist(lapply(waves, function(w)
manynet::node_attribute(w, node_color))))) {
vals <- vapply(waves, function(w)
as.numeric(manynet::node_attribute(w, node_color)),
numeric(igraph::vcount(waves[[1]])))
return(list(mapped = TRUE, diffusion = FALSE, continuous = TRUE,
values = vals))
}
vals <- vapply(waves, function(w)
as.character(manynet::node_attribute(w, node_color)),
character(igraph::vcount(waves[[1]])))
Expand Down Expand Up @@ -784,6 +795,10 @@ print.grapht <- function(x, ...) {
name = NULL,
values = c("Infected" = cols[1], "Susceptible" = cols[2],
"Exposed" = cols[3], "Recovered" = cols[4]))
} else if (ncolor_mapped && is.numeric(nodes_out$ncolor)) {
p <- p + ggplot2::scale_fill_gradientn(colours = ag_sequential(9),
guide = ggplot2::guide_colourbar(
title = node_color))
} else if (ncolor_mapped) {
nlevels <- length(unique(nodes_out$ncolor))
if (nlevels == 2) {
Expand Down
41 changes: 35 additions & 6 deletions R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,43 @@
if (!interactive()) return()

local_version <- utils::packageVersion("autograph")
snet_info("You are using {.auto autograph} version {.version {local_version}}.")
snet_info(c("i" = "Theme set to {.code {getOption('stocnet_theme')}}. Use {.fn stocnet_theme} to change the theme."))
snet_info("You are using {.auto autograph} version {.version {local_version}},",
"with theme {.code {getOption('stocnet_theme')}}.")

# Only after the interactive() guard above: a script or a check run should
# never reach into the IDE, whatever is remembered.
if (isTRUE(read_pref("completion")) && .completion_activate())
snet_info("Completion of argument values is on. Use {.fn stocnet_completion} to switch it off.")
}
# nocov end
completion_on <- isTRUE(read_pref("completion")) && .completion_activate()

# One short status line. The theme is always there, because there is always a
# theme. The medium and the completion appear only when they are away from
# their defaults, so the common case stays to a few words. What is left out of
# the status line can still be reached through a tip below.
greet_startup_cli <- function() {
medium <- getOption("stocnet_medium")
status <- paste0("Theme {.code ", getOption("stocnet_theme"), "}")
if (!identical(medium, "screen"))
status <- paste0(status, ", medium {.code ", medium, "}")
if (completion_on) status <- paste0(status, ", completion {.code on}")
# snet_info(c("i" = paste0(status, ".")))

tips <- c(
"i" = "Change the theme with {.run [stocnet_theme()](autograph::stocnet_theme())}.",
"i" = "Keep a theme for later sessions with {.code stocnet_theme(persist = TRUE)}.",
"i" = "Set output medium with {.run [stocnet_medium()](autograph::stocnet_medium())}. Currently {.code getOption('stocnet_medium')}",
"i" = "Autocomplete arguments with {.run [stocnet_completion()](autograph::stocnet_completion())}."
# "i" = "Share bugs, issues, or feature requests at {.url https://github.com/stocnet/autograph/issues}.",
# "i" = "Explore changes since the last version with {.run [news(package = 'autograph')](utils::news(package = 'autograph'))}.",
# "i" = "Visit {.url https://stocnet.github.io/autograph/} to learn more.",
# "i" = "Discover new functions at {.url https://stocnet.github.io/autograph/reference/index.html}.",
# "i" = "Discover {.emph stocnet} R packages at {.url https://github.com/stocnet/}."
)
# Do not offer to switch on what is already on, or to set what the status
# line already reports.
if (completion_on) tips <- tips[-4]
if (!identical(medium, "screen")) tips <- tips[-3]
snet_info(sample(tips, 1))
}

greet_startup_cli()
}
# nocov end
55 changes: 5 additions & 50 deletions cran-comments.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,12 @@
## Resubmission

This is a resubmission of 1.2.0, which failed the incoming checks with a test error
on Windows and on Debian. Both are fixed here.

* `layout_valence()` started its nodes at random points, and a start that placed two
nodes close together gave a force large enough to spoil the layout. The nodes now
start on a circle, the force is bounded, and the test sets a seed.
* On Windows, the tutorial test failed on a deprecation warning that the tutorial code
does not raise itself: `netrics::tie_by_closeness()` (0.4.1) calls
`manynet::to_ties()`, which manynet deprecates in 2.3.0. netrics 1.0.0, to be
submitted, calls the current function. The test now fails only if the tutorial calls
a deprecated function itself, so it passes with either netrics version.

The full test suite passes with each of these three pairs: manynet 2.2.3 with
netrics 0.4.0, manynet 2.3.1 with netrics 0.4.1 (the pair that failed on Windows),
and manynet 2.3.0 with netrics 1.0.0. manynet 2.3.1 and netrics 1.0.0 are to be
submitted; this version does not require either of them.

## Test environments

* local R installation, aarch64-apple-darwin20, R 4.5.1
* macOS 14.7.6 (on Github), R 4.5.1
* Microsoft Windows Server 2022 10.0.20348 (on Github), R 4.5.1
* Ubuntu 24.04.2 (on Github), R 4.5.1
* local R installation, aarch64-apple-darwin23, R 4.6.1
* macOS 26.5.2 (on GitHub), R 4.6.1
* Microsoft Windows Server 2022 10.0.26100 (on GitHub), R 4.6.1
* Ubuntu 24.04.4 (on GitHub), R 4.6.1

## R CMD check results

0 errors | 0 warnings | 0 notes

## User filespace

This version adds an optional `persist` argument to `stocnet_theme()`. When, and only when, a user
passes `persist = TRUE`, the chosen theme is written to `tools::R_user_dir("autograph", "config")`.
Nothing is written on load, on attach, or by any default code path, and the package is fully
functional if the directory is absent or unwritable. No other location is written to.

## Backward/forward compatibility

This version works and tests alongside both manynet 2.2.3 and 2.3.0.
manynet 2.3.0 ships several networks in a list-based class,
and spells a layer as the tie attribute "layer" rather than "type" and
a sign as a negative weight rather than as a "sign".
Which tie attribute records the layer is now read from the network,
so a multiplex network is still coloured by layer under either spelling.
Signs are read through `manynet::tie_signs()`, in `graphr()` and in `layout_valence()`,
rather than from a "sign" tie attribute.
Attribute names are read through `manynet::net_node_attributes()`/`net_tie_attributes()`,
which accept a network in either class.
`layout_concentric()`, `layout_multilevel()` and `layout_lineage()` coerce what they are given,
as the other layouts already did.
`grapht()` and `graphs()` fall back to `manynet::to_times()` where `to_waves()`
cannot split a network, which covers a panel recording its waves as "time"
(e.g. `ison_monks`) and a diffusion result.
Each guard tests for the function or attribute rather than for the manynet version,
so a development build is treated by what it offers.

This fixes the 'Additional issues' relating to a test depending on BLAS
4 changes: 4 additions & 0 deletions man/plot_graphr.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions man/plot_grapht.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading