The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
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[@]metanet.ch.
The mouse editor is not limited to CFA. The examples below start with several different model objects, draw a first diagram, and then open that diagram for editing. The fitting code runs before Shiny starts. Moving a node or a number never changes the fitted model.
For the basic editing gestures, see Mouse editing in lvmPlot. This tutorial concentrates on what each input contains and what its diagram can reasonably show. Most examples use fitted objects. The bifactor example near the end is deliberately only a model specification.
Install only the model packages you intend to use. The editor
additionally needs shiny and jsonlite;
svglite and ragg provide SVG and PNG
devices.
install.packages(c("lvmPlot", "lavaan", "shiny", "jsonlite", "svglite", "ragg"))
# Optional packages for the later sections:
install.packages(c("psych", "mirt", "eRm", "mclust", "OpenMx", "semPlot"))
library(lvmPlot)The table records the packages available when this HTML file was built. A missing package leaves its example code visible but omits the computed figure. The interactive launch calls are not run during document construction.
| Package | Version |
|---|---|
| lavaan | 0.6.19 |
| psych | 2.4.6.26 |
| mirt | 1.46.1 |
| eRm | 1.0.9 |
| mclust | 6.1.2 |
| OpenMx | 2.21.13 |
| semPlot | 1.1.6 |
The nine test scores in HolzingerSwineford1939 give us a
small, familiar example. Keep this fitted object: the ordinal,
multigroup, EFA, and OpenMx sections reuse the same data, not the same
estimates.
cfa_model <- '
visual =~ x1 + x2 + x3
textual =~ x4 + x5 + x6
speed =~ x7 + x8 + x9
'
hs <- lavaan::HolzingerSwineford1939
cfa_fit <- lavaan::cfa(cfa_model, data = hs)
plot_lvm(cfa_fit, diagram = "all", label = "std", stars = FALSE)lvmPlot(cfa_fit, mode = "edit", diagram = "all", label = "std",
stars = FALSE, export_name = "cfa", launch = TRUE)Move the visual factor first, then drag one loading
label away from a nearby arrow. diagram = "all" keeps the
factor covariances in this example. It does not automatically add every
residual variance or intercept to the drawing.
Here the same three factors also predict one another. This small example keeps the measurement blocks from the CFA so that the structural paths are easy to identify. These regressions illustrate the plotting workflow; fitting them to these cross-sectional scores does not establish a causal ordering.
sem_model <- '
visual =~ x1 + x2 + x3
textual =~ x4 + x5 + x6
speed =~ x7 + x8 + x9
textual ~ visual
speed ~ visual + textual
'
sem_fit <- lavaan::sem(sem_model, data = hs)
plot_lvm(sem_fit, diagram = "all", label = "std", stars = FALSE)lvmPlot(sem_fit, mode = "edit", diagram = "all", label = "std",
stars = FALSE, export_name = "sem", launch = TRUE)Try shifting a whole measurement block rather than moving each indicator independently. Select its nodes with Shift-click, move the selection, then adjust the structural-path labels. Check Export preview before deciding that the paths between the three factors have enough room. Models with many correlated residuals can take substantially longer to arrange; this example does not exercise that more demanding case.
The adapter also accepts a lavaan fit with ordered indicators. To make this example self-contained, we divide each score into four categories. This is a coding demonstration, not a recommendation to discretize continuous scores in an analysis.
ordinal_data <- hs
items <- paste0("x", 1:9)
for (item in items) {
ordinal_data[[item]] <- ordered(cut(hs[[item]],
breaks = stats::quantile(hs[[item]], seq(0, 1, length.out = 5)),
include.lowest = TRUE))
}
ordinal_fit <- lavaan::cfa(cfa_model, data = ordinal_data, ordered = items)
plot_lvm(ordinal_fit, diagram = "all", label = "std", stars = FALSE)lvmPlot(ordinal_fit, mode = "edit", diagram = "all", label = "std",
stars = FALSE, export_name = "ordinal-cfa", launch = TRUE)The figure shows the factor-indicator structure and standardized
loadings. It is not a threshold table. Inspect the thresholds separately
with lavaan::parameterEstimates(ordinal_fit).
An intercept factor and a slope factor share the repeated measurements. The fixed slope loadings encode time, so unstandardized labels are the useful starting point here: 0, 1, 2, and 3 remain recognizable.
growth_model <- '
i =~ 1*t1 + 1*t2 + 1*t3 + 1*t4
s =~ 0*t1 + 1*t2 + 2*t3 + 3*t4
'
growth_fit <- lavaan::growth(growth_model, data = lavaan::Demo.growth)
plot_lvm(growth_fit, diagram = "all", label = "est", stars = FALSE)lvmPlot(growth_fit, mode = "edit", diagram = "all", label = "est",
stars = FALSE, export_name = "growth", launch = TRUE)Keep t1 through t4 in time order. Move the
two growth factors apart if their arrows crowd the middle of the
diagram, then place the fixed-loading labels. The factor means and their
uncertainty still belong in the model results; the path diagram is not a
complete growth-model report.
Within-cluster and between-cluster versions of an observed variable must not be merged into one node. This example fits the first 30 clusters of lavaan’s simulated two-level data.
twolevel_data <- subset(lavaan::Demo.twolevel, cluster <= 30)
twolevel_model <- '
level: 1
fw =~ y1 + y2 + y3
level: 2
fb =~ y1 + y2 + y3
'
twolevel_fit <- lavaan::sem(twolevel_model, data = twolevel_data, cluster = "cluster")
#> Warning: lavaan->lav_object_post_check():
#> some estimated ov variances are negative
plot_lvm(twolevel_fit, diagram = "all", label = "std", stars = FALSE)lvmPlot(twolevel_fit, mode = "edit", diagram = "all", label = "std",
stars = FALSE, export_name = "twolevel-cfa", launch = TRUE)The displayed item names can repeat across levels. Internally, names
such as L1__y1 and L2__y1 keep them distinct.
Leave space between the two level blocks when editing, and use those
internal names in a scripted layout.
A shared measurement structure does not imply shared standardized estimates. Do not average group coefficients to make a single numerical diagram. Extract the parameter table and pass one group at a time.
group_fit <- lavaan::cfa(cfa_model, data = hs, group = "school",
group.equal = "loadings")
group_parameters <- lavaan::parameterEstimates(group_fit, standardized = TRUE)
group_names <- lavaan::lavInspect(group_fit, "group.label")
group_tables <- lapply(seq_along(group_names), function(g) {
group_parameters[group_parameters$group == g, , drop = FALSE]
})
names(group_tables) <- group_names
plot_lvm(group_tables[[1]], diagram = "all", label = "std", stars = FALSE)lvmPlot(group_tables[[1]], mode = "edit", diagram = "all", label = "std",
stars = FALSE, export_name = "school-1", launch = TRUE)
# Stop that editor before opening the second one.
lvmPlot(group_tables[[2]], mode = "edit", diagram = "all", label = "std",
stars = FALSE, export_name = "school-2", launch = TRUE)If both group diagrams have the same nodes and paths, State JSON can transfer the arrangement between them. Their estimates remain group-specific. Passing the whole multigroup fit instead gives a shared structural view with a warning and without pooled numerical labels.
EFA can contain substantial cross-loadings.
diagram = "auto" selects a primary-loading summary; use
diagram = "all" when those other loadings are part of what
you want to discuss. The following figure uses the latter.
efa_fit <- psych::fa(hs[paste0("x", 1:9)], nfactors = 3,
rotate = "oblimin", fm = "minres")
plot_lvm(efa_fit, diagram = "all", label = "est", stars = FALSE)lvmPlot(efa_fit, mode = "edit", diagram = "all", label = "est",
stars = FALSE, export_name = "efa", launch = TRUE)Arrange the factors before working on cross-loading labels. Removing a line from a summary diagram does not remove that loading from the fitted EFA.
LSAT7 is supplied as response patterns and their
frequencies. Expand that table before fitting the five-item model. The
diagram’s a= labels are item discrimination parameters in
mirt’s internal parameterization, not CFA standardized loadings and not
item difficulties.
irt_data <- mirt::expand.table(mirt::LSAT7)
irt_fit <- mirt::mirt(irt_data, 1, itemtype = "2PL", verbose = FALSE,
quadpts = 21L, TOL = 1e-3,
technical = list(NCYCLES = 200L))
plot_lvm(irt_fit, diagram = "all", label = "est", stars = FALSE)lvmPlot(irt_fit, mode = "edit", diagram = "all", label = "est",
stars = FALSE, export_name = "irt-2pl", launch = TRUE)A single trait with five items is easy to arrange in one row. Move
individual a= labels if necessary. For difficulty,
guessing, or item-response curves, inspect the fitted model separately;
this diagram does not display all IRT parameters. In a real analysis,
choose estimation settings for that analysis rather than copying this
small demonstration’s stopping tolerance.
A Rasch model fixes discrimination. Its useful item label is
difficulty, so these edges carry b= instead of estimated
CFA loadings. We use six items to keep the example readable.
rasch_fit <- eRm::RM(eRm::raschdat1[, 1:6])
plot_lvm(rasch_fit, diagram = "all", label = "est", stars = FALSE)lvmPlot(rasch_fit, mode = "edit", diagram = "all", label = "est",
stars = FALSE, export_name = "rasch", launch = TRUE)There is a sign convention worth checking: eRm stores
betapar as item easiness. The displayed
difficulty is b = -betapar. An easier item thus has a lower
difficulty. This example and its numerical check concern binary
RM objects, not the interpretation of polytomous category
parameters.
Here the figure has a different purpose. It shows a latent profile variable and the indicators used to fit the mixture. It does not draw a separate profile-mean curve for each class.
library(mclust)
#> Package 'mclust' version 6.1.2
#> Type 'citation("mclust")' for citing this R package in publications.
profile_fit <- mclust::Mclust(iris[, 1:4], G = 3, modelNames = "EII", verbose = FALSE)
plot_lvm(profile_fit, diagram = "all", label = "none")lvmPlot(profile_fit, mode = "edit", diagram = "all", label = "none",
export_name = "mixture", launch = TRUE)The Profile node records the number of components. The
arrows describe the model structure, not regression coefficients.
Arrange the indicators and give them readable display names. Use
profile_fit$parameters$mean for class-specific means and
profile_fit$parameters$pro for mixing proportions. The iris
example is a compact mixture demonstration, not an argument for treating
species as psychological latent profiles.
This example estimates a RAM model from the covariance matrix of three scores. The first loading fixes the factor’s scale. The adapter reads the model’s RAM matrices; it is not a converter for arbitrary OpenMx algebra models.
variables <- c("x1", "x2", "x3")
mx_fit <- OpenMx::mxModel("one_factor", type = "RAM",
manifestVars = variables, latentVars = "F",
OpenMx::mxPath(from = "F", to = variables, arrows = 1,
free = c(FALSE, TRUE, TRUE), values = c(1, .8, .8)),
OpenMx::mxPath(from = variables, arrows = 2, free = TRUE, values = 1),
OpenMx::mxPath(from = "F", arrows = 2, free = TRUE, values = 1),
OpenMx::mxData(observed = stats::cov(hs[variables]), type = "cov", numObs = nrow(hs)))
mx_fit <- OpenMx::mxRun(mx_fit, silent = TRUE)
plot_lvm(mx_fit, diagram = "all", label = "est", stars = FALSE)lvmPlot(mx_fit, mode = "edit", diagram = "all", label = "est",
stars = FALSE, export_name = "openmx-ram", launch = TRUE)This is an unstandardized diagram. The fixed loading of one identifies the model; it should not be described as an estimated standardized loading.
If a workflow already produces a semPlotModel, it can be
passed to lvmPlot. The example constructs the bridge from the CFA fit
above, so the expected nodes and paths are known.
semplot_object <- semPlot::semPlotModel(cfa_fit)
plot_lvm(semplot_object, diagram = "all", label = "est", stars = FALSE)lvmPlot(semplot_object, mode = "edit", diagram = "all", label = "est",
stars = FALSE, export_name = "semplot-bridge", launch = TRUE)Compatibility here means this converted object’s parameters and structure are read correctly. It does not establish compatibility with every model class that semPlot itself can parse.
Sometimes the immediate task is explaining a proposed model. A lavaan-style parameter table can describe it without observations or fitted estimates. The general factor below loads on all nine indicators; each specific factor loads on its own three indicators.
bifactor_paths <- data.frame(
lhs = c(rep("g", 9), rep(c("s1", "s2", "s3"), each = 3)),
op = "=~", rhs = rep(paste0("x", 1:9), 2))
plot_lvm(bifactor_paths, diagram = "all", label = "none")lvmPlot(bifactor_paths, mode = "edit", diagram = "all", label = "none",
export_name = "bifactor-specification", launch = TRUE)Keep the general factor on one side of the indicator row and the specific factors on the other. With no fitted coefficients there are no numerical labels to drag. This example checks the graphical specification; it does not fit the model, test identification, or impose orthogonality constraints in a statistical estimator.
For every example, open Export preview after arranging the nodes and labels. Download the finished figure, and keep State JSON if you will resume editing. Figure R contains a graph snapshot and code to reproduce it without the original fitted object. Neither file replaces the analysis script.
Each export request has its own prepared file. A later edit or export cannot rewrite that file. Download URLs are temporary: the session retains at most eight prepared files for two minutes, then asks for a new export rather than serving a different figure under an old URL. Files already downloaded to your computer are unaffected.
State can transfer between models only when the diagram’s node and path identities are compatible. A CFA state file is not a layout template for a Rasch model simply because both diagrams have a latent variable and items.
The following examples were evaluated in this build. Fitted-object examples, converted parameter tables, and the unfitted bifactor specification are listed separately by input type so that the latter is not mistaken for a fitted-model compatibility check.
| Example | Input | Labels |
|---|---|---|
| cfa | fitted object | std |
| sem | fitted object | std |
| ordinal-cfa | fitted object | std |
| growth | fitted object | est |
| twolevel-cfa | fitted object | std |
| group-1 | group-specific fitted parameter table | std |
| group-2 | group-specific fitted parameter table | std |
| efa | fitted object | est |
| irt-2pl | fitted object | est |
| rasch | fitted object | est |
| gaussian-mixture | fitted object | none |
| openmx-ram | fitted object | est |
| semplot-bridge | semPlotModel converted from a fitted CFA | est |
| bifactor-specification | unfitted parameter-table specification | none |
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.