---
title: "Editing a path diagram with the mouse"
author: "Feng Ji"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Editing a path diagram with the mouse}
  %\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; 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; }
figure.ui-figure { margin: 1.2em auto; width: 100%; break-inside: avoid; }
.ui-screen { position: relative; width: 100%; overflow: hidden; border: 1px solid #b8b8b8; background: #ffffff; box-sizing: border-box; }
.ui-screen img.ui-image { position: absolute; max-width: none !important; height: auto; margin: 0; display: block; }
.ui-screen svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.ui-screen svg text { font-family: "Times New Roman", Times, serif; font-weight: bold; fill: #000000; }
figcaption { margin-top: 0.5em; font-size: 11pt; line-height: 1.4; color: #000000; }
.ui-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; align-items: start; }
.ui-pair figure.ui-figure { margin-top: 0.5em; }
details.ui-full { margin-top: 0.55em; font-size: 10.5pt; }
details.ui-full summary { cursor: pointer; text-decoration: underline; }
.ui-pan { overflow: auto; max-height: 760px; margin-top: 8px; border: 1px solid #b8b8b8; }
.ui-pan img { display: block; width: 1280px !important; max-width: none !important; height: 720px; margin: 0; }
@media (max-width: 640px) { .ui-pair { grid-template-columns: 1fr; gap: 4px; } body { padding: 0 12px; } }
@media print { h1, h2, h3 { break-after: avoid; } p { orphans: 3; widows: 3; } }
@media print { details.ui-full { display: none; } .ui-pair { gap: 12px; } }
</style>

```{r setup, include=FALSE}
knitr::opts_chunk$set(eval = FALSE, collapse = TRUE, comment = "#>")
.shot_id <- 0L
ui_shot <- function(file, caption, crop = c(0, 0, 1280, 720),
                    marks = list(), drag = NULL, max_width = 900) {
  .shot_id <<- .shot_id + 1L
  arrow_id <- paste0("tutorial-arrow-", .shot_id)
  src <- knitr::image_uri(file.path("figures", "drag-editor", file))
  esc <- function(x) as.character(htmltools::htmlEscape(x, attribute = TRUE))
  x <- crop[1]; y <- crop[2]; w <- crop[3]; h <- crop[4]
  cat(sprintf('<figure class="ui-figure" style="max-width:%spx">', max_width))
  cat(sprintf('<div class="ui-screen" style="padding-top:%.6f%%">', 100 * h / w))
  cat(sprintf(paste0('<img class="ui-image" src="%s" alt="%s" ',
                     'style="width:%.6f%%;left:%.6f%%;top:%.6f%%">'),
              src, esc(caption), 128000 / w, -100 * x / w, -100 * y / h))
  cat(sprintf('<svg viewBox="%s %s %s %s" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">', x, y, w, h))
  if (!is.null(drag)) {
    cat(sprintf(paste0('<defs><marker id="%s" markerWidth="8" markerHeight="8" ',
                       'refX="7" refY="4" orient="auto"><path d="M0 0L8 4L0 8Z" ',
                       'fill="#000000"/></marker></defs>'), arrow_id))
    cat(sprintf('<path d="M%s %s L%s %s" fill="none" stroke="#ffffff" stroke-width="5"/>',
                drag[1], drag[2], drag[3], drag[4]))
    cat(sprintf(paste0('<path d="M%s %s L%s %s" fill="none" stroke="#000000" ',
                       'stroke-width="2" stroke-dasharray="6 4" marker-end="url(#%s)"/>'),
                drag[1], drag[2], drag[3], drag[4], arrow_id))
  }
  for (m in marks) {
    cat(sprintf('<line x1="%s" y1="%s" x2="%s" y2="%s" stroke="#ffffff" stroke-width="4"/>', m[2], m[3], m[4], m[5]))
    cat(sprintf('<line x1="%s" y1="%s" x2="%s" y2="%s" stroke="#000000" stroke-width="1.5"/>', m[2], m[3], m[4], m[5]))
    cat(sprintf('<circle cx="%s" cy="%s" r="12" fill="#ffffff" stroke="#000000" stroke-width="1.5"/>', m[2], m[3]))
    cat(sprintf('<text x="%s" y="%s" text-anchor="middle" dominant-baseline="central" font-size="16">%s</text>', m[2], m[3], esc(m[1])))
  }
  cat('</svg></div>')
  cat(sprintf('<figcaption>%s</figcaption>', esc(caption)))
  cat('<details class="ui-full"><summary>Show the unannotated full-size screenshot</summary><div class="ui-pan">')
  cat(sprintf('<img src="%s" alt="Unannotated editor screenshot: %s" width="1280" height="720">', src, esc(caption)))
  cat('</div></details></figure>\n')
}
```

This example uses a three-factor CFA to introduce the lvmPlot editor. We will
move a factor and its indicators, reposition a loading label, and save the
edited diagram. All of the positioning is done with the mouse. The coefficient
values still come from the fitted model.

The screenshots were taken from the local lvmPlot 0.1.1 development build,
using the example below. The numbered circles and dashed black arrows are
tutorial annotations, not editor controls. Each figure has an expandable,
unannotated screenshot if you need to read a small button or label.

## Find your way around the editor

The large area on the right is the drawing surface. The left sidebar contains
selection, alignment, appearance, and file controls. **The sidebar scrolls
separately from the drawing surface.** The download buttons are near its bottom;
they are not missing when only the upper controls are visible.

```{r interface-overview, echo=FALSE, eval=TRUE, results='asis'}
ui_shot("01-editor-overview.png",
  "Figure 1. The editor before any changes. The download controls are farther down the left sidebar.",
  marks = list(c(1, 364, 47, 364, 77), c(2, 473, 430, 520, 456),
               c(3, 660, 510, 542, 529), c(4, 313, 165, 277, 165)))
```

| Number | Where to start | What you can do there |
|:--|:--|:--|
| 1 | **Edit** tab | Drag the diagram here. **Export preview**, next to it, shows the R-rendered result. |
| 2 | The `visual` ellipse | Press inside the node and move it while holding the left mouse button. |
| 3 | A coefficient such as `0.42` | Press on the number or its white background to move only that label. |
| 4 | **Selected nodes** | See which nodes are selected. The controls below act on that selection. |

## 1. Open the example

This walkthrough keeps the same CFA throughout. The
[general vignette](lvmPlot.html) covers scripted layouts and other inputs.

Install the packages used below if you do not already have them:

```{r install}
install.packages(c("lvmPlot", "lavaan", "shiny", "jsonlite", "svglite", "ragg"))
```

If you have a newer `.tar.gz` archive from the maintainer, install it after
these dependencies. The export preview and snapshot behavior described here
require version 0.1.1 or later:

```{r install-archive}
install.packages(file.choose(), repos = NULL, type = "source")
packageVersion("lvmPlot")
```

Restart R if you replaced a version that was already loaded. Then run this
example in the console. The data are included with `lavaan`.

```{r open-mouse-editor}
library(lavaan)
library(lvmPlot)

model <- '
  visual  =~ x1 + x2 + x3
  textual =~ x4 + x5 + x6
  speed   =~ x7 + x8 + x9
'

fit <- cfa(model, data = HolzingerSwineford1939)

lvmPlot(
  fit,
  mode = "edit",
  label = "std",
  stars = FALSE,
  diagram = "all",
  export_name = "three-factor-cfa",
  launch = TRUE
)
```

The browser opens a local Shiny application. Leave R running while you use it.
You should see three factor ellipses and nine indicator rectangles, with
loadings and factor covariances. The call uses `label = "std"` to display
standardized coefficients. With the default automatic label setting, the
estimates may be hidden, leaving no numerical labels to drag.

## 2. Drag a node

1. Stay in **Edit**. Put the pointer inside the `visual` ellipse, not on a number beside an arrow.
2. Hold the left mouse button and move upward, slightly to the left.
3. Release the button. The factor stays at its new location and the attached arrows follow it.

```{r node-before-after, echo=FALSE, eval=TRUE, results='asis'}
cat('<div class="ui-pair">')
ui_shot("01-editor-overview.png",
  "Figure 2a. Before: press inside visual and drag upward in the direction shown.",
  crop = c(415, 290, 260, 315), drag = c(547, 438, 521, 355))
ui_shot("02-node-moved.png",
  "Figure 2b. After: visual has moved, while x1, x2 and x3 remain on the original row.",
  crop = c(415, 290, 260, 315))
cat('</div>')
```

The blue outline means that `visual` is selected. It is an editing highlight,
not a change to the model or an element of the exported figure. You can move
an indicator rectangle in the same way. For the next step, leave the indicators
where they are so that your diagram resembles the screenshots.

If a node refuses to move, check whether it is locked. Unlock all releases
the locked nodes. If it moves in larger steps than you want, turn off Snap to
grid. You can also focus the canvas and use the arrow keys to nudge selected
nodes; Shift plus an arrow gives a larger step.

## 3. Drag the coefficient, not the node

Find the loading from `visual` to `x2`. Its standardized value is approximately
`0.42` in this example.

1. Put the pointer directly on `0.42` or its white label background.
2. Hold the left mouse button and move the number upward, a little to the right.
3. Release it. Check that the nodes stayed put and the displayed value is still `0.42`.

```{r coefficient-before-after, echo=FALSE, eval=TRUE, results='asis'}
cat('<div class="ui-pair">')
ui_shot("02-node-moved.png",
  "Figure 3a. Before: the numbered pointer identifies the label to grab. Drag the number itself, not its path.",
  crop = c(435, 315, 230, 290), marks = list(c(1, 452, 422, 507, 468)),
  drag = c(532, 451, 550, 432))
ui_shot("03-coefficient-moved.png",
  "Figure 3b. After: 0.42 is higher on the page. The factor and all three indicators have stayed in place.",
  crop = c(435, 315, 230, 290), marks = list(c(1, 617, 412, 566, 416)))
cat('</div>')
```

Keep the number close enough to its path that it cannot be mistaken for an
estimate on a nearby arrow. An overlap can sometimes be fixed by a small move;
placing the label much farther away may make the diagram harder to read.

Double-click the coefficient label to return it to automatic placement. Try
that once, then place it by hand again. You can leave most coefficients in
their automatic positions and move just the few that need more room.

A manually placed label stays at its graph coordinates. If you move `visual`
or `x2` again, the `0.42` may need another adjustment. This is why it is usually
easier to arrange the nodes first and place individual coefficients afterward.
Dragging does not change the loading estimate or refit the model.

## 4. Change a displayed name

Select the `visual` ellipse again. Enter `Visual ability` in Selected label
and click Apply label. The longer name appears in the diagram, but the model
variable remains `visual`. Layouts and parameter tables still use that original
name.

```{r display-name-controls, echo=FALSE, eval=TRUE, results='asis'}
ui_shot("04-display-label.png",
  "Figure 4. Select the node, type its display name in Selected label (1), then click Apply label (2). The diagram now says Visual ability.",
  crop = c(20, 125, 665, 320),
  marks = list(c(1, 310, 311, 282, 311), c(2, 150, 361, 125, 361)))
```

Undo and redo restore node positions, display labels, and coefficient-label
positions. They do not track every style control. Use undo for a single edit;
Reset layout resets the arrangement and also clears locks.

## 5. Look at what will be exported

Open Export preview and click Refresh export preview. Check the `visual` node
and the loading you moved. The factor should have its new name and position,
and the `0.42` should appear beside the arrow where you placed it. If it still
looks crowded, return to Edit, make a small adjustment, and refresh again.

```{r export-preview-interface, echo=FALSE, eval=TRUE, results='asis'}
ui_shot("05-export-preview.png",
  "Figure 5. Choose Export preview (1), then Refresh export preview (2). In this R-rendered image, the renamed factor is higher (3), and the manually positioned 0.42 remains near its path (4).",
  crop = c(325, 60, 940, 565),
  marks = list(c(1, 552, 84, 516, 84), c(2, 561, 151, 502, 151),
               c(3, 572, 236, 537, 236), c(4, 588, 328, 535, 328)))
```

This preview is drawn by R rather than by the editing canvas. The two renderers
can differ in font widths, node sizes, and line routing. Those differences are
more noticeable with long labels or small output figures, so the export
preview is the useful view for judging the result. It updates when you request
a refresh, not continuously while you drag.

The preview image is raster; PDF and SVG downloads remain vector graphics.
Selection highlights, the editing grid, and lock indicators do not appear in
the files. Background and Grid controls affect the editing canvas, while
standard exports have a white background.

## 6. Save both the figure and the editable arrangement

Put the pointer over the **left sidebar** and scroll down until you see
**SVG**, **PDF**, and **PNG**. Scrolling over the diagram is not the same
operation. The screenshot below shows the bottom of the sidebar.

```{r save-controls-closeup, echo=FALSE, eval=TRUE, results='asis'}
ui_shot("06-save-and-restore.png",
  "Figure 6. The bottom of the sidebar: image downloads (1), Figure R (2), State JSON (3), and Browse under Load state JSON (4).",
  crop = c(15, 345, 310, 348), max_width = 410,
  marks = list(c(1, 264, 394, 214, 394), c(2, 264, 474, 204, 474),
               c(3, 214, 514, 139, 514), c(4, 216, 626, 101, 591)))
```

| If you need to... | Use this control | What to keep |
|:--|:--|:--|
| Put the edited figure in a paper or presentation | **PDF**, **SVG**, or **PNG** (1) | The rendered artwork. PDF and SVG are vector formats. |
| Reproduce the saved figure from R | **Figure R** (2) | A script containing the graph snapshot, positions, and rendering code. |
| Continue dragging this arrangement later | **State JSON** (3) | The editor state; keep the fitted model separately. |
| Reopen that arrangement | **Browse...** under **Load state JSON** (4) | Choose the previously downloaded State JSON in a compatible editor session. |

Download a PDF, SVG, or PNG from the editor. The download waits for the current
browser edits to reach R before preparing the file. If the R session has
stopped or synchronization fails, the editor reports the problem rather than
downloading an older diagram. Restore the connection and retry.

A point that is easy to miss: `fit` itself has not changed. If you stop Shiny
and call `plot_lvm(fit)`, R draws another diagram from the original fitted
object. That call cannot retrieve the positions you chose in the browser.

Save State JSON before closing the editor if you want to continue working on
the arrangement. It records node positions and display labels, manual
coefficient positions, styling, selection, and locks, along with display
settings such as label type and decimal places. It also records information
used to check that the state belongs to a compatible diagram.

Figure R is useful for a different reason. It contains the saved graph and
its estimates, together with code to reproduce the figure. For a manuscript,
keep this script and the State JSON alongside the artwork. Layout CSV and
Layout R are narrower exports for transferring node coordinates; they are
not substitutes for the complete editor state.

## 7. Return to the figure later

To resume editing, rerun the example with the same model and diagram settings,
then select the saved file under Load state JSON. You can also save `fit` with
`saveRDS()` and load it again instead of repeating the estimation. State JSON
does not contain the observations or the fitted lavaan object.

Node names and paths must be compatible. Changing a display label from
`visual` to `Visual ability` is fine; renaming the model variable or adding a
path can make an old state file incompatible. A structural-only view may also
have different nodes and paths from the full diagram.

If you refit the same model to updated data, compatible state can reuse your
arrangement with the new coefficients. Check the labels again if those numbers
take up more space. The old Figure R script still contains the estimates from
when it was downloaded, not the new fit.

A short way to check your saving routine is to move a node somewhere obvious,
save State JSON, and reopen the same model in another editing session. Load
that JSON using control 4 in Figure 6. The deliberately moved node should
return to its saved position. Do this before spending a long time on a layout.

Run that script when you need to reproduce the saved figure exactly as a graph
snapshot. For the export name used above:

```{r reproduce-edited-figure}
# Run in the directory where you want the output figures.
source("three-factor-cfa-figure.R")
```

With lvmPlot installed, the script can run in a fresh R session without the
original `fit`. It reconstructs the graph, applies the saved positions and
style, and writes PDF, SVG, and PNG files in the working directory. Rendering
may still depend on the fonts available to the graphics device.

Keep the statistical analysis separately so that you can refit the model if
needed. When you finish an editing session, use RStudio's Stop button or press
Esc in the R console. Closing the browser tab alone may leave Shiny running.

## Working with several nodes

After the single-node example, try selecting a small group. Click `x1`, hold
Shift, and click `x2` and `x3`. All three rectangles should have a blue outline.
Scroll within the sidebar to find the alignment controls.

```{r alignment-controls, echo=FALSE, eval=TRUE, results='asis'}
ui_shot("07-align-selection.png",
  "Figure 7. The three indicators are selected (1). Align row (2) puts selected, unlocked nodes on one horizontal line; Distribute X (3) spaces them evenly. These indicators already form a row, so Align row makes no visible change here.",
  crop = c(15, 45, 1250, 575),
  marks = list(c(1, 680, 554, 652, 554), c(2, 95, 318, 95, 344),
               c(3, 248, 414, 155, 400)))
```

**Align row** uses the average vertical coordinate of the selected, unlocked
nodes. **Distribute X** needs at least three such nodes and uses the outermost
horizontal positions. A locked node is excluded, rather than used as an
alignment anchor. If only one node changes, look at the selection before
repeating the command.

To protect a finished group, scroll back to the top of the sidebar and click
**Lock**. **Unlock all** releases locked nodes. **Fit view** recenters the
canvas if the diagram has moved out of view. Arrange the nodes before placing
individual coefficients; later node moves can leave manually placed numbers
behind at their saved graph coordinates.

## When the result is not what you expected

| What you see | What to try |
|:--|:--|
| There are no numbers to drag | Use `label = "std"` in this CFA example, or choose a coefficient display under **Edge labels**. |
| A node will not move | Check whether it is locked. Use **Unlock all**, then try again inside the node. |
| A node moves in jumps | Clear **Snap to grid** for finer mouse placement. |
| A coefficient is in the wrong place | Double-click that number to restore automatic placement, or drag it again. |
| The canvas moved out of view | Scroll to **Fit view** in the sidebar. |
| You cannot find the save buttons | Scroll inside the left sidebar, as shown in Figure 6. |
| Export preview still shows an earlier edit | Click **Refresh export preview** after the change. The preview is not continuously refreshed. |
| A download cannot be prepared | Keep the R session running, restore the connection if needed, and click the export button again. |
| `plot_lvm(fit)` shows the original arrangement | That call does not contain your browser edits. Use the downloaded **Figure R**, or reopen **State JSON** in the editor. |

For a figure you intend to keep, save the artwork, State JSON, and Figure R
before closing Shiny. Those files serve different purposes; a PDF alone does
not preserve the editable arrangement.
