| Title: | Manually Plot Path Diagrams for Structural Equation Models |
|---|---|
| Description: | Manually plot fully customisable path diagrams for structural equation models (SEM). Map out node positions using simple coordinates and specify where on the perimeter of each node paths begin and end. Extensive fine-tuning options allow the creation of a path diagram exactly as envisioned, entirely within R. |
| Authors: | Sebastian Nagy [aut, cre, cph] |
| Maintainer: | Sebastian Nagy <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.0.0.9000 |
| Built: | 2026-10-03 13:47:36 UTC |
| Source: | https://github.com/snagy86/diy.sem.plot |
Plot fully customisable path diagrams for structural equation models fitted with lavaan, rendered using ggplot2.
To render the ggplot object of the diagram, the function only requires users to supply a fitted lavaan model, specify node positions using x-y coordinates, and detail where on
the perimeter of each node (top, bottom, left, or right) each path should begin and end. The render automatically inserts estimates centered on the midpoint and adjusts each node's
shape to match its variable type. A range of optional fine-tuning arguments are included, facilitating the creation of a path diagram exactly as you envision it, entirely within R.
diyPaths( fit, node_positions, path_positions, standardised = FALSE, digits = 3, est_stars = FALSE, est_p = FALSE, est_ci = FALSE, show_variances = FALSE, variance_stars = FALSE, variance_p = FALSE, variance_ci = FALSE, sig_linetype = FALSE, p_threshold = 0.05, node_width = 1.5, node_height = 1, latent_node_size_adjust = 1, observed_node_size_adjust = 1, latent_node_text_size = 4, observed_node_text_size = 4, path_transparent_text = FALSE, path_text_size = 3.5, line_thickness = 0.6, arrow_size = 0.2, panel_cols = NULL, show_group_labels = FALSE, title_text_size = 25, panel_titles = NULL, panel_notes = NULL, note_text_size = 4, note_transparent_text = FALSE, note_outline = TRUE, show_grid = FALSE, grid_axis_scale = 1, margin_x = 0.5, margin_y = 0.5, look_up_table = FALSE )diyPaths( fit, node_positions, path_positions, standardised = FALSE, digits = 3, est_stars = FALSE, est_p = FALSE, est_ci = FALSE, show_variances = FALSE, variance_stars = FALSE, variance_p = FALSE, variance_ci = FALSE, sig_linetype = FALSE, p_threshold = 0.05, node_width = 1.5, node_height = 1, latent_node_size_adjust = 1, observed_node_size_adjust = 1, latent_node_text_size = 4, observed_node_text_size = 4, path_transparent_text = FALSE, path_text_size = 3.5, line_thickness = 0.6, arrow_size = 0.2, panel_cols = NULL, show_group_labels = FALSE, title_text_size = 25, panel_titles = NULL, panel_notes = NULL, note_text_size = 4, note_transparent_text = FALSE, note_outline = TRUE, show_grid = FALSE, grid_axis_scale = 1, margin_x = 0.5, margin_y = 0.5, look_up_table = FALSE )
fit |
A fitted model object of class |
node_positions |
Specify a list of node position objects. Use the |
path_positions |
Specify a list of path configuration objects. Use the |
standardised |
Logical. If |
digits |
Number of digits to display for estimates. Default is |
est_stars |
Logical. Whether to display significance stars on path estimates. Default is |
est_p |
Logical. Whether to display p-values on path estimates. Default is |
est_ci |
Logical. Whether to display confidence intervals on path estimates. Default is |
show_variances |
Logical. Whether to display variance and residual paths. Default is |
variance_stars |
Logical. Whether to display significance stars on variance paths. Default is |
variance_p |
Logical. Whether to display p-values on variance paths. Default is |
variance_ci |
Logical. Whether to display confidence intervals on variance paths. Default is |
sig_linetype |
Logical. If |
p_threshold |
Statistical significance threshold used to determine non-significant paths when |
node_width |
Base width for nodes. Default is |
node_height |
Base height for node shapes. Default is |
latent_node_size_adjust |
Numeric multiplier scaling latent node ellipses. Default is |
observed_node_size_adjust |
Numeric multiplier scaling observed node rectangles. Default is |
latent_node_text_size |
Text font size for latent node labels. Default is |
observed_node_text_size |
Text font size for observed node labels. Default is |
path_transparent_text |
Logical. If |
path_text_size |
Text font size for path estimate labels. Default is |
line_thickness |
Sets thickness of paths. Default is |
arrow_size |
Sets the size of arrow heads. Default is |
panel_cols |
Integer for number of columns to use when arranging multi-group panels. Default is |
show_group_labels |
Logical. For multi-group models, whether to annotate each panel
with its panel number and the raw group value it represents (e.g. "Panel 1: Group = male").
Needed to identify which diagram represents each group and its internal panel number when creating panel titles and notes.
Default is |
title_text_size |
Font size for panel titles. Default is |
panel_titles |
Specify a list of titles for panels. Use the |
panel_notes |
Specify a list of notes and their positions for panels. Use the |
note_text_size |
Font size for panel notes. Default is |
note_transparent_text |
Logical. If |
note_outline |
Logical. If |
show_grid |
Logical. Whether to overlay a coordinate grid. Default is |
grid_axis_scale |
Sets the spacing of grid-lines when |
margin_x |
Padding for plot limits along the x-axis. Default is |
margin_y |
Padding for plot limits along the y-axis. Default is |
look_up_table |
Logical. If |
Using the function requires 4 steps and is illustrated by the example below. To save time and avoid syntax/logical errors, template() prints starting code
for node_positions, path_positions, panel_titles and the diyPaths() call based on your fitted model.
See vignette("diy.sem.plot") for in-depth examples and guidance on using the function's arguments.
Specify and fit the SEM using lavaan.
Specify node_positions as a list (node_positions = list(...)) and,
within it, define each node's position and label using the node()
helper function.
Specify path_positions as a list (path_positions = list(...)) and,
within it, define each path's connection points, curvature if covariance/correlations, and fine-tune estimate positions
adjustments using the path() helper function.
Call diyPaths(), passing in the fitted model, node_positions,
and path_positions, along with any additional display augmentations (i.e. show significance stars on estimates).
Creating a diagram for a specific model is inherently an iterative process. As such, steps 2-4 will likely need to be repeated with additional fine-tuning adjustments to achieve the desired result.
A ggplot (or patchwork, for multi-group models) object representing the SEM
path diagram(s). If look_up_table = TRUE, a list containing the diagram(s) ($plot) and a look-up
data.frame of node width/height and text sizes for latent, observed, and path labels
($look_up_table).
template() to generate starting code for node_positions, path_positions, panel_titles and panel_notes;
node(), path(), panel_title(), and panel_note() for the helper functions used within them.
#full SEM example, this model may not make theoretical sense. library(lavaan) library(ggplot2) data(HolzingerSwineford1939, package = "lavaan") #specify the model sem_model <- ' visual =~ x1 + x2 + x3 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9 speed ~ visual + textual visual ~~ textual ' #fit the model fit <- sem(sem_model, data = HolzingerSwineford1939) #generate a starting template temp <- template(fit = fit, diyPaths_arguments = "max") #### edited template #### node_list <- list( node(name = "visual", x = 1, y = 1, label = "Visual"), node(name = "textual", x = 1, y = 2, label = "Textual"), node(name = "speed", x = 4, y = 1.5, label = "Speed"), node(name = "x1", x = -0.06, y = -0.5, label = "Visual\nPerception"), node(name = "x2", x = 1, y = -0.5, label = "Cubes"), node(name = "x3", x = 2.06, y = -0.5, label = "Lozenges"), node(name = "x4", x = -0.06, y = 3.5, label = "Paragraph\nComprehension"), node(name = "x5", x = 1, y = 3.5, label = "Sentence\nCompletion"), node(name = "x6", x = 2.06, y = 3.5, label = "Word\nMeaning"), node(name = "x7", x = 6, y = 0.5, label = "Speeded\nAddition"), node(name = "x8", x = 6, y = 1.5, label = "Speeded\nCounting"), node(name = "x9", x = 6, y = 2.5, label = "Speeded\nDiscrimination") ) path_list <- list( # regression paths path(from = "visual", to = "speed", side_from = "right", side_to = "left"), path(from = "textual", to = "speed", side_from = "right", side_to = "left"), # loading paths path(from = "visual", to = "x1", side_from = "bottom", side_to = "top", nudge_text_x = -0.1), path(from = "visual", to = "x2", side_from = "bottom", side_to = "top"), path(from = "visual", to = "x3", side_from = "bottom", side_to = "top", nudge_text_x = 0.1), path(from = "textual", to = "x4", side_from = "top", side_to = "bottom", nudge_text_x = -0.1), path(from = "textual", to = "x5", side_from = "top", side_to = "bottom"), path(from = "textual", to = "x6", side_from = "top", side_to = "bottom", nudge_text_x = 0.1), path(from = "speed", to = "x7", side_from = "right", side_to = "left"), path(from = "speed", to = "x8", side_from = "right", side_to = "left"), path(from = "speed", to = "x9", side_from = "right", side_to = "left"), # covariance/correlation paths path(from = "visual", to = "textual", side_from = "left", side_to = "left", cov_curve = -0.6) ) title_list <- list( panel_title(panel_num = 1, title = "My SEM Plot") ) p <- diyPaths(fit = fit, node_positions = node_list, path_positions = path_list, standardised = TRUE, digits = 3, est_stars = TRUE, est_p = FALSE, est_ci = FALSE, show_variances = FALSE, variance_stars = FALSE, variance_p = FALSE, variance_ci = FALSE, sig_linetype = FALSE, p_threshold = 0.05, panel_titles = title_list, show_grid = TRUE, grid_axis_scale = 0.5, path_transparent_text = FALSE, latent_node_text_size = 4, observed_node_text_size = 3, path_text_size = 3.5, line_thickness = 0.6, arrow_size = 0.2, node_width = 1.5, node_height = 1, latent_node_size_adjust = 0.8, observed_node_size_adjust = 0.55, show_group_labels = FALSE, panel_cols = NULL, margin_x = 0.5, margin_y = 0.5, look_up_table = TRUE) print(p)#full SEM example, this model may not make theoretical sense. library(lavaan) library(ggplot2) data(HolzingerSwineford1939, package = "lavaan") #specify the model sem_model <- ' visual =~ x1 + x2 + x3 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9 speed ~ visual + textual visual ~~ textual ' #fit the model fit <- sem(sem_model, data = HolzingerSwineford1939) #generate a starting template temp <- template(fit = fit, diyPaths_arguments = "max") #### edited template #### node_list <- list( node(name = "visual", x = 1, y = 1, label = "Visual"), node(name = "textual", x = 1, y = 2, label = "Textual"), node(name = "speed", x = 4, y = 1.5, label = "Speed"), node(name = "x1", x = -0.06, y = -0.5, label = "Visual\nPerception"), node(name = "x2", x = 1, y = -0.5, label = "Cubes"), node(name = "x3", x = 2.06, y = -0.5, label = "Lozenges"), node(name = "x4", x = -0.06, y = 3.5, label = "Paragraph\nComprehension"), node(name = "x5", x = 1, y = 3.5, label = "Sentence\nCompletion"), node(name = "x6", x = 2.06, y = 3.5, label = "Word\nMeaning"), node(name = "x7", x = 6, y = 0.5, label = "Speeded\nAddition"), node(name = "x8", x = 6, y = 1.5, label = "Speeded\nCounting"), node(name = "x9", x = 6, y = 2.5, label = "Speeded\nDiscrimination") ) path_list <- list( # regression paths path(from = "visual", to = "speed", side_from = "right", side_to = "left"), path(from = "textual", to = "speed", side_from = "right", side_to = "left"), # loading paths path(from = "visual", to = "x1", side_from = "bottom", side_to = "top", nudge_text_x = -0.1), path(from = "visual", to = "x2", side_from = "bottom", side_to = "top"), path(from = "visual", to = "x3", side_from = "bottom", side_to = "top", nudge_text_x = 0.1), path(from = "textual", to = "x4", side_from = "top", side_to = "bottom", nudge_text_x = -0.1), path(from = "textual", to = "x5", side_from = "top", side_to = "bottom"), path(from = "textual", to = "x6", side_from = "top", side_to = "bottom", nudge_text_x = 0.1), path(from = "speed", to = "x7", side_from = "right", side_to = "left"), path(from = "speed", to = "x8", side_from = "right", side_to = "left"), path(from = "speed", to = "x9", side_from = "right", side_to = "left"), # covariance/correlation paths path(from = "visual", to = "textual", side_from = "left", side_to = "left", cov_curve = -0.6) ) title_list <- list( panel_title(panel_num = 1, title = "My SEM Plot") ) p <- diyPaths(fit = fit, node_positions = node_list, path_positions = path_list, standardised = TRUE, digits = 3, est_stars = TRUE, est_p = FALSE, est_ci = FALSE, show_variances = FALSE, variance_stars = FALSE, variance_p = FALSE, variance_ci = FALSE, sig_linetype = FALSE, p_threshold = 0.05, panel_titles = title_list, show_grid = TRUE, grid_axis_scale = 0.5, path_transparent_text = FALSE, latent_node_text_size = 4, observed_node_text_size = 3, path_text_size = 3.5, line_thickness = 0.6, arrow_size = 0.2, node_width = 1.5, node_height = 1, latent_node_size_adjust = 0.8, observed_node_size_adjust = 0.55, show_group_labels = FALSE, panel_cols = NULL, margin_x = 0.5, margin_y = 0.5, look_up_table = TRUE) print(p)
Helper function that creates a list of arguments which specify a given node's position and its name in the diagram,
designed for use within the node_positions argument of diyPaths().
node(name, x = 0, y = 0, label = NULL)node(name, x = 0, y = 0, label = NULL)
name |
The name of the variable within the |
x |
Numeric value for the x-coordinate of the node's centre. Default is |
y |
Numeric value for the y-coordinate of the node's centre. Default is |
label |
Custom label to display instead of #' @seealso |
A list containing arguments that specify a node's position and label, for use within the node_positions argument of diyPaths().
#a list that specifies a node positioned on x = 1, y = 2, #and relabelled from its `lavaan` model name. node(name = "bpm", x = 1, y = 2, label = "Beats per Minute")#a list that specifies a node positioned on x = 1, y = 2, #and relabelled from its `lavaan` model name. node(name = "bpm", x = 1, y = 2, label = "Beats per Minute")
Helper function that creates a list of arguments which specify a custom note and its target panel,
designed for use within the panel_notes argument of diyPaths(). Use the
show_group_labels argument in diyPaths() to view each panel's number and
which group it refers to.
panel_note(panel_num, note_text = NULL, x = 0, y = 0)panel_note(panel_num, note_text = NULL, x = 0, y = 0)
panel_num |
Integer value for the panel number this note applies to. |
note_text |
Text to display in the note. If |
x |
Numeric value for the x-coordinate of the note. Default is |
y |
Numeric value for the y-coordinate of the note. Default is |
A list containing arguments that specify a panel's note text and position, for use within the panel_notes argument of diyPaths().
diyPaths(), where the list of notes is passed to panel_notes;
panel_title() for adding titles to panels; template(), whose output includes a starting note list.
# a note on the first panel, placed at x = 2.5, y = 0 panel_note(panel_num = 1, note_text = "n = 100", x = 2.5, y = 0) # a note on the second panel of a multi-group model panel_note(panel_num = 2, note_text = "Source: my data", x = 2.5, y = 0)# a note on the first panel, placed at x = 2.5, y = 0 panel_note(panel_num = 1, note_text = "n = 100", x = 2.5, y = 0) # a note on the second panel of a multi-group model panel_note(panel_num = 2, note_text = "Source: my data", x = 2.5, y = 0)
Helper function that creates a list of arguments which specify custom titles and its target panel,
designed for use within the panel_titles argument of diyPaths(). Use the
show_group_labels argument in diyPaths() to view each panel's number and
which group it refers to.
panel_title(panel_num = 1, title = NULL)panel_title(panel_num = 1, title = NULL)
panel_num |
Integer value for the panel number this title applies to. Default is |
title |
The title text to display. |
A list containing arguments that specify a panel's number and title, for use within the panel_titles argument of diyPaths().
diyPaths(), where the list of titles is passed to panel_titles;
panel_note() for adding notes to panels; template(), whose output includes a starting title list.
# titling a single, non-grouped model panel_title(title = "My SEM Model") # titling the second panel of a multi-group model panel_title(panel_num = 2, title = "Female Participants")# titling a single, non-grouped model panel_title(title = "My SEM Model") # titling the second panel of a multi-group model panel_title(panel_num = 2, title = "Female Participants")
Helper function that creates a list of arguments which specify a given path's position and fine-tuning adjustments,
designed for use within the path_positions argument of diyPaths().
path( from, to, side_from = "right", side_to = "left", cov_curve = NULL, nudge_text_x = 0, nudge_text_y = 0, variance_position = "top" )path( from, to, side_from = "right", side_to = "left", cov_curve = NULL, nudge_text_x = 0, nudge_text_y = 0, variance_position = "top" )
from |
The source variable name within the |
to |
The target variable name within the |
side_from |
Side of source node where path starts ("top", "bottom", "left", "right"). Default is "right". |
side_to |
Side of target node where path ends ("top", "bottom", "left", "right"). Default is "left". |
cov_curve |
Numeric value for curvature of covariance/correlation paths. Default is NULL. |
nudge_text_x |
Numeric fine tuning adjustment for path estimate text along the x-axis. Default is |
nudge_text_y |
Numeric fine tuning adjustment for path estimate text along the y-axis. Default is |
variance_position |
Placement of variance/residual paths ("top", "bottom", "left", or "right"). Default is "top". |
from/to in path() must exactly match the variable name used in the lavaan model syntax.
Furthermore, the order must also be correct for regression or loading paths. A misspelled or mismatched
path() entry will not raise an error, instead, it will use default attachment points and values, not applying
specific customisations. This can be completely avoided using template().
cov_curve's value can be used to adjust direction of curve on covariance/correlation path.
For a mostly vertical path (i.e. node1: x = 0, y = 1 -> node2: x = 0, y = 2), positive curvature bends it left and negative curvature
bends it right. For a mostly horizontal path (i.e. node1: x = 1, y = 0 -> node2: x = 2, y = 0), positive curvature bends
it down and negative curvature bends it up. Best results typically range from -1 to 1.
A list containing arguments that specify a path's position and fine-tuning adjustments, for use within the path_positions argument of diyPaths().
node() to specify node positions;
diyPaths() to render the diagram; template() to generate a starting path_positions list.
#a list that specifies a regression path of node "anxiety" predicting node "depression". path(from = "anx", to = "dep", side_from = "right", side_to = "left") #a list that specifies a covariance/correlation path of node "anxiety" and node "depression". #Order of "from" and "to" does not matter for covariance/correlation. path(from = "dep", to = "anx", side_from = "left", side_to = "left", cov_curve = -0.6)#a list that specifies a regression path of node "anxiety" predicting node "depression". path(from = "anx", to = "dep", side_from = "right", side_to = "left") #a list that specifies a covariance/correlation path of node "anxiety" and node "depression". #Order of "from" and "to" does not matter for covariance/correlation. path(from = "dep", to = "anx", side_from = "left", side_to = "left", cov_curve = -0.6)
Takes your fitted model and prints a ready-to-run starting template (node list, path list, title list, and
diyPaths() call) to the console. Every variable and path from your model is already filled in
with the correct names, so the code will render a diagram as soon as you run it. All arguments will be at their default values,
so your job is to fine-tune: copy the output into your script, then edit the lines marked #EDIT ME!.
The template is also returned invisibly as a character string, so it can be stored as an object then viewed
with writeLines() (see examples).
template( fit, include_variance_paths = TRUE, diyPaths_arguments = c("max", "min") )template( fit, include_variance_paths = TRUE, diyPaths_arguments = c("max", "min") )
fit |
the |
include_variance_paths |
Logical. Whether to include specification for variance/residual paths in paths_lists |
diyPaths_arguments |
Use either "max" (the printed call shows every |
Invisibly returns the template as a single character string, and prints it to the console.
diyPaths() to render the diagram from the template;
node(), path(), panel_title(), and panel_note() for the helper functions the template uses.
library(lavaan) library(ggplot2) data(HolzingerSwineford1939, package = "lavaan") #specify the model sem_model <- ' visual =~ x1 + x2 + x3 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9 speed ~ visual + textual visual ~~ textual ' #fit the model fit <- sem(sem_model, data = HolzingerSwineford1939) #print the template to the console # template(fit = fit, include_variance_paths = TRUE, diyPaths_arguments = "max") #store the template, then view it properly formatted tmpl <- template(fit = fit, diyPaths_arguments = "min") writeLines(tmpl)library(lavaan) library(ggplot2) data(HolzingerSwineford1939, package = "lavaan") #specify the model sem_model <- ' visual =~ x1 + x2 + x3 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9 speed ~ visual + textual visual ~~ textual ' #fit the model fit <- sem(sem_model, data = HolzingerSwineford1939) #print the template to the console # template(fit = fit, include_variance_paths = TRUE, diyPaths_arguments = "max") #store the template, then view it properly formatted tmpl <- template(fit = fit, diyPaths_arguments = "min") writeLines(tmpl)