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