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").
Install lvmPlot and the packages used below:
If you have a newer source archive from the maintainer, install it
after the dependencies above. Select the .tar.gz file when
prompted:
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.
Here is a static rendering of the model. Shiny is not launched when this vignette is built.
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.
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.
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.
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.
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.
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.
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.
The following arguments are useful when preparing a diagram before opening the editor, or when adapting a saved figure script.
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.
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.85Use 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| 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.
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:
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.