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 into interactive, accessible formats with:
MAIDR helps data scientists and researchers create inclusive visualizations that everyone can explore, regardless of visual ability.
Install the development version from GitHub:
MAIDR works with two main functions:
show() - Display an interactive plot
in RStudio Viewer or browsersave_html() - Save a plot as an HTML
file, with the MAIDR.js library in a lib/ folder beside
itlibrary(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")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()library(maidr) is all it
takes. Printing a ggplot2 object, by typing p or
print(p), opens it in the maidr viewer;
show(p) is the explicit form. Base R plotting calls are
recorded, and show() with no argument opens the recorded
chart. save_html() writes either kind to a file.maidr_on()
once in a setup chunk. It installs the knitr hooks that turn every plot
the document draws into an accessible chart; library(maidr)
alone does not install them.maidr_output() in the UI
and render_maidr() in the server; see
vignette("shiny-integration", package = "maidr").maidr_off() stops
interception for the session and maidr_on() starts it
again. options(maidr.ggplot2 = FALSE) leaves ggplot2
printing alone, options(maidr.base_r = FALSE) stops
recording Base R calls, and
options(maidr.auto_show = FALSE), in .Rprofile
to make it permanent, turns everything off. See
?"maidr-options".methods::show(), ahead of the originals; R lists them at
library(maidr). Each records the call and passes through to
the original, and show() hands anything that is not a plot
back to methods::show(). In a script or a package call
maidr::show() by name, and attach vioplot, wordcloud or
quantmod before maidr, or their own functions mask the wrappers
and their charts go unrecorded. See ?"base-r-wrappers".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.
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. Widgets, knitr documents
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
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, knitr 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.
When you open a MAIDR plot, you can explore it using:
MAIDR plots include:
Plots can be heard through:
A revealjs deck needs nothing special from this package:
call maidr_on() once in a setup chunk, as in any other
Quarto or R Markdown document, and every plot the deck draws becomes an
accessible MAIDR chart.
A chart on a revealjs slide is keyboard reachable on its
own: Tab moves into it, the arrow keys explore it, and
Shift+Tab hands focus back to the slide, so
Space advances the deck again. None of that needs
configuring.
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, so Shift+Tab lands there rather than on the slide element itself; either way Space still moves to the next slide.
MAIDR supports a comprehensive range of visualizations:
geom_col()/geom_bar() +
coord_polar("y"); Base R via pie()geom_step() in ggplot2,
plot(type = "s") / plot(type = "S") in Base R
— for values that are piecewise constant, such as a sleep-stage
hypnogramgeom_violin())geom_ma() moving-average overlays and a patchwork volume
sub-panel); Base R via quantmod::chartSeries(), with its
addVo() volume panel read as a bar layer (other TA overlays
such as addSMA() and addEMA() are not
supported and fall back to native graphics)contour())See the Heat Map and Candlestick Examples article for the full candlestick + MA + volume pipeline and the Base R support matrix.
facet_wrap() and
facet_grid() in ggplot2par(mfrow/mfcol) for Base REvery 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:
geom_contour() [experimental], error bars
[experimental], Gantt charts via maidr_gantt()
[experimental], hexbin plots [experimental] and ROC curves via
maidr_roc() [experimental]stars() [experimental], mosaic plots
[experimental], violin plots via vioplot::vioplot()
[experimental] and word clouds [experimental]The full list is under “Experimental Plot Types” in the README.
?maidr::show for function detailsmaidr::run_example() to see all available plot typeslibrary(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)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)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)?maidr::show for function documentationhelp(package = "maidr")