Welcome to ClientVPS Mirrors

Editing and saving path diagrams with lvmPlot

Editing and saving path diagrams with lvmPlot

Feng Ji

This vignette uses a three-factor confirmatory factor analysis to show how to edit a path diagram in lvmPlot. In the browser editor, you can drag the nodes and move coefficient labels separately. The example follows those edits through to an exported figure and an R script that reproduces it.

The data come with lavaan. The editor instructions describe lvmPlot 0.1.1; check packageVersion("lvmPlot") if your controls differ. There is also a shorter walkthrough devoted to mouse editing in vignette("drag-and-drop", package = "lvmPlot").

A CFA example

Install lvmPlot and the packages used below:

install.packages(c("lvmPlot", "lavaan", "shiny", "jsonlite", "svglite", "ragg"))

If you have a newer source archive from the maintainer, install it after the dependencies above. Select the .tar.gz file when prompted:

install.packages(file.choose(), repos = NULL, type = "source")
packageVersion("lvmPlot")

Restart R if the old package was loaded when you replaced it. The editor uses shiny and jsonlite. The SVG and PNG exports use svglite and ragg when available, with fallback devices otherwise; PDF uses R’s built-in device.

The model is the three-factor example from the lavaan CFA tutorial. It relates nine test scores to visual, textual, and speed factors:

library(lavaan)
#> This is lavaan 0.6-19
#> lavaan is FREE software! Please report any bugs.
library(lvmPlot)

cfa_model <- '
  visual  =~ x1 + x2 + x3
  textual =~ x4 + x5 + x6
  speed   =~ x7 + x8 + x9
'

fit <- cfa(cfa_model, data = HolzingerSwineford1939)
stopifnot(lavInspect(fit, "converged"))

Pass the fitted object to lvmPlot() with mode = "edit":

lvmPlot(
  fit,
  mode = "edit",
  label = "std",
  stars = FALSE,
  diagram = "all",
  export_name = "three-factor-cfa"
)

A local Shiny application opens. Leave R running while you edit. When you have finished, use RStudio’s Stop button or press Esc in the R console; closing the browser tab alone may leave the application running.

Use mode = "plot" instead if you only want a static figure. The default, mode = "ask", offers a choice in an interactive session and draws a static plot in a non-interactive script. To start the editor from Rscript, also set launch = TRUE.

What the diagram includes

Here is a static rendering of the model. Shiny is not launched when this vignette is built.

plot_lvm(fit, diagram = "all", label = "std", stars = FALSE)
The three-factor CFA with standardized coefficients and factor covariances.

The three-factor CFA with standardized coefficients and factor covariances.

Ellipses represent latent variables and rectangles represent observed variables. Loading arrows run from a factor to its indicators. A covariance has arrowheads at both ends. In a regression such as speed ~ textual, the arrow runs from textual to speed.

The call uses diagram = "all" to retain the factor covariances. The default, diagram = "auto", simplifies some diagrams, including the covariance mesh in a pure CFA. It can also summarize very wide item batteries and dense models. If an expected path is missing, try diagram = "all" before changing the model.

diagram = "all" retains the graph edges supplied by the adapter. It does not turn every row of a statistical output table into a path. Variances require residuals = TRUE; intercepts, thresholds, constraints, and defined effects such as indirect := a*b are not drawn as separate nodes.

lvmPlot(fit, mode = "edit", diagram = "all", residuals = TRUE,
        label = "std", stars = FALSE)

Check the model itself with summary(fit, fit.measures = TRUE) and the diagnostics appropriate to the analysis. Editing the diagram does not change the fitted model, assess identification, or establish a causal interpretation.

Editing with the mouse

Start with the visual ellipse. Hold the left mouse button inside it, drag it upward, and release. The attached arrows follow the node. Move the x1 rectangle a little to the left in the same way. These two changes give the left-hand group of indicators more space without altering the model.

The coefficients can be moved separately. Find the loading from visual to x2, approximately 0.42 in this example. Drag the number into an empty space beside its arrow. The label moves; its value and the endpoints of the path do not. Keep it close to that arrow so that it cannot be mistaken for a label on another path. Double-click the number to return it to automatic placement.

It is easier to arrange the nodes before placing individual coefficients. A label you have positioned by hand stays at those graph coordinates, so it may need another adjustment if you subsequently move a connected node.

To give the factor a longer name, select visual, enter Visual ability in Selected label, and click Apply label. This changes the displayed text only; the variable in the model is still called visual.

You can straighten a row without lining up each node by eye. Select x1, then Shift-click x2 and x3, and use Align row. Distribute X spaces three or more selected nodes between their outermost positions. Align row and Align column use the average selected coordinate and exclude locked nodes; a locked node does not serve as an alignment anchor.

If a block is finished, select its nodes and click Lock. Locked nodes stay put during dragging, nudging, alignment, and layout repair. Unlock all releases them. A node that refuses to move is often simply locked. If a node moves but the steps are too coarse, turn off Snap to grid. With the canvas focused, the arrow keys make small adjustments to selected nodes; Shift plus an arrow makes a larger one.

Undo and redo restore node positions, display labels, and coefficient positions. They do not track every style control. Reset layout is a full reset of the arrangement and clears locks, so use undo to reverse an individual edit. Fit view recenters the canvas. Repair layout can help with collisions, although a dense diagram may still need some labels placed by hand.

Checking the exported appearance

Open Export preview and click Refresh export preview. Look for the moved visual node, its new name, and the coefficient you placed beside the arrow. Return to Edit if they need more space, then refresh the preview again.

The editing canvas and exported files use different renderers. The canvas responds quickly to mouse movements; R graphics determines the fonts, node sizes, and routing in the files. The export preview uses that R renderer, which is why it is worth checking before downloading, particularly with long labels. The preview itself is a raster image, while PDF and SVG downloads remain vector files.

The grid, selection highlights, and lock indicators are absent from the exported figure. Background and Grid affect the editing canvas; standard exports have a white background.

Saving the edited figure

Use the editor’s download controls to save the figure with your changes. Calling plot_lvm(fit) in the console will draw a new diagram from the original fit; it will not retrieve the browser edits. To reuse those edits, save State JSON or Figure R as well as the artwork.

The downloads serve different purposes:

Download What it contains When to use it
SVG, PDF, PNG The rendered figure Insert the artwork into a paper or slides.
State JSON Node positions and labels, coefficient positions, style, selection and locks, and model identity information Continue editing the matching model.
Figure R A self-contained graph snapshot, estimates, layout, labels, style, and plotting/export calls Reproduce this figure in a fresh R session.
Layout CSV / Layout R Node coordinates; Layout R also includes current style overrides Transfer a layout to another plot with matching node names.
Matrix R An approximate matrix representation plus the actual coordinates Read the arrangement as rows and columns. Use the coordinate section for exact placement.

Downloads wait for R to acknowledge the current browser state. If the session has disconnected or synchronization fails, the editor reports it rather than using an older figure. Keep the R session running, reconnect if needed, and retry the download.

Returning to the editor

State JSON is the useful file when you expect to keep adjusting the diagram. A PDF alone cannot restore the editing session. Rerun the model, or load a saved fit, then open the editor with the same diagram settings and select the JSON file under Load state JSON:

saveRDS(fit, "cfa-fit.rds")

# In a later R session:
library(lvmPlot)
fit <- readRDS("cfa-fit.rds")
lvmPlot(fit, mode = "edit", label = "std", stars = FALSE,
        diagram = "all", export_name = "three-factor-cfa")
# In the editor, use Load state JSON.

State JSON contains the editing state, not the observations or a fitted lavaan object. The editor checks node names and the path information stored in current state files. For example, state saved from a structural summary may not match an editor opened with the full measurement model. A mismatched or invalid file produces a notification without closing the current session.

If you refit the model without changing its nodes or paths, loading compatible state reuses the arrangement with the new coefficients. Figure R behaves differently: it keeps the estimates recorded when the script was downloaded.

Reproducing a saved figure in R

The Figure R download starts with library(lvmPlot) and reconstructs the graph from saved node and edge tables. It can run in a fresh R session without the original fit. It draws the figure and writes PDF, SVG, and PNG files in the working directory:

# Run in the directory where you want the figure files to be written.
source("three-factor-cfa-figure.R")

The script creates an object called object containing the selected graph and its manual positions. After sourcing it, you can open that graph in the editor to continue adjusting the layout:

lvmPlot(object, mode = "edit", layout = layout, label = label,
        theme = theme, digits = digits, stars = stars, style = style)

Keep style = style in this call so that the editor uses the font sizes and colours saved with the figure. The downloaded script defines style as NULL when the figure has no global style overrides.

Keep the analysis script as well. If the data or model changes, refit the model and apply the layout to the new fit. Editing an estimate inside a saved graph only changes the number in the figure.

Labels and layout in R

The following arguments are useful when preparing a diagram before opening the editor, or when adapting a saved figure script.

Coefficient labels

label Meaning
"auto" Hide automatically estimated coefficients; preserve explicit custom edge labels on ordinary diagrams.
"std" Show fully standardized values from std.all, or est.std in a standardized-solution table.
"est" Show raw estimates from est.
"both" Show available raw and standardized values, separated by a slash.
"none" Hide every edge label, including custom labels.

If standardized values are missing, label = "std" leaves the labels blank; it does not substitute raw estimates. std.lv and std.all describe different standardizations, so a column called std.lv is not treated as std.all.

Explicit edge_label values take precedence over numeric labels unless you choose label = "none". This is useful for a model schematic with labels such as a and b, but check custom labels when refitting a model: they are text, not automatically updated estimates.

plot_lvm(fit, label = "est", digits = 3, stars = FALSE)
plot_lvm(fit, label = "both", digits = 2, stars = FALSE)
plot_lvm(fit, label = "none")

stars = FALSE is convenient when a separate results table reports inferential statistics. With stars = TRUE, significance symbols use the available p-values; fixed parameters may not have a p-value. stars = "auto" may suppress stars on crowded diagrams. Geometric label rules may also omit a coefficient that cannot fit on a very short edge. Increase spacing rather than inferring that a blank label means a zero coefficient.

Node positions

For several figures that need the same arrangement, it can be convenient to specify a layout in R. Matrix rows run from top to bottom and columns from left to right; "", NA, and "." leave a cell empty.

positions <- layout_matrix(rbind(
  c("", "visual", "", "", "textual", "", "", "speed", ""),
  paste0("x", 1:9)
))

positions
#>       name    x     y
#> 1   visual -5.4  0.85
#> 2  textual  0.0  0.85
#> 3    speed  5.4  0.85
#> 4       x1 -7.2 -0.85
#> 5       x2 -5.4 -0.85
#> 6       x3 -3.6 -0.85
#> 7       x4 -1.8 -0.85
#> 8       x5  0.0 -0.85
#> 9       x6  1.8 -0.85
#> 10      x7  3.6 -0.85
#> 11      x8  5.4 -0.85
#> 12      x9  7.2 -0.85

Use model variable names in the layout, even if you have changed their display labels. Every displayed node must have a coordinate. Extra rows are useful when reusing a full-model layout for a smaller diagram.

node_labels <- c(
  visual = "Visual ability",
  textual = "Textual ability",
  speed = "Processing speed"
)

A coordinate data frame is equally valid:

positions <- data.frame(
  name = c("factor", "item1", "item2", "item3"),
  x = c(0, -2, 0, 2),
  y = c(2, 0, 0, 0)
)

Increasing x moves right; increasing y moves up. Duplicate names, missing coordinates, and infinite coordinates are rejected with an error. Row-named x/y data frames and matrices are also supported. Keep spacing comparable across figures; extreme coordinate ranges can make a block appear very small.

Fonts and other styling

This example uses black lines, white nodes, and Times text. The longer factor names come from the node_labels vector above:

manuscript_style <- lvm_style(
  font_family = "Times",
  node_font_size = 11,
  edge_font_size = 9,
  latent_fill = "#FFFFFF",
  observed_fill = "#FFFFFF",
  node_color = "#000000",
  node_text_color = "#000000",
  edge_color = "#000000",
  label_color = "#000000",
  label_fill = "#FFFFFF"
)

plot_lvm(
  fit, layout = positions, diagram = "measurement",
  node_labels = node_labels, label = "std", stars = FALSE,
  theme = "classic", style = manuscript_style
)

This measurement-only figure omits factor covariances deliberately. Use diagram = "all" when they belong in the final figure. List the available themes with lvm_themes(); a theme supplies defaults, and explicit style fields override them.

The font must be available to the graphics device. Standard families such as Times, Helvetica, and Courier work with base PDF. Text widths can differ between browser, SVG, and PDF fonts, so inspect the file at the size at which it will appear in the manuscript.

For long node labels, insert \n in a named label vector to choose line breaks. Automatic wrapping is useful for variable names, but an explicit display label is more predictable for a manuscript. Increase observed_width or reduce node_font_size modestly if necessary.

Coefficient positions

When you drag a coefficient in the editor, Figure R records its position in edge_style. You can also supply the coordinates directly. For example, this places the loading label for visual to x2 beside the factor’s x-coordinate:

edge_positions <- data.frame(
  from = "visual", to = "x2", type = "loading",
  label_x = positions$x[positions$name == "visual"] + 0.45,
  label_y = mean(positions$y)
)

plot_lvm(fit, layout = positions, diagram = "measurement",
         label = "std", stars = FALSE, edge_style = edge_positions)

label_x and label_y use the same graph coordinates as the nodes, not pixels or millimetres. These positions override automatic placement and contribute to the exported plot bounds, even when they lie beyond the nodes. Automatic placement will not move a manually positioned label away from an overlap.

For graphs with parallel edges, an explicit unique edge_id can identify the edge unambiguously. Downloaded Figure R scripts retain those IDs along with the node-pair information. The same manual-position support applies to SVG, PDF, and PNG. Do not assume a TikZ export preserves all interactive label positioning; check that output separately.

Writing figure files from R

To give several exports the same layout and labels, prepare the graph once and pass it to each save function:

figure_graph <- as_lvm_graph(fit, layout = positions)
figure_graph$nodes$label <- ifelse(
  figure_graph$nodes$name %in% names(node_labels),
  node_labels[figure_graph$nodes$name],
  figure_graph$nodes$name
)
save_lvm_pdf(figure_graph, "figures/cfa.pdf", width = 8, height = 4.5,
             label = "std", stars = FALSE, theme = "classic",
             style = manuscript_style)
save_lvm_svg(figure_graph, "figures/cfa.svg", width = 8, height = 4.5,
             label = "std", stars = FALSE, theme = "classic",
             style = manuscript_style)
save_lvm_png(figure_graph, "figures/cfa.png", width = 8, height = 4.5,
             res = 300, label = "std", stars = FALSE, theme = "classic",
             style = manuscript_style)

Widths and heights are in inches. An 8 by 4.5 inch PNG at 300 dpi is 2400 by 1350 pixels. SVG and PDF are vector formats and do not need a dpi setting. Missing output directories are created by the save helpers.

If you omit dimensions, the LVM save functions choose a canvas based on the graph. lvm_canvas_size(figure_graph) shows the recommendation. Use explicit dimensions when figures in a manuscript must share the same size.

export_lvm_bundle() writes several formats into one directory:

export_lvm_bundle(
  figure_graph, dir = "figures/cfa-bundle", name = "cfa",
  formats = c("pdf", "svg", "png"), label = "std", stars = FALSE,
  style = manuscript_style, check = FALSE
)

A bundle can also contain node and edge tables, geometry diagnostics, and session information. check = TRUE checks the diagram’s geometry, not the statistical model fit. Reusing a prepared graph avoids the cost of repeating automatic layout selection.

Other model inputs

For computed examples of SEM, ordinal and multigroup CFA, growth, two-level models, EFA, IRT, Rasch, Gaussian mixtures, and OpenMx RAM, see Working with different models. That companion tutorial also explains which figures show fitted estimates and which are schematics.

Structural paths

To illustrate structural paths, add regressions among the three factors. These regressions are chosen for the plotting example, not as a substantive explanation of the relationships in the test-score data.

sem_model <- paste(cfa_model, '
  textual ~ visual
  speed ~ visual + textual
')
sem_fit <- sem(sem_model, data = HolzingerSwineford1939)
plot_lvm(sem_fit, diagram = "structural", label = "std", stars = FALSE,
         theme = "classic", style = manuscript_style)
A structural view of the fitted example, with the measurement paths omitted.

A structural view of the fitted example, with the measurement paths omitted.

lvmPlot(sem_fit, mode = "edit", diagram = "all", label = "std",
        stars = FALSE, export_name = "structural-model")

diagram = "structural" is useful when the measurement model is described elsewhere. orientation = "left-right" can help emphasize a predictor-to-outcome sequence. A custom coordinate layout is applied after the automatic arrangement in the editor, so it remains your final placement instruction.

A parameter table

If the model was estimated elsewhere, you can supply a lavaan-style parameter table. It needs lhs, op, and rhs columns. Add est, std.all, and pvalue for the numerical labels you want to show.

params <- data.frame(
  lhs = c("engage", "engage", "engage", "achieve"),
  op = c("=~", "=~", "=~", "~"),
  rhs = c("item1", "item2", "item3", "engage"),
  est = c(1, .90, .85, .42),
  std.all = c(.78, .72, .69, .46),
  pvalue = c(NA, .001, .002, .004)
)

These numbers are illustrative. In the last row, achieve ~ engage becomes an arrow from engage to achieve. Defined parameters and constraints are omitted from the graph rather than being mistaken for variables.

lvmPlot(params, mode = "edit", label = "std", stars = FALSE)

A graph without a fitted model

You can construct the graph directly when drawing a model schematic. This item-response diagram uses a1, a2, and a3 as labels rather than estimates:

irt_graph <- lvm_graph(
  nodes = data.frame(
    name = c("theta", "item1", "item2", "item3"),
    label = c("Ability", "Item 1", "Item 2", "Item 3"),
    type = c("latent", rep("observed", 3)),
    role = c("trait", rep("item", 3))
  ),
  edges = data.frame(
    from = "theta", to = paste0("item", 1:3), type = "loading",
    edge_label = c("a1", "a2", "a3")
  ),
  model_type = "irt", layout_family = "irt"
)
plot_lvm(irt_graph, label = "auto", theme = "classic")

Adapters for other fitted-model classes depend on the information available in those objects. With an unfamiliar class, inspect the $nodes and $edges returned by as_lvm_graph(your_object) to see which paths and estimates it supplies. The worked examples here use lavaan and directly constructed graphs.

Separate group diagrams

For paths shared across fitted groups, lvmPlot shows the common structure without pooling their coefficients, and issues a warning. An average of the group estimates would not be an estimate from a pooled model. To show the numbers, extract the parameter table and draw each group separately:

fit_groups <- cfa(cfa_model, data = HolzingerSwineford1939, group = "school")
pe <- parameterEstimates(fit_groups, standardized = TRUE)
group_names <- lavInspect(fit_groups, "group.label")
data.frame(group = seq_along(group_names), name = group_names)

first_group <- pe[pe$group == 1, , drop = FALSE]
second_group <- pe[pe$group == 2, , drop = FALSE]

plot_lvm(first_group, layout = positions, diagram = "all",
         label = "std", stars = FALSE)
plot_lvm(second_group, layout = positions, diagram = "all",
         label = "std", stars = FALSE)

Reuse positions so that a layout change does not distract from the coefficient differences. Put the group names in the captions or filenames, and keep each group’s significance annotations with its own estimates.

Troubleshooting

Symptom What to check
The editor does not open Install shiny and jsonlite, use mode = "edit", and keep R running. For a non-interactive launch, set launch = TRUE.
No coefficients appear label = "auto" usually hides estimates. Use "std" with a std.all column or "est" with an est column.
A few coefficients disappear Check for missing standardized values, very short edges, custom edge labels, and a summarized diagram.
Covariances are missing Use diagram = "all" or an explicit covariance view. For variance loops also set residuals = TRUE.
A custom layout errors Match original node names, include every displayed node once, and use finite numeric coordinates.
Load state JSON reports a mismatch Open the original model with the same diagram options. Renaming display labels is fine; changing model variable names or paths may make the state incompatible.
The download does not start Read the synchronization status and confirm that the local R/Shiny session is connected. The editor will not use a timed-out state.
A style reset seems incomplete Use Reset style, refresh Export preview, and inspect the current control values. Export-only per-node styles may differ from the fast canvas.
Text differs between preview and PDF Check device fonts and figure dimensions. Inspect the actual downloaded vector file at publication size.
Preparing a very large figure is slow Reuse a prepared graph and layout. Avoid repeated automatic layout searches. Model fitting and layout selection are separate costs.

For a bug report, include packageVersion("lvmPlot"), sessionInfo(), the plotting call, and a small model or parameter table that reproduces the problem. State JSON and Figure R are helpful when the issue occurs after a manual edit.

Keeping the analysis and figure together

Keep the downloaded figure script and State JSON with the analysis that produced the model. Retain the fit or parameter table and the data, or a record of where the data came from. Recording the R session is useful if you need to recreate the figure with the same package versions:

writeLines(capture.output(sessionInfo()), "figure-session-info.txt")

If you later refit the model, decide whether you need a new figure with updated estimates or an exact copy of the old one. Use the new fit with compatible editor state for the former, and the saved Figure R script for the latter.

Need a high-speed mirror for your open-source project?
Contact our mirror admin team at info@clientvps.com.

This archive is provided as a free public service to the community.
Proudly supported by infrastructure from VPSPulse , RxServers , BuyNumber , UnitVPS , OffshoreName and secure payment technology by ArionPay.