The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Tracing and Diagnosing Layouts

Why trace a layout?

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:

Tracing follows the same package decision rule:

library(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))
}

A first combinatorial trace

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:

Level traces versus round traces

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        6

Use:

Weighted traces

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.

Trace objects are analysis objects

A trace object is more than a list of pictures. It can support:

The most useful components are:

Practical guidance

Where to go next

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.