README.md

diy.sem.plot

R-CMD-check CRAN
status Downloads Total
downloads

diy.sem.plot allows users to manually plot fully customisable path diagrams for structural equation models (SEM) in R. It avoids the inflexibility of automated SEM plotting tools and the tedium of drawing diagrams in external applications.

Users manually specify node positions using x-y coordinates, and where on the perimeter of each node (top, bottom, left, or right) a paths should begin and end. From there, the primary function diyPaths() renders the diagram, automatically inserting estimates centered on the midpoint of paths and adjusting each node’s shape to match its variable type. A range of fine-tuning options are included, facilitating the creation of a path diagram exactly as envisioned, entirely within R.

Installation

Install the released version of diy.sem.plot from CRAN:

install.packages("diy.sem.plot")

Or install the development version from GitHub to get the latest features.

install.packages("remotes")
remotes::install_github("snagy86/diy.sem.plot", build_vignettes = TRUE)

Example

Below is a brief example of using the package. For more examples, plus a full explanation of the workflow and available arguments, see the package’s vignette.

In this example, I use template() to generate the starting code, saving time and avoiding syntax errors. It takes the fitted lavaan model and prints the node list, path list, title list, note list and diyPaths() call with default values. I then edited these values to position everything.

library(lavaan)
#> This is lavaan 0.7-2
#> lavaan is FREE software! Please report any bugs.
library(diy.sem.plot)

# Specify and fit the SEM

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

  speed ~ visual + textual
  visual ~~ textual
'

fit_sem <- sem(model = sem_model, data = HolzingerSwineford1939)

# Generate starting code with every diyPaths() argument

template(fit = fit_sem, include_variance_paths = TRUE, diyPaths_arguments = "max", quiet = TRUE) # quiet = TRUE stores the template without printing it

# Position and relabel each node

node_list <- list(
  #node(s) for latent variable(s) (<- section comments like this are generated by template())
  node(name = "visual", x = 1, y = 1, label = "Visual"),
  node(name = "textual", x = 1, y = 3, label = "Textual"),
  node(name = "speed", x = 4, y = 2, label = "Speed"),

  #node(s) for observed variables loading onto latent variable visual
  node(name = "x1", x = 0, y = -0.5, label = "Visual\nPerception"),
  node(name = "x2", x = 1, y = -0.5, label = "Cubes"),
  node(name = "x3", x = 2, y = -0.5, label = "Lozenges"),

  #node(s) for observed variables loading onto latent variable textual
  node(name = "x4", x = 0, y = 4.5, label = "Paragraph\nComprehension"),
  node(name = "x5", x = 1, y = 4.5, label = "Sentence\nCompletion"),
  node(name = "x6", x = 2, y = 4.5, label = "Word\nMeaning"),

  #node(s) for observed variables loading onto latent variable speed
  node(name = "x7", x = 6, y = 1.25, label = "Speeded\nAddition"),
  node(name = "x8", x = 6, y = 2, label = "Speeded\nCounting"),
  node(name = "x9", x = 6, y = 2.75, label = "Speeded\nDiscrimination")
)

# Set which side of each node every path starts and ends on

path_list <- list(
  # regression path(s)
  path(from = "visual", to = "speed", side_from = "right", side_to = "left", nudge_text_x = 0, nudge_text_y = 0),
  path(from = "textual", to = "speed", side_from = "right", side_to = "left", nudge_text_x = 0, nudge_text_y = 0),

  #loading path(s) from visual
  path(from = "visual", to = "x1", side_from = "bottom", side_to = "top", nudge_text_x = -0.1, nudge_text_y = 0),
  path(from = "visual", to = "x2", side_from = "bottom", side_to = "top", nudge_text_x = 0, nudge_text_y = 0),
  path(from = "visual", to = "x3", side_from = "bottom", side_to = "top", nudge_text_x = 0.1, nudge_text_y = 0),

  #loading path(s) from textual
  path(from = "textual", to = "x4", side_from = "top", side_to = "bottom", nudge_text_x = -0.1, nudge_text_y = 0),
  path(from = "textual", to = "x5", side_from = "top", side_to = "bottom", nudge_text_x = 0, nudge_text_y = 0),
  path(from = "textual", to = "x6", side_from = "top", side_to = "bottom", nudge_text_x = 0.1, nudge_text_y = 0),

  #loading path(s) from speed
  path(from = "speed", to = "x7", side_from = "right", side_to = "left", nudge_text_x = 0, nudge_text_y = -0.1),
  path(from = "speed", to = "x8", side_from = "right", side_to = "left", nudge_text_x = 0, nudge_text_y = 0),
  path(from = "speed", to = "x9", side_from = "right", side_to = "left", nudge_text_x = 0, nudge_text_y = 0.1),

  # covariance/correlation path(s)
  path(from = "textual", to = "visual", side_from = "left", side_to = "left", nudge_text_x = 0, nudge_text_y = 0, cov_curve = -0.6), # cov_curve sets how strongly the path curves; its sign sets the direction

  # variance path(s)
  # REMEMBER TO SET show_variances IN diyPaths CALL = TRUE TO INCLUDE IN DIAGRAM!
  path(from = "x1", to = "x1", nudge_text_x = 0, nudge_text_y = 0, variance_position = "bottom"),
  path(from = "x2", to = "x2", nudge_text_x = 0, nudge_text_y = 0, variance_position = "bottom"),
  path(from = "x3", to = "x3", nudge_text_x = 0, nudge_text_y = 0, variance_position = "bottom"),
  path(from = "x4", to = "x4", nudge_text_x = 0, nudge_text_y = 0, variance_position = "top"),
  path(from = "x5", to = "x5", nudge_text_x = 0, nudge_text_y = 0, variance_position = "top"),
  path(from = "x6", to = "x6", nudge_text_x = 0, nudge_text_y = 0, variance_position = "top"),
  path(from = "x7", to = "x7", nudge_text_x = 0, nudge_text_y = 0, variance_position = "right"),
  path(from = "x8", to = "x8", nudge_text_x = 0, nudge_text_y = 0, variance_position = "right"),
  path(from = "x9", to = "x9", nudge_text_x = 0, nudge_text_y = 0, variance_position = "right"),
  path(from = "visual", to = "visual", nudge_text_x = 0, nudge_text_y = 0, variance_position = "top"),
  path(from = "textual", to = "textual", nudge_text_x = 0, nudge_text_y = 0, variance_position = "bottom"),
  path(from = "speed", to = "speed", nudge_text_x = 0, nudge_text_y = 0, variance_position = "top")
)

#creating a title for diagram, (supports Markdown and HTML)

title_list <- list(
  panel_title(panel_num = 1, title = "**Figure 1**<br>*Visual and Textual Ability Predicting Speeded Cognitive Processing*")
)

# Add a note, displayed as a caption beneath the diagram

note_list <- list(
  panel_note(panel_num = 1, note_text = "*Note.* Standardised estimates with 95% confidence intervals. Dashed lines indicate non-significant paths.")
)

# All arguments are shown to illustrate the fine-tuning available, though most are at their defaults and could be omitted

my_sem_diagram <- diyPaths(fit = fit_sem, node_positions = node_list, path_positions = path_list, 
                           standardised = TRUE,
                           digits = 3,
                           est_stars = TRUE,
                           est_p = FALSE,
                           est_ci = TRUE,
                           show_variances = TRUE,
                           variance_stars = FALSE,
                           variance_p = FALSE,
                           variance_ci = FALSE,
                           sig_linetype = TRUE,
                           p_threshold = 0.05,
                           node_width = 1.5, node_height = 1,
                           latent_node_size_adjust = 1,
                           observed_node_size_adjust = 0.5,
                           latent_node_text_size = 6,
                           observed_node_text_size = 4.5,
                           non_transparent_text = TRUE,
                           path_text_size = 4.5,
                           line_thickness = 0.6,
                           arrow_size = 0.2,
                           panel_cols = NULL,
                           show_group_labels = FALSE,
                           title_text_size = 22,
                           panel_titles = title_list,
                           panel_notes = note_list,
                           note_text_size = 16,
                           font_family = "sans",
                           title_note_font_family = "serif",
                           show_grid = FALSE,
                           grid_axis_scale = 1,
                           margin_x = 0.3,
                           margin_y = 0.3,
                           look_up_table = FALSE)

print(my_sem_diagram)



Try the diy.sem.plot package in your browser

Any scripts or data that you put into this service are public.

diy.sem.plot documentation built on Nov. 11, 2026, 1:11 a.m.