---
title: "Working with different models in lvmPlot"
author: "Feng Ji"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Working with different models in lvmPlot}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

<style>
body { font-family: "Times New Roman", Times, serif; font-size: 12pt; color: #000000; background: #ffffff; max-width: 900px; line-height: 1.55; }
h1, h2, h3, h4, p, li, th, td, caption, a, a:visited { color: #000000; font-family: inherit; }
h1 { font-size: 16pt; } h2 { font-size: 13pt; margin-top: 1.8em; border: 0; } h3 { font-size: 12pt; }
a { text-decoration: underline; }
pre, code { font-family: "Courier New", monospace; font-size: 10pt; color: #000000; }
pre { background: #ffffff; border: 1px solid #d0d0d0; box-shadow: none; }
pre span, code span { color: #000000 !important; }
th, td { border-bottom: 1px solid #d0d0d0; padding: 6px 8px; background: #ffffff; }
</style>

```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 8,
                     fig.height = 5.5, out.width = "100%")
library(lvmPlot)
packages <- c("lavaan", "psych", "mirt", "eRm", "mclust", "OpenMx", "semPlot")
available <- setNames(vapply(packages, requireNamespace, logical(1), quietly = TRUE), packages)
.models <- list()
remember_model <- function(name, object, label = "std", input = "fitted object", diagram = "all") {
  .models[[name]] <<- list(object = object, label = label, input = input, diagram = diagram)
}
```

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](drag-and-drop.html).
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.

## Packages and example data

Install only the model packages you intend to use. The editor additionally
needs `shiny` and `jsonlite`; `svglite` and `ragg` provide SVG and PNG devices.

```{r installation, eval=FALSE, purl=FALSE}
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.

```{r package-versions, echo=FALSE}
knitr::kable(data.frame(Package = packages,
  Version = vapply(packages, function(p) {
    if (available[[p]]) as.character(utils::packageVersion(p)) else "Not installed; example not run"
  }, character(1))), row.names = FALSE)
```

## CFA: the starting point

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.

```{r cfa, eval=available[["lavaan"]]}
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)
```

```{r cfa-record, include=FALSE, eval=available[["lavaan"]]}
stopifnot(lavaan::lavInspect(cfa_fit, "converged"))
remember_model("cfa", cfa_fit)
```

```{r cfa-editor, eval=FALSE, purl=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.

## SEM: measurement and structural paths

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.

```{r sem, eval=available[["lavaan"]]}
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)
```

```{r sem-record, include=FALSE, eval=available[["lavaan"]]}
stopifnot(lavaan::lavInspect(sem_fit, "converged"))
remember_model("sem", sem_fit)
```

```{r sem-editor, eval=FALSE, purl=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.

## Ordinal CFA

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.

```{r ordinal, eval=available[["lavaan"]]}
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)
```

```{r ordinal-record, include=FALSE, eval=available[["lavaan"]]}
stopifnot(lavaan::lavInspect(ordinal_fit, "converged"))
remember_model("ordinal-cfa", ordinal_fit)
```

```{r ordinal-editor, eval=FALSE, purl=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)`.

## Linear growth

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.

```{r growth, eval=available[["lavaan"]]}
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)
```

```{r growth-record, include=FALSE, eval=available[["lavaan"]]}
stopifnot(lavaan::lavInspect(growth_fit, "converged"))
remember_model("growth", growth_fit, "est")
```

```{r growth-editor, eval=FALSE, purl=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.

## Two-level CFA

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.

```{r multilevel, eval=available[["lavaan"]]}
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")
plot_lvm(twolevel_fit, diagram = "all", label = "std", stars = FALSE)
```

```{r multilevel-record, include=FALSE, eval=available[["lavaan"]]}
stopifnot(lavaan::lavInspect(twolevel_fit, "converged"))
remember_model("twolevel-cfa", twolevel_fit)
```

```{r multilevel-editor, eval=FALSE, purl=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.

## Multigroup CFA: one figure per group

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.

```{r multigroup, eval=available[["lavaan"]]}
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)
plot_lvm(group_tables[[2]], diagram = "all", label = "std", stars = FALSE)
```

```{r multigroup-record, include=FALSE, eval=available[["lavaan"]]}
stopifnot(lavaan::lavInspect(group_fit, "converged"))
for (g in seq_along(group_tables)) {
  remember_model(paste0("group-", g), group_tables[[g]], input = "group-specific fitted parameter table")
}
```

```{r multigroup-editor, eval=FALSE, purl=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 with psych

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.

```{r efa, eval=available[["psych"]] && available[["lavaan"]]}
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)
```

```{r efa-record, include=FALSE, eval=available[["psych"]] && available[["lavaan"]]}
remember_model("efa", efa_fit, "est")
```

```{r efa-editor, eval=FALSE, purl=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.

## Two-parameter IRT with mirt

`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.

```{r irt, eval=available[["mirt"]]}
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)
```

```{r irt-record, include=FALSE, eval=available[["mirt"]]}
stopifnot(mirt::extract.mirt(irt_fit, "converged"))
remember_model("irt-2pl", irt_fit, "est")
```

```{r irt-editor, eval=FALSE, purl=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.

## Binary Rasch with eRm

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.

```{r rasch, eval=available[["eRm"]]}
rasch_fit <- eRm::RM(eRm::raschdat1[, 1:6])
plot_lvm(rasch_fit, diagram = "all", label = "est", stars = FALSE)
```

```{r rasch-record, include=FALSE, eval=available[["eRm"]]}
remember_model("rasch", rasch_fit, "est")
```

```{r rasch-editor, eval=FALSE, purl=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.

## A Gaussian mixture with mclust

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.

```{r mixture, eval=available[["mclust"]]}
library(mclust)
profile_fit <- mclust::Mclust(iris[, 1:4], G = 3, modelNames = "EII", verbose = FALSE)
plot_lvm(profile_fit, diagram = "all", label = "none")
```

```{r mixture-record, include=FALSE, eval=available[["mclust"]]}
remember_model("gaussian-mixture", profile_fit, "none")
```

```{r mixture-editor, eval=FALSE, purl=FALSE}
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.

## OpenMx RAM: a fitted one-factor model

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.

```{r openmx, eval=available[["OpenMx"]] && available[["lavaan"]]}
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)
```

```{r openmx-record, include=FALSE, eval=available[["OpenMx"]] && available[["lavaan"]]}
stopifnot(mx_fit$output$status$code == 0)
remember_model("openmx-ram", mx_fit, "est")
```

```{r openmx-editor, eval=FALSE, purl=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.

## A semPlotModel bridge

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.

```{r semplot, eval=available[["semPlot"]] && available[["lavaan"]]}
semplot_object <- semPlot::semPlotModel(cfa_fit)
plot_lvm(semplot_object, diagram = "all", label = "est", stars = FALSE)
```

```{r semplot-record, include=FALSE, eval=available[["semPlot"]] && available[["lavaan"]]}
remember_model("semplot-bridge", semplot_object, "est", input = "semPlotModel converted from a fitted CFA")
```

```{r semplot-editor, eval=FALSE, purl=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.

## A bifactor specification without fitting

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.

```{r bifactor}
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")
```

```{r bifactor-record, include=FALSE}
remember_model("bifactor-specification", bifactor_paths, "none", input = "unfitted parameter-table specification")
```

```{r bifactor-editor, eval=FALSE, purl=FALSE}
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.

## Saving work across these models

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.

```{r evaluated-examples, echo=FALSE}
knitr::kable(data.frame(Example = names(.models),
  Input = vapply(.models, `[[`, character(1), "input"),
  Labels = vapply(.models, `[[`, character(1), "label")), row.names = FALSE)
```
