Introduction to MAIDR
MAIDR (Multimodal Access and Interactive Data Representation) is an R package that makes data visualizations accessible to users with visual impairments. It converts ggplot2 and Base R plots, and experimentally lattice charts, into interactive, accessible formats with:
- Keyboard navigation - Explore data using arrow keys
- Screen reader support - Full ARIA labels and descriptions
- Sonification - Hear data patterns through sound
- HTML/SVG output - Accessible charts that open in any browser
MAIDR helps data scientists and researchers create inclusive visualizations that everyone can explore, regardless of visual ability.
Installation
Install the development version from GitHub:
# Install the released version from CRAN
install.packages("maidr")
# Or the development version from GitHub:
# install.packages("devtools")
devtools::install_github("xability/r-maidr")Basic Workflow
MAIDR works with two main functions:
-
show()- Display an interactive plot in RStudio Viewer or browser -
save_html()- Save a plot as an HTML file, with the MAIDR.js library in alib/folder beside it
Quick Example: ggplot2 Bar Chart
library(maidr)
library(ggplot2)
# Create sample data
sales_data <- data.frame(
Product = c("A", "B", "C", "D"),
Sales = c(150, 230, 180, 290)
)
# Create a bar chart
p <- ggplot(sales_data, aes(x = Product, y = Sales)) +
geom_bar(stat = "identity", fill = "steelblue") +
labs(
title = "Product Sales by Category",
x = "Product",
y = "Sales Amount"
) +
theme_minimal()
# Display interactively
show(p)
# Or save as HTML file
save_html(p, "sales_chart.html")Quick Example: Base R Plot
MAIDR also works with Base R plotting functions:
library(maidr)
# Create a simple barplot
categories <- c("A", "B", "C", "D")
values <- c(150, 230, 180, 290)
barplot(
values,
names.arg = categories,
col = "steelblue",
main = "Product Sales by Category",
xlab = "Product",
ylab = "Sales Amount"
)
# Note: For Base R plots, call show() with NO arguments
# after creating the plot
show()How maidr hooks into your session
-
Console.
library(maidr)is all it takes. Printing a ggplot2 object, by typingporprint(p), opens it in the maidr viewer;show(p)is the explicit form. Base R plotting calls are recorded, andshow()with no argument opens the recorded chart.save_html()writes any of them to a file. -
lattice. Printing a lattice chart opens the viewer
too, through lattice’s own
print.functionoption, which maidr sets once lattice is loaded. A print that shares its page with other charts (split,position,more = TRUE,newpage = FALSE), a print into a file device such aspdf()orpng(), andplot(p), which lattice does not route through that option, are drawn by lattice as before, and so is a chart maidr cannot read. -
R Markdown and Quarto.
library(maidr)in a setup chunk is enough. Every plot the document draws becomes an accessible chart in place of its figure: a chart a chunk returns, each chart it prints withprint(p), as in a loop, and each Base R chart, several to a chunk if need be. In HTML output the charts are part of the page, which loads maidr.js once, and the figures that stay images are recorded with svglite rather than png (options(maidr.knitr_dev = FALSE)keeps png). In PDF, Word, Markdown, EPUB and xaringan output the plots are knitr’s figures, as without maidr. Return a chart from a chunk orprint()it:show()in a document opens a browser during the render and puts nothing in the page.maidr_on()in the setup chunk is no longer needed, and still works. -
Shiny. Put
maidr_output()in the UI andrender_maidr()in the server, with its expression returning the ggplot2 or lattice chart rather than printing it; seevignette("shiny-integration", package = "maidr"). -
Turning it off.
maidr_off()stops interception for the session andmaidr_on()starts it again.options(maidr.ggplot2 = FALSE)leaves ggplot2 printing alone,options(maidr.lattice = FALSE)leaves lattice printing alone,options(maidr.base_r = FALSE)stops recording Base R calls, andoptions(maidr.auto_show = FALSE), in.Rprofileto make it permanent, turns everything off. See?"maidr-options". -
What gets masked. Attaching maidr puts its own
copies of the Base R plotting functions, and of
methods::show(), ahead of the originals; R lists them atlibrary(maidr). Each records the call and passes through to the original, andshow()hands anything that is not a plot back tomethods::show(). In a script or a package callmaidr::show()by name, and attach vioplot, wordcloud, quantmod or ROCR before maidr, or their own functions mask the wrappers and their charts go unrecorded – ROCR’splot()masks maidr’s for every chart, not only its own. See?"base-r-wrappers".
Interactive htmlwidgets: plotly, highcharter and echarts4r
maidr_htmlwidget() takes an interactive chart drawn by
plotly, highcharter or echarts4r and returns it with MAIDR attached.
MAIDR reads the chart in the browser, through the adapter for the
library that draws it, so the widget keeps working everywhere an
htmlwidget does: the viewer, htmlwidgets::saveWidget(),
this document, and Shiny, where a re-rendered chart is read again.
library(maidr)
# plotly, including ggplotly()
plotly::plot_ly(mtcars, x = ~wt, y = ~mpg, type = "scatter", mode = "markers") |>
maidr_htmlwidget()
# highcharter
highcharter::hchart(mtcars, "scatter", highcharter::hcaes(wt, mpg)) |>
maidr_htmlwidget()
# echarts4r
mtcars |>
echarts4r::e_charts(wt) |>
echarts4r::e_scatter(mpg) |>
maidr_htmlwidget()
# In Shiny, wrap the widget inside its own render function
# output$chart <- plotly::renderPlotly(maidr_htmlwidget(plotly::plot_ly(...)))An echarts4r chart is switched to ECharts’ SVG renderer, which MAIDR needs to highlight the mark being read. While an echarts4r chart shows its legend, the visual highlight is off; audio, text and braille are not affected.
A fan chart drawn in echarts4r – a median line, and each band an area
of its width stacked on an invisible line at its lower edge – is read as
a percentile band once
maidr_htmlwidget(percentile_bands = ) names the median’s
series, each band’s filled series and the quantiles of its edges, as
fractions. It needs maidr.js 4.15.0 or later, which
use_cdn = TRUE loads; see
?maidr_htmlwidget.
lattice Charts [experimental]
A chart drawn by lattice’s own high-level functions –
barchart(), histogram(),
xyplot(), bwplot(), levelplot(),
contourplot(), densityplot(),
dotplot(), stripplot(), qqmath()
and qq() – is read as a layer type maidr already knows, and
used the way a ggplot2 object is. Every lattice reading is experimental:
none has been through a user study, and each may change without a
deprecation period.
library(maidr)
library(lattice)
p <- bwplot(voice.part ~ height,
data = singer,
xlab = "Height (inches)",
main = "Heights of Singers by Voice Part"
)
# Printing the chart opens it in the maidr viewer, as a ggplot2 object does
p
# show() is the explicit form; save_html() writes it to a file
show(p)
save_html(p, "singer_heights.html")A conditioned chart is read one panel at a time. Each panel is a subplot of its own, named after its strip: the chart opens a level above its panels, where the arrow keys choose one, Enter goes into it and Escape comes back out.
xyplot(mpg ~ wt | factor(cyl),
data = mtcars,
layout = c(3, 1),
xlab = "Weight (1000 lbs)",
ylab = "Miles per Gallon"
)In R Markdown and Quarto, a lattice chart a chunk returns, as above,
and one it prints with print(p), as in a loop, are both
accessible charts, each in place of its own figure. A print that shares
its page with other charts (split, position,
more = TRUE) stays lattice’s figure. A chart maidr cannot
read, such as cloud(), splom() or one drawn
with a panel function of your own, is drawn by lattice as before, and
save_html() saves it as a static image. The lattice
examples show every chart maidr reads from lattice.
Offline vs CDN Usage
By default, show() and save_html() use the
bundled MAIDR.js library, so the result works offline.
save_html() writes the library to a lib/
folder beside the file, and the two have to be shared together (zip the
folder that holds both): an .html sent on its own loads no
MAIDR.js and shows a plain, inaccessible chart. A knitted R Markdown or
Quarto page also uses the bundled library, once for all its charts, from
its _files folder or embedded in a
self_contained page. Widgets and Shiny apps auto-detect
internet availability and use the CDN when online. You can control this
behavior with the use_cdn parameter:
library(maidr)
library(ggplot2)
p <- ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
geom_bar(stat = "identity")
# Default - bundled files, works offline
show(p)
# Force CDN (requires internet when viewing; loads the latest MAIDR.js)
show(p, use_cdn = TRUE)
# Force bundled/local files (works offline)
show(p, use_cdn = FALSE)The same parameter works with save_html():
# One self-contained file; needs internet whenever it is viewed
save_html(p, "plot_cdn.html", use_cdn = TRUE)
# The file plus a lib/ folder beside it; works offline
save_html(p, "plot_offline.html", use_cdn = FALSE)The CDN paths load the latest published MAIDR.js,
not the copy bundled with the package, the same as the Python binding.
The first CDN document in an R session asks jsDelivr (falling back to
the npm registry) which version that is, allowing 3 seconds, and every
document in the session names that exact version. When the lookup cannot
be made – offline, or blocked – nothing fails: the document names the
bundled version, the copy use_cdn = FALSE would serve.
Documents rendered with use_cdn = FALSE make no network
request at all.
To load a fixed version from the CDN instead, pin it with an option
or the MAIDR_CDN_VERSION environment variable (the option
wins). A pinned document makes no lookup:
# The version bundled with this package
options(maidr.cdn_version = "bundled")
# A particular release
options(maidr.cdn_version = "4.9.0")
# Back to the latest
options(maidr.cdn_version = NULL)maidr.cdn_timeout (or MAIDR_CDN_TIMEOUT)
changes the time allowed for the lookup. See
?"maidr-options" for the details.
When to use use_cdn = FALSE: - Viewing
offline, or sharing with readers who may not have internet access: send
the file together with its lib/ folder (zip the two), never
the .html alone - Ensuring reproducibility with a specific
MAIDR.js version (or pin one on the CDN with
maidr.cdn_version)
When to use use_cdn = TRUE: - Attaching
or uploading a single file, for readers who will be online - Loading the
newest MAIDR.js without updating the R package
The DotPad SDK
One thing an offline document still fetches: the SDK for the DotPad tactile display. maidr.js does not bundle it (its licence does not permit redistribution) and imports the vendor’s copy from jsDelivr the first time a reader connects a DotPad. The document renders, sonifies and brailles without the network; only that first connect needs it.
To keep the DotPad offline as well, serve the SDK yourself and tell maidr where it is before rendering. Options and environment variables of the same names both work; an option wins when both are set:
options(
maidr.dotpad_sdk_url = "https://intranet.example/dotpad/DotPadSDK-3.0.3.js",
# Only if the braille engine (liblouis) is not in lib/ beside the module
maidr.dotpad_asset_base_url = "https://intranet.example/dotpad/lib/"
)
save_html(p, "plot_offline.html", use_cdn = FALSE)Every document maidr produces (show(),
save_html(), widgets, R Markdown and Quarto documents, and
Shiny) then declares window.MAIDR_DOTPAD_SDK_URL and
window.MAIDR_DOTPAD_ASSET_BASE_URL ahead of maidr.js, and
the CDN is never asked for the SDK. See ?"maidr-options"
for the details.
Exploring Accessible Plots
When you open a MAIDR plot, you can explore it using:
Keyboard Navigation
| Key | Action |
|---|---|
| Tab | Focus the chart; Shift + Tab leaves it |
| Left / Right | Move between data points |
| Up / Down | Move between series, stacked segments, heat map rows or box plot sections, on a chart that has them |
| Page Up / Page Down | Switch between the layers of a chart that has several |
| B | Toggle braille mode |
| T | Toggle text mode |
| S | Toggle sonification |
| R | Toggle review mode |
| C | Toggle high contrast mode |
| L, then X, Y or T | Announce the x axis label, the y axis label or the title |
| Space | Repeat the current sound |
| Ctrl + / (Cmd + / on macOS) | Show or hide the full keyboard shortcut help |
Every other shortcut, including autoplay, jumping to the ends, the command palette, settings and the AI chat, is on the MAIDR controls reference.
Quarto reveal.js Slides
A revealjs deck needs nothing special from this package:
library(maidr) in a setup chunk, as in any other Quarto or
R Markdown document, and every plot the deck draws becomes an accessible
MAIDR chart on its slide.
A chart on a revealjs slide is keyboard reachable on its
own: Tab moves into it, and while it has the focus the
arrow keys, Space and the deck’s other shortcuts go to
the chart rather than to the deck, so exploring a chart never changes
the slide. Shift+Tab leaves the chart, and the deck’s
keys work again: Space advances it. None of that needs
configuring. The same holds for the shortcuts of an ioslides or slidy
deck, a bookdown book, a Quarto website’s search and a flexdashboard
storyboard.
What does need attention is a reveal.js behavior that has nothing to
do with MAIDR. reveal.js keeps the slides on either side of the current
one rendered so that transitions stay smooth, and marking them
hidden does not take them out of the tab order — reveal’s
own inline style overrides the attribute. On a deck with a chart on
every slide, a single Tab can therefore land on an
off-screen slide’s chart rather than the one in front of the reader.
This is hakimel/reveal.js#1587,
open since 2016.
The fix is now on reveal.js master, which marks every
slide but the current one inert. It has not reached a
published release yet, and Quarto carries its own copy of reveal.js —
Quarto 1.10 ships 5.1.0 — so it will arrive in a Quarto release some
time after reveal.js cuts one. Nothing will need to change in your deck
when it does.
Until then, quarto-revealjs-a11y does the same thing for a Quarto deck. Add it once per project:
and enable it in the deck’s front matter:
Use 0.2.3 or newer. Earlier versions took off-slide
elements out of the tab order by setting tabindex="-1" on
them and could not find them again to put them back, which left the
chart on the current slide unreachable as well.
With the extension enabled, each slide gives one Tab to its own chart and Shift+Tab back out. The extension also adds a skip link ahead of the slides. Either way Space still moves to the next slide.
Supported Plot Types
MAIDR supports a comprehensive range of visualizations:
Basic Plot Types
- Bar charts (simple, grouped/dodged, stacked)
- Pie charts — ggplot2 via
geom_col()/geom_bar()+coord_polar("y"); Base R viapie() - Histograms
- Scatter plots
- Line plots (single and multi-line)
- Step plots —
geom_step()in ggplot2,plot(type = "s")/plot(type = "S")in Base R — for values that are piecewise constant, such as a sleep-stage hypnogram - Box plots
- Violin plots (ggplot2
geom_violin()) - Candlestick (OHLC) charts — ggplot2 via {tidyquant} (with optional
geom_ma()moving-average overlays and a patchwork volume sub-panel); Base R viaquantmod::chartSeries(), with itsaddVo()volume panel read as a bar layer (other TA overlays such asaddSMA()andaddEMA()are not supported and fall back to native graphics) - Heatmaps
- Contour plots (Base R
contour()) - Density/smooth curves
See the Heat Map and Candlestick Examples article for the full candlestick + MA + volume pipeline and the Base R support matrix.
Advanced Plot Types
-
Faceted plots -
facet_wrap()andfacet_grid()in ggplot2 -
Multi-panel layouts - patchwork for ggplot2,
par(mfrow/mfcol)for Base R - Multi-layered plots - Combine multiple geoms (e.g., histogram + density)
Experimental Plot Types
Every type above is stable. maidr also reads a longer list of charts as prototypes: none has been through a user study, and each may change without a deprecation period. Here and in the rest of the docs an experimental type is marked [experimental] after its name; a type with no mark is stable. Among them:
- ggplot2: area and stacked area charts [experimental], contour plots
via
geom_contour()[experimental], error bars [experimental], Gantt charts viamaidr_gantt()[experimental], hexbin plots [experimental], ROC curves viamaidr_roc()[experimental] and precision-recall curves viamaidr_pr_curve()[experimental] and percentile bands viaggdist::stat_lineribbon()or amedian_hilowribbon ofstat_summary()[experimental] and directed graphs viaggraph::ggraph()[experimental] - Base R: correlograms [experimental], Q-Q plots [experimental], radar
charts via
stars()[experimental], mosaic plots [experimental], violin plots viavioplot::vioplot()[experimental], word clouds [experimental] and precision-recall curves, a line titled Recall and Precision or ROCR’splot(performance(pred, "prec", "rec"))[experimental] - lattice, where every reading is experimental: bar charts via
barchart()[experimental], histograms viahistogram()[experimental], scatter, line and step plots viaxyplot()[experimental], box plots viabwplot()[experimental], heat maps vialevelplot()[experimental], contour plots viacontourplot()[experimental], density curves viadensityplot()[experimental], dot plots viadotplot()[experimental] and conditioned multi-panel charts [experimental]
The full list is under “Experimental Plot Types” in the README.
Next Steps
- Shiny Integration - Use MAIDR in Shiny apps
-
Package documentation - Run
?maidr::showfor function details -
Run examples - Try
maidr::run_example()to see all available plot types
Example Gallery
Histogram
library(maidr)
library(ggplot2)
# Normal distribution
hist_data <- data.frame(values = rnorm(1000, mean = 100, sd = 15))
p <- ggplot(hist_data, aes(x = values)) +
geom_histogram(bins = 30, fill = "skyblue", color = "black") +
labs(
title = "Distribution of Test Scores",
x = "Score",
y = "Frequency"
) +
theme_minimal()
show(p)Scatter Plot
library(maidr)
library(ggplot2)
# Create sample data
scatter_data <- data.frame(
height = rnorm(50, 170, 10),
weight = rnorm(50, 70, 8),
gender = sample(c("Male", "Female"), 50, replace = TRUE)
)
p <- ggplot(scatter_data, aes(x = height, y = weight, color = gender)) +
geom_point(size = 3, alpha = 0.7) +
labs(
title = "Height vs Weight",
x = "Height (cm)",
y = "Weight (kg)"
) +
theme_minimal()
show(p)Line Plot
library(maidr)
library(ggplot2)
# Time series data
months <- month.abb[1:12]
temperature <- c(5, 7, 12, 18, 22, 26, 28, 27, 23, 17, 11, 6)
temp_data <- data.frame(
Month = factor(months, levels = months),
Temperature = temperature
)
p <- ggplot(temp_data, aes(x = Month, y = Temperature, group = 1)) +
geom_line(color = "red", linewidth = 1.5) +
geom_point(color = "darkred", size = 3) +
labs(
title = "Average Monthly Temperature",
x = "Month",
y = "Temperature (°C)"
) +
theme_minimal()
show(p)Tips for Creating Accessible Plots
- Use clear titles - Describe what the plot shows
- Label axes properly - Include units of measurement
- Choose distinct colors - Ensure good contrast
- Add legends - Explain what colors/shapes mean
- Keep it simple - Avoid overcrowded visualizations
Getting Help
- Run
?maidr::showfor function documentation - Report a bug or ask for a feature at xability/r-maidr/issues
- Read the full documentation:
help(package = "maidr")
Learn More
- Accessibility standards: WCAG 2.1 Guidelines
- MAIDR website: More examples and tutorials
- Research papers: Understanding multimodal data representation