Converts output from latent variable model tools into publication-ready path diagrams and model schematics. 'lavaan' fit objects and parameter tables are supported as a primary workflow, with graph adapters for objects from 'blavaan', 'lavaan.mi', 'semPlot', 'mirt', 'eRm', 'OpenMx', 'psych', 'poLCA', 'mclust', 'flexmix', 'lcmm', 'tidyLPA', and 'MplusAutomation' workflows when those packages are available. Supports structural equation and confirmatory factor analysis diagrams, multilevel structural equation models, growth models, higher-order factor models, latent class and profile models, item response theory models, and common mixture outputs through a unified graph grammar with model-aware defaults, geometry diagnostics, layout quality scoring, automatic layout selection, customizable publication styles, 'RStudio' preview, SVG/PDF/PNG export, 'TikZ' output, and reproducible publication bundles. A local 'Shiny' editor supports mouse dragging of nodes and coefficient labels, synchronized figure downloads, and reusable editing state.
lvmPlot draws publication-ready diagrams for latent variable models.
lavaan is the main workflow, but the package now uses a common lvm_graph
grammar so blavaan/lavaan.mi, semPlot, mirt, eRm, OpenMx, psych,
poLCA, mclust, flexmix, lcmm, tidyLPA, and MplusAutomation-style
outputs can share the same RStudio, SVG, PDF, PNG, and TikZ rendering system.
The supported model families include SEM/CFA, multilevel SEM, bifactor and
higher-order models, latent class and profile models, IRT/MIRT, Rasch, OpenMx
RAM models, and Mplus-style parameter output.
The package is meant to cover the everyday strengths of common SEM drawing tools while reducing the amount of manual cleanup needed for publication:
semPlot::semPaths()-style model awareness, layout presets, rotations, and
parameter labelstidySEM-style editable layouts and data-frame-friendly graph objectslavaanPlot-style simple plotting from lavaan outputsemptools-style attention to factor/indicator placement and loading labelslvmPlot adds a common LVM graph grammar, TikZ-first export, publication and
presentation themes, orientation-aware label placement, multilevel layer bands, and adapters
for SEM, multilevel SEM, LCA/LPA, IRT/MIRT, Rasch, OpenMx, and Mplus-style
parameter tables.
Automatic diagrams prioritize a clean publication view. When a model contains
geometry that cannot be shown well as a straight-edge path diagram, such as a
60-item single-factor battery, dense latent structural regressions, dense LCA
probability matrices, or a covariance edge parallel to a directed path,
diagram = "auto" summarizes the display. Use diagram = "all" when you need
the complete parameter graph for audit or manual editing.
install.packages(c("lvmPlot", "lavaan", "shiny", "jsonlite", "svglite", "ragg"))
For a newer maintainer-provided source archive, install that .tar.gz after
the dependencies with install.packages(file.choose(), repos = NULL, type = "source").
Restart R if an older version was already loaded.
From the package directory:
install.packages("devtools")
devtools::install()
or without devtools:
R CMD INSTALL .
library(lavaan)
library(lvmPlot)
model <- '
visual =~ x1 + x2 + x3
textual =~ x4 + x5 + x6
speed =~ x7 + x8 + x9
textual ~ visual
speed ~ visual + textual
'
fit <- sem(model, data = HolzingerSwineford1939)
lvmPlot(fit, mode = "plot", label = "std")
lvmPlot(fit, mode = "edit", label = "std")
lvmPlot() is the high-level entry point. Use mode = "plot" for scripted
static output and mode = "edit" when a figure needs final manual adjustment
in the browser editor. In non-interactive scripts, mode = "ask" falls back to
static plotting so package checks and batch jobs never wait for input.
The lower-level plotting and export helpers are useful when you want explicit control over the output device:
plot_lvm(fit, label = "std")
save_lvm_svg(fit, "holzinger-swineford.svg", width = 9.2, height = 5.4)
write_lvm_tikz(fit, "holzinger-swineford.tex", label = "std")
For the detailed fitted-model tutorial, including Shiny editing, formal export preview, restoring state, reproducible scripts, multigroup figures, and troubleshooting, see:
vignette("lvmPlot", package = "lvmPlot")
The older SEM-specific helpers still work:
plot_sem(fit, label = "std")
write_sem_tikz(fit, "holzinger-swineford.tex")
You can try the package from a small lavaan-style parameter table. This is often the fastest way to test layouts, styling, and editor export behavior:
library(lvmPlot)
params <- data.frame(
lhs = c("engage", "engage", "engage", "achieve"),
op = c("=~", "=~", "=~", "~"),
rhs = c("item1", "item2", "item3", "engage"),
std.all = c(.78, .72, .69, .46),
pvalue = c(.001, .001, .002, .004)
)
lvmPlot(params, mode = "plot", label = "std")
Open the interactive editor:
lvmPlot(params, mode = "edit", label = "std")
Use a reproducible layout and light styling:
layout <- layout_matrix(rbind(
c("", "achieve", ""),
c("", "engage", ""),
c("item1", "item2", "item3")
))
plot_lvm(
params,
layout = layout,
label = "std",
theme = "journal",
style = lvm_style(
node_font_size = 12,
edge_font_size = 9,
latent_fill = "#F8FAFC",
edge_color = "#334155"
)
)
Manual coefficient-label positions can be supplied through edge_style. The
Shiny editor writes the same idea into downloaded Figure R scripts:
edge_style <- data.frame(
from = c("engage", "engage"),
to = c("item2", "achieve"),
type = c("loading", "path"),
label_x = c(0.35, 0.25),
label_y = c(-0.75, 0.35)
)
plot_lvm(params, layout = layout, label = "std", edge_style = edge_style)
For day-to-day work in RStudio, draw directly into the Plots pane:
plot_lvm(fit, label = "std")
In RStudio, the package also installs Addins. Select a model object name or an
expression in the editor, then use Preview lvmPlot Diagram to draw it in the
Plots pane or Export lvmPlot TikZ to write a .tex file.
Export the same diagram as vector artwork:
save_lvm_svg(fit, "holzinger-swineford.svg")
save_lvm_pdf(fit, "holzinger-swineford.pdf")
save_lvm_png(fit, "holzinger-swineford.png", res = 300)
The LVM save helpers use width = "auto" and height = "auto" by default.
lvm_canvas_size() gives the recommended inches before export, and explicit
numeric width/height values still override it.
When an automatic layout needs final human judgment, open the browser editor, drag nodes and coefficient labels into place, and download the final figure or reusable layout:
lvmPlot_editor(fit, label = "std", theme = "journal")
The editor supports click/shift-click node selection, selected-node label
editing, grid snapping, arrow-key nudging, locked nodes, node dragging,
draggable coefficient labels, undo/redo, multi-node alignment, horizontal or
vertical distribution, smart polishing for selected nodes, one-click layout
repair, X/Y compact/expand controls, and live preview updates when edge labels,
themes, style presets, colors, font sizes, node sizes, and line widths change.
Double-click a coefficient label to return it to automatic placement. Style
edits are exported through the same lvm_style() system used by plot_lvm()
and the save helpers, so the browser preview is not a separate cosmetic layer.
It is meant for the final publishing pass after the automatic layout has done
the heavy structural work.
For reproducible editing sessions, download State JSON to save the full editor state, including edited node labels, manual coefficient-label positions, and locked/selected nodes, and load it later to continue editing. Download Figure R for a script that reconstructs the final layout, node labels, style, theme, edge labels, edge-label positions, and export calls inside an analysis project.
The main editor downloads are:
In version 0.1.1, Export preview renders the current edits with the full R renderer. Refresh it after editing. The fast editing canvas uses different font metrics and simplified styling, so inspect this preview before exporting. Downloads wait for synchronization confirmation; a timeout does not download an older state. Figure R includes a self-contained graph snapshot with its coefficients and settings. State JSON restores visual edits onto a matching model; it does not contain a fitted model or the original data.
When standardized values are missing, label = "std" leaves those labels blank
instead of substituting raw estimates. Multigroup inputs show shared structure
with a warning and without averaged coefficients or minimum p-values. Subset
lavaan::parameterEstimates(fit, standardized = TRUE) by group for numerical
group comparisons.
The editor keeps publication export inside lvmPlot: SVG/PDF/PNG downloads use
the same renderer as save_lvm_svg(), save_lvm_pdf(), and save_lvm_png(),
and layout downloads can be reused with plot_lvm(object, layout = layout).
If a TeX engine is installed, compile the file:
write_lvm_tikz(fit, "holzinger-swineford.tex", compile = TRUE)
For manuscript workflows, export_lvm_bundle() writes the whole figure artifact
set in one call: PDF, PNG, SVG, standalone TikZ, node and edge tables,
diagnostics, a quality score, session metadata, and a Markdown report.
bundle <- export_lvm_bundle(
fit,
dir = "figures/holzinger-swineford",
name = "figure-cfa",
label = "std",
theme = "journal",
check = TRUE,
optimize = TRUE,
optimize_orientation = c("top-down", "left-right")
)
bundle
This makes the diagram auditable: the exported folder contains both the figure
and the data/diagnostics needed to reproduce or review it.
When optimize = TRUE, the bundle also contains layout-selection.csv, which
records each candidate orientation/layout/routing combination and the selected
quality score.
You can run the same selection step without exporting files:
selection <- select_lvm_layout(
fit,
orientation = c("top-down", "left-right", "bottom-up", "right-left"),
label = "std"
)
selection
plot_lvm(selection$graph, label = "std")
All adapters return the same lvm_graph object:
graph <- as_lvm_graph(fit)
plot_lvm(graph)
Supported adapters include:
lavaan fit objects and lavaan-style parameter tableslavaan multilevel models with level informationblavaan and lavaan.mi lavaan-like objects when their parameter estimates
can be read through the lavaan-compatible APIsemPlot::semPlotModel objects, which makes lvmPlot usable as a high-quality
layout/export backend for models already understood by semPlotmirt SingleGroupClass objects for IRT/MIRTeRm Rasch modelsmclust latent profile / mixture modelsflexmix and lcmm-style mixture / longitudinal latent class schematicspoLCA latent class objectstidyLPA objects and tidyLPA-style profile datapsych::fa, stats::loadings, and EFA-style loading matricesOpenMx RAM modelsMplusAutomation mplus.model and result-wrapper parameter tables with
BY, ON, and WITHYou can also build diagrams directly:
nodes <- data.frame(
name = c("Theta", "i1", "i2", "i3"),
role = c("trait", "item", "item", "item"),
type = c("latent", "observed", "observed", "observed")
)
edges <- data.frame(
from = "Theta",
to = c("i1", "i2", "i3"),
type = "loading",
edge_label = c("a=.80", "a=1.10", "a=.65")
)
plot_lvm(lvm_graph(nodes, edges, model_type = "irt", layout_family = "irt"))
The main plotting functions expose the same grammar-level choices:
plot_lvm(
fit,
layout_family = "sem", # sem, bifactor, irt, mixture, growth, multilevel, circle
orientation = "left-right", # top-down, bottom-up, left-right, right-left
diagram = "measurement", # all, measurement, structural, paths, covariances
theme = "classic", # see lvm_themes()
label = "std",
aspect = "balanced", # balanced, preserve, fill
min_abs = .10,
significant = TRUE
)
Built-in themes are:
lvm_themes()
The defaults remain conservative (journal), with extra presets for
minimal, classic, apa, nature, colorblind, poster, compact,
slides, and blueprint.
The default aspect = "balanced" keeps diagrams from looking oddly stretched in
wide RStudio plot panes or exported PNG/SVG/PDF files. Use aspect = "preserve"
when equal x/y coordinate units matter, or aspect = "fill" when you explicitly
want the older full-panel stretch.
For dense diagrams, the default themes also make small automatic typography and
line-width adjustments so labels remain legible; explicit lvm_style() choices
always take precedence.
The default label = "auto" keeps the diagram itself clean by hiding
automatically estimated parameters while preserving explicit custom edge labels
on ordinary diagrams. Use label = "std", label = "est", or label = "both"
when you want coefficients printed on the paths, and label = "none" to
silence all edge labels including custom edge_label values.
Style overrides cover the usual publication tweaks:
plot_lvm(
fit,
label = "std",
style = lvm_style(
scale = 1.08, # whole figure polish
font_scale = 0.95, # keep labels modest after enlarging nodes/lines
node_font_size = 12,
edge_font_size = 9,
font_family = "Helvetica",
latent_size = 17,
observed_width = 22,
observed_height = 10,
node_line_width = 1.1,
edge_line_width = 1.0,
node_fill = "#F8FAFC",
edge_color = "#334155",
label_fill = "#FFFFFF"
)
)
For publication cleanup, use a layout matrix and local node/edge styling. The same controls work in the RStudio Plots pane and in TikZ output:
params <- data.frame(
lhs = c("f", "f", "y"),
op = c("=~", "=~", "~"),
rhs = c("x1", "x2", "f"),
est = c(1, .8, .4),
std.all = c(.7, .6, .35)
)
layout <- matrix(
c(NA, "y", NA,
NA, "f", NA,
"x1", ".", "x2"),
nrow = 3,
byrow = TRUE
)
plot_lvm(
params,
layout = layout,
label = "std",
node_style = data.frame(
name = c("f", "y"),
label = c("Factor", "Outcome"),
shape = c("diamond", "rounded"),
fill = c("#EEF2FF", "#ECFDF5"),
color = c("#3730A3", "#047857"),
font_size = c(12, 10)
),
edge_style = data.frame(
from = c("f", "f"),
to = c("x2", "y"),
label = c("lambda2", "beta"),
color = c("#B91C1C", "#0F766E"),
line_width = c(1.6, 1.4),
linetype = c("dashed", "solid"),
curvature = c(.20, -.18),
label_size = c(8, 9),
label_fill = c("#FFF7ED", "#F0FDFA")
)
)
Straight routing is the default, so model edges follow conventional path-diagram
geometry. layout_diagnostics() reports node overlaps,
edge/node collisions, and crossings before export. It can also estimate edge
label boxes so long labels such as .46*** are checked separately from node
layout:
diagnostics <- layout_diagnostics(as_lvm_graph(params))
diagnostics
layout_diagnostics(as_lvm_graph(params), label = "std", stars = "always")
layout_quality(as_lvm_graph(params), label = "std")
check_lvm_layout(params, layout = layout, label = "std")
plot_lvm(params, routing = "smart")
lvm_tikz(params, routing = "smart")
check_lvm_layout() is assertion-style: by default it errors if the diagram is
not ready, if the score is below 92, or if node/edge/label collisions remain.
Use action = "none" to return the quality object without stopping, or
minimum_status = "review" when a script should permit human-review cases.
If a user explicitly wants automatic curved avoidance, routing = "smart" is
still available.
For dense LCA/LPA probability matrices, diagram = "auto" uses a compact
representative view so the default plot stays readable. Use diagram = "all"
when you explicitly want every class-by-item probability edge.
For dense IRT/MIRT item axes, automatic diagrams use representative item blocks
and compact display labels such as i12 when long common prefixes would make
nodes collide. The original variable names remain in the graph tables, and
node_labels can override the display labels when needed.
Default automatic layouts keep ordinary CFA and IRT indicators aligned on clean semantic layers. When a strict row would force straight edges through nearby nodes, dense bifactor, growth, LCA/LPA, and MIRT diagrams use shallow path-diagram layers or arcs to preserve straight edges without node collisions.
Custom layouts can also use a data frame with name, x, and y:
layout <- data.frame(name = c("F", "x1", "x2"), x = c(0, -1, 1), y = c(0, -2, -2))
plot_lvm(graph, layout = layout)
lvmPlot also works with a plain lavaan-style table. This keeps it useful in
scripts where you want to cache model output first.
params <- data.frame(
lhs = c("visual", "visual", "visual", "textual", "textual", "textual",
"textual", "speed", "speed", "speed", "speed"),
op = c("=~", "=~", "=~", "=~", "=~", "=~", "~", "=~", "=~", "=~", "~"),
rhs = c("x1", "x2", "x3", "x4", "x5", "x6", "visual",
"x7", "x8", "x9", "textual"),
est = c(1, 0.55, 0.73, 1, 1.11, 0.93, 0.41, 1, 1.18, 1.08, 0.36),
std.all = c(0.77, 0.42, 0.58, 0.85, 0.81, 0.74, 0.39,
0.62, 0.71, 0.66, 0.34),
pvalue = c(NA, .001, .001, NA, .001, .001, .002, NA, .001, .001, .004)
)
plot_lvm(params)
lvm_tikz(params, standalone = FALSE)
For lavaan multilevel models, lvmPlot keeps same-named variables separate
across levels and draws light Within/Between bands:
model <- '
level: 1
fw =~ y1 + y2 + y3
out ~ fw
level: 2
fb =~ y1 + y2 + y3
out ~ fb + z
'
fit <- sem(model, data = dat, cluster = "school")
plot_lvm(fit, label = "std")
save_lvm_svg(fit, "multilevel-sem.svg", width = 9.4, height = 6.6)
The same works from cached parameter tables containing a level column.
The automatic layout is intentionally simple and SEM-shaped: latent variables and structural variables sit on a main row, while indicators are placed below their latent factor. For full control, pass coordinates:
layout <- data.frame(
name = c("visual", "textual", "speed", "x1", "x2", "x3",
"x4", "x5", "x6", "x7", "x8", "x9"),
x = c(-3, 0, 3, -4, -3, -2, -1, 0, 1, 2, 3, 4),
y = c(0, 0, 0, -2, -2, -2, -2, -2, -2, -2, -2, -2)
)
sem_tikz(params, layout = layout)
Or use the compact matrix form:
sem_tikz(
params,
layout = matrix(c("visual", "textual", "speed",
"x1", "x4", "x7",
"x2", "x5", "x8",
"x3", "x6", "x9"),
nrow = 4, byrow = TRUE)
)
lvmPlot(), with plot_lvm() and
lvmPlot_editor() available when scripts need explicit static or interactive
control.=~), structural regressions (~), and covariances
(~~) are supported for SEM-style models.diagram = "auto" and
can be expanded with diagram = "all".residuals = TRUE, but are hidden by
default to keep diagrams readable.