---
title: "Editing and saving path diagrams with lvmPlot"
author: "Feng Ji"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Editing and saving path diagrams with 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: 960px; 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; font-weight: bold; }
h2 { font-size: 13pt; margin-top: 1.8em; border: 0; }
h3, h4 { font-size: 12pt; }
a { text-decoration: underline; }
pre, code { font-family: "Courier New", monospace; color: #000000; font-size: 10pt; }
pre { background: #ffffff; border: 1px solid #d0d0d0; box-shadow: none; padding: 12px; }
pre span, code span { color: #000000 !important; }
table { border-collapse: collapse; width: 100%; }
th, td { border-bottom: 1px solid #d0d0d0; padding: 6px 8px; background: #ffffff; vertical-align: top; }
img { max-width: 100%; height: auto; }
@media print { h1, h2, h3 { break-after: avoid; } p { orphans: 3; widows: 3; } }
</style>

```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 8,
                      fig.height = 4.5, fig.align = "center", dpi = 120)
```

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:

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

```{r source-install, eval=FALSE}
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](https://lavaan.ugent.be/tutorial/cfa.html). It relates nine
test scores to visual, textual, and speed factors:

```{r fit-cfa}
library(lavaan)
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"`:

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

```{r initial-cfa, fig.cap="The three-factor CFA with standardized coefficients and factor covariances."}
plot_lvm(fit, diagram = "all", label = "std", stars = FALSE)
```

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.

```{r include-variances, eval=FALSE}
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:

```{r save-fit, eval=FALSE}
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:

```{r run-downloaded-code, eval=FALSE}
# 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:

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

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

```{r custom-layout}
positions <- layout_matrix(rbind(
  c("", "visual", "", "", "textual", "", "", "speed", ""),
  paste0("x", 1:9)
))

positions
```

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.

```{r named-labels}
node_labels <- c(
  visual = "Visual ability",
  textual = "Textual ability",
  speed = "Processing speed"
)
```

A coordinate data frame is equally valid:

```{r coordinate-layout, eval=FALSE}
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:

```{r manuscript-style}
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:

```{r manual-coefficient, eval=FALSE}
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:

```{r prepare-export-graph}
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
)
```

```{r save-figures, eval=FALSE}
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:

```{r bundle, eval=FALSE}
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](model-examples.html). 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.

```{r fit-sem}
sem_model <- paste(cfa_model, '
  textual ~ visual
  speed ~ visual + textual
')
sem_fit <- sem(sem_model, data = HolzingerSwineford1939)
```

```{r structural-diagram, fig.width=7, fig.height=4, fig.cap="A structural view of the fitted example, with the measurement paths omitted."}
plot_lvm(sem_fit, diagram = "structural", label = "std", stars = FALSE,
         theme = "classic", style = manuscript_style)
```

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

```{r parameter-table}
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.

```{r table-editor, eval=FALSE}
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:

```{r irt-schematic, fig.width=7, fig.height=3.5}
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:

```{r multiple-groups, eval=FALSE}
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:

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