connected <- cg$nodes$id[cg$nodes$id %in% c(cg$edges$from, cg$edges$to)]
core <- graph_from_data_frame(
cg$edges,
vertices = cg$nodes[cg$nodes$id %in% connected, ],
directed = TRUE
)
V(core)$type <- factor(V(core)$type, levels = type_levels)
set.seed(204)
# Use Fruchterman-Reingold for angular arrangement (connected nodes cluster
# together), then override each node's radius so the most foundational nodes
# land near the center.
fr_layout <- create_layout(core, layout = "fr", niter = 2000)
n_desc_vals <- V(core)$n_desc
fr_angle <- atan2(fr_layout$y, fr_layout$x)
# log1p radius: nodes with more descendants land closer to center (r = 0);
# nodes with 0 descendants land at the periphery (r = 1). The log scale
# compresses the high end so the few top hubs don't crowd the center, and
# pulls nodes with even a handful of descendants well off the outer rim.
max_desc <- max(n_desc_vals)
target_r <- 1 - log1p(n_desc_vals) / log1p(max_desc)
layout <- fr_layout
layout$x <- target_r * cos(fr_angle)
layout$y <- target_r * sin(fr_angle)
# Abbreviate long node labels for the diagram only (the descendants table
# keeps the full titles). First drop synonym lists -- the text after the first
# comma -- so e.g. "Expectation, expected value, population mean" becomes
# "Expectation". Then apply curated short forms for the few names that are
# still long; extend this map as needed.
layout$label <- sub(",.*$", "", layout$title)
short_forms <- c("def-hazard" = "Hazard")
hit <- match(layout$name, names(short_forms))
layout$label <- ifelse(is.na(hit), layout$label, short_forms[hit])
layout$label <- stringr::str_wrap(layout$label, width = 15)
ggraph(layout) +
geom_edge_link(
arrow = arrow(length = unit(1.8, "mm"), type = "closed"),
end_cap = circle(2, "mm"),
edge_alpha = 0.25, edge_width = 0.3
) +
geom_label_repel(
data = as.data.frame(layout),
aes(x = x, y = y, label = label, fill = type, size = n_desc),
inherit.aes = FALSE,
# point.size = NA: suppress the dot so the label itself is the node;
# ggrepel still repels labels away from each other to avoid overlap.
point.size = NA,
color = "white",
fontface = "bold",
max.overlaps = Inf,
force = 3,
force_pull = 1,
box.padding = unit(0.15, "lines"),
label.padding = unit(0.1, "lines"),
label.r = unit(0.05, "lines"),
label.size = 0.1,
alpha = 0.88,
segment.color = "grey60",
segment.alpha = 0.5,
segment.size = 0.3,
min.segment.length = unit(0.5, "lines"),
seed = 204
) +
scale_fill_manual(
values = type_palette, labels = type_labels, name = "Type", drop = FALSE
) +
scale_size_continuous(range = c(8.0, 16.0), guide = "none") +
theme_void() +
theme(legend.position = "bottom")