Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1681238
Initial plan
Copilot Feb 19, 2026
5f11a57
Change node aesthetics from color to fill
Copilot Feb 19, 2026
2017b4e
Use fillable shape codes (21-25) for nodes to work with fill aesthetic
Copilot Feb 19, 2026
c717ab5
Fix test failures: update for fill aesthetic and numeric shapes
Copilot Feb 19, 2026
0f53aee
Improve code coverage: add fill aesthetic tests and nocov markers
Copilot Feb 19, 2026
ea3da1f
Replace Rgraphviz with igraph::layout_with_sugiyama for Sugiyama layouts
Copilot Feb 24, 2026
8bc7164
Improve test name clarity for hierarchy layout test
Copilot Feb 24, 2026
ef2b3dd
Improve Sugiyama layout with dummy nodes and barycenter crossing mini…
Copilot Feb 24, 2026
9766630
Add clarifying comment for 0-based layer indexing
Copilot Feb 24, 2026
ac90d2e
Separated isolates where they complicate graphing
jhollway Feb 26, 2026
a68b5c1
Added legend option for annotating isolates
jhollway Feb 26, 2026
05ee30b
Added caption option for annotating isolates
jhollway Feb 26, 2026
eb3d70f
Changed legend labelling for Sign and Mode
jhollway Feb 26, 2026
20fc867
Closed #12 by using fill shapes
jhollway Feb 26, 2026
fd1e40c
Updated a number of outdated tests
jhollway Feb 26, 2026
203ff30
graphr testing now cycles through all data objects, excluding multi-o…
jhollway Feb 27, 2026
f6ddbf6
Moved isolate inference to helper
jhollway Feb 27, 2026
2817ad1
Consolidated all legend modifications to graph_legends()
jhollway Feb 27, 2026
3d17ee9
If graphr() passed a list of graphs, it will instead use graphs()
jhollway Feb 27, 2026
00db4f0
graphs() no longer requires patchwork, because this is now a regular …
jhollway Feb 27, 2026
89d1b33
#minor bump
jhollway Feb 27, 2026
c6e28f5
Updated logo
jhollway Feb 27, 2026
f868ebd
Initial plan
Copilot Feb 27, 2026
13aa438
Update NEWS.md for v0.6.0 with all PR changes
Copilot Feb 27, 2026
dc4b477
Updated news and tests
jhollway Feb 27, 2026
1407362
Fixed check errors
jhollway Feb 27, 2026
eb60ac5
Removed scales from the package as unnecessary with current theming
jhollway Feb 27, 2026
84cc664
Improved snap performance for lattices
jhollway Feb 27, 2026
d3b9571
Dropped scale_*() functions for each institution as redundant
jhollway Feb 27, 2026
908ac75
Closed #18 by correcting rotation for lattice snapping, still unique …
jhollway Feb 27, 2026
8192f84
No more palette gen
jhollway Feb 27, 2026
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
6 changes: 2 additions & 4 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: autograph
Title: Automatic Plotting and Theming of Many Graphs
Version: 0.5.1
Date: 2025-12-08
Version: 0.6.0
Comment thread
jhollway marked this conversation as resolved.
Date: 2026-02-27
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 All @@ -25,7 +25,6 @@ Imports:
patchwork,
tidygraph
Suggests:
BiocManager,
gganimate,
ggforce (>= 0.5.0),
gifski,
Expand All @@ -34,7 +33,6 @@ Suggests:
testthat (>= 3.0.0)
Enhances:
ergm,
Rgraphviz,
RSiena
Authors@R:
c(person(given = "James",
Expand Down
30 changes: 0 additions & 30 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -79,37 +79,7 @@ export(layout_tetrad)
export(layout_triad)
export(layout_valence)
export(match_color)
export(scale_color_centres)
export(scale_color_ethz)
export(scale_color_iheid)
export(scale_color_rug)
export(scale_color_sdgs)
export(scale_color_uzh)
export(scale_colour_centres)
export(scale_colour_ethz)
export(scale_colour_hue)
export(scale_colour_iheid)
export(scale_colour_rug)
export(scale_colour_sdgs)
export(scale_colour_uzh)
export(scale_edge_color_centres)
export(scale_edge_color_ethz)
export(scale_edge_color_iheid)
export(scale_edge_color_rug)
export(scale_edge_color_sdgs)
export(scale_edge_color_uzh)
export(scale_edge_colour_centres)
export(scale_edge_colour_ethz)
export(scale_edge_colour_iheid)
export(scale_edge_colour_rug)
export(scale_edge_colour_sdgs)
export(scale_edge_colour_uzh)
export(scale_fill_centres)
export(scale_fill_ethz)
export(scale_fill_iheid)
export(scale_fill_rug)
export(scale_fill_sdgs)
export(scale_fill_uzh)
export(scale_size)
export(scale_x_continuous)
export(set_stocnet_theme)
Expand Down
31 changes: 31 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# autograph 0.6.0

## Package

- Updated logo

## Graphing

- `graphr()` now auto-dispatches to `graphs()` when passed a list of graphs
- `graphs()` no longer requires `{patchwork}` to be loaded separately
- Closed #12 by changing node aesthetics from `color` to `fill` using fillable shape codes (21–25) for nodes to support fill-based colour scales
- Consolidated legend modifications into internal `graph_legends()` helper
- Improved legend labelling:
- edge sign legend now labelled "Sign"
- edge weight/width now labelled "Weight"/"Width"
- node mode/shape legend now labelled "Mode"/"Shape"
- Closed #8 by adding "isolates" argument to `graphr()`:
- `"legend"` (default) removes isolates from the graph but notes them in the legend
- `"caption"` removes isolates from the graph but notes them in the caption
- `"keep"` retains isolates in the graph as-is (closes #12)

## Theming

- Removed `scale_*()` functions as redundant with better theme support and `match_color()`

## Layouts

- Replaced `{Rgraphviz}` (Bioconductor) with `igraph::layout_with_sugiyama` for Sugiyama/hierarchy layouts, removing the Bioconductor dependency
- Improved Sugiyama layout with dummy node insertion and barycenter crossing minimisation for better edge routing
- Fixed #18 for lattice layout snapping by rotating the layout to optimise edge verticality and horizontality

# autograph 0.5.1

## Plotting
Expand Down
11 changes: 2 additions & 9 deletions R/autograph_utilities.R
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# nocov start
# Helper function for checking and downloading packages
thisRequires <- function(pkgname){
if (!requireNamespace(pkgname, quietly = TRUE) & interactive()) {
Expand All @@ -9,15 +10,7 @@ thisRequires <- function(pkgname){
}
}
}

thisRequiresBio <- function(pkgname) {
if (!requireNamespace(pkgname, quietly = TRUE) & interactive()) {
if(utils::askYesNo(msg = paste("The", pkgname,
"package is required to run this function. Would you like to install", pkgname, "from BioConductor?"))) {
thisRequires("BiocManager")
BiocManager::install(pkgname)
}}
}
# nocov end

seq_nodes <- function(.data){
seq.int(manynet::net_nodes(.data))
Expand Down
6 changes: 3 additions & 3 deletions R/graph_edges.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ graph_edges <- function(p, g, edge_color, edge_size, node_size) {
guide = ggplot2::guide_legend(
ifelse(is.null(edge_size) &
manynet::is_weighted(g),
"Edge Weight", "Edge Size")))
"Weight", "Width")))
if (length(unique(out[["ecolor"]])) == 1) {
p <- p + ggplot2::guides(edge_colour = "none")
} else if (length(unique(out[["ecolor"]])) == 2){
p <- p + ggraph::scale_edge_colour_manual(values = getOption("snet_highlight", default = c("grey","black")),
guide = ggplot2::guide_legend(
ifelse(is.null(edge_color) &
manynet::is_signed(g),
"Edge Sign", edge_color)))
"Sign", edge_color)))
} else p <- p + ggraph::scale_edge_colour_manual(values = ag_qualitative(length(unique(out[["ecolor"]]))),
guide = ggplot2::guide_legend(
ifelse(is.null(edge_color) &
manynet::is_signed(g),
"Edge Sign", edge_color)))
"Sign", edge_color)))
p
}

Expand Down
17 changes: 10 additions & 7 deletions R/graph_labels.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ graph_labels <- function(p, g, layout) {
if (layout == "circle" | layout == "concentric") {
angles <- as.data.frame(.cart2pol(as.matrix(p[["data"]][,1:2])))
angles$degree <- angles$phi * 180/pi
angles <- dplyr::case_when(p[["data"]][,2] == 0 & p[["data"]][,1] == 0 ~ 0.1,
p[["data"]][,2] >= 0 & p[["data"]][,1] > 0 ~ angles$degree,
p[["data"]][,2] < 0 & p[["data"]][,1] > 0 ~ angles$degree,
p[["data"]][,1] == 1 ~ angles$degree,
# Extract x and y as vectors for case_when
x_coord <- p[["data"]][[1]]
y_coord <- p[["data"]][[2]]
angles <- dplyr::case_when(y_coord == 0 & x_coord == 0 ~ 0.1,
y_coord >= 0 & x_coord > 0 ~ angles$degree,
y_coord < 0 & x_coord > 0 ~ angles$degree,
x_coord == 1 ~ angles$degree,
TRUE ~ angles$degree - 180)
if (manynet::net_nodes(g) < 10) {
hj <- ifelse(p[["data"]][,1] >= 0, -0.8, 1.8)
hj <- ifelse(x_coord >= 0, -0.8, 1.8)
} else if (manynet::net_nodes(g) < 20) {
hj <- ifelse(p[["data"]][,1] >= 0, -0.4, 1.4)
hj <- ifelse(x_coord >= 0, -0.4, 1.4)
} else {
hj <- ifelse(p[["data"]][,1] >= 0, -0.2, 1.2)
hj <- ifelse(x_coord >= 0, -0.2, 1.2)
}
p <- p + ggraph::geom_node_text(ggplot2::aes(label = name), repel = TRUE,
family = ag_font(),
Expand Down
45 changes: 40 additions & 5 deletions R/graph_layout.R
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,50 @@ graph_layout <- function(g, layout, labels, node_group, snap, ...) {
}
if(snap){
manynet::snet_info("Snapping layout coordinates to grid.")
if(grepl("lattice",
igraph::graph_attr(attr(p$data, "graph"), "grand")$name,
ignore.case = TRUE))
p$data[,c("x","y")] <- round(p$data[,c("x","y")])
else p$data[,c("x","y")] <- depth_first_recursive_search(p)
if(grepl("lattice", manynet::net_name(g), ignore.case = TRUE)){

angles <- seq(0, pi/2, length.out = 180)
scores <- sapply(angles, function(a) {
lay2 <- .rotate_layout(lo, a)
.edge_angle_deviation(lay2, g)
})

best_angle <- angles[which.min(scores)]
rotated_coords <- .rotate_layout(lo, best_angle)
# Make sure that the coordinates, if rounded to integers, are still unique
p$data[,c("x","y")] <- round(rotated_coords[,c("x","y")])
} else p$data[,c("x","y")] <- depth_first_recursive_search(p)
}
# Add background ----
if(getOption("snet_background", default = "#FFFFFF")!="#FFFFFF")
p <- p + ggplot2::theme(panel.background = ggplot2::element_rect(fill = getOption("snet_background",
default = "#FFFFFF")))
p
}

# Helper functions ----

.rotate_layout <- function(layout, angle) {
rot <- matrix(c(cos(angle), -sin(angle),
sin(angle), cos(angle)), ncol = 2)
coords <- as.matrix(layout[, c("x", "y")])
newcoords <- coords %*% rot
layout$x <- newcoords[,1]
layout$y <- newcoords[,2]
layout
}

.edge_angle_deviation <- function(layout, graph) {
ed <- igraph::as_edgelist(graph)
dx <- layout$x[ed[,2]] - layout$x[ed[,1]]
dy <- layout$y[ed[,2]] - layout$y[ed[,1]]
ang <- atan2(dy, dx)

# deviation from nearest multiple of 90°
dev <- abs((ang %% (pi/2)) - pi/4)
mean(dev)
}




28 changes: 28 additions & 0 deletions R/graph_legends.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

graph_legends <- function(p, g,
node_color = NULL, node_shape = NULL, node_size = NULL,
edge_color = NULL, edge_size = NULL) {
p +
ggplot2::guides(fill = ggplot2::guide_legend(order = 1,
title = ifelse(is.null(node_color),
"Color", node_color)),
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),
edge_colour = ggplot2::guide_legend(order = 6,
title = ifelse(is.null(edge_color),
ifelse(manynet::is_signed(g), "Sign", "Color"),
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 )))
}
72 changes: 43 additions & 29 deletions R/graph_nodes.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) {
p <- p + ggplot2::guides(size = ggplot2::guide_legend(title = node_size))
if (length(unique(out[["nshape"]])) > 1)
p <- p + ggplot2::guides(shape = ggplot2::guide_legend(
title = ifelse(manynet::is_twomode(g) & is.null(node_shape), "Node Mode", "Node Shape")))
title = ifelse(manynet::is_twomode(g) & is.null(node_shape),
"Mode", node_shape)))
if (length(unique(out[["ncolor"]])) > 1){
if(length(unique(out[["ncolor"]])) == 2){
p <- p + ggplot2::scale_colour_manual(values = getOption("snet_highlight", default = c("grey","black")),
p <- p + ggplot2::scale_fill_manual(values = getOption("snet_highlight",
default = c("grey","black")),
guide = ggplot2::guide_legend(node_color))
} else {
p <- p + ggplot2::scale_colour_manual(values = ag_qualitative(length(unique(out[["ncolor"]]))),
p <- p + ggplot2::scale_fill_manual(values = ag_qualitative(length(unique(out[["ncolor"]]))),
guide = ggplot2::guide_legend(node_color))
}
}
}
# Consider rescaling nodes
p <- p + ggplot2::scale_size(range = c(1/manynet::net_nodes(g)*50, 1/manynet::net_nodes(g)*100))
p <- p + ggplot2::scale_size(range = c(1/manynet::net_nodes(g)*50,
1/manynet::net_nodes(g)*100))
p
}

Expand Down Expand Up @@ -56,7 +59,7 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) {
} else if (is_twomode(g) & is.null(node_shape)) {
out <- ifelse(igraph::V(g)$type, "One", "Two")
} else {
out <- "circle"
out <- 21 # Use fillable circle shape (was "circle")
}
out
}
Expand Down Expand Up @@ -104,9 +107,9 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) {
"R" ~ "Recovered",
"S" ~ "Susceptible")
cols <- match_color(c("#d73027", "#4575b4", "#E6AB02", "#66A61E"))
p + ggraph::geom_node_point(ggplot2::aes(color = node_color),
p + ggraph::geom_node_point(ggplot2::aes(fill = node_color),
size = out[["nsize"]], shape = out[["nshape"]]) +
ggplot2::scale_color_manual(name = NULL, guide = ggplot2::guide_legend(""),
ggplot2::scale_fill_manual(name = NULL, guide = ggplot2::guide_legend(""),
values = c("Infected" = cols[1],
"Susceptible" = cols[2],
"Exposed" = cols[3],
Expand All @@ -121,9 +124,9 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) {
node_color <- ifelse(is.infinite(node_adopts),
max(node_adopts[!is.infinite(node_adopts)]) + 1,
node_adopts)
p + ggraph::geom_node_point(ggplot2::aes(shape = nshape, color = node_color),
p + ggraph::geom_node_point(ggplot2::aes(shape = nshape, fill = node_color),
size = out[["nsize"]]) +
ggplot2::scale_color_gradient(low = match_color("#d73027"), high = match_color("#4575b4"),
ggplot2::scale_fill_gradient(low = match_color("#d73027"), high = match_color("#4575b4"),
breaks=c(min(node_color)+1,
ifelse(any(nshape=="Non-Adopter"),
max(node_color)-1,
Expand All @@ -132,40 +135,51 @@ graph_nodes <- function(p, g, node_color, node_shape, node_size) {
name = "Time of\nAdoption\n") +
ggplot2::scale_shape_manual(name = "",
breaks = c("Seed(s)", "Adopter", "Non-Adopter"),
values = c("Seed(s)" = "triangle",
"Adopter" = "circle",
"Non-Adopter" = "square")) +
ggplot2::guides(color = ggplot2::guide_colorbar(order = 1, reverse = TRUE),
values = c("Seed(s)" = 24, # triangle
"Adopter" = 21, # circle
"Non-Adopter" = 22)) + # square
ggplot2::guides(fill = ggplot2::guide_colorbar(order = 1, reverse = TRUE),
shape = ggplot2::guide_legend(order = 2))
}

.map_nodes <- function(p, out) {
if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) == 1) {
p <- p + ggraph::geom_node_point(colour = out[["ncolor"]], size = out[["nsize"]],
if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 &
length(out[["nshape"]]) == 1) {
p <- p + ggraph::geom_node_point(fill = out[["ncolor"]], size = out[["nsize"]],
shape = out[["nshape"]])
} else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) == 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]]),
} else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 &
length(out[["nshape"]]) == 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]]),
size = out[["nsize"]], shape = out[["nshape"]])
} else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 & length(out[["nshape"]]) == 1) {
} else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 &
length(out[["nshape"]]) == 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(size = out[["nsize"]]),
colour = out[["ncolor"]], shape = out[["nshape"]])
} else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) > 1) {
fill = out[["ncolor"]], shape = out[["nshape"]])
} else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) == 1 &
length(out[["nshape"]]) > 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(shape = out[["nshape"]]),
colour = out[["ncolor"]], size = out[["nsize"]])
} else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) > 1 & length(out[["nshape"]]) == 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]], size = out[["nsize"]]),
fill = out[["ncolor"]], size = out[["nsize"]])
} else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) > 1 &
length(out[["nshape"]]) == 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]],
size = out[["nsize"]]),
shape = out[["nshape"]])
} else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 & length(out[["nshape"]]) > 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]], shape = out[["nshape"]]),
} else if (length(out[["ncolor"]]) > 1 & length(out[["nsize"]]) == 1 &
length(out[["nshape"]]) > 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]],
shape = out[["nshape"]]),
size = out[["nsize"]])
} else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 & length(out[["nshape"]]) > 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(size = out[["nsize"]], shape = out[["nshape"]]),
colour = out[["ncolor"]])
} else if (length(out[["ncolor"]]) == 1 & length(out[["nsize"]]) > 1 &
length(out[["nshape"]]) > 1) {
p <- p + ggraph::geom_node_point(ggplot2::aes(size = out[["nsize"]],
shape = out[["nshape"]]),
fill = out[["ncolor"]])
} else {
p <- p + ggraph::geom_node_point(ggplot2::aes(colour = out[["ncolor"]],
p <- p + ggraph::geom_node_point(ggplot2::aes(fill = out[["ncolor"]],
shape = out[["nshape"]],
size = out[["nsize"]]))
}
p <- p + ggplot2::scale_shape_manual(values = c(21, 22, 24, 23, 25))
p
}

Loading
Loading