depictr: One visual language from first look to final figure

Abstract

This tutorial follows one analysis of simulated lexical-decision data from the reaction-time distribution to mixed-model estimates and an accessibility-audited final figure. It demonstrates how depictr keeps visual conventions stable across exploratory and confirmatory stages while leaving the underlying ggplot2 objects editable.

The figures in an analysis are rarely made all at once. An exploratory density plot may survive into a talk, a model adds a forest plot and a reviewer asks for a diagnostic or a Bayesian comparison. When each stage uses different defaults, one analysis acquires several palettes, type sizes and conventions. Accessibility is then checked, if at all, after the visual decisions have already spread through the project.

I wrote depictr (Bernabeu, 2026) to give those stages a shared visual language. It provides plotting functions for common statistical objects, and each function returns an ordinary ggplot2 object that remains open to further editing. It also measures the finished figure against explicit checks for contrast, text size and reliance on colour. The same conventions are available in the R package and the Python package. The example data, bundled with the package, are simulated from a lexical-decision design in which participants respond to words preceded by semantically related or unrelated primes, presented visually or auditorily.

Development

The analysis below develops one visual argument in three stages. It describes the observations, estimates the planned effects and finally audits the figure that will be shared.

library(depictr)
library(ggplot2)

# Slightly larger axis and legend text for reading on screen, with titles
# centred on the whole figure instead of the panel
web_theme <- theme(
  plot.title.position = 'plot',
  axis.text = element_text(size = rel(0.9)),
  legend.text = element_text(size = rel(0.9))
)

packageVersion('depictr')
#> [1] '0.3.0'
data('lexical_decision', package = 'depictr')
correct <- subset(lexical_decision, accuracy == 1)
c(participants = length(unique(lexical_decision$participant)),
  items = length(unique(lexical_decision$item)),
  trials = nrow(lexical_decision))
#> participants        items       trials 
#>           24           40          960

Begin With the Observed Distribution

Reaction times are right-skewed, so a mean and a standard deviation do not show the whole pattern. An empirical cumulative distribution avoids a choice of bins and makes the quartiles directly visible, as Figure 1 shows.

ecdf_plot(
  correct,
  RT,
  group = condition,
  reference_quantiles = c(0.25, 0.5, 0.75),
  legend_inside = TRUE,
  title = 'Related primes are faster across the distribution'
) +
  labs(x = 'Reaction time (ms)', colour = NULL) +
  web_theme
Empirical cumulative distributions of lexical-decision reaction times for related and unrelated primes, with horizontal guides at the quartiles

Figure 1: Observed Reaction-Time Distributions After Related and Unrelated Primes, With the Quartiles Marked.

The separation extends well beyond a handful of slow responses. Related trials remain shifted towards shorter reaction times through much of the distribution, which gives a visible empirical context to the model estimates that follow.

Keep the Same Language for the Model

The model, fitted to the same correct trials, includes priming condition, presentation modality and word frequency, with crossed random intercepts for participants and items. The coefficient_plot() function reads the fitted object directly and puts the fixed effects on a common scale in Figure 2, expressing each as the change in reaction time per standard deviation of its predictor. The two factors enter the model as 0/1 codes with a standard deviation near 0.5, so their plotted effects are about half the size of the estimates printed below.

fit <- lmerTest::lmer(
  RT ~ condition + modality + word_frequency +
    (1 | participant) + (1 | item),
  data = correct
)

term_labels <- c(
  conditionunrelated = 'Unrelated priming',
  modalityauditory = 'Auditory modality',
  word_frequency = 'Word frequency (Zipf)'
)

tidy_estimates(fit)
#>                 term  estimate std.error   conf.low conf.high
#> 1        (Intercept) 708.01334 27.253962 654.596553 761.43012
#> 2 conditionunrelated  31.95745  8.119043  16.044418  47.87048
#> 3   modalityauditory  25.34713  8.123180   9.425993  41.26827
#> 4     word_frequency -21.89472  5.860301 -33.380696 -10.40874
coefficient_plot(
  fit,
  standardise = TRUE,
  order = 'ascending',
  labels = term_labels,
  point_size = 3.4,
  line_size = 1.1,
  title = 'Estimated effects on reaction time',
  x_lab = 'Change in reaction time (ms) per SD of predictor'
) +
  scale_x_continuous(breaks = scales::breaks_width(10)) +
  web_theme
Forest plot of the fixed effects, each per standard deviation of its predictor, for unrelated priming, auditory presentation and word frequency, with 95% confidence intervals

Figure 2: Fixed Effects on Reaction Time, Each per Standard Deviation of Its Predictor, With 95% Confidence Intervals.

The forest plot keeps the colours and typography of the distribution plot, so a reader moving from observations to estimates can concentrate on the result. That continuity is the point of a shared visual language, since nothing in Figure 2 has to be learned again.

Audit the Figure That Will Be Published

A colourblind-safe palette does not guarantee an accessible figure. Contrast against the background and the use of a second visual channel depend on the particular plot. The package therefore audits the whole built plot, including its palette.

first_draft <- explore_distribution(
  correct,
  RT,
  group = condition,
  type = 'density',
  legend_inside = TRUE,
  title = 'Lexical-decision time by priming condition'
) +
  web_theme

check_figure(first_draft)
#>                        check measured threshold verdict
#> 1        colour_separability   119.21       5.0    pass
#> 2 colour_separability_protan   108.92       5.0    pass
#> 3 colour_separability_deutan   121.29       5.0    pass
#> 4 colour_separability_tritan    77.90       5.0    pass
#> 5     greyscale_separability    33.34       5.0    pass
#> 6                  text_size     9.90       6.0    pass
#> 7              text_contrast     7.15       4.5    pass
#> 8          geometry_contrast     2.25       3.0    fail
#> 9         redundant_encoding     0.00       1.0    fail
#>                                                              detail
#> 1           Closest pair #005b96 and #e69f00 of 2 encoding colours.
#> 2           Closest pair #005b96 and #e69f00 of 2 encoding colours.
#> 3           Closest pair #005b96 and #e69f00 of 2 encoding colours.
#> 4           Closest pair #005b96 and #e69f00 of 2 encoding colours.
#> 5                Closest pair #005b96 and #e69f00 in CIE lightness.
#> 6 Smallest text 9.90 pt, drawn at 17.78 cm and printed at 17.78 cm.
#> 7                          Lowest-contrast text #005b96 on #ffffff.
#> 8                        Lowest-contrast colour #e69f00 on #ffffff.
#> 9                            Colour alone distinguishes the groups.

The default density plot fails two checks. Its orange falls below the package’s contrast threshold for graphical elements on a white panel, and colour is the only channel separating the conditions. The corrected plot in Figure 3 replaces the orange with a darker vermilion and adds line type as a second channel. Both checks then pass, and neither change touches the data or the statistical display.

final_figure <- explore_distribution(
  correct,
  RT,
  group = condition,
  type = 'density',
  palette = c('#005B96', '#B44600'),
  alpha = 0.35,
  legend_inside = TRUE,
  title = 'Priming conditions separated by colour and line type'
) +
  aes(linetype = condition) +
  labs(x = 'Reaction time (ms)', colour = NULL, fill = NULL, linetype = NULL) +
  web_theme

check_figure(final_figure)[, c('check', 'measured', 'threshold', 'verdict')]
#>                        check measured threshold verdict
#> 1        colour_separability   102.76       5.0    pass
#> 2 colour_separability_protan    81.30       5.0    pass
#> 3 colour_separability_deutan    96.85       5.0    pass
#> 4 colour_separability_tritan    93.25       5.0    pass
#> 5     greyscale_separability     7.09       5.0    pass
#> 6                  text_size     9.90       6.0    pass
#> 7              text_contrast     7.15       4.5    pass
#> 8          geometry_contrast     5.50       3.0    pass
#> 9         redundant_encoding     1.00       1.0    pass
final_figure
Corrected density curves of reaction times for related and unrelated primes, distinguished by both colour and line type

Figure 3: The Corrected Figure, in Which Colour and Line Type Both Separate the Priming Conditions.

The default palette starts from the colours proposed by Okabe and Ito (2008), and the audit simulates colour-vision deficiency with the model of Machado et al. (2009). Each measurement is reported beside its threshold, so a verdict can be revisited when a journal, display size or printing process calls for a different standard.

Discussion

The example supports a modest but useful claim: accessibility belongs in figure development from the first draft onwards. A stable visual language helps readers compare plots, while the audit identifies concrete properties to repair. The practical habit is to use one set of conventions throughout an analysis and to audit the exact figure that will leave the project. The result is a reproducible hand-off from analysis code to an editable, interpretable and checked figure. The R reference and Python reference cover the rest of the package, including diagnostics, posterior plots, power curves, model comparisons and multi-panel composition.

Limits

The depictr package draws and checks figures, leaving decisions about the model, estimand, contrasts, prior, uncertainty and audience to the analyst. A polished plot can faithfully present a poor analysis. The checks also cover only measurable properties, so alt text, a meaningful caption and a sensible figure order remain editorial work.

References

Bernabeu, P. (2026). depictr: A unified toolkit for visualising statistical models and data (Version 0.3.0) [Computer software]. Comprehensive R Archive Network (CRAN). https://doi.org/10.32614/CRAN.package.depictr

Machado, G. M., Oliveira, M. M., & Fernandes, L. A. F. (2009). A physiologically-based model for simulation of color vision deficiency. IEEE Transactions on Visualization and Computer Graphics, 15(6), 1291–1298. https://doi.org/10.1109/TVCG.2009.113

Okabe, M., & Ito, K. (2008). Color universal design (CUD): How to make figures and presentations that are friendly to colorblind people. Jfly. https://jfly.uni-koeln.de/color/

R Markdown source of this post

Documentation

R documentation Python documentation

Comments are provided by Disqus and are not loaded automatically. Loading them connects your browser to Disqus, which may use cookies and process data under its privacy policy. See this site's privacy notice for details.