Most graph-layout functions return only the final coordinate matrix. That is enough for plotting, but it hides the most useful diagnostic information:
The trace APIs in grip make those questions visible:
trace.grip() for combinatorial layouts,trace.weighted.grip() for weighted geometry-aware
layouts.Tracing follows the same package decision rule:
trace.grip(),trace.weighted.grip(),plot.trace.frame <- function(coords, edges, main = "", vertex.col = "black") {
active <- stats::complete.cases(coords[, 1:2, drop = FALSE])
xy <- coords[active, 1:2, drop = FALSE]
xlim <- range(xy[, 1])
ylim <- range(xy[, 2])
xpad <- 0.08 * diff(xlim)
ypad <- 0.08 * diff(ylim)
if (!is.finite(xpad) || xpad == 0) xpad <- 0.2
if (!is.finite(ypad) || ypad == 0) ypad <- 0.2
plot(
xy[, 1], xy[, 2],
type = "n",
asp = 1,
axes = FALSE,
xlab = "",
ylab = "",
xlim = xlim + c(-xpad, xpad),
ylim = ylim + c(-ypad, ypad),
main = main
)
active.edges <- edges[active[edges[, 1]] & active[edges[, 2]], , drop = FALSE]
if (nrow(active.edges) > 0) {
apply(active.edges, 1, function(e) {
graphics::segments(
coords[e[1], 1], coords[e[1], 2],
coords[e[2], 1], coords[e[2], 2],
col = "gray82"
)
})
}
points(xy[, 1], xy[, 2], pch = 16, cex = 0.55, col = vertex.col)
}
pick.trace.frames <- function(trace.obj) {
n <- length(trace.obj$frames)
unique(c(1L, max(2L, floor((n + 1L) / 2L)), n))
}The example below traces a 2D layout of a small mesh. Using
trace = "level" keeps the vignette readable by recording
the coarse initialization, the start of each new level, and the final
layout.
mesh.edges <- edges.mesh(5, 5)
mesh.trace <- trace.grip(
mesh.edges,
n = 25,
dim = 2,
preset = "mesh",
rounds = 12,
final_rounds = 16,
trace = "level",
diagnostics = "light",
seed = 1
)
knitr::kable(mesh.trace$meta)| frame | phase | level_index | misf_level | round_in_level | active_vertices |
|---|---|---|---|---|---|
| 1 | init | 1 | 1 | 0 | 12 |
| 2 | level_start | 2 | 0 | 0 | 25 |
| 3 | final | 2 | 0 | 16 | 25 |
knitr::kable(mesh.trace$diagnostics[, c(
"frame",
"edge.length.cv",
"median.edge.length",
"sampled.nonedge.sep.ratio"
)], digits = 3)| frame | edge.length.cv | median.edge.length | sampled.nonedge.sep.ratio |
|---|---|---|---|
| 1 | 0.380 | 234.009 | 0.109 |
| 2 | 0.346 | 35.365 | 0.172 |
| 3 | 0.073 | 27.020 | 0.969 |
sel <- pick.trace.frames(mesh.trace)
op <- par(mfrow = c(1, length(sel)), mar = c(1.2, 1.2, 3, 1.2), bg = "white")
on.exit(par(op), add = TRUE)
for (idx in sel) {
plot.trace.frame(
mesh.trace$frames[[idx]],
mesh.edges,
main = paste(mesh.trace$meta$phase[[idx]], "(frame", idx, ")")
)
}The two most useful tables are:
meta, which explains where each frame came from,diagnostics, which tracks simple quality signals frame
by frame.When you need more detail, trace = "round" records
intermediate refinement rounds within each active level.
mesh.trace.round <- trace.grip(
mesh.edges,
n = 25,
dim = 2,
preset = "mesh",
rounds = 6,
final_rounds = 8,
trace = "round",
trace.every = 4,
diagnostics = "light",
seed = 1
)
data.frame(
trace_mode = c("level", "round"),
n.frames = c(length(mesh.trace$frames), length(mesh.trace.round$frames))
)
#> trace_mode n.frames
#> 1 level 3
#> 2 round 6Use:
trace = "level" for most reporting and teaching,trace = "round" when you are diagnosing a tuning
issue.Weighted traces answer a slightly different question. Instead of only asking whether the layout becomes visually cleaner, we can also ask whether it moves toward a target geometry.
Here the graph is a mesh whose edge lengths come from a curved 3D saddle surface.
weighted.mesh <- mesh.surface.graph(
4, 4,
surface = "saddle",
amplitude = 0.8
)
weighted.trace <- trace.weighted.grip(
weighted.mesh$edges,
n = weighted.mesh$n,
edge_weights = weighted.mesh$edge_weights,
dim = 3,
preset = "mesh",
rounds = 8,
final_rounds = 12,
trace = "level",
diagnostics = "light",
target_coords = weighted.mesh$coords_surface,
seed = 2
)
knitr::kable(weighted.trace$meta)| frame | phase | level_index | misf_level | round_in_level | active_vertices |
|---|---|---|---|---|---|
| 1 | init | 1 | 1 | 0 | 12 |
| 2 | level_start | 2 | 0 | 0 | 16 |
| 3 | final | 2 | 0 | 12 | 16 |
knitr::kable(weighted.trace$diagnostics[, c(
"frame",
"edge.length.cv",
"sampled.nonedge.sep.ratio",
"procrustes.rmse"
)], digits = 3)| frame | edge.length.cv | sampled.nonedge.sep.ratio | procrustes.rmse |
|---|---|---|---|
| 1 | 0.381 | 0.154 | 0.887 |
| 2 | 0.303 | 0.411 | 0.430 |
| 3 | 0.103 | 1.088 | 0.360 |
sel.w <- pick.trace.frames(weighted.trace)
op <- par(mfrow = c(1, length(sel.w)), mar = c(1.2, 1.2, 3, 1.2), bg = "white")
on.exit(par(op), add = TRUE)
for (idx in sel.w) {
frame.xy <- project.3d(weighted.trace$frames[[idx]], azimuth = 35, elevation = 20)
plot.trace.frame(
frame.xy,
weighted.mesh$edges,
main = paste(weighted.trace$meta$phase[[idx]], "(frame", idx, ")"),
vertex.col = "#1F3B73"
)
}The important extra column is procrustes.rmse. When a
meaningful target geometry exists, it gives a direct per-frame measure
of whether the layout is getting closer to that target up to rigid
motion and scale.
A trace object is more than a list of pictures. It can support:
The most useful components are:
final: the final coordinate matrix,frames: the stored coordinate frames,meta: frame provenance,diagnostics: per-frame metrics,lgkk.polish: optional post-layout LGKK result when
enabled as an advanced experimental refinement step.trace = "level" and
diagnostics = "light".trace = "round" only when you need finer-grained
diagnostics.target_coords when a meaningful geometric target
exists.Getting Started with grip gives the short overview of
the main APIs.Weighted Graph Layouts with grip focuses on weighted
solving and scoring.Choosing Layouts for Real Data shows how trace fits
into a real-data selection workflow.